Skip to content

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.

Use these defaults when several tools could work:

TaskPreferUse instead of
Inspect declared inputs, outputs, dependenciesdescribe_modelread_file
Fetch one completed run outputget_run_outputget_run
Compare two completed runscompare_runscompare_branches
Change several related filescommit_filesmultiple create_file / update_file / delete_file calls
Save variance narrativecreate_commentary, then update_commentaryleaving 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.

Declare static contract metadata in each model file. These lists are parsed without executing the code.

model/revenue.py
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.

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.

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_name is missing, the output-not-found payload 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_run or list_runs first.

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.

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"
}
}

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: retry get_run_output with 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 retry commit_files with 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.