The 202
Locationpoints at the job. A caller told to poll should not have to construct the URL.Retry-Afteris a hint, not a promise — but without one, the first thing every integration writes is awhile Truewith no sleep in it.status: "processing"is the resource’s word, and it is deliberately not one of the job’s five states. The resource and the job are two things, and the job’s own status is one dereference away underjob_id.
Five states, and only these moves
completed, failed and cancelled are terminal and have no outgoing
edges. A stale retry delivered after a job finished cannot move it back to
running.
Two distinctions that deliberately are not extra states:
running → queued is a real edge and it is the retry. An internal retry does not
create a new public job — same job_id, and the attempt count is where the retry
lives.
Polling one
jobs:read.
result is populated only when status is completed.
error is present for failed and cancelled and absent otherwise —
including for a job that is queued again after a retryable failure. Reporting an
error on a job that is about to run again would tell you your work had failed when
it has not finished.
Cancelling
jobs:cancel, which is separate from jobs:read so a read-only monitoring
integration cannot stop work.
Idempotent, including on a job that has already finished.
Cancellation is a request, not a guarantee. A provider call already in
flight cannot be interrupted, so you may see
status: "running" with
cancel_requested: true for a while.There is no cancelling state, because a cancelling state promises hard
cancellation and the platform does not have it.POST /jobs creates nothing today
The endpoint is routed and demands jobs:create, and creation is allowlisted
twice: a job type must be registered and marked publicly creatable.
Nothing is currently marked publicly creatable, so this endpoint accepts no type.
POST /documents,
/extractions, /reports, /knowledge/ingestions — each of which returns a job
alongside its resource. POST /jobs exists for types that will have no domain
resource of their own.
“No such type” and “that type is not yours to create” give the same answer.
Telling them apart would let a caller enumerate the platform’s internal job
types by trying names.
Listing
jobs:read.
Not found means not found
A guessedjob_id, a job in another workspace, and an id that was never issued
all answer 404. A 403 would confirm the guess.
When the outcome is genuinely unknown
This is the part worth reading before you build a retry loop. A job step can call one of your own tools — a booking, an order, a charge. If that call times out, the platform does not know whether it landed. There are two tempting answers and both are wrong: retrying might place a second order, and recording a failure might tell a customer their order did not go through when it did. So the platform does a third thing.1
The ledger row stays pending, and stays there
Not
failed. A failed row is one a retry may re-run, and re-running a
charge is the outcome this whole mechanism exists to prevent.Nothing expires a pending row on a timer. A row disappearing is the
platform forgetting the one fact that stops it acting twice.2
The next identical call is refused without reaching your system
Not retryable. The same call with the same arguments does not go out again.
3
The customer is told it could not be confirmed
The model is instructed, in these words:
“This did not come back in time and it may or may not have gone through. Do NOT try it again. Do NOT tell them it worked and do NOT tell them it failed. Say plainly that you could not confirm it and that someone will check and come back to them.”An ambiguous outcome has to forbid three things at once: a retry, a claim of success, and a claim of failure. A model handed “it failed” will apologise for an order that exists; handed “it worked” it will confirm one that does not.
4
A human or a reconciler settles it
The only way out of
pending is somebody finding out what actually
happened — asking the far system what it has. Once settled as it happened,
a later identical call replays the recorded result rather than acting again.
Settled as it did not, the key is freed and the call may be retried.Idempotency
POST /jobs sets Idempotent-Replay: true on a replay. A replayed 202 is
the same body and the same status code — a replay is not a different outcome.
The key may also be sent in the body as idempotency_key, because a caller using
a generated client that cannot set headers is not a caller who should have to give
up idempotency. See Idempotency.
