Post-call analysis reads a call’s finished transcript — speech, tool calls and tool results — and writes back three things: a short summary, a verdict on whether the call achieved its purpose, and any named values you asked it to extract. It is configured on the agent, under config.analysis, and it is on by default. It is what makes a list of calls answerable. Without it you have durations and close reasons; with it you have “47 calls, 31 booked, and here are the six where the caller asked for a refund”.

What it costs and where it runs

Voice and video only. A text conversation is an idle window rather than a whole conversation, so analysing one would summarize the same thread several times over. Analysis never runs on channel: text.
Nothing analysis does can break a call. It runs after the caller has hung up and it never raises into the finalize path.

The configuration

boolean
default:"true"
Analyse calls after they end.
boolean
default:"true"
Write two or three sentences saying why the person called, what the agent did and how it ended. This is the summary shown on the calls list.
object
Judge each call. Takes one field, prompt (required, non-empty): what a successful call looks like for this agent. Leave outcome unset and calls are not judged at all — there is deliberately no inferred default, because a success rate whose definition you never wrote is a number you cannot act on.
array
Values to pull out of the transcript. Flat, and at most 25.
object
An LLM to run the analysis on instead of the agent’s own — the same { provider, model, reasoning_effort } shape the agent’s llm slot takes. Naming one means the workspace must hold that provider’s key. The priority lane is refused here: analysis runs after the caller has hung up, so it would cost more and save nobody any waiting.

One extracted field

summary, outcome and outcome_rationale are reserved names. They are built-in outputs, so a field of the same name would be unreachable; using one is a save-time error. So is defining the same name twice. Every field is nullable. A call that never mentions the value comes back null, by design — a required scalar pushes a model into inventing one, which is the failure that matters here. A value the model answers in a shape the field cannot hold also comes back null rather than costing you the rest of the reply.

Setting it

config is the whole agent definition, not a patch, so read it, change the analysis block and send it all back. Then publish — analysis runs from the published version.

The outcome is three-valued

success, failure or unknown — and unknown is not a bug. A judge that cannot say “I don’t know” invents a verdict for a wrong number, a hangup on the greeting and a call that ended before anything was settled. The model is told explicitly that a call which ended too early to tell is unknown, not a failure. Read your success rate over the calls that were actually judged, not over every call. That is why GET /v1/calls/stats reports judged_calls beside successful_calls, and why the observability summary rates success_rate over judged_sessions. See observability. outcome_rationale comes back with it: one or two sentences citing what in the transcript decided the verdict. It is the field that tells you whether your outcome.prompt says what you meant.

Reading the result

session.analysis on GET /v1/calls/{session_id}, and the identical analysis object inside a session.completed webhook.
string
none, pending, completed, failed or skipped.
string | null
Set only when status is skipped.
string | null
The prose summary, when summary was on.
string | null
success, failure or unknown. Null when no outcome was configured.
string | null
Why that verdict.
object
Your extracted values, keyed by field name. Always present; {} when no fields were configured. A value the call never established is null.

Every status

Every skip reason

A skip is not a failure, and nothing was charged for one. A turn counts if the caller said something or sent an image. A call made entirely of photos is a call where the caller communicated plenty, and it is analysed.

Trying a definition before you save it

POST /v1/calls/{session_id}/analysis/preview runs an analysis definition against one past call and returns what it found. It writes nothing — not the summary, not the outcome, not the fields — which makes it the way to tune an extraction prompt without shipping it blind and waiting a day. It is still a real model call, so it costs one. That is the trade: a few cents to find out that party_size comes back as "four" instead of 4.
A preview of a definition with enabled: false still runs. The endpoint is answering “what would this produce”, so the toggle’s order does not matter.
Pass agent_id to judge against a different agent’s prompt. Omit it and the call is judged against the definition it actually ran, which is what you want when tuning a spec for an agent that already exists. The response carries provider and model so you can see which model produced the result before committing to the spec.

Re-analysing calls that already happened

POST /v1/calls/analysis/backfill applies each call’s saved agent definition to calls that ran under an older one, or were never analysed. It is also the only retry path for a failed or a stuck pending analysis.
Running analysis twice on one call is charged twice. A re-analysis inserts a new usage row rather than replacing the old one — both model calls really happened and both spent your provider’s tokens — and then re-prices the call, so the call’s provider_cost grows by the second analysis.The platform fee does not move: a session is debited from your credit balance exactly once, ever, and re-pricing cannot change a fee that is a function of duration and channel alone. What grows is your own provider bill. Preview first.

Analysis is not userdata

Extracted fields are never merged into a call’s userdata, and this is deliberate. Merging them would let a guess overwrite a tool-confirmed value, with nothing downstream able to tell which was which. Read them separately: userdata for facts the agent acted on, analysis.fields for facts you want to report on. See userdata.

Where analysis feeds back into calls

An agent with conversation.context: "summary" opens each new call with summaries of that person’s recent conversations. Those summaries are the ones post-call analysis writes — nothing else produces them. So publishing an agent that asks for "summary" while analysis or its summary is off is refused:
Turn both on, or pick a different context. See conversation memory.

Other refusals at publish

See validation.

In the dashboard

The call detail page has a Call analysis panel: the outcome as a badge, the summary, why that verdict, and the extracted fields as a table. When there is nothing to show it says which of the reasons above applies rather than rendering an empty box. The reload control in its corner opens a dialog with both operations — Re-run uses the agent’s saved definition and writes the result, and Try current draft previews the draft you are still editing and writes nothing. Both are hidden on a call that is still running and on one whose content has been erased, because neither has a finished transcript to read.

Calls

The record analysis is written onto, transcript and all.

Webhooks

Push the summary, outcome and fields to your own systems.