Skip to content

Core concepts

ConceptIn one line
WorkbookThe working container (not a file). Bundles tables, sheets, versions and policies
SourceAn uploaded original (immutable). auto (fully automatic) or staged (explicit analyze → parse-spec edits → materialize)
TableA typed, row-identified, versioned dataset — the agent’s main object. Base (editable) or derived (transform output)
TransformA SQL (Jinja2) or Python template. {{ arg }} placeholders bind inputs via args and produce an output artifact — the unit of lineage
SheetA presentation composition that owns no data. Blocks reference tables (n:m); render composes an xlsx
ChartAn agent-made chart: a config plus the recipe (tool + parameters) that produced it
Versionversions.commit(label) = an immutable label on a snapshot. Revert restores derived tables through the snapshot machinery — base-table row edits are tracked in the edit log
JobA handle on an async operation (async=true uploads etc.). jobs.wait() or the job.completed webhook
WorkspaceThe boundary object (members, billing, governance). Developers manage many workspaces through Account API keys

Every write requires expected_version (the table’s etag). On a mismatch you get a 409 — the SDK raises ConflictError. There is no automatic retry (concurrent edits are never silently overwritten): re-read (rows() returns edit_version), re-apply, retry. That is the contract.

page = client.tables.rows(wb, "sales")
client.tables.upsert_rows(
wb, "sales",
rows=[{"__d2b_row_id": 3, "amount": 999}], # row_id present = update, absent = insert
expected_version=page["edit_version"],
)

The SDK attaches an Idempotency-Key to every mutating call (same key + same body replays the first response). Network-level retries can never double-apply. If you call HTTP directly, send the header yourself.

The promotion model (raw never disappears silently)

Section titled “The promotion model (raw never disappears silently)”

Auto ingestion builds D2B’s structured tables from a faithful raw copy of each sheet. The structured tables take over the file’s name and the raw tables are demoted to state=archived (the registry and lineage remain). tables.lineage() walks back to the raw tables, and tables.unarchive() re-materialises the original sheet when needed. With structuring=defer the raw tables land first and the same promotion happens when structuring finishes; if you built anything on a raw table by then, promotion is skipped automatically (downstream is never broken).

Interactive writes (row edits and the like) do not eagerly recompute downstream derived tables — they mark them stale. GET .../tables/{name} exposes freshness: {stale, stale_since}, and POST /workbooks/{id}/recompute (MCP recompute_stale) settles the whole backlog in dependency order. Transform runs propagate eagerly (their outputs are always fresh).

Column-tag × role policies (mask / deny) are enforced in the data layer. Masked columns come back as typed NULLs, reported in masked_columns. GET .../tables/{name}/access is a dry-run of what would happen. The same policy applies to export, profile, MCP and raw-file preview — there is no side door (on governed workbooks, raw-byte preview / revise are refused).

Per-row JSON Schema (for constrained decoding)

Section titled “Per-row JSON Schema (for constrained decoding)”

A static OpenAPI spec cannot type a table’s contents (row shape depends on the data). So each table serves a “one row” JSON Schema at runtime:

GET /api/v1/workbooks/{wb}/tables/{name}/schema?format=json-schema

Over MCP: get_schema(..., include_json_schema=true). The schema is generated from the policy-applied column set (denied columns don’t appear), with additionalProperties: false, every cell nullable, and __d2b_row_id meaning “absent = insert / present = update”. Agent harnesses can use it to constrain upsert_rows payloads at generation time.

  • snapshot: an immutable commit. POST /snapshots / POST /snapshots/{id}/restore
  • version: a name on a snapshot (POST /versions, POST /versions/{label}/revert)
  • op log: every mutation, ordered (GET /ops). Row ops can be undone via POST /ops/{id}/undo (history is append-only)
  • branch / merge: POST /export with record_branch=true freezes the exported rows; POST /tables/{name}/merge brings the edited file back as a row-id-keyed, cell-level 3-way merge. Cells changed on both sides land in the conflict queue