Skip to main content

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 writtena description of the app (a spec: measures, cuts, questions, filters), no codeReact and TypeScript from github.com/BicycleAI/data-app-template, plus a manifest
Queriesderived for you; no SQLdeclared by the builder in the manifest, proven with query_run first
Buttons that call functions, agents or workflowsnot yet (composed apps cannot call anything)yes, through the app SDK (bda.fn)
Changing it laterchange the description, compose againchange the code, upload a new version
Whoanyone, with a coding agent or through Claude's connectora 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​

  1. git clone https://github.com/BicycleAI/data-app-template.git, work in template/, npm install.
  2. The agent creates the app in Studio (dataapp_start with a name, a title and the model), which returns an app id and version 1 awaiting upload. The app id goes into bda.manifest.json.
  3. 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 with query_run before declaring it.
  4. 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.
  5. 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 the dataapp_sdk tool.
  6. npm run build produces exactly dist/app.js and dist/app.css; those two and the manifest are zipped.
  7. Upload (dataapp_upload_url, then dataapp_complete_upload). Studio compiles every declared query and boots the bundle; the version becomes validated or invalid with per-query errors.
  8. 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​