curl --request GET \
--url https://api-dev.narrative.io/agents/runs/{id} \
--header 'Authorization: Bearer <token>'import requests
url = "https://api-dev.narrative.io/agents/runs/{id}"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api-dev.narrative.io/agents/runs/{id}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api-dev.narrative.io/agents/runs/{id}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api-dev.narrative.io/agents/runs/{id}"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api-dev.narrative.io/agents/runs/{id}")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api-dev.narrative.io/agents/runs/{id}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"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": "pending",
"effective_config": {
"model": "anthropic.claude-opus-4.6",
"data_plane_id": "f79cbdae-4848-47ca-95e8-69588364d185",
"execution_cluster": "shared",
"compute_pool_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"max_iterations": 8,
"max_tokens": 2048,
"temperature": 0,
"thinking": {
"type": "effort",
"level": "high"
},
"output_format_schema": {
"type": "object",
"additionalProperties": false,
"required": [
"text"
],
"properties": {
"text": {
"type": "string"
}
}
},
"mcp_servers": [],
"tools": []
},
"submitted_inference_job_ids": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"pending_tool_calls": [
{
"tool_use_id": "tooluse_DWXPKZ50JDGib5GmShyUgJ",
"name": "confirm_booking",
"arguments": {
"proposed_slot": "tomorrow afternoon"
}
}
],
"started_at": "2026-05-18T13:21:30.355180Z",
"live": {
"messages": [
{
"turn_index": 0,
"role": "user",
"content_blocks": [
{
"type": "text",
"text": "What is 2 + 2?"
}
],
"final_answer_from": []
}
],
"current_name": "Tomorrow Afternoon Meeting"
},
"tool_choice": {
"kind": "auto"
},
"iterations_used": 6,
"usage": {
"prompt_tokens": 14842,
"completion_tokens": 510,
"total_tokens": 15352
},
"final_text": "<string>",
"final_structured_output": "<unknown>",
"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"
},
"completed_at": "2023-11-07T05:31:56Z"
}{
"error": "Unauthorized",
"error_description": "You are not authorized to use this endpoint."
}{
"type": "https://docs.narrative.io/reference/architecture/agent-conversations/errors/invalid-tool-alias",
"title": "Invalid Tool Alias",
"status": 400,
"detail": "invalid tool aliases (must match ^[a-zA-Z][a-zA-Z0-9]{0,7}$): with_underscore",
"instance": null,
"log_id": "1f6e5d4c-3b2a-1098-7654-3210fedcba98",
"debug": null
}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. Atypethe catalog doesn’t list (usually because the workflow workers were upgraded ahead of the API) gets the genericUnexpected Errortitle and docs page; rely ontypeandmessagein that case.
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",
"live": { "current_name": "Simple arithmetic", "messages": [] }
}
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"
},
...
}
curl --request GET \
--url https://api-dev.narrative.io/agents/runs/{id} \
--header 'Authorization: Bearer <token>'import requests
url = "https://api-dev.narrative.io/agents/runs/{id}"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api-dev.narrative.io/agents/runs/{id}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api-dev.narrative.io/agents/runs/{id}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api-dev.narrative.io/agents/runs/{id}"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api-dev.narrative.io/agents/runs/{id}")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api-dev.narrative.io/agents/runs/{id}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"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": "pending",
"effective_config": {
"model": "anthropic.claude-opus-4.6",
"data_plane_id": "f79cbdae-4848-47ca-95e8-69588364d185",
"execution_cluster": "shared",
"compute_pool_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"max_iterations": 8,
"max_tokens": 2048,
"temperature": 0,
"thinking": {
"type": "effort",
"level": "high"
},
"output_format_schema": {
"type": "object",
"additionalProperties": false,
"required": [
"text"
],
"properties": {
"text": {
"type": "string"
}
}
},
"mcp_servers": [],
"tools": []
},
"submitted_inference_job_ids": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"pending_tool_calls": [
{
"tool_use_id": "tooluse_DWXPKZ50JDGib5GmShyUgJ",
"name": "confirm_booking",
"arguments": {
"proposed_slot": "tomorrow afternoon"
}
}
],
"started_at": "2026-05-18T13:21:30.355180Z",
"live": {
"messages": [
{
"turn_index": 0,
"role": "user",
"content_blocks": [
{
"type": "text",
"text": "What is 2 + 2?"
}
],
"final_answer_from": []
}
],
"current_name": "Tomorrow Afternoon Meeting"
},
"tool_choice": {
"kind": "auto"
},
"iterations_used": 6,
"usage": {
"prompt_tokens": 14842,
"completion_tokens": 510,
"total_tokens": 15352
},
"final_text": "<string>",
"final_structured_output": "<unknown>",
"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"
},
"completed_at": "2023-11-07T05:31:56Z"
}{
"error": "Unauthorized",
"error_description": "You are not authorized to use this endpoint."
}{
"type": "https://docs.narrative.io/reference/architecture/agent-conversations/errors/invalid-tool-alias",
"title": "Invalid Tool Alias",
"status": 400,
"detail": "invalid tool aliases (must match ^[a-zA-Z][a-zA-Z0-9]{0,7}$): with_underscore",
"instance": null,
"log_id": "1f6e5d4c-3b2a-1098-7654-3210fedcba98",
"debug": null
}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 returns the original run row unchanged. This lets
you retry network blips, request timeouts, etc. without accidentally starting a
second workflow.
Scope: (company_id, user_id, client_op_id) is the uniqueness key. One
client_op_id means one run for you, on whichever conversation it first landed —
posting it again against a different conversation returns that first run rather than
starting a new one.
"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.
Show child attributes
Show child attributes
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.
Show child attributes
Show child attributes
"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.
Show child attributes
Show child attributes
The tool_choice the run was started with, or null if it had none.
- Auto
- Any
- SpecificTool
Show child attributes
Show child attributes
{ "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.
Show child attributes
Show child attributes
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.
Show child attributes
Show child attributes
When the run reached a terminal status. Null while in pending or running.
Was this page helpful?

