Skip to main content
Voice agents are probabilistic. A prompt edit that reads better can quietly stop the agent from asking for a booking reference, and nothing fails — the call just goes worse. Evals let you state what an agent must do and have TurnCall check it, on every change, before a caller finds out. An eval scenario is one saved test. A run executes it against a target over N iterations. There are two kinds:

Scripted

A fixed conversation with per-turn expectations. Answers “at this point, did the agent make the right next decision?”

Simulation

A persona, a goal and success criteria — an LLM plays the caller and improvises. Answers “by the end, did it reach the right outcome?”

Your first scenario

A scenario’s definition is a conversation and what you expect back.
Then run it:
The response is 202 Accepted with a batch_id: a dedicated worker executes runs, never the API process, so eval load can never become dead air on a live call.
Assert content, not just the event. {"event": "llm_response"} on its own can pass with the agent’s LLM completely broken — when a provider rejects the model name, an empty response is still emitted. Always pair the event with text_contains, matches, or an eval: criterion.

Targets: what you run against

An agent row is an immutable version, so pinning by id means your scenario stops testing production the moment someone publishes the next version. agent_name follows the publish. inline runs a configuration that was never published — which is how you check a prompt change before shipping it, and how you point a scenario at sandbox tools.

Tool mocking, and why it’s on by default

A scenario that exercises “book the appointment” against an agent with a real webhook books a real appointment, on every iteration. So scenarios carry mocks:
A mocked tool returns your canned response and the real webhook is never called — webhook tools, MCP tools and built-ins alike. Under mock_only, the default, a tool call with no mock is refused and the run ends as errored naming the tool, rather than executing. live is the opt-in for agents whose tools are read-only lookups. Mocks belong to the scenario, not the run: “the booking succeeds, does it report the confirmation correctly?” and “the booking fails, does it avoid claiming success?” are two different tests.

Text or audio

modality: "text" skips STT and TTS — fast, silent, cheap, and what you run on every pull request. modality: "audio" synthesizes the caller’s turns into the agent’s real STT and judges a transcription of the agent’s real TTS. Audio is what covers recognition, synthesis and turn timing; text cannot see any of it.
Audio runs use local models for the caller’s voice and the judge’s transcription, downloaded on first use. Budget for that the first time.

Simulations

Give a persona and a goal instead of turns, and an LLM improvises the call:
The judge rules on success once over the whole conversation; metrics are scored per agent turn, and one below its min_score fails the iteration. A persona never says the same thing twice, so use iterations — one run is an anecdote.

In CI

Exit 0 only if every run passed. A failure exits 1; an error — a judge outage, a pipeline that died — exits 2, so a broken checker never reads as a broken agent.
turncall eval show <run_id> prints a run’s transcript with its verdicts. Scenario files are the API request body unchanged — turncall eval run scenarios/*.json --agent-name support runs them without storing anything.

Start from a real conversation

Evals only find what someone thought to test. Turn a conversation that already happened into a draft — a voice call:
or a text conversation — SMS, chat or WhatsApp:
Both produce the same thing. The caller’s utterances become turns, the agent’s replies become expectations, and every tool the agent called becomes both an expectation and a mock seeded with what that tool actually returned — so the draft is safe to run straight away. save: false is the default and returns the draft without storing it; true stores it. A session names its agent directly, so from-session takes its default_target from the session. A call may have run an inline agent with no agent row at all, so from-call reads the call record instead. To convert a WebRTC call the browser just made, read the call id from the signalling answer — POST /v1/webrtc/connect returns it beside sdp and type. See WebRTC.
It is a draft. The conversation is faithful, but the expectations assert whatever the agent did that day, mistakes included. Trim each text_contains to the part that actually matters before trusting a green run.

Results

call.ended has no equivalent here — subscribe to eval.run.started and eval.run.completed instead. The completed event is comprehensive: status, counts, every iteration’s transcript and failures, and the configuration snapshots. The envelope carries eval_run_id beside call_id and session_id. Aggregate a whole batch without pulling transcripts:
A run reports passed_count/failed_count out of iterations — there is no score. errored is neither: it means the harness could not complete, so it counts toward no rate at all.

What evals cannot see

Worth knowing before you trust a green suite. An eval swaps only the transport, so everything inside it is invisible: Everything the pipeline builds is covered, which is where the regressions that shipped have actually lived: STT and TTS construction, provider model names, VAD and turn-detection wiring, the tool bridge, knowledge retrieval. The judge is an LLM and is not deterministic. Use binary criteria, use iterations, and treat a single flip as a signal to look rather than a verdict.