Your workbook in git
Pull a workbook’s contents into your repository as files, put them through git diff / review / PRs, and push them back. Transforms written by the chat agent appear in the same place — so “review the SQL an AI wrote before it counts” becomes an ordinary workflow.
d2b pull --workbook WB # → transforms/ sheets/ charts/ + d2b.jsond2b pull --data customers # also track a small base table as data/customers.csv (export = branch)git add -A && git commit -m "pull from D2B"# ... edit transforms/*.sql|py, sheets/*.json, data/*.csvd2b push --dry-run # what would be sent (only files that changed)d2b push --commit "$(git rev-parse --short HEAD)" # apply the changes → pin the git sha as a named versiongit commit -am "d2b push" # push updates d2b.json (sync hashes) — commit it tooLayout
Section titled “Layout”| Directory | Contents | pull | push |
|---|---|---|---|
transforms/ | SQL / Python transforms (nested paths like agg/monthly.sql work) | ✅ | ✅ Only what changed, re-run via POST /transforms (metered as data operations), upstream first |
sheets/ | Presentation sheets, {"blocks": [...]} | ✅ | ✅ PUT /sheets/{name} |
charts/ | Chart config + recipe (the tool and parameters that generated it) | ✅ | ❌ Read-only — charts are re-generated from their recipe; a hand-edited config could never be refreshed. History and inspection |
data/ | Opted-in base tables as CSV (__d2b_row_id carried), via --data | ✅ export = branch | ✅ Server-side row-id-keyed, cell-level 3-way merge. Cells changed on both sides land in the workbook’s conflict queue (D2B’s value stays) |
--data only applies to base tables (tables whose rows are their own source of truth). mode=auto structuring output is derived (a transform’s output) and cannot branch — to round-trip a table’s rows through git, ingest it with --mode staged, review the parse spec and materialize (that produces a base table), or create it via the rows API.
d2b.json is the manifest. A transform entry is {name, artifact_name, args, layer, hash} (adding a new transform = the file plus this entry). hash is the digest at the last sync (the merge base), maintained by the CLI — commit d2b.json after a push.
When hand-writing a new entry, omit hash (leaving it out — or null — is the same): the first push assigns it and writes it back.
{ "workbook_id": "…", "transforms": { "agg/monthly.sql": { "name": "agg/monthly", "artifact_name": "product_sales", "args": {"src": "sales"}, "layer": null, "hash": "…" } }, "sheets": {"summary.json": {"name": "summary", "hash": "…"}}, "charts": {"trend.json": {"name": "trend", "readonly": true, "hash": "…"}}, "data": {"customers.csv": {"table": "customers", "branch_id": "…", "hash": "…"}}}Syncing a whole workspace
Section titled “Syncing a whole workspace”Hundreds or thousands of workbooks are handled as one repository = one workspace. The root d2b.json (the ledger) pins the workspace, and every workbook lands in workbooks/<title>--<first 8 of its id>/ with the layout above, its own d2b.json included.
d2b pull --workspace WS # first time: writes the ledger and pulls every workbook of the workspace (in parallel, --jobs N)d2b pull # afterwards: from the ledger; a new workbook appears by itselfd2b pull --prune # remove the directories of workbooks that left the workspace (the removal shows in the diff)d2b status --strict # exit 2 when the ledger, the directories and the server disagree (make it a required CI check)d2b push --commit "$(git rev-parse --short HEAD)" # only the workbooks that changed are sent- Membership is the server’s fact:
pulllists every workbook of the workspace and never writes a directory the ledger does not know. Another workspace cannot be pulled into the same repository — there is no override flag. - A workbook’s directory is fixed at its first pull; a retitle does not move it. Its id is in the directory name and on the first line of every transform file,
-- d2b ws=… wb=… transform=…(the line is never sent to the server and never counts as a change). pushsends nothing at all when it finds a directory the ledger does not know, a file whose header names another workbook, or a manifest bound to a differentworkbook_id.- Give CI a PAT pinned to the workspace (
workspace_idonPOST /api/control/account/keys, orresource: "workspace:<id>"onPOST /api/v1/me/tokens): a misconfigured checkout still gets a 403 from the server for any other workspace. Ad2b logincredential reaches the whole account and is not the right key for CI. data/(rows) stays a per-workbook opt-in (d2b pull --data TABLEinsideworkbooks/<dir>/).
A directory without a ledger keeps the single-workbook behaviour above.
Conflict rules
Section titled “Conflict rules”- Text sections (transforms / sheets / charts) never overwrite silently (the same contract as the row API’s optimistic lock): if both sides moved since the last sync, pull and push are both refused with the file list as JSON.
--forcepoints differently per command: push--forceoverwrites with your local side; pull--forcetakes the server side (discarding local edits) data/is different: concurrent edits are arbitrated cell-by-cell by the server’s 3-way merge, so nothing is refused. After a push, the CSV is re-fetched with the merged result and a fresh branch is cut (D2B-side changes land locally too). Derived tables cannot branch (400). Merges cap at 100,000 rows — this is for small tables like masters and mappings- Files deleted locally never delete anything on the server (reported as
deleted_locally).--prunedeletes transform output tables and sheets (table deletion needsworkbooks:delete).data/is only untracked — the table stays d2b.jsonkeys must be normalised relative paths inside their own section directory: absolute paths,.., Windows drives and un-normalised paths are refused on both pull and push. A synced file that is a symbolic link (transforms/*.sql|pyand the like) is refused too — nothing is read or written through a link (a link to a file the section never reads is simply ignored)
Automating the loop with GitHub Actions
Section titled “Automating the loop with GitHub Actions”No GitHub App and no D2B-side integration — the CLI alone closes the loop between a repository and a workbook.
d2b github-workflow > .github/workflows/d2b.yml# Secrets: D2B_API_KEY (a workbooks:write PAT). Variables: D2B_BASE_URL- Merge to
main(touching synced files) →d2b push --commit <sha>→ the refreshedd2b.jsonis committed back automatically - On a schedule (hourly by default) and on demand →
d2b pull→ a PR when something changed (d2b/pullbranch). What the agent changed in the workbook gets human review before it merges
In the SDKs, the same surface is client.transforms.list(wb) / client.sheets.list(wb) / client.charts.list(wb) / client.export.branch(wb, [table]) / client.tables.merge(...).