Skip to content

Error taxonomy & recovery

When an API fails, it can fail in a hundred different shapes. Anvil turns all of them into one. Every failure — from any generated CLI or MCP tool — comes back as a single structured envelope with a code from the fixed list below.

{ "error": { "code": "rate_limited", "message": "...", "retryable": true, "safe_to_retry": true, "operation": "...", "trace_id": "..." } }
raw upstream failures 429 · too many requests 500 · internal error <html> error page socket timeout malformed JSON one structured envelope Error envelope one shape for every failure code retryable safe_to_retry trace_id the agent's recovery choice code fix the input the request was wrong — correct it and re-send retryable retry if safe retryable and safe_to_retry — retry with the idempotency key not safe stop & report not safe to repeat — halt and surface the trace_id the runtime redacts secrets and attaches a trace_id to every envelope.
fig 11 · every upstream failure becomes one structured, recoverable error

The codes on this page aren’t written by hand. They’re imported from apps/docs/src/data/errors.json, which a real compile produces (node scripts/gen-docs-data.mjs). A test — packages/generators/src/docs-data.test.ts — checks that file against the runtime’s own list of codes, so this page can’t show a code the runtime no longer returns.

The recovery advice comes from the same place agents read: it mirrors the reference/errors.md file that ships inside every generated skill (see errorsRef() in packages/generators/src/skill.ts). What you read here is what a deployed agent reads.

CodeRetry?Recovery rule
validation_errornoFix the input; check details.missing. Do not retry unchanged.
auth_requirednoThe auth profile lacks access. Check the credential env-var names in the generated setup.md, then stop and report.
permission_deniednoThe auth profile lacks access. Check the credential env-var names in the generated setup.md, then stop and report.
not_foundnoThe resource does not exist. Do not retry.
conflictnoThe resource already exists or a request with the same idempotency key is in flight. Do not blindly retry.
rate_limitedif safeTransient. Retry only if safe_to_retry is true; the tool already retried what it safely could.
upstream_timeoutif safeTransient. Retry only if safe_to_retry is true; the tool already retried what it safely could.
upstream_unavailableif safeTransient. Retry only if safe_to_retry is true; the tool already retried what it safely could.
unsafe_retry_blockednoThe runtime refused to auto-retry a non-idempotent mutation after an ambiguous failure. Check upstream state before doing anything else — the call may have landed.
confirmation_requiredwith flagRe-issue with --confirm only if the user intends the effect. Check required_flags for what to supply.
idempotency_requiredwith flagSupply --idempotency-key and re-issue.
schema_mismatchnoThe input does not match the compiled schema for this operation. Fix the shape; do not retry unchanged.
unsupported_operationnoThe operation is not exposed by this artifact — usually not approved, or removed at recompile. Do not retry; report it.
policy_deniednoA local policy blocked the call (often the ANVIL_ALLOWED_HOSTS egress allowlist — see setup.md). Stop and report.
unknown_upstream_errornoThe upstream failed in a way the taxonomy cannot classify. Do not retry a mutation; report with the trace_id.

Two fields in the envelope answer that, and they are not the same question:

  • retryable — is this kind of failure temporary? A rate limit or a timeout is; a validation error is not.
  • safe_to_retry — may this specific operation be repeated? This depends on its idempotency: an operation that is safe to repeat (calling it twice does the same thing as once) can be retried; a mutation that isn’t must not be.

An agent should act only on safe_to_retry. By the time it sees the error, the runtime has already retried whatever it safely could, so a safe_to_retry: false means “stop and check,” not “try harder.”

a call failed · retry or not? outcome an upstream call returned an error 1 · is it a mutation? reads change nothing — repeating one is always safe read auto-retry safe to auto-retry with backoff no key, no confirmation — a read can't cause harm twice mutation 2 · safe to repeat? · idempotent calling twice does the same thing as calling once — declared in the model, not guessed yes retry · with key retry using the idempotency key reusing a key repeats the same operation, not a new one no · not idempotent never auto-retry stop — ask a human or require confirm a refund charged twice can't be un-charged — repeating is not safe
fig 08 · a non-idempotent mutation is never auto-retried