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.--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 monitoringtraceId as a deep link into the full trace.
--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 MCPtools/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’snotify_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 themonitor 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
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 aRunAttribution 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 stableperson_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 asroute:{name}; a registered preset dispatch addspreset:{name}andpreset-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.
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.
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
- Live operations — status, retry, and runtime state.
- The MCP-citizen surface — progress and structured output that traces capture.
- CLI reference — the full
tai obs,tai traces, andtai runssurface.

