Get a run's current state
Returns the run’s current state — status, iterations used so far, token usage, submitted inference job IDs, the model’s final answer (if completed), pending tool calls (if requires_action), or the error (if failed).
Polling
Runs are asynchronous. After POST /agents/conversations/{id}/runs you poll this
endpoint until you see a terminal status (completed, requires_action, or
failed). A reasonable polling cadence:
- Start with 2-second polls — most simple runs complete in a single iteration (5–15 seconds wall-clock).
- Back off to 4–8 seconds for longer-running runs (multi-iteration tool loops).
- Hard-cap at maybe 5 minutes — if a run hasn’t terminated by then either something
is wrong (check
submitted_inference_job_idsin the data plane’sjobstable) or the workflow timed out and will landfailedshortly.
Which fields are populated when
status | Populated fields |
|---|---|
pending | id, conversation_id, client_op_id, effective_config, started_at |
running | above + submitted_inference_job_ids (one per iteration completed) |
completed | above + iterations_used, usage, exactly one of final_text / final_structured_output, completed_at |
requires_action | above + iterations_used, usage, pending_tool_calls, completed_at |
failed | above + error, possibly partial iterations_used/usage, completed_at |
effective_config is always populated — it’s the fully-merged config used for
this run (conversation defaults + this run’s config_override). Helpful for
confirming the model saw what you expected.
Run vs. inference iteration
Each run runs N iterations, where N ≤ effective_config.max_iterations. Each
iteration is one inference job submitted to the data plane (one Bedrock or Cortex
API call). submitted_inference_job_ids carries the platform’s jobs.id for each
iteration — use those to drill into the raw model request/response in the data
plane’s logs / job table.
iterations_used and submitted_inference_job_ids.length match for any terminal
state.
Error structure
On status: "failed" the error field carries:
type— opaque incident code (e.g.AgentLoopMaxIterationsExceeded,AgentLoopInvalidEffectiveConfig,UnknownTool). Stable across versions, used for log correlation.message— human-readable detail.titleanddocs_url— caller-facing presentation looked up from the platform’s error catalog. Absent if the API doesn’t recognize thetype(usually because the workflow workers have been upgraded ahead of the API); in that case rely ontypeandmessage.
Note: this endpoint always returns 200 for a known run — even one with status: "failed". The HTTP-level error path (RFC 7807) only applies when the call itself
fails (token invalid, run not found, etc.).
Scope: per-user, not per-company
Runs are scoped to the (company_id, user_id) pair that created them. Returns
404 for “run does not exist”, “run belongs to a different company”, and “run was
created by a different user in the same company” alike — the API does not
distinguish them to avoid leaking existence.
Permission
agent_conversations resource with read verb.
Example: completed run
{
"id": "82eb9cb7-619e-46bb-ac1c-8a32b0112c1c",
"conversation_id": "bc2505b7-068d-44ed-8055-a6f6ffe54ab1",
"company_id": 1,
"user_id": 407,
"client_op_id": "696b022c-e86d-4b3e-b6d7-ceb7eb1a495e",
"status": "completed",
"tool_choice": null,
"effective_config": { "...": "..." },
"iterations_used": 1,
"usage": { "prompt_tokens": 169, "completion_tokens": 8, "total_tokens": 177 },
"submitted_inference_job_ids": ["e94c6eab-019f-4e96-bb47-049fa60e7988"],
"pending_tool_calls": [],
"final_text": "4",
"final_structured_output": null,
"error": null,
"started_at": "2026-05-18T13:21:30.355180Z",
"completed_at": "2026-05-18T13:21:51.983952Z"
}
Example: completed structured-output run
{
"id": "8dda70ab-9f81-4613-a3c9-68b3b584e82d",
"status": "completed",
"iterations_used": 1,
"final_text": null,
"final_structured_output": {
"event": {
"type": "login",
"user_id": "u-123",
"session_duration_seconds": 1800
}
},
...
}
Example: requires_action run
{
"id": "19082f49-0280-4e32-a8b4-7df8dea9dbed",
"status": "requires_action",
"iterations_used": 1,
"pending_tool_calls": [{
"tool_use_id": "tooluse_DWXPKZ50JDGib5GmShyUgJ",
"name": "confirm_booking",
"arguments": { "proposed_slot": "tomorrow afternoon" }
}],
"final_text": null,
"final_structured_output": null,
...
}
Resume by posting a new run with payload.kind: "tool_outputs" carrying the same
tool_use_id.
Example: failed run
{
"id": "...",
"status": "failed",
"iterations_used": null,
"usage": null,
"final_text": null,
"final_structured_output": null,
"error": {
"type": "AgentLoopMaxIterationsExceeded",
"message": "max iterations exceeded after 5 iterations",
"title": "Max Iterations Exceeded",
"docs_url": "https://docs.narrative.io/reference/architecture/agent-conversations/errors/max-iterations-exceeded"
},
...
}
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Path Parameters
The run's UUID.
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"
Response
Run state. status indicates which lifecycle position the run is in; populated
fields vary accordingly (see description).
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.

