# Errors Every status and machine code the WAMP Cloud API returns, what causes it, and whether retrying helps — including the conflict semantics of idempotent PUT and what 410 Gone means. Source: https://docs.cloud.vampikez.fun/reference/errors/ Match on the machine code, never on the status alone and never on a message string. Codes are stable; statuses are shared by unrelated conditions and messages are not part of the contract. ## The error body Almost every error is one field: ```json { "error": "cloud_run_in_progress" } ``` Seven codes add a second field: ```json { "error": "insufficient_scope", "requiredScope": "wamp.cloud.turns:submit" } ``` ```json { "error": "cloud_rate_limit_exceeded", "retryAfterSeconds": 3 } ``` ```json { "error": "invalid_request", "issues": [{ "path": ["model"], "message": "model must be omitted for a runtime-managed agent" }] } ``` ```json { "error": "invalid_path_parameter", "parameter": "sessionId" } ``` ```json { "error": "unsupported_media_type", "mediaType": "application/x-www-form-urlencoded" } ``` ```json { "error": "method_not_allowed", "allow": ["GET", "HEAD", "OPTIONS", "PUT"] } ``` ```json { "error": "cloud_interaction_conflict", "outcome": { "interactionId": "toolu_01K2mWpR7yLd4bQx9sTfNvHe", "choice": "deny", "status": "resolved", "resolution": "denied" } } ``` `outcome` is present only when another approval decision already won. A closed or expired approval with no answer returns the same conflict code without it. ### Which field a 400 carries, and why there are three codes The three ways a request can be wrong before it reaches any resource have three different fixes, so they have three codes. Branch on the code, not on which field happens to be present. | Code | What is wrong | Second field | Where you fix it | |---|---|---|---| | `malformed_request_body` | The bytes were not JSON | — | Your serializer | | `invalid_path_parameter` | A path segment is not a valid id | `parameter` | The URL you built | | `invalid_request` | A field of the body or the query was refused | `issues` | The payload | `issues` has one entry per refused field. `path` is the field path with the outermost segment first — `["initialTurn", "id"]` for a nested field, and an integer segment is an array index. `message` is a short reason meant for a human to read; it is not part of the contract, so branch on the code and the `path`. Those two keys are the whole entry, and the WAMP Account API sends the same two under the same field name. A path segment is refused **on its own and first**: if the id in the URL and the body are both wrong you get `invalid_path_parameter`, because a body cannot be judged against a resource that is not addressable. Fix the URL and send again to see what the body says. Treat the body as extensible: new fields may appear, so read the ones you need rather than validating the object shape. Every `/v1` failure puts a machine code in `error` — including the `413` the body parser answers, which is `{ "error": "request_entity_too_large" }`. The only answers without a code are ones no WAMP server wrote: a `502`, `503` or `504` from the proxy in front of the API, with an HTML body or none. [Releases and answers with no code](#releases-and-answers-with-no-code) covers it. ### Read the delay from the header, not the body Five domain codes — `cloud_workspace_starting`, `cloud_session_finalizing`, `cloud_run_in_progress`, `cloud_agent_catalog_unavailable` and `cloud_extension_catalog_unavailable` — carry a delay as a **`Retry-After` header only**. It is always `2`. There is no `retryAfterSeconds` in their bodies. `503 service_unavailable`, the answer while the API restarts or while its database cannot keep up, also carries `Retry-After: 2` as a header only. `429 cloud_rate_limit_exceeded` carries the delay **both** as a `Retry-After` header and as a `retryAfterSeconds` body field. So read the **header** if you want one code path for all of them. A client that only looks at the body will silently fall back to its own default on every code where the server actually told it what to do. ## Transport, authentication and validation | Status | Code | Cause | Retry? | |---|---|---|---| | `400` | `invalid_request` | A body or query parameter failed validation — including an unknown property or an unknown query parameter. `issues` names each refused field | No. Fix the request | | `400` | `invalid_path_parameter` | A path segment is not a valid id. `parameter` names it | No. Fix the URL | | `400` | `malformed_request_body` | The request body was not parseable JSON, so no field was ever read | No. Fix your serializer | | `400` | `invalid_input` | A malformed `grantId` path parameter, or a value that cleared a route's character bound and then failed the byte bound behind it — a publication `body` or a merge `commitMessage` over 65 536 UTF-8 bytes | No. Fix the request | | `400` | `environment_invalid` | An environment document, secret name or value, cache path, extension, archive or operation body failed validation; `issues[].path` names the field. An archive refusal says which ingest check failed; an extension names a catalog slug that is not listed or an archive the environment does not own | No. Fix the request | | `400` | `audio_file_required` | A transcription request omitted its multipart `file` part | No. Attach one audio file | | `400` | `invalid_audio_duration` | `durationMs` is missing, fractional, below 100 ms, or above 10 minutes | No. Fix the request | | `400` | `invalid_audio_upload` | The multipart body is malformed or contains more than one file or field | No. Fix the request | | `400` | `cloud_repository_branch_not_found` | `source.baseBranch` does not resolve in the granted repository. The caller's own input, which is why it is a `400`: no change of authority makes a branch that does not exist exist | No. Send a branch that is there, or omit `baseBranch` for the default | | `400` | `invalid_organization` | A malformed or repeated `X-Wamp-Organization` header | No | | `400` | `organization_header_not_allowed` | `X-Wamp-Organization` sent together with a bearer | No. Remove the header | | `401` | `bearer_credential_required` | No `Authorization: Bearer ` header on a `/v1` path | No | | `401` | `invalid_or_expired_credential` | Token expired, revoked, wrong audience, or introspection rejected it | Mint a new token, retry **once** | | `403` | `cloud_access_denied` | The credential lacks `wamp.cloud.access` | No. The installation is not consented for Cloud | | `403` | `environment_forbidden` | The actor cannot read or manage organization environments, or an installation cannot select one | No. Obtain the required capability | | `403` | `insufficient_scope` | The credential lacks the operation's capability, named in `requiredScope` | No. Re-consent is required | | `403` | `installation_principal_required` | An installation-only operation was called with a human credential: repository grants, a GitHub-sourced Session create, runtime release probes, or `GET /v1/sessions/stream` | No | | `403` | `cloud_repository_grant_required` | Nobody has granted this repository to this installation, the grant was revoked, or the `grantId` + `repositoryId` pair does not match | No. An administrator configures access; `GET /v1/repositories` returns the exact usable pairs | | `403` | `cloud_github_connection_required` | A browser user chose the personal-repository path but their own GitHub connection is missing or expired | No. That person reconnects GitHub; organization grants do not depend on personal OAuth | | `403` | `cloud_repository_unavailable` | The grant is intact and the repository is no longer reachable through the GitHub App installation — removed from it, deleted, or renamed | No. Somebody restores it on the GitHub side | | `403` | `cloud_repository_permission_denied` | The App reaches the repository and GitHub refuses the operation: a missing App repository permission, or an organization policy such as unsatisfied SAML SSO | No. One action on the GitHub side | | `403` | `cloud_compute_not_configured` | The organization has no compute it may run on, or withdrew the compute it owns | No. Ask an administrator to add compute | | `409` | `cloud_compute_outdated` | The organization's own machine is connected and refuses this release's sandbox image — it is still holding an older one | No. Ask an administrator to update that machine's images | | `404` | `unknown_endpoint` | The path is not part of this API. Nothing was looked up, which is what separates it from the `404`s below | No. Fix the URL | | `405` | `method_not_allowed` | The path exists and does not answer this method. `Allow` and the `allow` field name the ones it does — Sessions are created with `PUT`, and there is no `POST /v1/sessions` | No. Use a listed method | | `406` | `representation_not_acceptable` | `Accept` did not match on `GET /v1/openapi.json` or `GET /v1/docs` | No. Fix `Accept` | | `415` | `unsupported_media_type` | The body carried a media type this API does not parse, or none at all. Bodies are read under `application/json` and RFC 6839 `application/*+json` only; `mediaType` echoes what you sent. Nothing of the body was read, so no field of it is named | No. Fix your `Content-Type` | | `415` | `unsupported_audio_format` | The transcription file is not FLAC, MP3, MP4/M4A, Ogg, WAV, or WebM audio | No. Encode to a supported format | | `413` | `request_entity_too_large` | JSON body over 1 MB, or an environment archive over 32 MiB | No. Send less | | `413` | `audio_too_large` | A transcription file is over 20 MiB | No. Record a shorter note | | `422` | `invalid_audio` | The declared audio format is supported but the provider cannot decode the bytes | No. Re-encode or record again | | `422` | `empty_transcript` | The audio decoded successfully but contained no transcribable speech | No. Record speech and retry | | `429` | `cloud_rate_limit_exceeded` | Your authority's token bucket is empty | **Yes**, after `Retry-After` | | `429` | `cloud_auth_rate_limit_exceeded` | The per-IP pre-authentication bucket is empty | **Yes**, after `Retry-After` | | `500` | `internal_error` | Unexpected server fault | Cautiously, with backoff | | `503` | `identity_service_unavailable` | Credential introspection is down. `Retry-After: 2` | **Yes** | | `503` | `event_stream_unavailable` | The Event wake listener has not proven delivery or the process has reached its 256-stream cap. `Retry-After: 2` | **Yes**, after `Retry-After` | | `503` | `cloud_rate_limit_unavailable` | The rate limiter faulted and fails closed. `Retry-After: 1` | **Yes** | | `503` | `cloud_extension_catalog_unavailable` | An environment write adds a catalog extension, and the extension catalog could not be reached to check it. Writes that add no catalog item are unaffected. `Retry-After: 2` | **Yes**, after `Retry-After` | | `503` | `cloud_agent_catalog_unavailable` | Agent/model capability discovery could not answer: it is down, or the bounded number of calls this server may have outstanding against it is spent — a wide concurrent fan-out reaches the second before it spends its own rate budget. `Retry-After: 2` | **Yes**, after `Retry-After`; alert the operator if persistent | | `503` | `transcription_unavailable` | The speech-to-text provider or Account relay is unavailable | **Yes**, with backoff | | `504` | `transcription_timeout` | Transcription did not complete within the bounded request window | **Yes**, once; record a shorter note if persistent | The five repository-authority codes above name five different parties' next action, and they are newer than the collapsed code they replace. Deployments roll independently, so an older one answers `403 cloud_repository_grant_required` for every case in the family — including a `source.baseBranch` that does not resolve. If you receive that code where the table promises a finer one, confirm the branch exists and that `GET /v1/repositories` still lists the exact grant/repository pair, which is the distinction the single code cannot draw. [Which build is answering you](/reference/api/#which-build-is-answering-you) names the deployment you are on. `401` versus `403` is the distinction to encode: `401` means *this credential* is no good, so re-mint and retry once. `403` means the **installation** is not permitted, and no amount of retrying will change it — surface it to whoever can grant the capability. ## Not found | Status | Code | Cause | |---|---|---| | `404` | `cloud_session_not_found` | No such session, or it belongs to another tenant | | `404` | `cloud_turn_not_found` | No such turn in this session | | `404` | `cloud_run_not_found` | No such run in this session | | `404` | `cloud_artifact_not_found` | No such artifact, or it is not publicly visible | | `404` | `cloud_publication_not_found` | No such publication | | `404` | `cloud_publication_merge_not_found` | No such merge under that publication | | `404` | `cloud_session_access_not_found` | `GET …/execution` on a session you cannot see, or that does not exist | | `404` | `environment_not_found` | No environment with this id or name belongs to this organization, or the requested revision does not exist | `404` is also what you get for a resource that exists but belongs to a different organization — the API does not distinguish "absent" from "not yours", because doing so would leak the existence of other tenants' data. Two practical consequences: a `404` right after a successful `PUT` usually means an id case or typo mismatch, and a `404` on a session you are sure exists usually means the token belongs to a different installation. ## Conflicts `409` covers two very different situations, and the code tells you which. ### Idempotency conflicts — your bug These mean the same caller-owned id was reused for **different** content. They are never transient and retrying is pointless. | Code | Operation | Trigger | |---|---|---| | `cloud_session_conflict` | Session create or an environment write with `If-Match` | Same Session id with a different normalized create command (including environment selection), or an environment revision mismatch | | `cloud_turn_conflict` | `PUT …/turns/{turnId}` | Same id, different `message` or `replyTo` | | `cloud_publication_conflict` | `PUT …/publications/{publicationId}` | Same id, a different stored request hash | | `cloud_interaction_conflict` | `PUT …/turns/{turnId}` with `replyTo`, or `PUT …/interactions/{interactionId}/decision` | The interaction is already resolved or expired, or another approval answer won; a decision conflict includes the winning `outcome` when one exists | The rule that keeps you out of this: an id identifies one exact command. To send different content, mint a new id. Replaying an identical body is always safe and returns `200` (or `202` with `created: false` for turns). An oversized publication `body` or merge `commitMessage` is **not** one of these. Either field is checked twice — 65 536 characters at the route, then 65 536 UTF-8 bytes behind it — and both refusals are a `400`: `invalid_request` on the character bound, `invalid_input` on the byte bound. The request is malformed, not in conflict with a stored one, so there is no conflict to resolve and no new id to mint. `cloud_publication_merge_conflict` is the exception and it is **not** always your bug, so it gets its own row: | Code | Operation | Triggers | |---|---|---| | `cloud_publication_merge_conflict` | `PUT …/merges/{mergeId}` | **(a)** the same merge id with a different stored request hash — your bug, permanent; **(b)** a *different*, brand-new merge id while another merge for the same publication is still `admitted`, `attempted`, `waiting` or `succeeded` — transient in case (b) except for `succeeded`; **(c)** the first-party `/cloud` request's `expectedHeadSha` does not match the locked publication head — `merge_head_mismatch`, which admits no merge | You cannot tell these apart from the public code alone. `GET …/publications/{publicationId}/merges` pages every merge for that publication, so you can see whether one is already live and, if so, resume polling that one instead of minting another. For a first-party head mismatch, refresh and review the publication before retrying. A publication admits at most one live merge and at most one successful merge ever. ### Busy conflicts — wait and retry | Code | Meaning | `Retry-After` | |---|---|---| | `cloud_run_in_progress` | A non-terminal run blocks this action, or a fork source changed during capture | `2` | | `cloud_session_finalizing` | The previous run is being closed out and its transcript captured | `2` | | `cloud_publication_in_progress` | A publication for this session is not terminal yet | — | | `cloud_agent_locked` | Session `PATCH` would change runtime or model after a Turn exists. Select the agent on a new Turn instead | — | | `cloud_publication_merge_not_ready` | A `when_ready` merge is still waiting on checks or protection | — | Only the first two carry a delay. For the rest, back off on your own schedule — a few seconds is right for `cloud_publication_in_progress`; a `when_ready` merge is a background wait measured in minutes, so poll it slowly. #### Concurrent session commands Each command checks the Session's durable state when it commits. A Turn submitted after Archive receives `cloud_session_not_found`; Archive following a Turn with an active Run receives `cloud_run_in_progress`. Publish after Archive also receives `cloud_session_not_found`, while Archive following publication admission receives `cloud_publication_in_progress`. A merge submitted before its Publication succeeds receives `cloud_publication_merge_not_ready`. Changing an agent after Turn admission receives `cloud_agent_locked`; submitting a Turn against an agent changed since preflight receives `cloud_session_conflict`. Re-read the Session or Publication before retrying a state conflict. :::danger[`cloud_conversation_busy` is not a busy conflict — do not retry it] It looks like one and it is not. When a plain turn is refused with `409 cloud_conversation_busy`, the usual cause is that the agent asked a question and is **waiting for you**. That suspended question has no timeout: it leaves the open state only when the workspace is lost, the session is archived, or the run is cancelled. Nothing clears it with time. So a retry loop on this code never terminates. Read `GET /v1/sessions/{sessionId}/interactions`, answer the open interaction with a turn carrying `replyTo.interactionId`, and the session moves again. It is listed under [state conflicts](#state-conflicts--act-then-retry) for that reason. ::: ### State conflicts — act, then retry | Code | Meaning | What to do | |---|---|---| | `cloud_conversation_busy` | A plain turn was submitted while an interaction is open, or another command holds the session's conversation | **Answer the open interaction** with `replyTo`. Never retry the plain turn | | `cloud_turn_unresolved` | A turn id already exists but its run does not — an inconsistent replay, not an interaction problem | Re-`PUT` the identical turn once; if it persists, mint a new turn id | | `cloud_agent_configuration_invalid` (`400`) | The agent selection cannot run: an unknown `runtime`, or a native Turn switched from a foreign runtime without a model | Use an advertised runtime and supply a catalog model for a native Turn that cannot inherit one | | `cloud_agent_model_unavailable` (`400`) | The explicit `model` is not one this organization may spend on, or a new native Session omitted `model` and no agent slot is available | Send a model from `models.items` in `GET /v1/capabilities`, or configure model access; exact Session replays keep their stored model | | `cloud_workspace_expired` | The sandbox lease has ended | Read `continuation` on the session to see whether work can resume | | `cloud_workspace_expiring` | The lease is too close to its deadline to start this work | Retry shortly; a new lease will be taken | | `cloud_resume_unavailable` | The session cannot be continued from its last checkpoint | Start a new session | | `cloud_turn_execution_lost` | The execution backing this turn disappeared | Re-submit as a **new** turn id | | `cloud_turn_limit_reached` (`409`) | The session reached its durable 1,000-turn boundary | Start a new session; exact retries of already accepted turn ids still work | | `environment_archived` (`409`) | The selected environment is archived | Unarchive it or choose a live environment | | `environment_name_conflict` (`409`) | Another live environment has this name, ignoring case | Rename it or archive the other environment | | `quota_exceeded` (`409`) | The organization already has 200 live environments, or its environment archives would exceed 1 GiB | Archive an environment before creating or unarchiving; remove archives from documents and let retention collect them | `cloud_turn_limit_reached` is a per-session state boundary, so archiving another session cannot change it. ## Upstream and infrastructure | Status | Code | Meaning | Retry? | |---|---|---|---| | `502` | `cloud_turn_start_failed` | The turn was admitted but starting execution failed | **Yes** — re-`PUT` the same turn id | | `502` | `cloud_environment_setup_failed` | The pinned environment setup failed or timed out; the apply event has a redacted output tail | No. Fix the environment, then submit a new Turn | | `502` | `cloud_run_control_failed` | A control operation on the run (such as cancel) failed downstream | Yes, with backoff | | `503` | `cloud_workspace_starting` | The sandbox is being provisioned. `Retry-After: 2` | **Yes** | | `503` | `cloud_workspace_unavailable` | No sandbox could be provisioned | Yes, with backoff | | `503` | `cloud_runtime_outdated` | The requested runtime is too old for this session | No. Create a new session | | `503` | `cloud_runtime_account_unavailable` | No configured account can currently admit this runtime | Yes if discovery advertises temporary unavailability; otherwise connect an account | | `503` | `service_unavailable` | The API is restarting for a WAMP release, no API server is up behind the edge yet, or the database could not serve the request in time (transient contention under load). `Retry-After: 2` | **Yes**, after `Retry-After`. See [Releases](#releases-and-answers-with-no-code) | A durable Run that exhausts bounded dispatch attempts reports the terminal Event code `runtime_account_unavailable` (without the `cloud_` HTTP prefix). It is not still queued: reconnect an account, then submit a new caller-owned Turn. The `502` codes are the ambiguous ones: the command may or may not have taken effect. This is exactly what caller-owned ids are for — re-send the identical `PUT` and the response tells you which world you are in. ### Releases and answers with no code A WAMP release restarts the API. For a few seconds, requests are answered with `503 service_unavailable` and `Retry-After: 2` — by the stopping server, or by the edge while no server is up. Wait `Retry-After` and send the identical request again. A request that was already running when the restart began can be cut off, so treat a `PUT` like any ambiguous failure: re-send the same id, never a new one. The same coded answer arrives outside a release when the database could not serve a request in time — a transaction that waited too long behind other work on the same Session, a write conflict, or no free connection. Handle it exactly like the restart: wait `Retry-After` and send the identical request. More rarely the proxy in front of the API fails on its own and answers `502`, `503` or `504` with no JSON `error` field — an HTML page or an empty body. The missing code is the signal: no WAMP server answered, and the request may or may not have reached one. Retry an idempotent request — a `GET`, or a `PUT` that carries your own id — with capped exponential backoff and jitter for up to about 30 seconds, then surface the failure. Do not resend a `POST` such as run cancellation on that answer; read the resource first. ## Repository and GitHub errors `GET /v1/sessions/{sessionId}/repository/review` can fail for repository-level reasons. The codes below are the one family on this page with no `cloud_` prefix, and they arrive in the same `{ "error": "" }` body as every other `/v1` failure. ### Workspace conditions | Status | Code | Meaning | |---|---|---| | `409` | `no_changes` | The agent changed nothing. There is nothing to publish | | `409` | `not_repository` | The session has no repository checkout | | `409` | `repository_conflict` | The checkout is not in a state that can be reviewed | | `409` | `review_changed` | The tree moved while the review was being computed | | `409` | `branch_conflict` | The working branch conflicts with what is on the remote | | `413` | `review_too_large` | The diff exceeds the review budget | | `404` | `not_found` | The workspace or repository is gone | | `502` | `checkout_failed` | The base revision could not be checked out into the sandbox | | `502` | `review_failed` | The diff could not be computed | | `502` | `publish_failed` | The reviewed tree could not be committed or pushed | | `502` | `workspace_lost` | The sandbox backing the checkout disappeared mid-operation | `no_changes` is the common one and it is not really an error — it is the honest answer when an agent decided nothing needed changing. Handle it as a normal outcome in your product. `branch_conflict` is the one row on this table you should **not** write an HTTP handler for. A push that conflicts is discovered after the publication is admitted, so the `PUT` answers `2xx` and the conflict arrives as `publication.lastError.code` with the publication terminal in `failed` — exactly like the merge-phase codes below. A client that treats the `2xx` as "pushed" reports a conflicting push as a success. Branch on the publication's terminal `status`, never on the status of the request that started it. See [Push directly to a branch](/guides/push-directly-to-a-branch/#when-it-is-refused). ### What GitHub said Everything the provider answers is mapped to its own code rather than collapsed into "GitHub is down", because the four cases need four different reactions. | Status | Code | Meaning | Retry? | |---|---|---|---| | `404` | `repository_not_found` | GitHub answered 404 or 410 — deleted, renamed, or never visible to this grant | No | | `409` | `pull_request_changed` | The pull-request head moved after the publication recorded it | No. Re-review, republish, then merge the new head | | `409` | `pull_request_closed` | Someone closed the pull request | No. There is nothing to merge | | `409` | `merge_not_ready` | Checks pending, a review outstanding, or the pull request is still a draft | Yes with `mode: "when_ready"`; for a draft, republish with `draft: false` | | `409` | `merge_not_allowed` | GitHub refused the merge for this actor or strategy — commonly branch protection, or a strategy the repository disallows | No. Change `strategy` or ask an administrator | | `403` | `repository_read_only` | The GitHub App installation cannot write the repository — its `contents` permission is read-only, or the repository is not in the installation | No. Ask an administrator | | `403` | `github_request_refused` | A permanent 4xx refusal: an organization policy, an unsatisfied SAML SSO authorization, a missing App permission, a rejected payload | No. Retrying is pure waste | | `429` | `github_rate_limited` | GitHub is throttling. The response carries the wait **GitHub itself asked for** in `Retry-After` | **Yes**, after `Retry-After` | | `401` | `not_connected` | No GitHub connection backs this grant | No. An administrator must connect one | | `401` | `authorization_expired` | The connection's authorization lapsed | No. An administrator must re-authorize | | `503` | `not_configured` | GitHub integration is not configured on this deployment | Yes, but alert the operator | | `502` | `github_unavailable` | GitHub was unreachable or returned 5xx | Yes, with backoff | | `502` | `invalid_state` / `oauth_failed` / `account_already_connected` / `invalid_return_url` | Connection-flow faults. These belong to the browser authorization flow and should not reach a `/v1` caller; if one does, it is a server-side fault | No. Report it | `github_rate_limited` is the one worth special-casing. It is the only code in this table whose `Retry-After` comes from the provider rather than from WAMP, and hammering through it is what deepens a secondary rate limit. Honour the header. The four merge-phase codes — `pull_request_changed`, `pull_request_closed`, `merge_not_ready` and `merge_not_allowed` — **never reach you as an HTTP status.** Publishing and merging are asynchronous, and the merge reconciler catches all four and records them, so they arrive only inside a `200` body: as `merge.lastError.code`, and as the `errorCode` of `wamp.publication.merge_failed`. Their `409` is what the code *means* — the observed pull request's own state, not an upstream fault — and it is what a synchronous answer would carry if one ever existed. Branch on the code; do not write a `502` handler expecting these. See [Failure codes inside successful responses](#failure-codes-inside-successful-responses). ## Failure codes inside successful responses Not every failure is an HTTP error. Long-running commands report their own outcome in a `200` body: | Field | Where | |---|---| | `run.lastError.code` | `GET …/runs/{runId}` | | `publication.lastError.code` | `GET …/publications/{publicationId}` | | `merge.lastError.code` | `GET …/merges/{mergeId}` | | `data.errorCode` | `wamp.publication.failed`, `wamp.publication.merge_failed` events | These are diagnostic strings drawn from several error families, and the set is open — treat an unrecognized value as a permanent failure of that command and report it verbatim rather than guessing. A publication or merge can also surface any code from [Repository and GitHub errors](#repository-and-github-errors) above, or any of the HTTP codes on this page, because the failure is recorded with whatever code raised it. Beyond those, each command has its own family. **`publication.lastError.code`** — the publish state machine's own codes: | Code | Meaning | |---|---| | `publication_failed` | The fallback when no more specific code applies | | `publication_not_found` | The publication vanished between admission and execution | | `publication_in_progress` | Another publication for this session is not terminal | | `publication_idempotency_conflict` | The stored request hash for this id does not match | | `publication_session_archived` | The session was archived before the publish ran | | `publication_workspace_changed` | The reviewed tree moved before the commit was made | | `publication_authority_lost` | The credential's authority was revoked mid-command | | `publication_repository_grant_required` | The session's repository grant is missing, revoked, or does not reach this repository | | `publication_claim_lost` | The worker's claim on the command expired and another took over | | `publication_state_conflict` | The publication was not in a state this step could advance | | `publication_invariant_violation` | A durable invariant failed. Report it | **`merge.lastError.code`** — the merge state machine's own codes: | Code | Meaning | |---|---| | `merge_failed` | The fallback when no more specific code applies | | `merge_not_found` | The merge vanished between admission and execution | | `merge_already_admitted` | Another merge for this publication is already live | | `merge_idempotency_conflict` | The stored request hash for this id does not match | | `merge_publication_not_ready` | The publication has not reached `succeeded` | | `merge_session_archived` | The session was archived, so commands are refused | | `merge_authority_lost` | The credential's authority was revoked mid-command | | `merge_claim_lost` | The worker's claim expired and another took over | | `merge_state_conflict` | The merge was not in a state this step could advance | **Repository-grant failures** — the grant service's own codes. Both publications and merges spread this family in, because the grant behind the session is re-checked at every attempt: | Code | Meaning | |---|---| | `installation_not_found` | The GitHub App installation the grant belongs to is not connected to the organization | | `grant_not_found` | The grant backing the session is missing or revoked | | `branch_not_found` | The branch the session pinned does not resolve in the repository. Reaches a `/v1` caller as `400 cloud_repository_branch_not_found` | | `grant_idempotency_conflict` | The same grant id was requested with different content | | `active_grant_exists` | An active grant already exists for that GitHub App installation | | `github_connection_required` | No GitHub connection is available to back the grant | | `repository_permission_denied` | GitHub refused the write: the person creating the grant lacks `push` on the repository, the App installation can no longer write it, or an organization policy such as unsatisfied SAML SSO blocks it | `run.lastError.code` values are catalogued with the events that carry them, in [Events](/reference/events/#run-error-codes). Note that `lastError` can be present on a **non**-failed resource: a `waiting` merge carries `lastError.code: "merge_not_ready"` while it is still retrying. Read `status` first, `lastError` second. ## A retry policy that matches the server ```js check const RETRY_AFTER_HEADER = new Set([ 'cloud_workspace_starting', // 503 'cloud_session_finalizing', // 409 'cloud_run_in_progress', // 409 'cloud_rate_limit_exceeded', // 429 'cloud_auth_rate_limit_exceeded', 'identity_service_unavailable', 'cloud_rate_limit_unavailable', 'service_unavailable', // 503, a release restart or database contention // Not a rate limit and not an outage: the bounded set of calls this server may // have in flight against its own identity and catalog dependency is spent. 'cloud_agent_catalog_unavailable', // 503 // An environment write that adds a catalog extension could not reach the catalog. 'cloud_extension_catalog_unavailable', // 503 // The wait here is GitHub's own, not ours. Ignoring it deepens the limit. 'github_rate_limited', // 429, repository/review ]); const BACKOFF = new Set([ 'cloud_workspace_unavailable', 'cloud_runtime_account_unavailable', 'cloud_publication_in_progress', 'cloud_run_control_failed', 'github_unavailable', 'internal_error', ]); // Not retryable at all: the agent is waiting on you, and nothing times it out. const ANSWER_THE_INTERACTION = new Set(['cloud_conversation_busy']); // The proxy in front of the API answered: no WAMP server wrote this response. const EDGE_FAILURE = new Set([502, 503, 504]); /** * @param {number} status * @param {string | undefined} code the body's `error`; undefined when the body is not JSON or has none * @param {string | null} retryAfterHeader * @param {boolean} idempotent a GET, or a PUT that carries your own id */ function plan(status, code, retryAfterHeader, idempotent) { if (status === 401) return { action: 'remint-once' }; if (code === undefined) { // Back off for up to about 30 seconds, then surface it. return EDGE_FAILURE.has(status) && idempotent ? { action: 'retry-backoff' } : { action: 'fail' }; } if (ANSWER_THE_INTERACTION.has(code)) return { action: 'answer-interaction' }; if (RETRY_AFTER_HEADER.has(code)) { return { action: 'retry', delayMs: (Number(retryAfterHeader) || 2) * 1000 }; } if (BACKOFF.has(code)) return { action: 'retry-backoff' }; // 502 on an idempotent PUT: re-send the SAME id. if (status === 502) return { action: 'resend-same-id' }; return { action: 'fail' }; } ``` Everything not in those sets is terminal. In particular: never retry a `400`, a `403`, a `404`, or a `410` — each of them means the request itself was wrong, and a retry loop on them is how an integration turns a bug into a rate limit. Give the whole loop a budget measured in time, not attempts: three attempts two seconds apart can end inside one restart. The source App SDK keeps retrying a read, or a request that carries your own id, until it has failed for 20 seconds and at least three times. Two conflicts are the exceptions to "never retry a `…_conflict`": `cloud_publication_merge_conflict` clears once the merge holding that publication ends `failed` — but never after one has `succeeded`, because a publication may land at most once, ever — and `cloud_conversation_busy` clears when you answer the open interaction, never on its own. ## Related - [API conventions](/reference/api/) — idempotent `PUT` and what "the same body" means. - [Limits](/reference/limits/) — the caps behind `429` and `413`. - [Follow a run live](/guides/follow-a-run/) — handling `409` while a run is in flight. - [the API reference](/api/) — the error object in the machine contract.