Concepts

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 with 409 widget_source_changed the 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-source with the draft's own expected_revision (not the widget's — the two are separate counters) and the source's current active_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 as 409 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:

  1. Pin the saved revision and the target source version.
  2. Re-validate every structural and semantic assumption the revision recorded.
  3. Execute every required calculation.
  4. 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):

WhatBound
Source name1 to 200 bytes
Uploaded file (original)CSV or XLSX only, 1 to 52,428,800 bytes (50 MiB)
Uploaded filenameAt most 255 bytes
Sheets per workbook64 nonempty sheets
Decoded workbook/CSV size64 MiB, aggregate across every sheet
Rows per table1,000,000 — checked per sheet/CSV at ingestion

Analysis and execution (one widget's preview, publish, refresh or filter):

WhatBound
Idempotency-KeyAt most 200 bytes; required on every source and widget write that spends work — see Idempotency-Key
Draft message1 to 8,000 UTF-8 bytes, non-whitespace
An authoring jobUp to twelve governed model calls, a nine-minute deadline
One execution attemptA 35-second deadline
Tables read by one analysisAt most 64, at most 2,000,000 rows and a 256 MiB expanded-data estimate in aggregate across all of them
Table output page100 rows
Chart outputComplete in one response, at most 1,000 rows, else refused with chart_too_large — never paginated
Parameter choice page100 values
Evidence/choice cursorsExpire 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 as highest_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}/restore creates a new active version from an earlier one's content.
  • A widget's design: POST /v1/widgets/{id}/draft with from_revision_id set 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) is 409 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.

On this page