Run an agent asynchronously

Publish an agent, start a durable run, follow it to the answer

A run is an agent doing a piece of work on its own. You hand it an instruction, you get a handle back straight away, and the agent thinks, calls tools and produces an answer without you holding a connection open. Runs survive a deploy, so a run that takes ten minutes is as safe as one that takes two seconds.

This is the primitive to reach for when the work is a task rather than a conversation: summarise a document, reconcile two records, draft a reply, answer a question about a file someone uploaded.

Durable runs are in beta and enabled per workspace. Without the grant every run endpoint answers 402 durable_runs_not_in_plan.

Publish the agent first

This is the step most integrations miss. Every workspace that can start runs is publish-gated: an agent runs only against a configuration that has passed its gate.

POST
/v1/agents/:agent_id/publish
1curl -X POST https://api.speechify.ai/v1/agents/agent_01k7m6etzwf057j6w0zmdsgppr/publish \
2 -H "Authorization: Bearer <token>" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "gate_override_reason": "example"
6}'

The gate is keyed to a fingerprint of the agent’s configuration, so an edited agent stops running until you publish it again. Until it passes, POST /runs returns 422 agent_publish_gate_required.

Start the run

POST
/v1/agents/:agent_id/runs
1curl -X POST https://api.speechify.ai/v1/agents/agent_01jqr8x9zg5k2m3n4p5q6r7s8t/runs \
2 -H "Authorization: Bearer <token>" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "instruction": "Summarize this week'\''s open support tickets and draft a reply to the oldest."
6}'

You get 202 and a run whose status is queued. Two fields are worth setting from the first call:

  • user_identity - who the run acts for, in your own vocabulary (acme_user_42). The agent opens the run knowing what it has already learned about that person, and every tool the run calls is told who it is acting for. Omit it and the run acts for nobody in particular.
  • Idempotency-Key - a retried POST replays the first run instead of starting a second one. Runs cost money; send a key.

max_turns bounds the agent’s internal plan-act-observe cycles. It defaults to 8 and is clamped silently to your plan’s ceiling, so read input.max_turns on the returned run to see what the run actually got.

Follow it

Open the event stream and read events until run.ended.

GET
/v1/agents/:agent_id/runs/:run_id/events
1curl https://api.speechify.ai/v1/agents/agent_01jqr8x9zg5k2m3n4p5q6r7s8t/runs/arun_01jqr8x9zg5k2m3n4p5q6r7s8t/events \
2 -H "Authorization: Bearer <token>"

Four things to build for:

  1. run.ended carries the answer. You do not need a follow-up request.
  2. The server closes the stream every 4 minutes whether or not the run has finished. A closed socket does not mean the run ended - only run.ended does. A browser EventSource reconnects on its own; a hand-rolled client must reconnect and send Last-Event-ID.
  3. Every event names its own type in the payload, so you can branch on data alone: run.step.added, run.status.changed, run.ended.
  4. Ignore event types you do not recognise. More will be added.

If you would rather not hold a connection, poll GET /runs/{run_id}. Do not poll faster than once a second; the run will not finish sooner.

Webhooks are the third option, with one thing to know before you build on them: run.completed fires on success and run.failed on a failure or an expiry, but a cancelled run fires neither, and only a root run fires at all - a delegated child is covered by its parent’s event. So a webhook tells you a run finished; it does not tell you every run stopped. Reconcile against the run list if you need that.

Read the answer

output.reply is the agent’s prose. usage tells you what the run cost in tokens and wall-clock time.

A run can finish successfully without finishing its work: check incomplete_reason. max_turns_exhausted means it ran out of budget mid-task - raise max_turns or narrow the instruction.

Ask for structured output

Pass an output_schema and the conforming object comes back on output.data, with the prose still on output.reply.

1{
2 "instruction": "List every renewal date in the attached contract.",
3 "output_schema": {
4 "type": "object",
5 "required": ["renewal_dates"],
6 "properties": {
7 "renewal_dates": { "type": "array", "items": { "type": "string" } }
8 }
9 }
10}

The top level must be type: object. If the agent’s answer does not match, the platform re-asks it with the violations - at most twice, and each attempt spends a turn from max_turns, so leave headroom. A run that never conforms settles succeeded with incomplete_reason: output_schema_violation and no output.data; you never receive an object the schema refused.

Handle a run that needs a person

A tool whose approval class is require_approval parks the run instead of acting. Its status becomes requires_action and pending_action describes what it wants to do, verbatim.

POST
/v1/agents/:agent_id/runs/:run_id/submit
1curl -X POST https://api.speechify.ai/v1/agents/agent_01jqr8x9zg5k2m3n4p5q6r7s8t/runs/arun_01kr410dd3yrst4chn5kn0t78f/submit \
2 -H "Authorization: Bearer <token>" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "decision": "approve",
6 "action_key": "string"
7}'

A parked run is not terminal - the stream keeps tailing, which is how your UI learns it has something to ask. Answer within expires_at or the run applies default_decision on its own; unless the action says otherwise that window is 4 hours and that decision is deny. Overnight approvals decide themselves, so build the inbox that reaches someone in time.

What a run can reach

Beyond the tools you attach, every run is offered a small platform toolbelt:

ToolWhat it does
search_knowledgeRetrieves from the knowledge bases attached to the agent
store_read_assetReads an uploaded file out of an attached store as text
store_query / store_get / store_put / store_deleteReads and writes documents in an attached store
fetch_urlFetches a public web page and reduces it to text

Two rules to know. A tool of yours with the same name wins - name a webhook tool fetch_url and yours is the one both offered and called. And store_read_asset reads PDF, plain text, Markdown and HTML only, up to 10 MiB; anything else comes back as an error the agent reads and works around, which means the run still succeeds - without the document. Convert before you upload if the format matters.

Next

  • Add tools - what a run can be given to act with, and which actions stop for a human
  • Add memory - what an agent remembers about the person a run acts for
  • Spend limits - bounding what runs can cost