Output & Input Lineage Authoring
render_native_sheet_cell_explanation
answers two questions with the same underlying trace: “why is this output
cell that value?” on an output sheet view cell (the precedents direction),
and “what does this input cell drive?” on an input sheet cell (the
dependents direction). Without any extra work, an uninstrumented run’s
answer is honest but minimal: an output cell is a literal value with no
dependency graph behind it, and an input cell’s dependents are inferred from
a legacy text match rather than a canonical cell reference. This guide shows
a model author or agent how to opt in to a richer answer for both directions
— a real input → logic → output trace — by writing
/outputs/output_lineage.json during the run.
Tracing is opt-in
Section titled “Tracing is opt-in”A run that never writes output_lineage.json is just as valid as one that
does. Add tracing where it earns its keep — headline outputs, anything an
analyst is likely to ask “why is this number what it is?” about — not to
every intermediate calculation.
Install the versioned helper
Section titled “Install the versioned helper”The authoring surface is OutputLineageBuilder, an ergonomic Python builder
that emits a valid output_lineage.json artifact. It has no dependency on
the Bridge Town server package — only the Python standard library and
pandas, both available inside the sandbox model-execution environment.
Download the immutable
output_lineage.py v1.1.0 source
from Bridge Town’s public GitHub release. Its SHA-256 digest is
a7e5bbb4eaff046b642e9c49b29e40ce530e85ab33869335d57e0ff2d503967e.
The release also publishes the complete
SHA256SUMS
file, source at the immutable tag,
example,
changelog, license, and ownership record.
Verify the file outside the network-isolated model sandbox:
curl -fL -o output_lineage.py \ https://github.com/Bridge-Town/financial-modeling-mcp/releases/download/model-authoring-helpers-v1.1.0/output_lineage.pyprintf '%s %s\n' \ a7e5bbb4eaff046b642e9c49b29e40ce530e85ab33869335d57e0ff2d503967e \ output_lineage.py | sha256sum --checkCopy the verified file byte-for-byte into the model repository as
lib/output_lineage.py using create_file or a one-entry commit_files
update, then import it from the model file:
from lib.output_lineage import OutputLineageBuilderDon’t reconstruct OutputLineageBuilder from the examples in this guide — a
near-miss implementation can look right and still fail run validation, or
silently emit a malformed trace. The examples illustrate usage; the tagged,
hash-verified helper is the implementation source of truth. The file imports
only Python’s standard library and pandas, which the model sandbox already
provides, and it makes no network calls.
The run id
Section titled “The run id”Every trace belongs to one run. Use OutputLineageBuilder.from_environment
rather than constructing the builder with your own run_id — the sandbox
exposes the platform’s run id for the current execution as the BT_RUN_ID
environment variable, and a trace with the wrong (or a hard-coded) run id
fails the run rather than silently mismatching:
from lib.output_lineage import OutputLineageBuilder
builder = OutputLineageBuilder.from_environment(model_name="revenue_model")from_environment raises LineageBuildError immediately if BT_RUN_ID is
unset — for example if the model was invoked outside the sandbox — instead
of writing an artifact that only fails later, at run validation.
The mental model
Section titled “The mental model”- Create one
OutputLineageBuilderfor the run. - Call
.output(...)once per model output file you want to trace — this returns anOutputTraceBuilderscoped to that one output. - On the output builder, declare nodes (
input_value,assumption,logic_step,output_node) as you compute values, threading precedent nodes through so the builder wires the dependency edges for you. - Register which output-sheet cell each output node explains
(
output_nodedoes this in one call; use.coordinate()for extra cells that reuse an existing node, e.g. every column of one table row). - Call
builder.write()at the end of your model — it writes/outputs/output_lineage.json.
Node ids are generated deterministically from each node’s label, so running
the same model twice produces byte-identical trace graphs. Every builder
call validates as it goes — a bad cell reference, an unknown precedent, or a
graph past the platform’s node/edge limits raises LineageBuildError
immediately, with a live traceback into your model code.
Example: driver inputs → revenue build → margin
Section titled “Example: driver inputs → revenue build → margin”from lib.output_lineage import OutputLineageBuilder
builder = OutputLineageBuilder.from_environment(model_name="revenue_model")out = builder.output("output.json")
price = out.input_value("Unit price", 42.0, group="Pricing")units = out.input_value("Units sold", 1_000, group="Volume")revenue = out.logic_step("Revenue = price * units", price, units, value=42_000.0)out.output_node("B2", "Total revenue", revenue)
cogs = out.input_value("Cost of goods sold", 25_000.0, group="Costs")gross_profit = out.logic_step("Gross profit = revenue - COGS", revenue, cogs, value=17_000.0)margin = out.logic_step( "Gross margin = gross profit / revenue", gross_profit, revenue, value=0.405)out.output_node("C3", "Gross margin %", margin)
builder.write()Example: table-row outputs (pandas)
Section titled “Example: table-row outputs (pandas)”table_rows traces a whole rendered DataFrame table without requiring a
call per cell — one node per row, with every column in that row mapped to
it:
import pandas as pd
df = pd.DataFrame({ "product": ["Widgets", "Gadgets", "Gizmos"], "revenue": [10_000.0, 20_000.0, 15_000.0],})
pricing_driver = out.input_value("Pricing driver", 1.0, group="Pricing")out.table_rows( df, anchor="A2", # top-left cell of the rendered table row_label="product", # column to use as each row's node label row_value="revenue", # column to use as each row's node value row_precedents=lambda index, row: (pricing_driver,),)This maps A2/B2 to the Widgets row’s node, A3/B3 to Gadgets, and
A4/B4 to Gizmos — three nodes total, not six.
sum_of and weighted_sum are convenience wrappers over logic_step for
SUM/SUMPRODUCT-style calculations that auto-compute a node’s value
from its precedents’ recorded values. XNPV/XIRR-style discounted cash-flow
calculations don’t need a dedicated builder method — wire them the same way
as any other logic step, passing the assumption and each cash-flow input as
precedents.
Grouping and the five-group limit
Section titled “Grouping and the five-group limit”Tag input_value/assumption nodes with group= to cluster related
drivers (e.g. group="Pricing", group="Regions"). Explain renders at most
five input groups per output; if your traced inputs span more, the builder
automatically collapses the lowest-materiality groups into a single “Other”
node when you call builder.build()/.write(). Pass materiality=
(default 1.0) to weight which groups survive the collapse — a node
referenced directly by an output coordinate is never collapsed away.
Canonical input-cell identity powers both directions
Section titled “Canonical input-cell identity powers both directions”source_ref on input_value (and assumption) additionally lets you
identify which input cell a value came from. By default it’s only a
free-text display string:
price = out.input_value( "Unit price", 42.0, source_ref=("input_sheet", "Drivers!B2"))If your model’s own input-loading code already resolved a real
sheet_id/tab_id, pass an InputSourceRef instead of the plain tuple to
attach canonical identity. This one call is what upgrades both directions
from an inferred, best-effort match to an exact one:
- Backward (
precedents, on the output cell): lets Explain jump straight to the one exact source cell instead of a best-effort text match. - Forward (
dependents, on the input cell): letsrender_native_sheet_cell_explanationanswer “what does this cell drive?” with an exact, run-scoped list of the output cells it feeds — see What does it drive? below.
from lib.output_lineage import InputSourceRef
price = out.input_value( "Unit price", 42.0, source_ref=InputSourceRef( "input_sheet", "Drivers!B2", sheet_id="sht_abc123", tab_id="tab_drivers", cell_ref="B2" ),)ref is still required and still shown — the canonical fields are
additive. sheet_id/tab_id must be real, canonical platform ids you
already have (not the tab’s display name, not something you invent); if you
don’t have them, omit them entirely and get a best-effort, never-exact
match at explain time, same as today.
What does it drive? (forward lineage on an input sheet)
Section titled “What does it drive? (forward lineage on an input sheet)”Once an output trace records a source_ref with a canonical sheet_id/
tab_id/cell_ref (as in the InputSourceRef example above), the same
recorded identity answers the reverse question directly from the input
sheet: “what does this cell drive?” Call
render_native_sheet_cell_explanation
on the input sheet’s cell — direction defaults to dependents there, so
you don’t need to pass it explicitly:
{ "name": "render_native_sheet_cell_explanation", "arguments": { "model_name": "revenue-forecast", "sheet_id": "sht_abc123", "tab_id": "tab_drivers", "cell_ref": "B2" }}With canonical identity recorded, the response’s identity_state is
exact and dependent_output_count/dependent_model_count report a real,
run-scoped count of the output cells this input feeds — bounded to the
latest eligible run on the current branch, never inferred or invented.
Without canonical identity, identity_state degrades to partial,
inferred, or ambiguous depending on how much the legacy text match could
resolve, and the response’s warning field names this guide so the model
author or agent knows exactly how to fix it: record an InputSourceRef
with sheet_id/tab_id/cell_ref on the corresponding input_value (or
assumption) call, as shown above. There is no separate opt-in for the
forward direction — the same output-trace instrumentation drives both.
What you don’t need to do
Section titled “What you don’t need to do”.output(...)does not accept outputsheet_idortab_id; trace matching usesoutput_nameplus cell coordinate. Stored artifacts containing those fields remain readable, but new authoring code cannot emit them.- Invent node ids — one is generated from the label unless you pass
node_id=for a specific, stable id you want to reference from two different code paths. - Record a code reference by hand —
logic_stepandoutput_nodecapture your model file’s own call-site file/line automatically. - Build the “Other” grouping node yourself, or worry about the platform’s size/node/edge limits ahead of time — the builder raises immediately, at the offending call, if a graph would exceed them (512 KB serialized artifact, 25 outputs per artifact, 60 nodes and 150 edges per output). These are deliberately conservative — the artifact renders as a small DAG inside an iframe — so trace what earns its keep, not every intermediate calculation.
What’s next
Section titled “What’s next”render_native_sheet_cell_explanation— the tool that reads the trace you write here.- Agent Workflow Cookbook — the end-to-end create/run/output/dashboard workflow this guide slots into after Step 3 (run the model).
- Native Sheets formula reference — what native sheet formulas can express, if the number you’re tracing itself lives in a sheet formula rather than model code.