Before you start
A token and a model, as in the quickstart. Nothing here needs a public HTTPS endpoint — the worker polls. Webhooks are the upgrade, and they come last.Dispatch: the half that must be fast
Idempotency-Key — your own job id, so a retried dispatch returns the
same session instead of creating a second one. Keys live 24 hours;
idempotency.md.
metadata — how you match a result back to the request that caused it. It
is yours; nothing interprets it. Without it you are joining on session id
alone, which is fine until you need to find every job for one tenant.
timeout_seconds — how long the agent waits for your function before
giving up. Set it to what your service actually does, not to a default.
The instruction against questions is not optional here. A batch worker has
no one to ask. Without that line an agent that hits ambiguity parks on a
question, which your code cannot answer — see
Require human approval.
Collect: the half that runs elsewhere
lookup_order, this worker
answered it over the API, and the agent used the answer in its reply.
Two ways a result comes back, and you usually want both. Prose is the
newest assistant message in /items; files are artifacts. A question like the
one above produces only prose, so an empty artifact list here is correct — not a
symptom. Ask for a table or a CSV and you get both.
Two subtleties, both of which cost an afternoon if you meet them by accident.
idle does not mean finished. It means no turn is running. A session
parked on your function is requires_action, and one waiting to be collected is
idle — so check required_actions before you conclude anything.
completed does not mean fetchable. Artifacts publish after the turn
settles; turn.artifacts leaving pending is the real signal. The loop above
waits for it.
Never put an exception in error
error goes to the model, and the model may quote it to your end user. A stack
trace is an efficient way to put your database host in a prompt. Send a fixed
sentence; log the real one.
Running many at once
An organization runs a limited number of sessions concurrently, so a hundred jobs is a queue you drain — not a hundred simultaneousPOSTs.
Two different 429s come back, and only one is worth retrying:
project_limit_exceeded hammers a wall until someone notices the bill.
Finished sessions hold their slot until deleted. DELETE /v1/sessions/{session_id}
when you have collected the artifacts you need — they outlive the session.
Replacing the poll with webhooks
Once you have a public HTTPS endpoint, stop polling. One subscription serves every session:secret comes back once. Verify every delivery’s signature before trusting
it — webhooks.md.
Deliveries are at-least-once and may arrive out of order. So the handler
does not act on the payload; it reads the session, exactly as the loop above
does. That is why moving to webhooks changes what wakes your worker and nothing
else — and why turn.completed is safe to act on directly: it is sent after
artifacts are published.
When it goes wrong
You have it working when
- Your endpoint returns in milliseconds and never blocks on the agent
- A retried dispatch with the same job id yields one session
- The worker answers function calls and collects artifacts without human help
- A
project_limit_exceededqueues rather than spins - Killing the worker mid-job and restarting it resumes cleanly
Next
- Require human approval before the agent acts — when a person must decide
- Stream the agent’s progress into your UI — showing the work, not a spinner
- limits.md — every ceiling, with its number