Create Agent Run

Beta
Enqueue a **durable async run**: hand the agent an instruction and get a run handle back immediately (`202`, status `queued`). The run executes on its own - poll it with `getRun` or receive a completion webhook - and it survives a deploy (it is backed by a durable job, not a request socket or a short-lived store). This is the managed-agent-platform primitive: start work, walk away, come back for the result. Idempotent via `Idempotency-Key`: a retry replays the first run instead of starting a duplicate (the `run_id` is the idempotency handle). ## Publish the agent first **Every workspace granted `durable_runs_access` is publish-gated**, so this is the first thing a new integration hits. Call `POST /v1/agents/{agent_id}/publish` before the first run, and again after any change to the agent's configuration: the gate is keyed to a fingerprint of that configuration, so an edited agent stops running until it is republished. Until it passes, this endpoint returns `422 agent_publish_gate_required`. Refused with `422 tool_transport_unsupported` when one of the agent's attached MCP tools uses the legacy `sse` transport, which durable runs cannot execute; the message names the tool. Switch it to `http_streamable` or detach it, then retry. Returns `429 concurrency_limit_reached` when the workspace already has 200 runs queued or running, and the same code when the run's PROJECT is at its own `max_concurrent_runs` ceiling - only the message says which bit, so one retry path handles both. Runs execute on a shared queue, so the workspace ceiling is what keeps one workspace's backlog from delaying everyone else's next run; it is not a plan limit. The project ceiling narrows it further, which is how an application keeps one of its customers from occupying every slot the workspace has. `Retry-After` is a hint at the scale runs take, not a promise - what actually frees a slot is one of your own runs ending, so follow the ones you have with the event stream and start the next when one does. ## The project a run bills to A run is attributed to its AGENT's project, captured at creation and frozen there, so moving the agent later never moves a finished run's cost. That project's money gates apply to the run exactly as they apply to a call in it: `402 project_spend_limit_exceeded` once its `monthly_budget` is reached, and `409 project_archived` while it is archived. Both can fire for a workspace-wide key, because the project charged is the agent's rather than the key's pin. This endpoint is in beta: it is available to workspaces granted `durable_runs_access`, and every other workspace receives `402 durable_runs_not_in_plan`. A field this endpoint does not define is refused with `400 validation_failed` naming every unknown field, rather than accepted and silently dropped. Keys inside `variables` and `metadata`, and the contents of `output_schema`, are your own data rather than field names, and are never refused.

Authentication

AuthorizationBearer

Enter your API key with the Bearer prefix, e.g. ‘Bearer sk_…’.

Path parameters

agent_idstringRequired

Agent id (prefixed external id, agent_...).

Headers

Speechify-VersionstringOptional
Idempotency-KeystringOptional<=255 characters
A client-generated key (an opaque string, max 255 chars) that makes a side-effect POST safe to retry: the server runs the operation exactly once and replays the first response (its status and body) for 24 hours. Reusing a key with a different request body, or while the first request is still in flight, returns `409 idempotency_conflict`. A replayed response carries the `Idempotent-Replayed: true` header.

Request

This endpoint expects an object.
instructionstringRequired<=8000 characters

The task or goal to give the agent. The agent runs its brain against this over a short internal conversation and returns its result. The server’s limit is 8000 bytes, so a mostly non-ASCII instruction reaches it before 8000 characters.

attachmentslist of stringsOptional

The files to hand the agent, as file_... ids from POST /v1/files. The agent is told what it holds and reads one with its read_file tool: PDF, HTML, Markdown and plain text are extracted, and an image is transcribed and described by a vision model. At most 10.

Each id is checked at admission against this workspace and against user_identity: a file uploaded for one person is not readable by a run acting for another. An id that does not resolve fails the request with a 400 naming attachments - the run is never started over a file it cannot read.

variablesmap from strings to anyOptional

Per-run values that seed the agent’s flow variables (override its stored defaults). The agent’s prompt renders against the result before every step: a declared variable the run does not supply takes its default, one the run supplies takes the run’s value, and a placeholder nothing supplies renders empty. The reserved system__caller_id, system__agent_id, system__language and system__memory keys are bound by the platform. The system__* namespace and the legacy memory alias belong to the platform and are rejected with a 400 naming variables, the same rule a conversation applies: the run binds its own values there, including system__caller_id for the person it acts for.

max_turnsintegerOptional

Upper bound on the run’s internal turn budget - one turn is one plan-act-observe cycle, so a run that calls three tools uses at least four. Defaults to 8 when omitted.

Clamped to the workspace’s per-run ceiling (5 on Free, 10 on Starter, 20 on Pro, 30 on Scale, 50 on Enterprise; per-workspace overrides apply): the run’s input.max_turns echoes the budget it actually got, and GET /v1/workspaces/current/entitlements (max_run_turns) reports the ceiling up front, so plan against that rather than the value you sent. An omitted max_turns takes the default, clamped to the ceiling. On Free the ceiling is below the default, so omitting this field there yields 5, not 8.

A run that exhausts its budget settles succeeded with incomplete_reason: max_turns_exhausted and whatever answer it had reached. Schema repairs count against this budget too - see output_schema.

user_identitystringOptional<=256 characters

The person this run acts for, in your own vocabulary - the same field a conversation and a widget session take, so one workspace never has two answers to who a person is. The agent opens the run knowing what it has already learned about them, and what a run that succeeds learns from its instruction and its reply is written back under this value, exactly as a call writes memory.

