Appearance
Errors
Branch on code, not on the message
Errors that need distinguishing carry a machine-readable code:
json
{ "detail": { "code": "active_version_changed", "message": "…", "current_active_sha": "83efc…" } }Messages are written for humans and may be reworded. code values are part of the contract and will not change under you.
The publish endpoint returns two different 409s and several 422s — code is how you tell them apart. Matching on the shape of the body works until the day it does not.
Every error
| Status | code | Cause | What to do |
|---|---|---|---|
401 | — | no key, or invalid or revoked | check the key. Not on the read endpoint, which returns 404 |
403 | — | read-scoped key on a publish | request a write key |
403 | — | owner lacks the approver tag on a Manual review project | ask the Instructions Manager team. Do not retry |
403 | — | not a member of that project or organisation (listing) | ask to be added to the project |
404 | — | unknown project or prompt, no active version, or no access | verify the ids. Deliberately vague — no existence leak |
409 | active_version_changed | stale expected_active_sha | re-fetch and reconcile. Never auto-retry |
409 | llm_not_configured | ai_assisted project with no AI provider | operator issue. Do not retry |
409 | — (available_environments in body) | read only: not released to that environment | pick a released environment |
413 | content_too_large | over the size cap | shrink the content. Body carries limit_bytes and content_bytes |
422 | review_suggestions_open | the reviewer agent has findings | fix the content, or re-send with acknowledge_review |
422 | — | missing expected_active_sha without force | send the sha, or force |
422 | — | content mismatches kind, unknown kind, blank message | fix the payload |
422 | — | both or neither scope on a listing | pass exactly one |
422 | skill_scan_blocked | skill security scan blocked the content | read scan.findings. Not overridable |
503 | review_unavailable | the reviewer agent could not run | nothing published. Retry with backoff |
5xx | — | server-side | retry with backoff |
Retry policy
Retry: 5xx, and 503 review_unavailable. Those are transient, and in the 503 case nothing was published.
Never auto-retry: 401, 403, 404, 409, 413, 422. None of them resolve on their own, and two are actively harmful:
- Retrying
409 active_version_changedoverwrites another person's edit a moment later — the exact failure the guard exists to prevent. - Retrying
422 review_suggestions_opengets you the same answer.
The two that trip people up
DANGER
404 from the read endpoint does not mean "not found". Five causes — bad key, unknown project, unknown prompt, no active version, no access — return byte-identical bodies, so the endpoint cannot be used to enumerate what exists. Check the key first; it is the most common cause and the least obvious.
DANGER
A 403 on publish is a configuration change, not a bug in your code. The key's owner lost the approver tag, left the project, or the key was revoked. Nothing on your side changed. Alert an operator rather than looping — see Authentication.
Handling them
try:
publish(content, message, expected_active_sha=base_sha)
except 422 where code == 'review_suggestions_open':
show detail.suggestions to the user
if they revise: go back and edit
if they override: publish(..., acknowledge_review=detail.review_id)
except 409 where code == 'active_version_changed':
reload(detail.current_active_sha)
tell the user the prompt changed underneath them
except 409 where code == 'llm_not_configured':
alert an operator — the project needs an AI provider. Do not retry.
except 503 where code == 'review_unavailable':
retry with backoff — nothing was published
except 413:
shrink the content; detail names the limit
except 403:
alert an operator; do not retryNote the shape: the override path calls the same publish function with one extra argument. Resist building a separate "force publish" path — that is how an override becomes the default.