Skip to content

Examples

Three end-to-end how-tos, each self-contained and copy-pasteable. Every workbook below is built inline with openpyxl — generic cell content, no client data — so you can run each example verbatim and get the exact output shown.

Extract a workbook and query its dependencies

First, build a tiny synthetic workbook: a 5-period amortization with two formula columns that depend on each other — Balance this period depends on last period's Balance and Decay, and Decay this period depends on this period's Balance.

Save as make_book.py:

import openpyxl

wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Sheet1"
ws["A1"] = "Month"
ws["B1"] = "Balance"
ws["C1"] = "Decay"
ws["A2"] = 0
ws["B2"] = 1000
ws["C2"] = 0
for row in range(3, 8):
    prev = row - 1
    ws[f"A{row}"] = row - 2
    ws[f"B{row}"] = f"=B{prev}-C{prev}"
    ws[f"C{row}"] = f"=B{row}*0.02"
wb.save("book.xlsx")
python make_book.py
marinade extract book.xlsx -o ir.db

Now query the binding-level dependency graph — agent_binding_dependencies — with the sqlite3 CLI:

sqlite3 -header -column ir.db \
  "SELECT from_label, to_label, kind, edge_count FROM agent_binding_dependencies
   ORDER BY from_label, to_label;"
from_label  to_label      kind     edge_count
----------  ------------  -------  ----------
Balance     Decay         formula  1
Balance     Sheet1!A1:C2  formula  1
Decay       Balance       formula  1

Three edges: Balance reads Decay and the seed row (A1:C2, the header + period-0 constants, which didn't get a label of its own), and Decay reads Balance right back — the cyclic binding graph described in Bindings & the graph. (The ORDER BY from_label, to_label gives a stable row order — edge rows are otherwise unordered.) Same query from Python:

import sqlite3

con = sqlite3.connect("ir.db")
con.row_factory = sqlite3.Row
rows = con.execute(
    """
    SELECT from_label, to_label, kind, edge_count
    FROM agent_binding_dependencies
    ORDER BY from_label, to_label
    """
).fetchall()
for row in rows:
    print(dict(row))
{'from_label': 'Balance', 'to_label': 'Decay', 'kind': 'formula', 'edge_count': 1}
{'from_label': 'Balance', 'to_label': 'Sheet1!A1:C2', 'kind': 'formula', 'edge_count': 1}
{'from_label': 'Decay', 'to_label': 'Balance', 'kind': 'formula', 'edge_count': 1}

Diff two versions of a model

Build two versions of the same tiny model — v2 changes the Decay formula's multiplier from 0.02 to 0.03, nothing else:

Save as make_versions.py:

import openpyxl


def build(decay_rate: float) -> openpyxl.Workbook:
    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = "Sheet1"
    ws["A1"] = "Month"
    ws["B1"] = "Balance"
    ws["C1"] = "Decay"
    ws["A2"] = 0
    ws["B2"] = 1000
    ws["C2"] = 0
    for row in range(3, 8):
        prev = row - 1
        ws[f"A{row}"] = row - 2
        ws[f"B{row}"] = f"=B{prev}-C{prev}"
        ws[f"C{row}"] = f"=B{row}*{decay_rate}"
    return wb


build(0.02).save("model_v1.xlsx")
build(0.03).save("model_v2.xlsx")
python make_versions.py
marinade extract model_v1.xlsx -o a.db
marinade extract model_v2.xlsx -o b.db
marinade diff a.db b.db -o changelist.json

The changelist (see Diffing workbooks for the full field reference) reports the change once, at the binding level, not once per cell:

{
  "summary": {
    "total_changes": 8,
    "bindings_formula_changed": 1,
    "cells_formula_changed": 5,
    "ir_inference_changes": 1
  },
  "changes": [
    {
      "seq": 3,
      "type": "BINDING_FORMULA_CHANGED",
      "sheet": "Sheet1",
      "address": "C3:C7",
      "old_formula": "RC[-1]*0.02",
      "new_formula": "RC[-1]*0.03",
      "modification_kind": "logic_change"
    }
  ]
}

bindings_formula_changed: 1 is the number that matters for review — the five cells_formula_changed entries are the same edit, once per cell in the Decay column, rolled up into that one BINDING_FORMULA_CHANGED entry. modification_kind: "logic_change" (rather than reference_shift) confirms this is a real formula edit, not just references moving because of an insertion elsewhere in the sheet. ir_inference_changes: 1 is unrelated noise — a table-candidate re-ranking that shifted because the underlying bindings changed, not because you edited a cell — and is worth filtering out before you review a changelist:

import json
from pathlib import Path

changelist = json.loads(Path("changelist.json").read_text())

workbook_edits = [
    c for c in changelist["changes"] if c.get("layer") != "ir_inference"
]
for change in workbook_edits:
    if change["type"] == "BINDING_FORMULA_CHANGED":
        print(
            f"{change['address']}: {change['old_formula']} -> "
            f"{change['new_formula']} ({change['modification_kind']})"
        )
C3:C7: RC[-1]*0.02 -> RC[-1]*0.03 (logic_change)

Generate documentation (deterministic, then enriched)

Using the ir.db from the first example:

marinade document ir.db -o out/

Writes out/documentation.md (human-readable) and out/model_spec.json (machine-readable) — no network call is made. Both describe the same two calculated variables from the extraction above (Balance, Decay), their inferred role (Calculation), and which of them need reconciliation against a reference implementation.

To layer LLM-written narrative documentation on top, install the extra, set a key, and pass --enrich (see LLM add-on for provider setup):

pip install 'xl-marinade[llm]'
export OPENAI_API_KEY="sk-..."
marinade document ir.db -o out/ --enrich

--enrich degrades gracefully rather than failing: without the [llm] extra installed, marinade document ... --enrich prints note: xl-marinade[llm] not installed — deterministic documentation only and writes the exact same deterministic documentation.md/model_spec.json as the plain command above. The same fallback applies if the extra is installed but no key is configured, or the request errors — you always get the deterministic output, and enrichment can only add to it, never replace it with something worse or block the run.

Next

  • Quickstart — the same extract → document → diff workflow, narrated end to end.
  • CLI reference — every flag used above.
  • SQLite schema — the full set of agent_*/atlas_* views to query.