Omit it to run the agent for nobody in particular, which is how a run behaves with no memory of anyone and learns nothing. Must not begin with user_, embed_ or anon_, which name identities the platform derives.

Every tool the run calls is told this value: a webhook receives it as user_identity inside the signed body, an MCP server as the Speechify-User-Identity header, and it renders in a tool’s templated URL or headers as {{system__caller_id}}. A connector you wrote can therefore look up that person’s own third-party token, which is how you integrate a system Speechify holds no credentials for.

output_schemamap from strings to anyOptional

Optional JSON Schema (2020-12) the run’s final answer must satisfy. When set, the agent answers with a JSON object, the platform validates it, and the conforming object is returned as output.data.

The top level must be type: object - an array-typed or scalar schema is refused at create with 400. At most 16 KiB.

On a mismatch the platform re-asks the agent, feeding back up to 8 of the violations. At most two repair attempts, and each one spends a turn from max_turns - so a schema-constrained run on a 5-turn ceiling has little room left for tool calls. Each attempt is journaled as an observation step whose tool is the reserved name output_schema, which a timeline renderer should expect alongside real tool names.

A run that never produces a conforming object settles succeeded with incomplete_reason: output_schema_violation and no output.data - its prose answer is still on output.reply. The platform never returns an object the schema refused.

metadatamap from strings to stringsOptional

Up to 16 arbitrary key/value pairs echoed back on the run. Your own correlation ids belong here - the platform never reads them.

Response headers

Speechify-Request-IdstringOptional
Unique identifier for this request, present on every response (2xx and non-2xx alike). If the caller sends a `Speechify-Request-Id` request header the server echoes it back (sanitized and length-capped) so one logical request can be traced end-to-end; otherwise the server generates a fresh value. Log it on every response and quote it in support requests - it is the stable handle that ties your observation to Speechify's server-side logs, and it matches the `request_id` field in the error envelope. The legacy alias `X-Request-ID` carries the same value and is still accepted on requests, until 2027-07-24. Prefer the un-prefixed name (RFC 6648).

Response

The run was accepted and queued.
idstring

Run id (prefixed external id, arun_...).

agent_idstring
The agent that ran. On a delegated child this is the member agent, not the manager.
fileslist of objects

The files this run’s own tool calls produced, in the order they were made: a picture from generate_image or edit_image, a chart from render_chart, a file run_code kept, a tool response that was not text. Each is the same object GET /v1/files/{file_id} returns, so content_path streams the bytes and source.step is the journal step of the tool call that made it, with no second lookup.

Always present. It grows while the run is going - the event stream carries each file on the step that produced it - and an empty list on a finished run means it produced nothing, never that the files are on another page. A run that failed or was cancelled after making a file still lists it. A file scoped to the run’s user_identity is listed here and is readable by a run acting for that same person, exactly as under /v1/files. A file deleted or expired since is no longer listed.

statusenum

Lifecycle: queued -> running -> succeeded | failed | canceled | expired. requires_action (a pending human approval) and canceling are transient. Terminal set: succeeded, failed, canceled, expired.

inputobject
The frozen request the run was created with.
created_atdatetime
When the run was accepted and queued.
project_idstring or nullOptionalformat: "^proj_[0-9a-hjkmnp-tv-z]{26}$"

The project this run belongs to; null when it belongs to none. Captured when the run was created, so it does not move if the agent later does.

outputobjectOptional

The run’s result, present only when status is succeeded.

errorobjectOptional

Present only when status is failed.

incomplete_reasonenumOptional

Why a non-failed run stopped short: max_turns_exhausted, budget_exhausted, or output_schema_violation (the agent never produced an object matching input.output_schema; output.reply keeps its prose and output.data is absent).

usageobjectOptional

What the run spent, present only once it has settled: wall-clock, the tokens summed across every step, the same tokens split per model, and what they cost at your plan’s rates. Written once at settle and never restated; the webhook payload carries the same values. input_tokens includes the cached reads and cached_input_tokens is that subset, so the two are never added. cost_micro_usd is an integer in millionths of a US dollar, computed from the rate card version named alongside it, and is informational: your invoice is authoritative. A delegated child run reports its own usage; the parent never sums it.

pending_actionobjectOptional

A human approval a run is durably parked on (present on AgentRun only while status is requires_action). Rendered VERBATIM for the approver - never a summary the agent wrote - so an injected agent cannot misrepresent what it is about to do. Resolve it with submitRun.

metadatamap from strings to stringsOptional
parent_run_idstringOptionalformat: "^arun_[0-9a-hjkmnp-tv-z]{26}$"

The run that delegated this one a sub-goal, absent on a root run. It is the upward half of lineage - what makes a child run of a member agent attributable to the team run that caused it (listRunChildren is the downward half).

started_atdatetimeOptional

When a worker picked the run up. Absent while queued; the gap between this and created_at is queue wait, not agent time.

ended_atdatetimeOptional

When the run settled. Present for every terminal status - succeeded, failed, canceled and expired alike - and absent otherwise.

Errors

400
Bad Request Error
401
Unauthorized Error
402
Payment Required Error
404
Not Found Error
409
Conflict Error
422
Unprocessable Entity Error
429
Too Many Requests Error