The four endpoints that take them:
agent_id / agent / agent_version / agent_override and agent_team are
mutually exclusive: one agent, or a team of them. agent_version requires
agent_id — an inline definition has no versions. Sending both agent_id and
agent is a 422.agent_version — run a version that is not live
Omit it and the call runs whatever is published. Give it a number and it runs
that frozen version. Give it "draft" and it runs the current unpublished
working copy — validated and tool-pinned at request time, and refused with the
publish errors if it does not hold together.
"draft" is how you try an edit without publishing over what live callers are
hearing. Save the draft, mint a web-call token with agent_version: "draft",
and talk to it in the browser; the number on the wall keeps answering with the
published version.
The TypeScript and Python samples below assume a configured client — see
SDKs.
agent_override — change one thing for one call
An AgentOverride is the same field tree as an AgentConfig with every field
optional. It is generated from AgentConfig, so it never lags behind it, and it
merges by four rules:
Deep merge is what makes a small override small.
{"tts": {"voice": "priya"}}
changes the voice and leaves the provider, the model and the speed alone. With
shallow replace it would have reset provider and model to their defaults,
and the agent would have answered in a different voice from a different vendor
with nothing saying so.
Three field types are the exception and replace whole even though they look
like objects: ToolSelection (tools, and each of the three lifecycle hooks),
McpSelection (mcps) and HandoffTarget (handoffs). Each is a reference,
and half a reference is not a smaller reference — it is a different one. So
{"on_enter": {"tool_id": "…"}} attaches that tool at its currently published
version, pinned when the request is made — it does not inherit the base’s
tool_version. Send them complete; where the model requires a field — a HandoffTarget needs name and
description — a partial one is a 422 naming it.
channel cannot be overridden at all. The endpoint decides the channel —
/calls/token runs voice or video, /calls/outbound runs voice,
/conversations runs text — so an override that changed it is always a mistake.
Sending it is a 422. A full inline agent still declares its own channel, and
that one is checked against the endpoint.
null:
An override pins the version
A call whose base is a stored agent and which carries an override pins the version it merged against, because the merged result was validated against that exact base. A republish underneath would leave it unvalidated. A bareagent_id
with no override follows whatever is published, as it always has.
This matters most on the two surfaces that re-resolve: an outbound batch
re-reads the agent on every dispatcher pass, and a text conversation on every
message.
agent_override.vars is not the same as vars
Both appear on the same request and they do different jobs.
They do not conflict: the override decides what is declared, the top-level bag
decides what it is worth. The top-level bag is one bag for the whole session and
reaches every agent on it, including a handoff target that was never in the
plan. See variables.
agent — a whole definition for this call
Send a complete AgentConfig and the call runs it. Nothing is stored: there is
no row, no id, no editor, no version history and no diff. The definition is
frozen into the call’s plan so you can read back later what actually ran, and
that is all.
It may carry inline tools and inline MCP servers, which are compiled and
validated for that call and thrown away with it — unlike an inline tool on a
stored agent, which is materialized into a real, published tool.
agent_override layers onto an inline agent too, though there is rarely a
reason — you already control every field.
agent_team — a cast for one call
agent_team is {"members": [{name, …the same four fields…}]}, up to ten
members. members[0] answers, and the rest are reachable through
handoffs that name them. Each member is described exactly as
a single-agent call is: an agent_id with an optional agent_version and
agent_override, or a whole inline agent. The member’s name wins over the
stored agent’s own, and is what handoff edges reference and the transcript
records.
Which one to reach for
Preferagent_id plus userdata. Most of what looks like a per-call
difference is really a per-person fact, and userdata is what carries those:
{{userdata.name}} in the prompt and greeting is that person’s name, with one
published agent behind every call. See userdata.
Reach for agent_override when one call genuinely differs from the published
agent — a language, a voice, a greeting that only makes sense for this campaign.
Reach for agent or agent_team only for definitions generated per request that
would never be reused. An agent that exists in the workspace has an editor, a
test panel, version history, a diff, AgentCoPilot and reuse
across calls. An inline one has none of those and disappears with the call.
Composing inline agents by habit ends with a workspace that has no agents in it
and no way to see what your callers are actually hearing.
The merged result is validated
Whatever you send, the resolved config is checked byvalidate_agent_draft(..., for_publish=True) — the same function that guards
publishing. There is one validator and no second one to drift.
So a bad override is a 400 at create, naming the problem, rather than a call
that connects and then fails. The response body is the standard envelope with
message: "the resolved agent config is invalid"; on a team, each error is
prefixed with the member’s name.
Checked in this order, before anything is compiled and before any provider is
called:
- The merged JSON must parse as an
AgentConfig(422if not). - Member names must be unique across the team.
- The resolved
channelmust match the endpoint —this endpoint runs voice agents; 'Support' resolves to a text agent. - Every
requiredvariable with no default must have a value —this session needs values it was not given. - Every inline
codeoperation across the whole roster is compiled. - Full publish-grade validation of every member. See validation.
warnings, on
CallTokenResponse, OutboundCallResponse and the batch — including team-only
ones such as nothing hands off to 'Escalations', so it will never take a turn on this call.
Two hard limits: at most 10 team members, and a resolved plan of at most
256 KB (a 413). A batch multiplies its plan by its recipient count, which
is why it has a number where a single call would not notice.
Reading back what ran
A call that ran exactly the published agent stores no plan at all. Anything else freezes what it ran, so the record stays readable after the agent has moved on:agent_versiononCallTokenResponseandOutboundCallResponsesays which version answered —nullfor an inline agent or a draft, neither of which has one.GET /v1/calls/{session_id}returns thetools,starting_promptandgreetingthe call actually ran, plus the frozenagent_plan, not the agent’s current draft.GET /v1/conversations/{conversation_id}/sessionscarries anagent_version_idper session, so you can see which version handled each stretch of a thread.GET /v1/calls/batches/{batch_id}returns the campaign’s ownagent_plan.- In the dashboard, the call list badges a call
Inline,Overridden,PinnedorTeam · 3, and the call detail shows the config it ran.
Versions
Draft and published, publishing, rollback, and tool pinning.
Agent teams on a call
Casts,
members[0], and how handoffs resolve inside one.Validation
Every refusal the merged config can hit, and the fix.