FP&A Observe Workflows
Observe-side workflows start after a model exists. The agent is inspecting what ran, comparing persisted results, making coordinated fixes, and saving narrative analysis next to the run output.
Tool Preferences
Section titled “Tool Preferences”Use these defaults when several tools could work:
| Task | Prefer | Use instead of |
|---|---|---|
| Inspect declared inputs, outputs, dependencies | describe_model | read_file |
| Fetch one completed run output | get_run_output | get_run |
| Compare two completed runs | compare_runs | compare_branches |
| Change several related files | commit_files | multiple create_file / update_file / delete_file calls |
| Save variance narrative | create_commentary, then update_commentary | leaving analysis only in chat |
compare_branches is still the right tool when Bridge Town must execute two
branches as part of the comparison. compare_runs is better once you already
have successful run IDs.
1. Declare and Inspect Model Metadata
Section titled “1. Declare and Inspect Model Metadata”Declare static contract metadata in each model file. These lists are parsed without executing the code.
inputs = ["actuals", "plan"]outputs = ["variance_summary", "revenue_bridge"]dependencies = []
result = { "variance_summary": [ {"month": "2026-03", "actual": 128000, "plan": 120000, "variance": 8000} ], "revenue_bridge": {"new_logo": 4200, "expansion": 6100, "churn": -2300},}Inspect the contract:
{ "name": "describe_model", "arguments": { "project_name": "forecasts", "path": "model/revenue.py" }}Representative response:
{ "project": "forecasts", "path": "model/revenue.py", "sha": "abc1234def5678901234567890abcdef12345678", "inputs": ["actuals", "plan"], "outputs": ["variance_summary", "revenue_bridge"], "dependencies": [], "warnings": [], "confidence": "high"}If warnings reports invalid metadata, fix the module-level declarations.
Common causes are dynamic values, non-string entries, duplicate names, or
outputs = {...} as a runtime dict. New models should use outputs = [...]
for the contract and result = {...} for runtime values.
2. Commit a Multi-File Model Change
Section titled “2. Commit a Multi-File Model Change”Use commit_files when a change touches the model, run.py, and docs or
fixtures. It validates every operation first and creates one git commit.
{ "name": "commit_files", "arguments": { "project_name": "forecasts", "branch": "scenario/actuals-rebase", "commit_message": "feat: add actuals variance workflow", "files": [ { "action": "update", "path": "model/revenue.py", "content": "<full updated Python source>", "encoding": "text", "expected_sha": "0123456789abcdef0123456789abcdef01234567" }, { "action": "update", "path": "run.py", "content": "PIPELINE = ['revenue']\n", "encoding": "text", "expected_sha": "fedcba9876543210fedcba9876543210fedcba98" }, { "action": "create", "path": "README.md", "content": "# Actuals variance workflow\n\nCompares plan to latest actuals.\n", "encoding": "text" } ] }}Representative response:
{ "project": "forecasts", "branch": "scenario/actuals-rebase", "commit_sha": "abc1234def5678901234567890abcdef12345678", "files_changed": 3}If any file has a stale expected_sha, no files are committed. The error
payload includes each failing path, reason, current_sha, and
your_sha. Re-read or merge from current_content when present, then retry
with the new expected_sha.
3. Fetch One Run Output
Section titled “3. Fetch One Run Output”Use get_run_output when the agent needs one output from a completed run,
especially after list_runs or when the full run payload would be noisy.
{ "name": "get_run_output", "arguments": { "run_id": "66666666-6666-6666-6666-666666666666", "output_name": "variance_summary", "encoding": "auto" }}Representative response:
{ "run_id": "66666666-6666-6666-6666-666666666666", "project_name": "forecasts", "output_name": "variance_summary", "resolved_output_name": "variance_summary", "content": [ {"month": "2026-03", "actual": 128000, "plan": 120000, "variance": 8000} ], "encoding": "json", "size_bytes": 78, "truncated": false, "resolution_path": "literal"}Important limits and recovery:
- Individual outputs are returned inline up to 10 MiB. Larger outputs return an error instead of truncating.
- If
output_nameis missing, theoutput-not-foundpayload includes available names when the run output can be inspected. Use one of those names and retry. - Only successful runs have retrievable output data. For pending or failed
runs, inspect status with
get_runorlist_runsfirst.
4. Compare Forecast Versus Actuals
Section titled “4. Compare Forecast Versus Actuals”After both forecast and actuals runs have completed, compare their persisted outputs directly.
{ "name": "compare_runs", "arguments": { "base_run_id": "66666666-6666-6666-6666-666666666666", "comparison_run_id": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee", "output_name": "variance_summary" }}Representative response:
{ "base_run_id": "66666666-6666-6666-6666-666666666666", "comparison_run_id": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee", "project_name": "forecasts", "base_status": "success", "comparison_status": "success", "base_branch": "main", "comparison_branch": "scenario/actuals-rebase", "diff": [ { "metric": "variance_summary[0].variance", "base_value": 0, "comparison_value": 8000, "absolute_delta": 8000, "pct_delta": null, "significant": true } ], "summary": {"total_diff": 8000, "rows_compared": 1}, "output_name": "variance_summary"}If either run is not successful, belongs to a different project, or is not visible to the caller, the tool fails without leaking inaccessible project details. Re-run or choose accessible successful run IDs.
5. Save and Revise Commentary
Section titled “5. Save and Revise Commentary”Create commentary after the variance analysis so the interpretation is stored with the run output.
{ "name": "create_commentary", "arguments": { "run_id": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee", "output_name": "variance_summary", "text": "March revenue beat plan by 6.7%, driven by expansion ARR.", "change_note": "Initial variance readout" }}Representative response:
{ "commentary": { "commentary_id": "ffffffff-ffff-ffff-ffff-ffffffffffff", "project_name": "forecasts", "run_id": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee", "output_name": "variance_summary", "text": "March revenue beat plan by 6.7%, driven by expansion ARR.", "version": 1, "archived_at": null, "created_at": "2026-04-28T10:00:00+00:00", "updated_at": "2026-04-28T10:00:00+00:00" }}Update it after the user revises the analysis:
{ "name": "update_commentary", "arguments": { "commentary_id": "ffffffff-ffff-ffff-ffff-ffffffffffff", "text": "March revenue beat plan by 6.7%; expansion ARR offset a small churn miss.", "expected_version": 1, "change_note": "Added churn context" }}If another session already edited the row, update_commentary returns a
stale expected_version error with the actual stored version. Call
get_commentary with include_versions=true, merge the user’s intended text
with the current row, and retry with the latest version.
Use list_commentary to discover notes by project, run, or output:
{ "name": "list_commentary", "arguments": { "project_name": "forecasts", "run_id": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee", "output_name": "variance_summary" }}Access and Error Handling
Section titled “Access and Error Handling”All observe-side tools enforce tenant and project access. A no-access case is reported as not found or not visible rather than disclosing that a foreign run, project, or commentary row exists.
For deterministic recovery:
output-not-found: retryget_run_outputwith an available output name.- 10 MiB output cap: produce a smaller output or split the model output by tab.
- Stale
expected_sha: merge the current file content and retrycommit_fileswith the new SHA. - Stale
expected_version: fetch commentary versions, merge text, and retry. - Invalid metadata: change module-level metadata to literal lists of strings,
then re-run
describe_model. - No access: ask an Owner for project access or choose a run/commentary row in an accessible project.