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
reuseon 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_atand 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 onerror.code(for exampleconnector_not_connectedmeans "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
usagewhen 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.