Widgets and sources
Version and authority semantics behind Widgets v1 — named sources, drafts, publication, refresh, and privacy.
A named source holds a person's own uploaded CSV or Excel workbook as many immutable versions, one of them active at a time. A widget is a private interactive app built over one source: a person describes what they want in plain language, the engine designs the analysis and layout, and the result runs deterministically — refresh and filtering make no model call. See Build a widget over a source for the request-by-request walkthrough; this page is the vocabulary and the rules that stay true across every request.
Lifecycle
Sources and versions
A source is a stable identity — an id, a name, an owner, an active version
pointer — independent of any widget. Uploading a file
(POST /v1/sources/{id}/versions, multipart, field file) never overwrites
the current version; it creates a new one and validates it before activation.
Failed processing leaves the previously active version exactly where it was —
there is no partial or silent replacement.
A workbook where any formula cell has no saved value fails the whole
version — formula_cache_missing, naming one such cell — and the version
never activates or becomes queryable; no widget can ever read any of its
tables. This is a whole-version failure regardless of whether any widget's
design would ever read that cell, because the engine has no way to tell in
advance which cells a future design will need: recalculate and save
the workbook in a spreadsheet application, then upload it again.
A version is immutable once committed: the original file bytes and the typed,
queryable table projections read from it (one table per CSV, one per Excel
sheet) never change under the same id. History is queryable
(GET /v1/sources/{id}/versions), the original file can be downloaded
(GET /v1/sources/{id}/versions/{version}/file), and a ready version can be
restored (POST /v1/sources/{id}/versions/{version}/restore) — restoring
creates a new version and activates it the same way any other upload
does, but does not re-run validation: it copies the historical version's
already-validated original file and table projections directly, rather than
re-parsing them. Restoring does not move history backwards or delete
anything.
A source's kind is always "upload" in v1 — there is no crawling, no
scheduled refetch and no multi-file version; one physical file activates or
fails as a whole.
Drafts pin a version, widgets do not follow it automatically
A widget's one active draft starts pinned to the source's active version
at creation (or at POST /v1/widgets/{id}/draft, which resumes it). When a
newer version activates:
- The published widget refreshes on its own — see Refresh below.
- The draft does not move. It keeps analyzing the version it started on,
and
POST /v1/widgets/{id}/draft/publish(Save) refuses with409 widget_source_changedthe moment the pinned version is no longer active — publication never silently rebases onto data nobody reviewed. - The creator moves the draft explicitly:
POST /v1/widgets/{id}/draft/update-sourcewith the draft's ownexpected_revision(not the widget's — the two are separate counters) and the source's currentactive_version_id. It re-pins the draft, clears the stale preview and problems, and — if the draft has a design — runs it synchronously against the new version, exactly as restoring a design revision does. A design that fails on the new version is recorded as the draft's failure; Save stays blocked and the published widget is untouched. Any other version — a superseded one, a different source's, an unknown id — is refused as409 widget_source_changed: there is no moving a draft to a version of the caller's choosing.
Save re-checks the active version again in the same transaction as publication, so an activation racing a Save cannot publish a design nobody validated against what actually went live.
Publication is atomic
POST /v1/widgets/{id}/draft/publish takes expected_revision and the
draft's own preview_execution_id — the execution the person just reviewed —
and writes one immutable widget revision: the accepted analysis and
layout, the source version and execution it was accepted against. It either
records the whole revision and retires the draft, or it changes nothing.
There is no partial publish, and no model or sidecar call happens in Save
itself — the analysis already ran when the preview was produced.
A successful preview may still carry explicitly unavailable arithmetic cells (a division by zero, a value outside the 38-digit decimal range) and remains publishable: the null value and its reason ride along in the saved result. What blocks Save is a required failure — an unresolved join assumption, a missing column, an open clarification question — never an explained gap.
Refresh is atomic and independent per widget
Activating a new source version fires an independent refresh for every published widget pinned to that source; one widget's refresh failing changes nothing about another's. A widget's own refresh is also atomic:
- Pin the saved revision and the target source version.
- Re-validate every structural and semantic assumption the revision recorded.
- Execute every required calculation.
- Publish the new result only when the complete result succeeds.
A failed refresh leaves the previous complete result and its evidence
exactly in place and records why, rather than showing a half-updated
dashboard. POST /v1/widgets/{id}/refresh retries the saved design against
the widget's current target; it makes no model call and takes no billing
hold — refresh re-runs stored statements, it never writes them again.
Idempotency-Key is required: a resent request with the same key dedupes
onto the same job rather than queuing a second one.
Filtering reads the full source, not a captured preview
POST /v1/widgets/{id}/views (base_execution_id, parameters) re-runs the
base result's exact analysis with new parameter values against the whole
pinned source version — every row a table holds (up to its own 1,000,000-row
ingestion ceiling; see Limits), never only whatever rows an earlier
preview happened to capture. This is a deliberate difference from
agent-run surfaces, whose
capture cap is a per-query display limit on the STORED rows themselves.
Pagination behaves differently by output kind, and only one kind pages at
all: a table output pages (GET .../outputs/{output}?cursor=, 100 rows a
page); a chart output never pages — its complete result rides in the
execution response in one piece, and if it would exceed 1,000 rows the whole
execution is refused with chart_too_large rather than silently truncated,
so a chart's design has to group or narrow the data itself; a scalar
output is always exactly one row. (There is no separate "list" output kind —
those are the three the analysis compiler accepts.) None of this bounds the
population a total is computed over — only how much of a table's own result
one page returns. Parameter choices themselves
(GET .../parameters/{parameter}/options?search=&cursor=) are the distinct
values of the full source too, 100 per page, with availability and reason
explaining a choice scope the engine cannot resolve (a fraction where a whole
number is required, a value that itself depends on another parameter).
Evidence is pinned to one result
POST /v1/widgets/{id}/executions/{execution}/evidence explains one displayed
value: the formula that produced it in plain language, its intermediate
inputs, and the contributing source rows, paginated. It is pinned to the
exact execution, source version and filter parameters that produced the
value — never re-derived against whatever is current — so a value from an
older, superseded result stays explainable with the rows that actually built
it. Evidence and page cursors expire 24 hours after they were issued;
re-reading the execution reissues them.
Excel values are read as saved
For an Excel workbook, the engine reads the values the workbook last saved.
It does not recalculate formulas, execute macros, or resolve external
workbook links. A formula cell with no saved value at all fails the whole
version at upload — see formula_cache_missing —
so the fix, recalculate and save the workbook in a spreadsheet application
before uploading again, is asked before any table from that version is ever
read. What the engine cannot catch is a saved value that is merely STALE —
a formula whose cached result no longer matches its inputs, but which the
workbook still carries a value for: nothing here re-executes the formula to
notice, so a design can read a number the workbook computed at an earlier
save. A number outside the exact 38-digit decimal range is preserved as text
with a column warning rather than rounded or rejected; a numeric calculation
over that column is refused until the design accounts for it.
Limits
Upload and ingestion (checked as a version is processed, independent of any widget):
| What | Bound |
|---|---|
| Source name | 1 to 200 bytes |
| Uploaded file (original) | CSV or XLSX only, 1 to 52,428,800 bytes (50 MiB) |
| Uploaded filename | At most 255 bytes |
| Sheets per workbook | 64 nonempty sheets |
| Decoded workbook/CSV size | 64 MiB, aggregate across every sheet |
| Rows per table | 1,000,000 — checked per sheet/CSV at ingestion |
Analysis and execution (one widget's preview, publish, refresh or filter):
| What | Bound |
|---|---|
Idempotency-Key | At most 200 bytes; required on every source and widget write that spends work — see Idempotency-Key |
| Draft message | 1 to 8,000 UTF-8 bytes, non-whitespace |
| An authoring job | Up to twelve governed model calls, a nine-minute deadline |
| One execution attempt | A 35-second deadline |
| Tables read by one analysis | At most 64, at most 2,000,000 rows and a 256 MiB expanded-data estimate in aggregate across all of them |
| Table output page | 100 rows |
| Chart output | Complete in one response, at most 1,000 rows, else refused with chart_too_large — never paginated |
| Parameter choice page | 100 values |
| Evidence/choice cursors | Expire 24 hours after being issued |
These bound one request or one job; they are not a promise of model accuracy
or a fixed price, and a large source can still be refused before ingestion —
watch for a 413 or a validation problem naming the reason. The aggregate
analysis envelope (64 tables, 2,000,000 rows, 256 MiB expanded) is why a
single table can hold up to 1,000,000 rows on its own but an analysis that
reads several large tables together can still be refused — the per-table
ceiling and the aggregate ceiling are checked separately.
Failures and repair
A source version's own validation is independent of whether any widget can use it: a source can activate even though a dependent widget's refresh then fails. Two different kinds of failure, both leaving the widget's last good result and evidence untouched:
- Structural — a required sheet or column the design reads is gone from the new version. Clears when the design changes to match, OR on its own the next time a compatible version activates (or a source restore reactivates one) — the widget's independent refresh just succeeds then; nothing has to be republished by hand for that case.
- Semantic — a required relationship the version's data no longer
supports, most commonly duplicate join keys the design assumed were unique:
a repeated key on the side its declared cardinality requires to be unique
(the lookup side of a many-to-one join). Repeats on the "many" side are the
relationship itself and are never asked about.
During conversational authoring this surfaces as a clarification question
(
state: "clarification") with the exact resolution option offered, such ashighest_per_key; answering it with a decision reply ({"decision":{"question_id":"...","option_id":"highest_per_key"}}on the next draft message) confirms the rule for that calculation. A confirmed rule persists across compatible source versions and is rechecked, without a model call, on every restore, refresh and filter.
Repair is always explicit: reopen the draft, resolve what needs resolving
(a new message, or update-source to move to a newer compatible version),
and Save again. Nothing here republishes automatically — a refresh either
reproduces the saved design's result or leaves the last good one standing.
A transient failure (503, widget_execution_retryable) is retried by
sending a fresh Idempotency-Key; a required failure (400, an analysis
code such as assumption_failed, or widget_definition_invalid) needs a
changed design, not a retry.
Restore
Two independent histories, restored independently:
- A source version:
POST /v1/sources/{id}/versions/{version}/restorecreates a new active version from an earlier one's content. - A widget's design:
POST /v1/widgets/{id}/draftwithfrom_revision_idset opens a draft carrying that historical revision's analysis and layout, pinned to the source's current active version, and runs it there at once. Restoring a design does not change what is published — the restored draft still needs Save. A draft already open from a different origin (already resumed, or mid-restore of a different revision) is409 widget_draft_exists; discard it first.
Both require Idempotency-Key, and both refuse a concurrent attempt rather
than queuing one: a source restore or upload already in flight for the same
source answers 409 source_update_in_progress, and a widget design restore
(or an update-source move, which runs the same synchronous claim) already
in flight for the same widget answers 409 widget_restore_in_progress.
Privacy
Every source, version, original file, widget, draft, conversation, revision,
execution and piece of evidence is private to its creator, in the
organization it was made in. Organization membership by itself grants
nothing — there is no organization-shared source or widget in v1, and no
sharing endpoint to grant one. org-admin is not an exception: the same
ownership check a peer member fails is the one an organization administrator
fails against another creator's data, and both get the same answer a stranger
gets. Every route in this family is 404 for a resource that does not exist
and for one that exists but is not the caller's — there is no way to tell
those two cases apart from the response.
Account-closure erasure
When an application built on Deeplinq closes one of its own accounts, it asks the engine to durably erase that account's widgets and named sources on the application-credential lane:
POST /v1/application/widget-data-erasures
{"issuer": "<the application's own issuer>", "subject": "<the account's subject>", "operation_id": "<key>"}
→ 202 {"erasure_id": "...", "state": "queued" | "running" | "succeeded", "failure_code": null | "..."}
GET /v1/application/widget-data-erasures/{erasure_id}
→ 200 {"erasure_id": "...", "state": "queued" | "running" | "succeeded" | "failed", "failure_code": null | "..."}state on the POST is not always queued, and resubmitting the same
operation_id does not unconditionally reopen the erasure. A failed
erasure is always reopened (reset to queued in the same transaction — a
POST response can therefore never read failed; only GET can). A
succeeded or still-running erasure is reopened only when THIS submit
finds a principal of the account, in one of the application's
organizations, that the erasure does not yet cover — a binding created
since the erasure's own previous submit, including one created while it was
queued or running; otherwise the POST reports that unchanged current
state, and resubmitting a genuinely finished erasure does not re-run it.
failure_code on a POST is not always null either: a retryable failure
leaves the erasure queued (or later running, once a worker claims the
retry) WITH its code still attached — the field is cleared only by a
reopen or a clean run — so a non-null code on a queued or running
response is the engine's own "still retrying" signal, not a stale
leftover. This is not an end-user route: it runs on the
application credential the consuming product holds, naming the account by
issuer and subject rather than by an organization or principal id, and
operation_id is the real idempotency key — the Idempotency-Key header is
optional and, if sent, must equal it exactly. Creating or spending owned
work — a new source, version or widget, a staged upload, an opened or
patched draft, a message, a publication, a refresh or view reservation, and
claiming an authoring or execution job — refuses a tombstoned owner
immediately (403 widget_owner_erased) once the request is accepted, before
the durable worker has removed a single row; finishing, cancelling,
discarding and deleting stay open so in-flight work can still wind down. The
erasure is scoped to Widgets and Sources data alone: conversations,
memories, usage, audit records and identity bindings keep their existing,
separate retention policy.