Core concepts
Objects
Section titled “Objects”| Concept | In one line |
|---|---|
| Workbook | The working container (not a file). Bundles tables, sheets, versions and policies |
| Source | An uploaded original (immutable). auto (fully automatic) or staged (explicit analyze → parse-spec edits → materialize) |
| Table | A typed, row-identified, versioned dataset — the agent’s main object. Base (editable) or derived (transform output) |
| Transform | A SQL (Jinja2) or Python template. {{ arg }} placeholders bind inputs via args and produce an output artifact — the unit of lineage |
| Sheet | A presentation composition that owns no data. Blocks reference tables (n:m); render composes an xlsx |
| Chart | An agent-made chart: a config plus the recipe (tool + parameters) that produced it |
| Version | versions.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 |
| Job | A handle on an async operation (async=true uploads etc.). jobs.wait() or the job.completed webhook |
| Workspace | The boundary object (members, billing, governance). Developers manage many workspaces through Account API keys |
Row edits and optimistic locking
Section titled “Row edits and optimistic locking”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"],)Idempotency
Section titled “Idempotency”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).
Freshness (staleness)
Section titled “Freshness (staleness)”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).
Governance
Section titled “Governance”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-schemaOver 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.
History
Section titled “History”- 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 viaPOST /ops/{id}/undo(history is append-only) - branch / merge:
POST /exportwithrecord_branch=truefreezes the exported rows;POST /tables/{name}/mergebrings the edited file back as a row-id-keyed, cell-level 3-way merge. Cells changed on both sides land in the conflict queue