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); 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
| Operation | Handle returned by default | Poll it with |
|---|---|---|
POST /agents/{agent_id}/generate | generation_id, trace_id | GET /generations/{generation_id} |
POST /sessions/{session_id}/generate | session_id | GET /conversations/{conversation_id}/messages |
POST /conversations/{conversation_id}/generate | conversation_id | GET /conversations/{conversation_id}/messages |
POST /documents/ingest and POST /documents/{document_id}/ingest | the document, in status: pending | GET /documents/{document_id}/status |
POST /orchestration-runs | the run, in status: queued | GET /orchestration-runs/{orchestration_run_id} |
POST /evals/{eval_id}/runs | the run, in status: queued | GET /evals/{eval_id}/runs/{eval_run_id} |
POST /deciders/{decider_id}/decisions | the decision, in status: queued | GET /decisions/{decision_id} |
POST /decisions | the decision, in status: queued | GET /decisions/{decision_id} |
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 reportsin_progressuntil 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 | 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 |
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 and create-inline-decision (requesting a decision, inline questions), 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, 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.