Skip to content

Use D2B from a coding agent

D2B has two paths for agents. MCP (tool calling: Claude Code / Claude Desktop / Cursor / VS Code and others) and the CLI (d2b: shell-driven agents, git sync, batch work). Both expose the same surface: JSON out, errors with a suggested_fix, an idempotency key on every mutation, no interactive prompts.

The operating norms come from the server. When a host connects over MCP, the initialize response carries instructions — which tool when, in what order — and the same text is readable as the d2b://guide resource. You do not paste a long procedure into your repo; you paste the connection and the choice of path (below).

  1. Mint a least-privilege PAT — for an agent, a workbook-scoped token with a rate limit (to delegate a whole workspace use "resource": "workspace:<WS_ID>"; creation is pinned there too. A token always belongs to one account; omit account_id for the default):
Terminal window
curl -X POST $BASE/api/v1/me/tokens -H "Authorization: Bearer $JWT" \
-d '{"name": "agent", "scopes": ["workbooks:read", "workbooks:write"],
"resource": "workbook:<WB_ID>", "rate_limit_per_minute": 60}'
  1. Connect — over MCP, Claude Code is one command (other hosts: Connect over MCP):
Terminal window
claude mcp add d2b --transport http https://d2b.dev/mcp/ --header "Authorization: Bearer $PAT"

d2b init writes the repo side — adds the AGENTS.md section below and prints the canonical MCP entry any host can take. Known hosts merge straight into their file with --host claude-code|cursor|vscode|codex|windsurf|claude-desktop; anything else via --config PATH (JSON or TOML). The token stays an environment-variable reference, never written to a file.

For the CLI, export D2B_API_KEY / D2B_BASE_URL (Claude Code does not read .env by itself) and install with pipx install d2b-sdk or uvx --from d2b-sdk d2b (uv add d2b-sdk + uv run d2b as a project dependency).

The repo snippet (connection and choice only)

Section titled “The repo snippet (connection and choice only)”

The norms come from the server, so do not copy the procedure here — a copy cannot follow new tools and goes stale.

## D2B
- Data work goes through D2B. The MCP server `d2b` is connected — follow the norms in `d2b://guide` first.
- For git management (`d2b pull` / `d2b push`) and batch work use the CLI `uvx --from d2b-sdk d2b ...` (JSON output; auth from env D2B_API_KEY / D2B_BASE_URL).
- Destinations, how to bring files in and how versions work are in the guide. When unsure, ask the human.

The norms an agent follows (delivered by the server)

Section titled “The norms an agent follows (delivered by the server)”

The gist of instructions / d2b://guide, with the CLI equivalent. The norms do not depend on the surface; only the tool names differ.

NormMCPCLI
Orient firstlist_my_data → get_schemad2b workbooks list → d2b tables list --workbook WB
Confirm the destination by name before creatinglist_my_workspaces → create_workbookd2b workspaces list → d2b workbooks create --workspace-id …
Bring files in by reference (never through your own context)ingest_url / request_upload → ingest_upload / import_cloud_filed2b upload FILE --workbook WB --wait (d2b jobs wait JOB_ID for long runs)
Messy sheets: inspect the structure before materialisingingest_file (staged) → analyze_source → update_parse_spec → materialize_sourced2b upload --mode staged → d2b sources analyze → … materialize
Derive with transforms, not row editsadd_transform / list_transformsd2b transforms add; d2b pull writes them to transforms/
On 409 re-read and re-apply (never overwrite blindly)re-read edit_version with read_tablere-read edit_version with d2b tables rows NAME --workbook WB
Cut a version at milestones so you can go backcommit_snapshot / restore_snapshot / undo_opd2b versions commit / d2b versions revert
Check numbers before reporting themreview_table / review_workbook (agent mode: follow the job with get_job)d2b review --workbook WB [--table NAME] [--agent]
Clean updelete_workbookd2b workbooks delete WB

d2b pull --workbook $WB writes transforms/ sheets/ charts/ + d2b.json (--data TABLE adds a base table’s CSV). Edit → check with d2b push --dry-run → d2b push --commit $(git rev-parse --short HEAD) → commit d2b.json. push sends only what changed (transforms re-run, data is 3-way merged); if refused, pull, merge, push again. charts/ is read-only. Details: Manage a workbook in git.

Faithful revision (revise = fix just those rows, get the original Excel back)

