Data apps, compose or hand-build
A data app is a small web page bound to one data model. It shows numbers (tiles, trends, breakdowns, tables), lets viewers narrow by dimension and time, has an "Ask AI" chat drawn by Studio, and can have buttons that run functions or workflows. It reads data only through queries it declares; Studio runs them as the person viewing.
Two ways to make one
| Compose from a description (default) | Hand-build from the template | |
|---|---|---|
| What is written | a description of the app (a spec: measures, cuts, questions, filters), no code | React and TypeScript from github.com/BicycleAI/data-app-template, plus a manifest |
| Queries | derived for you; no SQL | declared by the builder in the manifest, proven with query_run first |
| Buttons that call functions, agents or workflows | not yet (composed apps cannot call anything) | yes, through the app SDK (bda.fn) |
| Changing it later | change the description, compose again | change the code, upload a new version |
| Who | anyone, with a coding agent or through Claude's connector | a coding agent, or an engineer |
Use compose unless the app must call a function, agent or workflow, or needs a screen no recipe draws. The agent will tell you when it switches: the tell-tale is that it asks you to confirm a function's first publish before the app is ready.
Composing, in one paragraph
The agent interviews you (the "Ask, Show, Ship" skill): what you will decide, what the data can say, who reads it, which questions it answers (each question is one recipe such as a verdict, a trend by a dimension, a ranking, a heatmap or a table), what viewers can narrow by, and the words and thresholds to use. It previews every question with real rows before committing, validates the description, composes it into a draft version, and asks you before publishing. A description can also declare a little persistence: a shared cache (a target a PM sets and every viewer sees) or read-only blobs (a lookup table, a forecast file) uploaded by the builder. Nothing undeclared is served.
Hand-building, in one loop
git clone https://github.com/BicycleAI/data-app-template.git, work intemplate/,npm install.- The agent creates the app in Studio (
dataapp_startwith a name, a title and the model), which returns an app id and version 1 awaiting upload. The app id goes intobda.manifest.json. - It explores the model (
query_describe_model: the metric columns and the dates each covers;query_search_fields;query_dimension_values) and proves every query withquery_runbefore declaring it. - It declares the queries in the manifest (id, semantic SQL, parameters, columns, a row cap; at most 32). Semantic SQL is simple: metrics are columns, one model, a bounded time range, no joins.
- It declares the functions and workflows the app may call under
functions, pinned to a version, and adds the app SDK files (src/studio/fn.ts,src/studio/bda.ts) that Studio serves through thedataapp_sdktool. npm run buildproduces exactlydist/app.jsanddist/app.css; those two and the manifest are zipped.- Upload (
dataapp_upload_url, thendataapp_complete_upload). Studio compiles every declared query and boots the bundle; the version becomes validated or invalid with per-query errors. - Publish (
dataapp_publish). A later change is a new version (dataapp_new_version) and steps 6 to 8 again. Versions are private drafts until published.
Manifest details: The manifest.
Rules every app follows
- Only declared queries run. An undeclared query is refused (
query_not_allowed): declare it and upload a new version. - No network, no storage. The app runs in a sandboxed frame: no fetches to other origins, no local storage or cookies, no HTML forms. Buttons call things from their click handler.
- Chat belongs to the host. Studio draws the "Ask AI" drawer; the app never builds a chat UI. The app reports what is on screen (each card's title, kind and query) so chat can see it.
- Widgets never blank. Layout renders first; each card waits for its own query with a placeholder of the right size; an error shows in that card with a Retry, and the rest of the app stays usable.
- Output is data. Anything a function, agent or workflow returns is rendered as text, never as HTML.
Calling a function, agent or workflow from a button
A hand-built app declares what it may call and names the local name in code, never the pinned reference:
"functions": {
"order_kpis": { "ref": "fn:<tenant>/order_kpis@3" },
"weekly_digest": { "ref": "wf:<tenant>/weekly_digest@2" }
}
import { bda } from './studio/bda.js'
// quick call (Code, Ask AI, Sort into categories): waits and returns the output
const out = await bda.fn.call('order_kpis', { day: '2026-09-20' })
// slow call (Agent, workflow): start it, show progress, allow Cancel
const started = await bda.fn.call('weekly_digest', {}, { wait: false })
const watch = bda.fn.watch(started.invocation_id, batch => showProgress(batch.items))
cancelButton.onclick = () => bda.fn.cancel(started.invocation_id)
const value = bda.fn.outputOf(await watch.done)
// on page load: reuse the viewer's recent result, and show "as of <time> · Refresh"
const inv = await bda.fn.call('order_kpis', input, { wait: false, reuse: '6h' })
- The call runs as the viewer and appears on the Runs page with its trace.
reuse: "6h"(up to 24h) answers from the viewer's own recent run of the same call at no cost; always use it for page-load calls and show "as of" with a Refresh (refresh: true). Reuse does not apply to workflows.- A workflow's output is its run receipt; an email it sends is delivered only when the receipt's state is
sent. - Never run an agent on page load unless the product asks for it; agents are slow and cost money.
- Publishing a version that adds or re-pins an import is confirmed by a person.
Studio generates a per-function snippet on the function's Use it in an app tab; more in Use functions in apps.
Chat that uses your functions
The app's Ask AI chat may call the app's imported functions and workflows that are exposed to agents, as tools, with the same run and trace. The owner or a workspace admin switches this in the app's settings: Functions in chat: Inherit / Off. Exposing a function to agents is a person-only step. Worked example: Chat that answers from your functions.
Detect and Explain in an app
An app can declare analyses in its manifest; Studio's analysis panel lists them and viewers run them with their current window and filters. See Detect and Explain.
Next
- The manifest: every field.
- Publish, share and schedules.
- KPI app example: a hand-built app with a Code function and a Refresh button.