--- title: "Observability" description: "How to monitor, inspect, and analyze workflow runs" --- Fabro captures a structured event for every significant action during a workflow run — stage starts and completions, agent tool calls, edge selections, retries, failovers, sandbox lifecycle, and more. These events power real-time monitoring, post-run analysis, and cross-run analytics. ## Event stream The event stream is the foundation of Fabro's observability. Every workflow run emits a sequence of `WorkflowRunEvent` records that are: - **Written to `progress.jsonl`** in the run's directory (one JSON object per line) - **Broadcast via SSE** to connected API clients in real time - **Logged via `tracing`** to the daily log file at `~/.fabro/logs/` ### Event types Events fall into several categories: **Run lifecycle** — bookend events for the entire run: | Event | Key fields | Description | |---|---|---| | `WorkflowRunStarted` | `name`, `run_id`, `base_sha`, `run_branch`, `goal` | Run begins | | `WorkflowRunCompleted` | `duration_ms`, `artifact_count`, `total_cost`, `status`, `usage` | Run finishes successfully | | `WorkflowRunFailed` | `error`, `duration_ms` | Run terminates with an error | **Stage lifecycle** — events for each node execution: | Event | Key fields | Description | |---|---|---| | `StageStarted` | `node_id`, `name`, `handler_type`, `attempt`, `max_attempts` | Node begins executing | | `StageCompleted` | `node_id`, `duration_ms`, `status`, `usage`, `files_touched` | Node finishes | | `StageFailed` | `node_id`, `failure`, `will_retry` | Node fails (may or may not retry) | | `StageRetrying` | `node_id`, `attempt`, `max_attempts`, `delay_ms` | Retry scheduled after failure | **Agent activity** — forwarded from the LLM agent session, prefixed with `Agent.`: | Event | Key fields | Description | |---|---|---| | `Agent.SessionStarted` | `stage` | Agent session begins | | `Agent.ToolCallStarted` | `stage`, `tool_name`, `arguments` | Agent invokes a tool | | `Agent.ToolCallCompleted` | `stage`, `tool_name`, `output`, `is_error` | Tool returns a result | | `Agent.AssistantMessage` | `stage`, `text`, `model`, `usage` | LLM responds with text | | `Agent.Error` | `stage`, `error` | Agent-level error | | `Agent.LoopDetected` | `stage` | Repeated tool call pattern detected | | `Agent.SteeringInjected` | `stage`, `text` | Human steering message injected | | `Agent.ContextWindowWarning` | `stage`, `estimated_tokens`, `context_window_size`, `usage_percent` | Token usage exceeds context window threshold | | `Agent.CompactionStarted` | `stage`, `estimated_tokens`, `context_window_size` | Context compaction triggered | | `Agent.CompactionCompleted` | `stage`, `original_turn_count`, `preserved_turn_count`, `summary_token_estimate`, `tracked_file_count` | Context compaction finished | | `Agent.LlmRetry` | `stage`, `provider`, `model`, `attempt`, `delay_secs` | LLM API call retried | | `Agent.SubAgentSpawned` | `stage`, `agent_id`, `task` | Sub-agent launched | | `Agent.SubAgentCompleted` | `stage`, `agent_id`, `success`, `turns_used` | Sub-agent finished | **Routing and control flow:** | Event | Key fields | Description | |---|---|---| | `EdgeSelected` | `from_node`, `to_node`, `label`, `condition`, `reason`, `stage_status` | Transition between nodes | | `LoopRestart` | `from_node`, `to_node` | Loop restart edge taken | | `CheckpointCompleted` | `node_id`, `git_commit_sha` (optional) | Checkpoint saved (with git SHA when git is enabled) | | `GitCommit` | `node_id`, `sha` | Git commit created | | `GitPush` | `branch`, `success` | Git push attempted | | `GitBranch` | `branch`, `sha` | Git branch created | | `GitWorktreeAdd` | `path`, `branch` | Git worktree added | | `GitWorktreeRemove` | `path` | Git worktree removed | | `GitFetch` | `branch`, `success` | Git fetch attempted | | `GitReset` | `sha` | Git reset executed | | `Failover` | `stage`, `from_provider`, `to_provider`, `error` | LLM provider failover | | `RetroStarted` | — | Retrospective generation begins | | `RetroCompleted` | `duration_ms` | Retrospective generation finished | | `RetroFailed` | `error`, `duration_ms` | Retrospective generation failed | **Parallel execution:** | Event | Key fields | Description | |---|---|---| | `ParallelStarted` | `branch_count`, `join_policy`, `error_policy` | Fan-out begins | | `ParallelBranchStarted` | `branch`, `index` | Individual branch begins | | `ParallelBranchCompleted` | `branch`, `duration_ms`, `status` | Branch finishes | | `ParallelCompleted` | `duration_ms`, `success_count`, `failure_count` | All branches done | **Human-in-the-loop:** | Event | Key fields | Description | |---|---|---| | `InterviewStarted` | `question`, `stage`, `question_type` | Human input requested | | `InterviewCompleted` | `question`, `answer`, `duration_ms` | Human responded | | `InterviewTimeout` | `question`, `stage`, `duration_ms` | Human didn't respond in time | **Sandbox and setup:** | Event | Key fields | Description | |---|---|---| | `Sandbox.Initializing` | `provider` | Sandbox creation started | | `Sandbox.Ready` | `provider`, `duration_ms`, `cpu`, `memory` | Sandbox ready | | `SetupStarted` | `command_count` | Setup commands beginning | | `SetupCommandCompleted` | `command`, `exit_code`, `duration_ms` | Single setup command finished | | `SetupFailed` | `command`, `exit_code`, `stderr` | Setup command failed | | `SshAccessReady` | `ssh_command` | SSH connection command printed | | `StallWatchdogTimeout` | `node`, `idle_seconds` | No activity for too long | ### Event envelope format Each line in `progress.jsonl` is a JSON object with a standard envelope: ```json { "ts": "2026-03-05T14:30:01.234Z", "run_id": "01JKXYZ...", "event": "Agent.ToolCallStarted", "stage": "implement", "tool_name": "shell", "arguments": {"command": "cargo test"} } ``` The `ts`, `run_id`, and `event` fields are always present. The remaining fields vary by event type. Events are flattened — nested variants like agent and sandbox events use dot notation (e.g. `Agent.ToolCallStarted`, `Sandbox.Ready`). ## Log files Fabro writes two kinds of logs: ### Run logs (`progress.jsonl`) Every run writes its event stream to `{run_dir}/progress.jsonl`. This is the primary data source for post-run analysis — [retros](/execution/retros) read it, and you can query it directly with standard tools: ```bash # Count tool calls in a run grep "ToolCallStarted" ~/.fabro/runs/01JKXYZ.../progress.jsonl | wc -l # Find all failures grep -E "StageFailed|WorkflowRunFailed" ~/.fabro/runs/01JKXYZ.../progress.jsonl | jq . # See which edges were taken grep "EdgeSelected" ~/.fabro/runs/01JKXYZ.../progress.jsonl | jq '{from: .from_node, to: .to_node}' ``` ### Live snapshot (`live.json`) During execution, Fabro also writes `live.json` — a pretty-printed copy of the most recent event. This is useful for quick status checks while a run is in progress: ```bash cat ~/.fabro/runs/01JKXYZ.../live.json ``` ### Application logs Fabro uses the `tracing` crate to write structured logs to `~/.fabro/logs/YYYY-MM-DD.log`. Control the log level with the `FABRO_LOG` environment variable: ```bash FABRO_LOG=debug fabro run workflow.fabro ``` | Level | What's logged | |---|---| | `error` | Run failures, stage failures that won't retry | | `warn` | Retries, timeouts, interview timeouts, failovers, early terminations | | `info` | Run start/complete, SSH access ready | | `debug` | Stage start/complete, edge selections, checkpoints, parallel branches, tool calls | ## Real-time monitoring ### API: Server-Sent Events When running workflows through the API server, subscribe to a live event stream via the [run events endpoint](/api-reference/runs#get-events). Each event is a JSON-serialized `WorkflowRunEvent`. The stream stays open until the run completes. ### Web UI The web frontend connects to the SSE stream automatically and displays run progress in real time — stage transitions, agent tool calls, and human gate prompts are all visible as they happen. Fabro web UI run stages showing agent conversation with tool calls ### CLI progress The CLI displays a live progress bar during execution with per-stage status, duration, and cost tracking. This is rendered to stderr so it doesn't interfere with output piping. ## Post-run analysis ### Listing runs Browse your run history with `fabro ps`: ```bash fabro ps fabro ps --workflow PlanImplement fabro ps --before 2026-03-01 fabro ps --label team=platform fabro ps --json ``` This scans `~/.fabro/runs/` and displays each run's ID, workflow name, status, and start time. Use `--json` for machine-readable output. ### Run artifacts Each run's directory contains a standard set of files: | File | Description | |---|---| | `manifest.json` | Run metadata — ID, workflow name, start time, labels | | `progress.jsonl` | Full event stream | | `live.json` | Last event snapshot (overwritten during run) | | `checkpoint.json` | Final execution state | | `retro.json` | Retrospective (if retro generation is enabled) | | `conclusion.json` | Terminal status (`completed`, `failed`, `canceled`) | ### Inspecting stages and turns The API provides endpoints for drilling into individual stages and the agent turns within them. See the [stages](/api-reference/run-internals#get-stages) and [turns](/api-reference/run-internals#get-stage-turns) API reference for details. ## Insights (SQL analytics) The Insights feature lets you run SQL queries across your run data using DuckDB. This is useful for aggregate analysis — finding slow workflows, tracking failure rates, comparing model costs, and spotting trends. Insights is managed through the [Insights API endpoints](/api-reference/insights). You can save, update, and execute queries programmatically. ### Example queries **Average run duration by workflow:** ```sql SELECT workflow_name, AVG(duration_seconds) as avg_duration, COUNT(*) as run_count FROM runs GROUP BY workflow_name ORDER BY avg_duration DESC LIMIT 20 ``` **Daily failure rate:** ```sql SELECT date_trunc('day', created_at) as day, COUNT(*) FILTER (WHERE status = 'failed') as failures, COUNT(*) as total FROM runs GROUP BY 1 ORDER BY 1 DESC LIMIT 30 ``` **Top repositories by activity:** ```sql SELECT repo, COUNT(*) as runs FROM runs GROUP BY repo ORDER BY runs DESC ``` ## Aggregate usage The API server tracks aggregate usage counters across all runs — total run count, total runtime, and per-model breakdowns of token usage and cost. See the [usage endpoint](/api-reference/run-outputs#get-usage) in the API reference. Counters reset on server restart. Fabro web UI run usage showing per-stage and per-model token and cost breakdown ## Credential redaction All event output — `progress.jsonl`, SSE streams, and log files — is automatically redacted before being written. Fabro detects and replaces patterns that look like API keys, AWS credentials, bearer tokens, and other secrets with `REDACTED`. This ensures sensitive values that appear in tool call arguments or command output are never persisted to disk.