Skip to main content
When an agent or prompt node finishes, Fabro captures its response text and produces an outcome that feeds into context, transition logic, and downstream nodes. Fabro also tracks every file change per stage, offloads large outputs into content-addressed blob storage, and automatically collects test artifacts like screenshots and reports.

Response capture

After an agent or prompt node completes, Fabro captures the full response text and persists it to stages/{rank:03}-{node_id}@{visit}/response.md in metadata snapshots and fabro dump output. It also writes the final outcome (status, context updates, routing directives) to stages/{rank:03}-{node_id}@{visit}/status.json.

Context updates

Every agent and prompt node sets three context keys from its response:
KeyValue
last_stageThe node ID of the stage that just completed
last_responseThe response text, truncated to the first 200 characters
response.{node_id}The full response text
These keys are available to downstream nodes via the context. The last_response key provides a quick preview, while response.{node_id} preserves the complete output for nodes that need it.
plan -> implement -> review -> exit

// In the review node's prompt, you can reference prior outputs:
// The context key response.plan contains the full plan text
// The context key response.implement contains the full implementation

Routing directives

Agent and prompt nodes can influence which edge is taken after they complete by including a JSON object with routing fields in their response. Fabro scans the LLM output for the last JSON object containing any recognized routing field:
{
  "outcome": "failed",
  "failure_reason": "tests failed",
  "preferred_next_label": "fix",
  "suggested_next_ids": ["implement", "review"],
  "context_updates": { "tests_passed": true, "coverage": 85 }
}
FieldEffect
outcomeSets the node outcome: succeeded, failed, partially_succeeded, or skipped
failure_reasonWhen outcome is failed, provides a structured failure message
preferred_next_labelMatched against edge labels to select the next node
suggested_next_idsOrdered list of preferred target node IDs
context_updatesKey-value pairs merged into the run context

How extraction works

Fabro finds all balanced {...} JSON objects in the response text, parses each one, and uses the last object that contains at least one recognized field (preferred_next_label, outcome, failure_reason, suggested_next_ids, context_updates). The JSON can appear anywhere in the response — inside a fenced code block, inline with natural language, or at the end.
I've reviewed the code and found several issues that need fixing.
The test coverage is below the threshold.

{"preferred_next_label": "fix", "context_updates": {"coverage": 72}}
JSON objects without recognized fields are ignored.

Validated routing output

Set output_schema="routing" on an agent or prompt node to require Fabro’s built-in routing directive schema:
review [
  shape=tab,
  prompt="Review the implementation and return routing JSON.",
  output_schema="routing",
  output_retries=2
]
With output_schema="routing", the routing JSON must be an object with at least one recognized routing field (preferred_next_label, outcome, failure_reason, suggested_next_ids, or context_updates) and those fields must have the expected types. Malformed routing JSON fails validation instead of being silently ignored. Validated routing uses the same reverse scan as normal routing extraction: Fabro validates the last parsable JSON object that contains a recognized routing field. If a routing object is present but malformed or has invalid field types, Fabro repairs that response instead of falling through to a file fallback. Fabro repairs invalid structured output inside the same LLM context before failing the node. For prompt nodes, Fabro appends the invalid assistant response and a corrective user message to the same message list. For agent nodes using the API backend, Fabro sends the corrective message to the same live agent session. output_retries controls these repair turns and defaults to 2; output_retries=0 validates once and fails without a repair turn. Negative values are treated as 0. These repair turns are separate from workflow max_retries and do not consume node retry attempts.

Routing fallback sources

Agent nodes can provide routing directives through fallback files. Fabro checks sources in this order:
OrderSource
1The final response text
2status.json in the sandbox working directory
3The last file touched by the agent
This fallback chain applies to normal routing extraction and to output_schema="routing". For validated routing, Fabro only advances to the next source when the current source has no JSON object or no object with recognized routing fields. If the current source contains malformed routing JSON or valid JSON with wrong routing field types, validation fails and Fabro starts the repair loop instead. Prompt nodes do not use file fallbacks; they validate or extract routing directives from the response text only. If no source provides routing directives, the transition falls through to condition matching, unconditional edges, or weight-based tiebreaking as described in Transitions.

Instructing the agent

Fabro does not automatically instruct agents to emit routing JSON. You must include instructions in your prompt:
review [
    label="Review",
    shape=tab,
    prompt="Review the implementation. If changes are needed, \
        respond with: {\"preferred_next_label\": \"fix\"}. \
        If everything looks good, respond with: \
        {\"preferred_next_label\": \"approve\"}."
]

review -> fix     [label="Fix"]
review -> approve [label="Approve"]

Custom structured outputs

Agent and prompt nodes can also validate their final JSON object against a JSON Schema file:
audit [
  shape=tab,
  prompt="Audit the change and return JSON that matches the schema.",
  output_schema="@schemas/audit-result.schema.json",
  output_retries=2
]
output_schema="@path/to/schema.json" uses the same workflow file-reference rules as prompt files: the schema is loaded relative to the workflow file and inlined before execution. The final JSON object in the LLM response is validated with jsonschema. Custom schema validation only reads the response text. It does not fall back to status.json or the last file touched by the agent. When custom schema validation succeeds, Fabro stores the parsed JSON value in context at:
KeyValue
output.{node_id}The parsed JSON object that matched the custom schema
For example, node audit writes its parsed custom output to output.audit. Fabro still stores the raw response text at response.audit. If custom schema validation fails, Fabro sends concise validation feedback to the same prompt conversation or agent session and asks for corrected JSON. After output_retries repair turns are exhausted, the node fails terminally with output schema validation failed after N repair attempt(s).
Structured output validation currently applies to agent and prompt nodes. backend="acp" does not support output_schema in this release. Custom schemas update output.{node_id}; routing schemas update routing fields and context_updates instead.

