Data Programs
A data program is a named, stored, agent-authored JS script that fetches,
joins, filters, or aggregates data — provider APIs via providerFetch /
providerFetchAll / providerSearchAll, app data via appAction, or
Resources-backed workspace files — and emits a small { rows, schema }
result that gets cached in SQL. Any app can render that result through its
own table/chart components; nothing about the primitive is analytics-specific.
This ships automatically wherever run-code is available — there is nothing
to mount, wire, or configure per app. If your app already has run-code
enabled, save-data-program / preview-data-program / run-data-program /
list-data-programs / get-data-program / delete-data-program are already
registered actions.
Why this exists
Without it, "build a live view of X" tends to end in one of two bad places:
a hardcoded action per vendor/query shape (brittle, doesn't scale to every
customer's custom fields), or re-running the same fetch/join logic from
scratch on every page view (slow, and it re-spends the same provider quota
every time someone looks at a chart). A data program is the middle ground:
write the fetch/join/aggregate logic once as ordinary run-code, save it, and
let the cache + refresh policy handle "how often does this actually need to
re-run."
Authoring workflow
- Prototype the fetch/join/aggregate logic as a normal
run-code script and
confirm it returns the rows you expect.
- Change it to call
emit(rows, schema?) exactly once at the end instead of
returning or printing the result.
- Save it with
save-data-program({ name, title, description, code, defaultParams?, refreshMode?, refreshTtlMs?, background? }). This
dry-runs the code with defaultParams before persisting and rejects
the save with a structured error if it fails, so a broken program is never
stored. On success it returns { programId, rowCount, columns, sampleRows } — use that as proof the program produces the rows you
expect.
- Bind whatever UI needs the result. Your app's own read path calls
run-data-program({ programId, params?, forceRefresh?, includeRows? })
and gets back rows/schema/cache metadata to hand to your own table/chart
components — there is no dashboard-specific plumbing baked into the
primitive itself. (The analytics template's "program" dashboard panel
source is one such adoption, not a required pattern — see its
data-programs skill if you want a concrete UI wiring reference.)
- Iterate without persisting via
preview-data-program({ code, params? }) —
same dry-run path, no stored row.
The sandbox surface
Exactly the run-code globals (providerFetch, providerFetchAll,
providerSearchAll, appAction, webFetch, workspace* Resources helpers),
plus:
params — a frozen global object holding the params for this run.
Read-only; mutating it throws.
emit(rows, schema?) — call exactly once, at the end. rows must be an
array of plain objects. schema, when provided, is
{ name: string, type: string }[]; when omitted it's inferred from the
first 50 rows (a column is "json" if rows disagree on primitive type,
otherwise "number" / "string" / "boolean"). console.log remains free
for debugging and never interferes with parsing the emitted result.
Caps enforced on every run:
| Limit |
Value |
| Max emitted rows |
10,000 (excess dropped, truncated: true) |
| Max emitted result size |
4 MiB (rows dropped from the end to fit; a single oversized row is a hard failure) |
| Max active programs per app |
200 |
| Minimum refresh TTL |
60,000 ms |
| Run rows kept per (program, params) |
5 most recent |
Truncation is always explicit (truncated: true) — never a silent drop.
Caching and refresh model
Every (programId, paramsHash) pair has its own run history:
| Situation |
Behavior |
Fresh cache (younger than refreshTtlMs, or refreshMode: "manual" with a prior success) |
Returns cached rows instantly, cacheHit: true |
| Stale cache |
Re-executes synchronously (25s budget for view-triggered reads, 120s for agent/manual calls), replacing the cache on success |
refreshMode: "manual" |
Only refreshes on explicit forceRefresh: true |
| No cache yet |
Executes synchronously like a normal first fetch |
background: true program with no fresh cache |
Serves the last good run with stale: true and enqueues a durable background execution; a later call finalizes it once complete |
| Background program still running, no prior success |
background_pending failure — surface an explicit "still computing" state, never a blank result |
| Execution fails |
Structured { code, message } failure; a previous success is attached as lastGoodRun for stale-serve |
| Program archived (soft-deleted) |
Explicit archived failure |
Use background: true only for scripts that routinely exceed the foreground
timeout (large multi-provider joins, deep pagination). Everything else should
stay foreground.
Security notes
- Credentials are always the calling viewer's, never the program author's.
providerFetch inside a program resolves auth using the caller's own
request context. A viewer without a configured provider key sees that
provider's normal auth error on that program's result — never another
user's data.
- Programs are shareable org-internal only — never public. Sharing a
program means teammates can view its cached result; because execution uses
the viewer's credentials, only bind to or view a program you trust.
- Raw provider tokens never reach the model or the browser. Only the
{ rows, schema } an emit() call produces ever leaves the sandbox.
- The mutating/executing actions (
save-data-program, preview-data-program,
run-data-program, delete-data-program) are not callable from a
sandboxed extension/iframe bridge — only from the agent or a server-side
read path.
Related skills
actions for run-code conventions and the sandbox globals this primitive
is built on.
sharing for the org-internal, never-public sharing model programs use.
security for credential-handling and access-scoping invariants.
1---2name: data-programs3description: Save a run-code fetch/join/aggregate script as a stored, refreshable data source any app's own charts/tables can render, instead of a hardcoded provider action or a per-view re-fetch. Use when an ad-hoc run-code or provider-api-request analysis should become a live, cached data source other users or panels can reuse.4---56# Data Programs78A **data program** is a named, stored, agent-authored JS script that fetches,9joins, filters, or aggregates data — provider APIs via `providerFetch` /10`providerFetchAll` / `providerSearchAll`, app data via `appAction`, or11Resources-backed workspace files — and emits a small `{ rows, schema }`12result that gets cached in SQL. Any app can render that result through its13own table/chart components; nothing about the primitive is analytics-specific.1415This ships automatically wherever `run-code` is available — there is nothing16to mount, wire, or configure per app. If your app already has run-code17enabled, `save-data-program` / `preview-data-program` / `run-data-program` /18`list-data-programs` / `get-data-program` / `delete-data-program` are already19registered actions.2021## Why this exists2223Without it, "build a live view of X" tends to end in one of two bad places:24a hardcoded action per vendor/query shape (brittle, doesn't scale to every25customer's custom fields), or re-running the same fetch/join logic from26scratch on every page view (slow, and it re-spends the same provider quota27every time someone looks at a chart). A data program is the middle ground:28write the fetch/join/aggregate logic once as ordinary `run-code`, save it, and29let the cache + refresh policy handle "how often does this actually need to30re-run."3132## Authoring workflow33341. Prototype the fetch/join/aggregate logic as a normal `run-code` script and35 confirm it returns the rows you expect.362. Change it to call `emit(rows, schema?)` exactly once at the end instead of37 returning or printing the result.383. Save it with `save-data-program({ name, title, description, code,39 defaultParams?, refreshMode?, refreshTtlMs?, background? })`. This40 **dry-runs the code with `defaultParams` before persisting** and rejects41 the save with a structured error if it fails, so a broken program is never42 stored. On success it returns `{ programId, rowCount, columns,43 sampleRows }` — use that as proof the program produces the rows you44 expect.454. Bind whatever UI needs the result. Your app's own read path calls46 `run-data-program({ programId, params?, forceRefresh?, includeRows? })`47 and gets back rows/schema/cache metadata to hand to your own table/chart48 components — there is no dashboard-specific plumbing baked into the49 primitive itself. (The analytics template's `"program"` dashboard panel50 source is one such adoption, not a required pattern — see its51 `data-programs` skill if you want a concrete UI wiring reference.)525. Iterate without persisting via `preview-data-program({ code, params? })` —53 same dry-run path, no stored row.5455## The sandbox surface5657Exactly the `run-code` globals (`providerFetch`, `providerFetchAll`,58`providerSearchAll`, `appAction`, `webFetch`, `workspace*` Resources helpers),59plus:6061- `params` — a **frozen** global object holding the params for this run.62 Read-only; mutating it throws.63- `emit(rows, schema?)` — call exactly once, at the end. `rows` must be an64 array of plain objects. `schema`, when provided, is65 `{ name: string, type: string }[]`; when omitted it's inferred from the66 first 50 rows (a column is `"json"` if rows disagree on primitive type,67 otherwise `"number"` / `"string"` / `"boolean"`). `console.log` remains free68 for debugging and never interferes with parsing the emitted result.6970Caps enforced on every run:7172| Limit | Value |73| --- | --- |74| Max emitted rows | 10,000 (excess dropped, `truncated: true`) |75| Max emitted result size | 4 MiB (rows dropped from the end to fit; a single oversized row is a hard failure) |76| Max active programs per app | 200 |77| Minimum refresh TTL | 60,000 ms |78| Run rows kept per (program, params) | 5 most recent |7980Truncation is always explicit (`truncated: true`) — never a silent drop.8182## Caching and refresh model8384Every `(programId, paramsHash)` pair has its own run history:8586| Situation | Behavior |87| --- | --- |88| Fresh cache (younger than `refreshTtlMs`, or `refreshMode: "manual"` with a prior success) | Returns cached rows instantly, `cacheHit: true` |89| Stale cache | Re-executes synchronously (25s budget for view-triggered reads, 120s for agent/manual calls), replacing the cache on success |90| `refreshMode: "manual"` | Only refreshes on explicit `forceRefresh: true` |91| No cache yet | Executes synchronously like a normal first fetch |92| `background: true` program with no fresh cache | Serves the last good run with `stale: true` and enqueues a durable background execution; a later call finalizes it once complete |93| Background program still running, no prior success | `background_pending` failure — surface an explicit "still computing" state, never a blank result |94| Execution fails | Structured `{ code, message }` failure; a previous success is attached as `lastGoodRun` for stale-serve |95| Program archived (soft-deleted) | Explicit `archived` failure |9697Use `background: true` only for scripts that routinely exceed the foreground98timeout (large multi-provider joins, deep pagination). Everything else should99stay foreground.100101## Security notes102103- **Credentials are always the calling viewer's, never the program author's.**104 `providerFetch` inside a program resolves auth using the caller's own105 request context. A viewer without a configured provider key sees that106 provider's normal auth error on that program's result — never another107 user's data.108- **Programs are shareable org-internal only — never public.** Sharing a109 program means teammates can view its cached result; because execution uses110 the *viewer's* credentials, only bind to or view a program you trust.111- Raw provider tokens never reach the model or the browser. Only the112 `{ rows, schema }` an `emit()` call produces ever leaves the sandbox.113- The mutating/executing actions (`save-data-program`, `preview-data-program`,114 `run-data-program`, `delete-data-program`) are not callable from a115 sandboxed extension/iframe bridge — only from the agent or a server-side116 read path.117118## Related skills119120- `actions` for `run-code` conventions and the sandbox globals this primitive121 is built on.122- `sharing` for the org-internal, never-public sharing model programs use.123- `security` for credential-handling and access-scoping invariants.