Synchronous & Asynchronous Execution
Operations that outlast a request — an LLM generation, a document ingestion, an orchestration run — are controlled by one toggle, wait:
An operation runs in the background by default and answers immediately with a handle; wait=true blocks until it settles and answers with the result.
This page is the canonical definition. Module pages describe their own handle and link here.
The toggle
| Value | Response | Use when | |
|---|---|---|---|
| Default | wait omitted or false | 202 Accepted — or 201 Created when a run is created — plus a handle to poll (see Status codes) | The work may take a while and you have somewhere to put the result: a poll loop, a webhook, a UI that refreshes |
| Blocking | wait=true | 200/201 + the settled result | A script that needs the answer on the next line, or any flow that must observe requires_action |
wait is a query parameter on the generation and ingestion endpoints, and a body field on the run endpoints (start-orchestration-run, start-eval-run, create-decision, create-inline-decision, create-workflow-migration); same name, same meaning.
- CLI
- SDK
- curl
# Background (default): returns a handle immediately
soat create-agent-generation --agent-id agent_01 \
--messages '[{"role":"user","content":"Summarize Q1"}]'
# Blocking: returns the finished generation
soat create-agent-generation --agent-id agent_01 --wait true \
--messages '[{"role":"user","content":"Summarize Q1"}]'
import { SoatClient } from '@soat/sdk';
const soat = new SoatClient({
baseUrl: 'https://api.example.com',
token: 'sk_...',
});
// Background (default): returns a handle immediately
const { data: handle, error } = await soat.agents.createAgentGeneration({
path: { agent_id: 'agent_01' },
body: { messages: [{ role: 'user', content: 'Summarize Q1' }] },
});
if (error) throw new Error(JSON.stringify(error));
// Blocking: returns the finished generation
const { data: generation, error: waitError } =
await soat.agents.createAgentGeneration({
path: { agent_id: 'agent_01' },
query: { wait: true },
body: { messages: [{ role: 'user', content: 'Summarize Q1' }] },
});
if (waitError) throw new Error(JSON.stringify(waitError));
# Background (default): returns a handle immediately
curl -X POST "$SOAT_URL/api/v1/agents/agent_01/generate" \
-H "Authorization: Bearer $SOAT_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"messages":[{"role":"user","content":"Summarize Q1"}]}'
# Blocking: returns the finished generation
curl -X POST "$SOAT_URL/api/v1/agents/agent_01/generate?wait=true" \
-H "Authorization: Bearer $SOAT_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"messages":[{"role":"user","content":"Summarize Q1"}]}'
Where it applies
What the default does not change
Backgrounding defers the slow part, never the checks. Everything that can reject a request still runs before the accepted response:
- Authentication and permissions — a caller without the IAM action gets
401/403. - Input validation — a malformed body is
400 VALIDATION_FAILED. - Resource resolution — an unknown agent, session, or conversation is
404. - Admission control — a breached quota is
429, and the agent-to-agent call-depth guard still fires. - The record write — the generation record exists before the response is written, so the
generation_idis immediately readable. It reportsrunninguntil the run reachescompletedorfailed.
An accepted response means admitted; the only failures discovered by polling are those of the model call itself.
Status codes
| Family | Background | Blocking | Why |
|---|---|---|---|
| Work on an existing resource — agent, session and conversation generation, document ingestion | 202 Accepted | 200 (201 for ingestion) | The request is accepting work; no new resource's creation to report |
| Run creation — orchestration runs, eval runs, decisions, inline decisions, playbook node runs, playbook exports, workflow migrations | 201 Created | 201 Created | A run row is created either way and is immediately readable; the mode shows in its status (queued), not the status code |
| Writes that start a playbook cascade — inputs, confirmation, upgrade | 200 | 200 | The write is done either way; wait only holds the answer for the executions it queued, and the mode shows in the case's automation_status |
Branch on wait and on the run's own status field, never on 202 alone. The uniform rule: the response always carries something you can poll, and a caller that omitted wait never receives a settled result.
A trigger firing has no wait: it always starts an eval run in the background, and the firing record names the evrun_… to poll.
Two combinations that are resolved for you
Streaming implies waiting. stream: true holds the response open, so it is a blocking call whether or not you pass wait. stream: true with ?wait=false is contradictory and returns 400 VALIDATION_FAILED.
A tool call always waits where the result is the point of the call and the work is bounded. That is create-agent-generation, generate-conversation-message, generate-session-response, create-decision, create-inline-decision (requesting a decision, inline questions) and export-playbook-case-node (exports), as builtin tools and as the MCP tools derived from the same specs: a tool call is one request returning one result, and an agent step or an orchestration tool node reading a queued record would find no answer in it. The field is not offered on the tool surface, like stream. See Agent-to-Agent Calls. Runs that can pause or run long keep wait on the tool surface, since polling them is the point: start-orchestration-run, start-eval-run, run-playbook-case-node, write-playbook-case-inputs, confirm-playbook-case-node, upgrade-playbook-case, create-workflow-migration, ingest-document and reingest-document.
Choosing a mode
wait=true when the result is the next thing you need: a shell script, a smoke test, a tutorial step, or any client-tool flow (requires_action is only observable in a blocking response).
The default when the work is detached: a UI that renders a pending state, a batch ingestion, a run inspected later.
- Webhooks deliver generation lifecycle events, so you can react to completion instead of polling. Chat with an LLM wires this up.
- Status endpoints are cheap.
GET /documents/{id}/statusreturns only lifecycle fields and advances during processing — see Polling Ingestion Status.