Output logging

Fabro writes several files per stage to stages/{rank:03}-{node_id}@{visit}/ in metadata snapshots and fabro dump output:
FileContents
prompt.mdThe assembled prompt (preamble + expanded prompt text)
response.mdThe full LLM response text
status.jsonThe outcome: status, context updates, routing directives, usage stats
These files are written for every agent and prompt node execution, including retries. Use them for debugging unexpected agent behavior or verifying that routing directives were extracted correctly.

File tracking

Fabro records which files each stage touches. When an agent calls write_file or edit_file, Fabro tracks the file path. When using a CLI-based agent backend, Fabro snapshots the Git working tree before and after execution and diffs the results. The tracked paths are stored as files_touched on the stage outcome:
{
  "status": "succeeded",
  "files_touched": ["src/main.rs", "tests/api_test.rs", "README.md"]
}

Where files_touched appears

LocationHow it’s used
StageCompleted eventEmitted with files_touched in the event stream and surfaced by fabro events / exported event streams
PreamblesListed under each completed stage so downstream agents know what changed
status.jsonWritten to the stage’s logs directory after each node completes

How tracking works

For the API backend, Fabro subscribes to agent session events. When a ToolCallStarted event fires for write_file or edit_file, Fabro records the file_path argument as pending. When the corresponding ToolCallCompleted arrives without an error, the path is confirmed as touched. Failed tool calls are discarded. For the CLI and ACP backends, Fabro takes a different approach: it runs git diff --name-only and git ls-files --others --exclude-standard before and after the external agent session, then computes the difference. Any files that appear in the “after” snapshot but not “before” are recorded as touched.

Artifact offloading

When a stage produces a large context value — an LLM response or any context update — Fabro automatically offloads it into a global content-addressed blob store instead of leaving the full value inline in durable context. Command output is streamed to a stage log file while the command runs, then finalized into a durable blob ref after completion.

How offloading works

After each node completes, Fabro checks every context update. If the serialized JSON of a value exceeds 100KB, it is stored once by SHA-256 hash and replaced with a durable blob ref. Command output is always stored this way after completion, even when it is small or empty:
response.plan  -->  blob://sha256/2cf24dba5fb0...
command.output -->  blob://sha256/a4f3c1d9c2e1...
Non-command values under 100KB remain inline. Checkpoints, checkpoint-completed events, forks, and resumes persist these blob:// refs, not host-specific file paths.

Preamble rendering

When Fabro builds a preamble for a downstream stage, it first materializes any blob refs into execution-local files and then renders a reference instead of inlining the full content:
## Completed stages
- **plan**: success
  - Model: claude-sonnet-4-5, 12.4k tokens in / 3.2k out
  - Files: src/main.rs, tests/api_test.rs
  - Response: See: /path/to/runtime/blobs/<blob_id>.json
- **test**: success
  - Script: `cargo test 2>&1 || true`
  - Stdout: See: /path/to/runtime/blobs/<blob_id>.json
This keeps preambles concise while still giving agents a path to read the full output if needed.

Git storage

Large offloaded context values are not stored on the Git metadata branch. The metadata branch keeps checkpoint JSON and stage metadata; blob payloads live in the durable blob store and are referenced by blob://sha256/.... Captured stage artifacts such as screenshots, videos, reports, and traces still use the artifact store and metadata export paths described below.

Remote sandbox syncing

For remote sandboxes (Docker, Daytona), execution-time file access happens inside the sandbox filesystem.
  • Blob refs are materialized into {working_directory}/.fabro/blobs/{blob_id}.json
  • Explicit non-blob file:// refs keep the existing copy-on-demand behavior and are copied into {working_directory}/.fabro/artifacts/{filename} when needed
In both cases, downstream handlers and agents continue to consume ordinary file:// pointers during execution.
For local sandboxes, syncing is a no-op since the agent can already access the host filesystem directly.

Automatic asset capture

After each node executes a command, Fabro automatically scans the sandbox for test artifacts — screenshots, videos, reports, and traces — and copies any new or changed files to the run’s directory. This happens without any agent or workflow configuration.

How asset capture works

  1. Before the command runs, Fabro takes a baseline snapshot of known artifact paths in the sandbox
  2. After the command completes, Fabro re-scans and diffs against the baseline
  3. Files that are new or modified since the command started are downloaded to the stage’s artifact directory
Only files modified after the command started are collected. Files that match the baseline fingerprint (same size and mtime) are skipped. Individual files over 10 MB and total collections over 50 MB are also skipped.

What gets captured

Fabro looks for files inside these directories:
DirectoryTypical contents
playwright-report/Playwright HTML reports
test-results/Playwright screenshots, videos, and traces
cypress/videos/Cypress test recordings
cypress/screenshots/Cypress failure screenshots
And files matching these filename patterns anywhere in the tree:
PatternTypical contents
junit*.xmlJUnit XML test reports
*.trace.zipPlaywright trace archives
Tool caches and dependency directories (node_modules, .cache/ms-playwright, .yarn/cache, etc.) are excluded from scanning.

Asset storage layout

Collected assets are written to the run’s directory, organized by node and retry attempt:
~/.fabro/scratch/{run_id}/
  cache/
    artifacts/
      files/
        {node_slug}/
          retry_1/
            test-results/
              screenshot.png
              video.webm

Observability

Outputs and artifacts appear in several observability surfaces:
SurfaceWhat’s reported
StageCompleted eventfiles_touched list for the stage
WorkflowRunCompleted eventartifact_count — total number of offloaded artifacts across the run
Web UIRun stage output, stage artifacts, and downloadable artifact files
PreamblesFile list and artifact pointer references for completed stages
Stage logsstatus.json in each stage’s run directory contains the full outcome including files_touched