Section titled “Faithful revision (revise = fix just those rows, get the original Excel back)”

“Keep the whole original Excel, fix only the relevant rows, return a new file” is one revise call (internally it folds analyze → materialize → transform → write-back). The computation stays server-side as a SQL transform (lineage available); styles, charts, other sheets and out-of-region formulas are preserved.

Terminal window
d2b sources revise equipment.xlsx --workbook $WB \
--transform-name merge_tokyo_sites --range A3:N8 --sheet Summary \
--sql-file merge.sql -o output.xlsx

merge.sql is a {{ artifact_name }} view ({{ src }} is bound to the region table). How to write row-folding aggregations correctly:

  • Additive columns (counts, amounts): SUM.
  • Ratios (utilisation, completion): never average them. Recompute after summing: SUM(numerator) / NULLIF(SUM(denominator), 0).
  • Averages (MTBF, MTTR …): weighted, not naive: SUM(value * weight) / NULLIF(SUM(weight), 0) (weight = a meaningful column like unit count).
  • Categorical columns (ratings): a representative value: arg_max(rating, units_managed).
  • To keep row positions, include MIN("__d2b_row_id") in the SELECT (the folded row lands at its first member’s position; vacated rows blank out in place; nothing else moves).
CREATE OR REPLACE VIEW "{{ artifact_name }}" AS
SELECT
CASE WHEN site IN ('Tokyo Site 1','Tokyo Site 2') THEN 'Tokyo' ELSE site END AS site,
SUM(units_managed) AS units_managed,
SUM(units_active) * 1.0 / NULLIF(SUM(units_managed), 0) AS utilization,
SUM("MTBF(h)" * units_managed) / NULLIF(SUM(units_managed), 0) AS "MTBF(h)",
arg_max(rating, units_managed) AS rating,
MIN("__d2b_row_id") AS "__d2b_row_id"
FROM {{ src }}
GROUP BY 1
ORDER BY MIN("__d2b_row_id")

If the region contains total/subtotal rows (=SUM(...)), those formula cells are preserved untouched (Excel recalculates). Edits that would grow the region beyond the template’s geometry are refused (409). Over MCP this is revise_source; in the SDK, sources.revise(...).

Delivering constants as live formulas (formula columns)

Section titled “Delivering constants as live formulas (formula columns)”

A column of constants (say, a utilisation rate) can be delivered as a per-row Excel formula at export time. The values themselves are untouched — this is a delivery projection that only adds “how to re-derive it” to the shipped .xlsx. References use {column} placeholders (never A1 addresses — they are anchored to identity so sorting and filtering can’t shift them). The server resolves coordinates at export.

Terminal window
# Deliver utilization = units_active / units_total as a live per-row formula
d2b tables set-formula equipment utilization "{units_active} / {units_total}" --workbook <WB>
out = client.tables.set_formula(wb, "equipment", "utilization", "{units_active} / {units_total}")
out["verification"] # {checked, rows, matches, mismatches} — does it reproduce the current constants?

Because the values are canonical, the write returns a verification report (does the formula reproduce the current column within tolerance?). You can confirm “replace constants with a formula” before delivering. Over MCP: set_formula_column / list_formula_columns / clear_formula_column. Currently applies to xlsx export (composable with {{ ... }} SQL transforms and revise write-back). Functions (SUM/IF …) get structural validation only; underline is unsupported.

Cross-sheet references: {table!column} references another table’s full column range (when several tables are exported into one file, it compiles to an absolute reference into that sheet, e.g. 'rates'!$B$2:$B$N). Wrap it in an aggregate:

# Revenue share = this row's amount ÷ the sum of rates.weight
client.tables.set_formula(wb, "sales", "share", "{amount} / SUM({rates!weight})")
client.export.tables(wb, tables=["sales", "rates"], format="xlsx")
# → each share row compiles to =B2/SUM('rates'!$B$2:$B$N) (same-table {amount} relative, cross-refs absolute)

If the referenced table isn’t part of the same export, the formula can’t resolve and falls back to values. Express VLOOKUP-style key joins as JOIN transforms (add_transform), not formulas (lineage is preserved).

  • Cloud sandboxes (Codex etc.) may need egress allow-listing before the API is reachable
  • For agents with short Bash timeouts, “async upload → jobs wait” is safer than upload --wait