Start a new run on the conversation
Inserts a row in agent_runs, kicks off the workflow that drives the agent loop, and
returns immediately. The response has status: "pending" — poll
GET /agents/runs/{id} to track progress.
Runs are asynchronous. This endpoint does not wait for the model to reply. Typical end-to-end latency is 3–10 seconds for a single-iteration run, longer for multi-iteration tool-use loops (60–120 seconds is common for 6–8 iterations against an MCP server).
Required fields
client_op_id— UUID you generate. Used as the idempotency key, scoped to(conversation_id, client_op_id). Re-sending the same value returns the original run row unchanged. Always generate a fresh UUID for a fresh logical request; reusing one is only safe if the second call is a retry of the first.expected_version— the conversation’s current version, read fromGET /agents/conversations/{id}just before this call. If it doesn’t match at run-creation time, you get a 409 Version Conflict. Recovery: refetch the conversation, re-readversion, retry the run with the fresh value.payload—kind: "user_message"to start a fresh user turn, orkind: "tool_outputs"to resume a previously-paused (requires_action) run.
Optional fields
tool_choice— bias the model toward (or away from) using tools on iteration 1 of this run. Three values:auto,any,specific_tool(see schema). Applies only to iteration 1 of this run; iteration 2+ are back onauto. To re-force a specific tool on the next run, settool_choiceagain on its request body.config_override— sparse override of the conversation’sdefaults. Any field you set replaces the conversation-level value for this single run. The two list fields (mcp_servers,tools) are wholesale-replaced, not merged. Thesystem_promptcannot be overridden.
Run lifecycle (state diagram)
pending → running → completed ← model produced a final answer
↘ requires_action ← model called a caller-declared tool
↘ failed ← non-recoverable error
You stop polling on any of the three terminal states. For requires_action, the run
can be resumed by posting a new run on the same conversation with
payload.kind: "tool_outputs". For completed and failed, the run is done.
Examples
Example 1 — fresh user message, no tools
{
"client_op_id": "696b022c-e86d-4b3e-b6d7-ceb7eb1a495e",
"expected_version": 0,
"payload": { "kind": "user_message", "text": "What is 2 + 2?" }
}
Returns:
{
"id": "82eb9cb7-619e-46bb-ac1c-8a32b0112c1c",
"status": "pending",
"started_at": "2026-05-18T13:21:30.355180Z",
"submitted_inference_job_ids": [],
"pending_tool_calls": [],
"effective_config": { ... },
...
}
Then poll GET /agents/runs/{id} until status: completed. Final
final_text: "4".
Example 2 — force a specific tool, override max_iterations
{
"client_op_id": "c8aef867-eca6-46f7-8a99-e22dd0ee6062",
"expected_version": 0,
"payload": { "kind": "user_message", "text": "Find me a meeting time for tomorrow afternoon." },
"tool_choice": { "kind": "specific_tool", "name": "confirm_booking" },
"config_override": { "max_iterations": 10 }
}
The model is forced to call confirm_booking on iteration 1. Since confirm_booking
is a caller-declared tool (dash-free name, listed in tools[]), the run terminates
with status: requires_action after one iteration; pending_tool_calls contains the
model’s proposed arguments.
To pin an MCP-resolved tool instead, include the mcp_alias field:
"tool_choice": { "kind": "specific_tool", "mcp_alias": "docs", "name": "search_kb" }
Example 3 — resume a paused run
First, refetch the conversation to get the new version (it has advanced because the paused run’s assistant turn was persisted):
curl "$API/agents/conversations/{id}" -H "Authorization: Bearer $TOKEN"
# → { "version": 2, ... }
Then post the resume:
{
"client_op_id": "7fb6795c-60a2-4cf9-a0db-0cc645279704",
"expected_version": 2,
"payload": {
"kind": "tool_outputs",
"outputs": [{
"tool_use_id": "tooluse_DWXPKZ50JDGib5GmShyUgJ",
"content": "User confirmed the proposed slot.",
"is_error": false
}]
}
}
The model sees the tool result and continues; this run usually terminates
completed after one iteration with a contextual final answer.
Idempotency
Re-sending the same body with the same client_op_id returns the original
agent_runs row verbatim. Idempotency is on (conversation_id, client_op_id):
- Same
client_op_id, same conversation → same run row returned, no new workflow started. - Same
client_op_id, different conversation → new run, new workflow.
Use a fresh UUID per logical request, never per retry of an in-flight request.
Common error responses
| Status | type slug | Cause | Recovery |
|---|---|---|---|
| 400 | /invalid-tool-alias | config_override.mcp_servers[] has a bad alias | Fix the alias |
| 400 | /tool-name-too-long | config_override introduced a wire name > 64 chars | Shorten alias or tool name |
| 400 | /unknown-tool-use-id | tool_outputs references an id not on the latest assistant turn | Refetch the prior run’s pending_tool_calls |
| 400 | /not-a-client-tool-call | tool_outputs references a server-resolved call | Filter to client-side ids only |
| 400 | /incomplete-tool-outputs | Missing or extra outputs[] entries | Match the pending set exactly |
| 400 | /no-assistant-turn | tool_outputs payload on a conversation with no assistant turn yet | Use user_message payload instead |
| 404 | /conversation-not-found | Conversation doesn’t exist, is in another company, or was created by a different user | Verify the id; conversations are scoped to the bearer-token’s (company_id, user_id) |
| 409 | /version-conflict | expected_version mismatch | Refetch the conversation, retry |
| 500 | /workflow-start-failed | Platform couldn’t reach the workflow engine | Retry with the same client_op_id |
Permission
agent_conversations resource with write verb.
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Path Parameters
The conversation's UUID.
UUID identifying a conversation. Returned from POST /agents/conversations and used
in every other agent endpoint that operates on this conversation.
"bc2505b7-068d-44ed-8055-a6f6ffe54ab1"
Body
Start a new run on the conversation.
Required fields:
client_op_id— your-generated UUID for idempotency.expected_version— the conversation's current version, read just before this call. Triggers a 409 Version Conflict if the version moved.payload— the run's input.
Optional fields:
tool_choice— biases tool selection on iteration 1 of this run only.config_override— sparse override of the conversation'sdefaultsfor this run.
A UUID you generate for each POST .../runs call. Acts as an idempotency key:
re-sending the same client_op_id against the same conversation returns the
original run row unchanged. This lets you retry network blips, request timeouts, etc.
without accidentally starting a second workflow.
Scope: (conversation_id, client_op_id) is the uniqueness key. You can reuse the
same client_op_id across different conversations; the platform doesn't deduplicate
cross-conversation.
"696b022c-e86d-4b3e-b6d7-ceb7eb1a495e"
Monotonic per-conversation counter, bumped by 1 (or more) every time a successful run appends messages. Two roles:
- Delta cursor for
GET .../messages?since=N— fetch only messages withsequence_no > N. - Compare-and-swap token for
POST .../runs— setexpected_versionto the conversation's current head; if it doesn't match at run-creation time, you get a Version Conflict (HTTP 409).
Always start a new run cycle by reading the current version from
GET /agents/conversations/{id} rather than caching a value from earlier.
x >= 04
The run's input. Discriminated by kind:
user_message— fresh user turn.tool_outputs— resuming a paused run with the tool answers.
- Option 1
- Option 2
Per-run policy that biases the model toward (or away from) using tools on the first
iteration of the run. From iteration 2 onward the model is back on {"kind": "auto"} regardless.
Per-run, not per-conversation: every new run picks its own tool_choice. To
re-force a particular tool on every follow-up run (for example, always asking the user
for confirmation), set it on each POST .../runs body.
Three shapes, discriminated by kind:
- Auto
- Any
- SpecificTool
{ "kind": "auto" }Sparse override applied to a single run. Any field set here replaces the
corresponding defaults field for that run only. Fields not set inherit from
defaults.
Two list fields (mcp_servers, tools) replace the whole list when set, not
merge — if you provide them in the override, the conversation defaults' lists are
ignored entirely for that run.
system_prompt is not in this object — it's pinned at conversation creation and
cannot be overridden.
Response
Idempotent retry — the same client_op_id was posted before, and the
original run row is returned verbatim. May carry a non-pending status if the
original run has already advanced.
Run state as returned by POST /agents/conversations/{id}/runs (Accepted, 202) and
GET /agents/runs/{id} (OK, 200). Which optional fields are populated depends on
status:
completed→iterations_used,usage,submitted_inference_job_ids, and exactly one offinal_text/final_structured_output. The two are mutually exclusive: text-mode runs (nooutput_format_schemasupplied) populatefinal_text; structured-mode runs populatefinal_structured_outputwith the caller-schema-conforming JSON.requires_action→pending_tool_calls,iterations_used,usage,submitted_inference_job_ids.failed→error, possibly partialiterations_used/usage/submitted_inference_job_ids.pending/running→ only the at-creation fields (id,conversation_id,started_at, etc.) plussubmitted_inference_job_idsif any iterations have started.
UUID identifying a single run. Returned by POST /agents/conversations/{id}/runs and
used in GET /agents/runs/{id} to poll progress.
"82eb9cb7-619e-46bb-ac1c-8a32b0112c1c"
UUID identifying a conversation. Returned from POST /agents/conversations and used
in every other agent endpoint that operates on this conversation.
"bc2505b7-068d-44ed-8055-a6f6ffe54ab1"
1
407
A UUID you generate for each POST .../runs call. Acts as an idempotency key:
re-sending the same client_op_id against the same conversation returns the
original run row unchanged. This lets you retry network blips, request timeouts, etc.
without accidentally starting a second workflow.
Scope: (conversation_id, client_op_id) is the uniqueness key. You can reuse the
same client_op_id across different conversations; the platform doesn't deduplicate
cross-conversation.
"696b022c-e86d-4b3e-b6d7-ceb7eb1a495e"
Where the run is in its lifecycle. Three terminal states (completed,
requires_action, failed) — you stop polling when you see any of them. The two
in-flight states (pending, running) mean "keep polling."
pending— run row inserted by the API but the workflow hasn't picked it up yet. Usually < 1 second; if a run stays here for tens of seconds the workflow workers may be down.running— workflow is mid-execution. One "running" you see roughly corresponds to one inference iteration; you might cycle through this state several times for a multi-iteration tool-use loop.completed— model produced a final answer. Exactly one offinal_text/final_structured_outputis populated depending on whether the caller supplied anoutput_format_schemaon the conversation defaults or run override.requires_action— model called a client-side tool.pending_tool_callsis populated; resume by posting a new run withpayload.kind: tool_outputs.failed— non-recoverable error.error.type,error.message,error.title,error.docs_urlare populated. See the error catalog for the per-type meaning. A run that was deliberately cancelled also lands here, tagged witherror.type: AgentLoopCancelledso it is distinguishable from a genuine failure.
pending, running, completed, requires_action, failed The fully-merged config that was used to run this turn — conversation defaults
with config_override applied. Echoed back so you can see exactly what the model
saw, especially helpful when debugging unexpected behavior caused by overrides.
Inference job IDs (one per iteration) for cross-system tracing. Each ID is a row
in the platform's jobs table — use it to pull the raw model request/response
if you need to debug a specific iteration.
Tool calls the run is waiting for the caller to answer. Empty unless status is
requires_action.
"2026-05-18T13:21:30.355180Z"
The conversation's current, still-mutating state (e.g. its auto-generated name).
Populated on every read, independent of status.
Per-run policy that biases the model toward (or away from) using tools on the first
iteration of the run. From iteration 2 onward the model is back on {"kind": "auto"} regardless.
Per-run, not per-conversation: every new run picks its own tool_choice. To
re-force a particular tool on every follow-up run (for example, always asking the user
for confirmation), set it on each POST .../runs body.
Three shapes, discriminated by kind:
- Auto
- Any
- SpecificTool
{ "kind": "auto" }Number of inference iterations this run completed. Populated on terminal states.
Equal to submitted_inference_job_ids.length for the same run.
x >= 06
Token usage totals across every inference iteration in this run. Used for billing
and capacity planning. Note that prompt_tokens grows with each iteration because
each round re-sends the full conversation history to the model.
The model's final answer in plain text. Populated on status: completed when the
caller did NOT supply an output_format_schema (text mode). Mutually exclusive with
final_structured_output.
The model's final answer as a JSON object conforming to the caller's
output_format_schema. Populated on status: completed when the caller supplied an
output_format_schema on the conversation defaults or run override (structured
mode). The shape is the caller's schema verbatim — no top-level text field is
required or implied. Mutually exclusive with final_text.
Populated only on status: failed.
When the run reached a terminal status. Null while in pending or running.

