Every request that starts a conversation takes the same four fields for deciding what actually runs on it. They let you try an unpublished edit, run an older version, change one setting for one call, or supply a definition that lives nowhere else — without touching what live callers hear. 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.
All four of these need the editor role. They run an arbitrary prompt on an arbitrary model, paid for with your own provider keys.

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.
A draft moves, so the whole config travels with the call rather than being pointed at. That means a draft-pinned call is decided the moment you make the request: editing the draft afterwards does not change the call you just started. A number, by contrast, points at a frozen version, so it costs nothing and reads exactly like the published path.
Nothing about a call started this way is special afterwards. A "draft" call on POST /v1/calls/outbound rings a real phone and is billed like any other call.

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.
Clearing a field is an explicit 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 bare agent_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.
Full detail — reachability warnings, how a member’s handoffs resolve against the roster, and when a team beats stored handoffs — is on agent teams on a call.

Which one to reach for

Prefer agent_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 by validate_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:
  1. The merged JSON must parse as an AgentConfig (422 if not).
  2. Member names must be unique across the team.
  3. The resolved channel must match the endpoint — this endpoint runs voice agents; 'Support' resolves to a text agent.
  4. Every required variable with no default must have a value — this session needs values it was not given.
  5. Every inline code operation across the whole roster is compiled.
  6. Full publish-grade validation of every member. See validation.
Warnings do not block the call. They come back on the response as 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_version on CallTokenResponse and OutboundCallResponse says which version answered — null for an inline agent or a draft, neither of which has one.
  • GET /v1/calls/{session_id} returns the tools, starting_prompt and greeting the call actually ran, plus the frozen agent_plan, not the agent’s current draft.
  • GET /v1/conversations/{conversation_id}/sessions carries an agent_version_id per session, so you can see which version handled each stretch of a thread.
  • GET /v1/calls/batches/{batch_id} returns the campaign’s own agent_plan.
  • In the dashboard, the call list badges a call Inline, Overridden, Pinned or Team · 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.