Skip to main content
Collect metrics and traces from a running server, and enumerate its runs. The runtime records what runs. Tool and agent runs produce traces; aggregate counts and latencies roll up into metrics; a platform-side runs index enumerates every preset run. You query all three from the CLI. A monitoring backend plugin persists and serves the metrics and traces; the monitor extension traces a standalone tool call as one span.

Query metrics

Query aggregate metrics over a time range. --from and --to take an ISO instant or a relative token (7d, 30d); --granularity is hour, day, or week.

List and read traces

1

List runs

List observability runs, or download the filtered list with --export.
2

Read one trace

Get one run’s full trace, or download it as JSON with --export.
Narrow the list to one subject with the identity dimensions the runtime stamps: --user, --session, and --version filter to one person, one conversation thread, or one preset version.

List runs without the vendor

The platform keeps its own runs index: one row per registered-preset run, persisted in the platform’s own Postgres — so a deployment enumerates its runs without reaching for the monitoring backend at all. Each row carries the run id, the preset name and version, the run’s attribution user and session, its outcome, its start/end window, and the monitoring traceId as a deep link into the full trace.
Filter by --preset, --version, --user, --session, --interaction, --outcome, and a --from/--to start-time range; the same filters ride GET /api/runs as query params. Outcomes are running, success, error, parked, and aborted — a cancelled run records aborted, never error, and a crash-interrupted run stays honestly running with no end time. A run that parks on an async question and resumes later is two dispatches — two rows. The shared interactionId joins them: the parked row records the interaction it parked on, the resume’s row records the same id, and --interaction <id> returns that park’s lifecycle pair. Rows are kept forever by default. Set TAI_RUNS_INDEX_RETENTION_DAYS and call the prune on a cadence to bound the table:
Only a registered preset run lands in the index, stamped once at the outermost dispatch — a nested sub-preset adds no second row, and a raw tool call none. The index is fail-safe: a store outage never breaks a run.

One run, one trace

A run that dispatches nested work keeps a single trace. A subflow invoked as a tool and an agent driven as a flow node join the dispatching run’s trace — their spans nest inside it rather than minting a fresh, orphaned trace — so one participant interaction reads end to end as one trace. A run that parks on an async question and resumes later — days later, on another worker — rejoins its original trace. A run dispatched directly — through the sync run-tool API, a background or scheduled tool run, or the MCP tools/call edge — opens its own trace root when no trace is already open, and nests under the existing root otherwise. That decision is made once at the shared run chokepoint where the run record is registered, agnostic to what the preset wraps. Every runs-index row therefore carries a traceId deep link whenever a monitoring backend is active; with the no-op default there is no trace to link.

Send outcomes and delivery receipts

Every outbound channel send made inside a run’s trace — a flow’s notify_user send, a question delivered to a person, an inbound participant notice — records a send:{channel} span around the provider call. The span carries the recipient as its input, and the notify_user seam adds the provider’s message ids as its output; a raised delivery or input error marks it ERROR with the typed failure detail (error.kind, retryable, any retry_after). A seam that retries emits one span per attempt, stamped with retry.attempt, so “attempt 1 failed retryable, attempt 2 accepted” is visible rather than collapsed. Acceptance is not delivery. On channels with a delivery-status webhook (WhatsApp, Twilio), the provider’s later status closes the loop: the status is resolved back to the originating send and posted as a delivery_receipt event on that run’s trace, nested under its send span — a failed delivery as an ERROR event carrying the provider’s error detail. The receipt is observability only: it annotates the trace, never re-sends, and never fails the flow.
A conversation-bridge reply is covered by receipts, not spans. Bridge delivery runs in a detached task after the turn’s trace has closed, so a span there would attach to no run; the record’s own delivery ledger and its delivery-status receipts are the authoritative accepted-versus- delivered signal for bridge messages instead.
The webhook wiring and per-channel status semantics live on the channel plugin pages: channel-whatsapp and channel-twilio. A send outside any trace emits no span — a rootless span would attach to no run.

Trace a standalone tool call

A tool call outside a flow or agent has no trace of its own. Load the monitor extension and stack it onto a tool to record the call as one live span. Load the module, then attach monitor to a tool.
manifest.yml
When a flow or agent trace already owns the call, the extension suppresses its own span and just runs the tool.
Metrics and traces are served by a monitoring backend plugin, registered under monitoring_module. Without one, the runtime uses a no-op default and there is nothing to query.

Attribute a run and roll up cost

Every run’s trace can be tagged at the shared run chokepoint with the run’s own identity — the flow kwargs the operator defines per delivery, carried verbatim as a RunAttribution of tags and metadata, wired once at the shared seam so every door attributes. That lets you group cost and tokens by identity when you query traces: per-delivery attribution, with no tenant system and no per-tenant isolation — attribution only. Metrics aggregate over a time range alone — tai obs metrics takes no identity filter, so slice by identity on the trace list and roll the cost up from there. There is no billing module. Billing is a flow or a view built over these attributed traces: the platform ships the generic attribution enabler, and assembling spend into a cost view is delivery work on top of the trace data.

The identity dimensions

Beyond the per-delivery tags, the runtime stamps every conversation-driven run with generic identity at the same shared seam, and the monitoring backend maps them to its native trace dimensions:
  • user_id — person-id-first: a run with a resolved person is attributed by that person’s stable person_id, so one human’s runs group together across channels. With no person it falls back to the raw {channel}:{address} the door saw, or the bare address on the API door.
  • session_id — the resolved conversation thread, so a whole back-and-forth reads as one session.
  • tags — the conversation route as route:{name}; a registered preset dispatch adds preset:{name} and preset-v:{version} and lifts the version onto the trace’s native version dimension. Only the outermost preset stamps: a nested sub-preset never pollutes the root identity, and a draft run carries no preset stamp at all.
The run list filters by exactly those dimensions: GET /api/observability/runs takes user, session, and version params, plus any number of meta.<key>=<value> params — each one a string-equality clause on the trace’s metadata.
That identity is personal data, and it lands in your monitoring backend — so retention and erasure there are yours to govern. The platform sets no trace retention on your behalf, and forgetting a person erases the bridge’s own stores, never the monitoring backend’s traces. Set a deliberate retention policy, and honour an erasure request by deleting the subject’s traces keyed on their user_id — person-id-first attribution keeps a linked subject’s cross-channel history under one deletable key. See the monitoring-langfuse page for the backend-side duties.
Because the Claude Agent SDK’s model calls run inside the sandbox session and bypass the platform LLM seam, the claude_code adapter emits the SDK-reported usage and cost into the active trace itself — so an attributed claude_code run’s spend rolls up with everything else. The boundary: the toolbox prometheus_metrics extension records latency and count only; it is not the cost seam.

See also