Skip to main content
Your service takes a request, hands the work to an agent, and answers immediately. Minutes later the agent finishes, your code collects the result, and whoever asked gets told. The whole problem is that your HTTP handler cannot wait, and an agent takes minutes. So the work is split in two:

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

Three things doing real work: 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

That is the whole round trip: the agent called 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 simultaneous POSTs. Two different 429s come back, and only one is worth retrying:
Telling them apart on status alone is the mistake: a worker that retries 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:
The 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_exceeded queues rather than spins
  • Killing the worker mid-job and restarting it resumes cleanly

Next