Skip to main content

Use functions in apps

A hand-built app calls a function or a workflow in three steps: declare it in the manifest, put the app SDK files in place, and call it by its local name from a button. Studio generates the exact snippet for each function on its Use it in an app tab; this page explains what the snippet does.

1. Declare it​

// bda.manifest.json
"functions": {
"order_kpis": { "ref": "fn:<tenant>/order_kpis@3" }
}

The app may call only names declared here. Code never passes a reference; the host refuses undeclared names (fn_not_declared). The function must be exposed to apps and shared with your workspace. Publishing a version that adds or re-pins an import is confirmed by a person.

If the function lists a semantic.query capability, the app must also declare those query ids under queries, with the SQL. The function carries no SQL.

2. Add the SDK​

src/studio/fn.ts and src/studio/bda.ts go next to the template's src/studio/types.ts. Studio serves them: the dataapp_sdk MCP tool, or GET /api/data-apps/sdk. They speak to the host page; the app itself never fetches anything.

3. Call it​

import { bda } from "./studio/bda.js";

try {
const out = await bda.fn.call("order_kpis", { day: "2026-09-20" });
// out: { orders: 120, failed_pct: 3.4 }
} catch (error) {
// A BdaError with a code and a readable message, e.g. fn_not_declared, input_invalid,
// function_disabled, budget_exceeded, cancelled. Show error.message to the viewer.
}

For anything slow (an Agent, a workflow, a long Code function):

const started = await bda.fn.call("triage_failed_order", input, { wait: false });
const watch = bda.fn.watch(started.invocation_id, batch => {
// batch.items: [{ title: "Step 3", detail, meta }, ...] in plain words
showProgress(batch.items);
});
cancelButton.onclick = () => bda.fn.cancel(started.invocation_id);
const final = await watch.done; // status, output, error, usage { llm_usd, wall_ms }
const out = bda.fn.outputOf(final); // throws a BdaError in words if it failed or was cancelled

Or in one call: bda.fn.run(name, input, { onEvent, onStart }) resolves with the final run.

Running on page load​

const inv = await bda.fn.call("order_kpis", input, { wait: false, reuse: "6h" });
// inv.reused === true: a stored result, no run and no cost.
// inv.attached === true: the same run was already in flight; you joined it.
showAsOf(inv.created_at); // "as of 09:14 · Refresh"
refreshButton.onclick = () =>
bda.fn.call("order_kpis", input, { wait: false, refresh: true }); // always a new run
  • Always pass reuse on a page-load call ("6h", at most "24h"): the viewer's own recent result of the same input comes back at once.
  • Show "as of" from the run's created_at and offer Refresh (refresh: true).
  • Reuse is per viewer and does not apply to workflows.
  • Never run an Agent on page load unless the product asks for it.

Queued agents​

An Agent may wait its turn when the workspace's agent slots are full: the run has status: "queued" and a queue_position (0 means next). Show "Queued · 2 ahead" ("Queued · next" at 0) and keep watching; the run starts on its own. queue_timeout means it waited too long: say so and offer to try again.

Rules for app code​

  • Call only the declared local name, through bda.fn. No direct fetches, no references in code.
  • The output is data: render it as text (text nodes, table cells). Never inject it as HTML.
  • Keep the input small (under 64 KB as JSON); large inputs belong in a blob.
  • Start a call from a button's onClick, not a form submit: the sandbox has no forms, so submit never fires.
  • Show a busy state while it runs; disable the button that started it.
  • Show error.message; branch on error.code (for example connector_not_connected means "ask the viewer to connect").
  • It runs as the viewer: what it reads is what the viewer may read.
  • Show cost and time from the final run's usage when it helps the viewer.

Workflows from an app​

Same SDK, one difference: the input is { logical_date?, targets? }, the output is the run's receipt (artifacts and send receipts), and an email is delivered only when its receipt's state is "sent". See Call a workflow from an app.

Where the run shows up​

Every call from an app is one run, as the viewer, with surface: app and the app's id, on the Runs page and on the function's Runs tab, with the same trace as any other caller.

Worked examples: KPI app, Triage button backed by an agent.