Public beta — not for production use. Data may be wiped at any time. Questions? Contact us.
Documentation menu

Error catalog

Every user-facing Axiom error — flow run failures in the editor, edge transform errors, and HTTP API error responses — with what causes each one and how to fix it.

View as Markdown

This page lists the errors Axiom shows users, grouped by where you see them: in the editor when a flow run fails, and in HTTP responses when you call the API. Each entry gives the exact error shape, the cause, and the fix.

How errors name nodes and edges

Flow run errors identify nodes by their canvas id — the per-placement id you see on the node card in the editor (for example id-bad or node-1716492048192) — never by the marketplace node name. This matters when a flow contains two placements of the same node: the error names the specific placement that failed, so you can find it on the canvas directly.

Edges are named by quoting both endpoints. An edge error reads:

edge "id-bad" → "echo-merge": EvaluateEdge adapter: transform TRANSFORM_PREFIX failed: prefix needs a string to prepend, e.g. prefix("hi ")

Here id-bad and echo-merge are the canvas ids of the edge's source and destination nodes.

When a failure is attributed to a specific edge or node, the result panel at the bottom of the editor also shows the failure kind (for example EDGE_ADAPTER) and a Show on canvas button. Clicking it selects the offending edge — or node, when the failure has no edge attribution — on the canvas so you can open and fix it. See Debug a flow for the full debugging workflow.

Flow run errors in the editor

When an execution fails, the flow status pill above the canvas shows Failed, the result panel header shows Execution Failed, and the panel's Output tab contains the error text. The errors you can see:

ErrorCauseFix
edge "<src>" → "<dst>": EvaluateEdge adapter: <detail>The edge's adapter (a bare CEL expression) failed to compile or evaluate at runtime — a parse error, a type mismatch, or an eval error.Open the edge and fix the CEL expression. See Edge adapter (CEL) errors.
edge "<src>" → "<dst>": EvaluateEdge condition: <detail>The edge's condition expression failed to evaluate (for example CEL evaluation failed: …).Fix the condition expression on that edge.
invalid JSON for <InputType>: <detail>The JSON input you submitted does not match the entry node's input message — unknown field, wrong type.Match the field names and types of the entry node's input message. See Type system.
graph produced no terminal resultThe execution ended without the terminal node producing output — typically every outgoing edge condition evaluated to false somewhere along the path.Check edge conditions; make sure at least one path reaches the terminal node for this input.
exceeded max steps (1000)The execution dispatched more than 1000 node steps — almost always a loop that never meets its exit condition.Fix the loop's exit condition.
... daily executions quota exceeded (cap <n>)The flow tried to spawn child executions (parallel branches, runtime mutation) after the daily execution quota ran out; the whole flow fails.Wait for the midnight-UTC reset, or ask the operator for a higher limit.
downstream join <n> failed: <reason>A node feeding a join failed, and the join's failure policy aborted the flow.Fix the failing branch, or relax the join's failure policy.
open pipeline stream for node "<id>", send to node "<id>", recv from node "<id>"In pipeline mode, the platform lost the connection to that node's container.Usually transient — re-run; if it persists, check the node's logs.

When your own node code raises an exception, the handler's error message is surfaced verbatim: it appears on the NODE_FAILED event in the result panel's Execution tab, attributed to the failing placement's canvas id.

Secret authorization errors (grant_required)

A node receives one of your secrets only after you have explicitly granted that secret to that exact node version, in the console, after reviewing its source. When that consent is missing the run is refused with HTTP 403 and a machine-readable body:

{
  "error": "grant_required",
  "message": "this flow cannot run: 1 secret requirement(s) are not authorized — …",
  "missing": [
    {
      "node_ulid": "01J…",
      "slot": "ANTHROPIC_API_KEY",
      "reason": "not_granted",
      "remedy": "grant it in the console, after reviewing the node's source",
      "console_url": "https://…/console/grants/new?node=01J…&slot=ANTHROPIC_API_KEY"
    }
  ]
}

An AI agent that hits this cannot resolve it itself — that is deliberate. Granting is a human review step, so the agent's job is to relay the missing[] list (each entry has a console_url) to a person, who reviews the code and grants. The same reason values also appear as node-visible status markers, so ax.secrets.status(name) inside a node reports the same cause.

reasonCauseFix
not_grantedThe node is published, but you have never granted this secret to this node version.Review the node's source in the console and grant it.
content_changedYou granted this node, but its code, protos, or dependencies changed since — so the grant no longer covers what would run.Review the diff and re-grant.
dev_not_armedYour OWN in-development node, but the secret is not armed for development.Arm that secret on the Secrets page (armings last 7 days).
no_materialConsent exists, but you have no secret configured under that name (deleted, or never created).Create the secret in the console.
not_publishedThe node is unpublished and belongs to another user, so there is no immutable code to consent to.Ask its author to publish a version.
generic_untrustedAn Instance whose underlying generic node is unpublished and not yours — the code that would receive the secret is neither reviewable nor pinned.Ask the generic's author to publish it.
unverifiableThe node's contents could not be verified at delivery time.Transient; retry. If it persists, the artifact may be broken — contact the author.
provenance_mismatchThe secret was provisioned by the platform for one specific generic node and slot, and this node is not it. Armed secrets provisioned for an app reach only that app's own code.Declare the secret on the generic it was provisioned for, or provision a separate secret for this node. Re-arming will not help — it is already armed.

Edge adapter (CEL) errors

An edge adapter's right-hand side is a bare CEL expression (or a plain field pick) evaluated against the source message. A failing expression produces a CEL error inside an edge error (see above). The failure modes:

StageErrorMeaning
ParseCEL parse error: <detail>The expression is not syntactically valid CEL.
Type-check / compileCEL compilation failed: <detail> (e.g. no matching overload, undeclared reference)The expression is well-formed but ill-typed — e.g. text + 1 mixes string and int, or it references a field/function that is not in scope.
EvalCEL evaluation failed: <detail>The expression compiled but failed against this input — e.g. an index out of range or a division by zero.

CEL string helpers such as toUpper, toLower, trim, size, and join are available in the adapter scope; call them directly (toUpper(text)), not through a pipe. A destination field path (the mapping key) that contains ( or ) is rejected at compile time as a malformed field name.

Whole message-typed field picks copy the message byte-for-byte, so they are only valid between the same message type. These are rejected at compile time (never at run) — the fix is to reconstruct the destination in CEL, field by field:

ErrorMeaningFix
adapter: field pick "<src>" maps message type "<A>" into differently-typed message field "<dst>" (type "<B>") …A whole-message pick across two different message types — a verbatim byte copy would misread the wire.Map each leaf field explicitly in CEL ("<dst>.field": "<src>.field"), or use a CEL object literal, so the reshape is declared.
adapter: field pick "<src>" maps a <kind> value into <kind> field "<dst>" … cannot convert between a message and a scalar …A pick tried to bridge a message field and a scalar field.Write a CEL expression that constructs the destination shape.
adapter: field pick "<src>" maps a <repeated/singular> message into a <singular/repeated> field "<dst>" … requires matching cardinality …A whole-message pick with mismatched cardinality (a repeated message into a singular field, or vice versa).Reshape in CEL (e.g. index a single element, or map(…) a list).

Constructing a message in CEL

The reshape those compile-time errors point you to — a CEL object or array literal, or a .map() building elements — is the supported way to fill a message-typed or repeated-message destination field:

adapter:
  # a repeated message field, built element by element
  working_hours: '[{"start": window_start, "end": window_end}]'
  # the same thing from a list already in scope
  busy: 'attendees.map(a, {"intervals": a.busy})'
  # the whole destination message at once (the reserved `self` target)
  self: '{"working_hours": [{"start": start, "end": end}], "duration_seconds": 1800}'

The constructed value is serialized to JSON and decoded into the destination message, so its keys must be the destination's proto field names and its leaf values must match those fields' kinds. Keys that match no field are ignored; an object matching no field at all is rejected (see the from_json rows below).

ErrorMeaningFix
from_json: destination message "<pkg>.<Type>" not foundThe runtime could not resolve the destination message type. Since 2026-08-03 this is an internal fault, not an authoring mistake — the worker now loads the destination node's type closure before the edge is evaluated. Before that fix it hit every CEL-constructed message whose destination was a package node, at run time only, while validate / compile / preview all passed.Nothing to change in the flow. If you see it, the worker is running a build older than that fix — report it.
from_json: none of the <n> supplied key(s) [<keys>] match any field of destination message "<pkg>.<Type>" [<fields>] …The constructed object is a different shape than the destination declares — usually the source message's field names rather than the destination's.Rename the object's keys to the destination's proto field names (axiom info <package> lists them).
from_json: field "<name>": expected a JSON array, got <type>A repeated destination field was given a single object instead of a list.Wrap it: [{…}].
from_json: field "<name>": element <i>: expected a JSON object, got <type>An element of a repeated message field is a scalar.Build each element as an object: xs.map(x, {"value": x}).

In-band node errors — they are DATA, and yours to handle (ADR-228)

A node that returns successfully with an error-shaped output field populated is a SUCCESSFUL node. The engine does not inspect your output payload and does not infer a failure from a field's name. A field called error, error_message or *_error is ordinary data: it flows down edges, an adapter can read it, a condition can test it, a compose can pick it.

A node reports a real failure through one channel only — NodeResponse.error (ADR-115) — which the worker classifies, retries (ADR-114) and routes down on_error edges.

Nothing forces you to read an in-band error field. ADR-187's compile gate was removed by ADR-225, and ADR-226's runtime promotion was reverted by ADR-228. An in-band failure you do not read is silently dropped, and nothing warns you (F-020/F-027, accepted deliberately).

SymptomCauseFix
A downstream row holds an empty/garbage value and the flow reported successAn upstream node reported its failure in-band and the flow never read the field.Gate the dependent edge on it (below), or surface it in your output facade.
(removed 2026-08-28, ADR-225) node <label>: error-shaped output field "<field>" of <Message> is bound nowhere in the flow …This compile error no longer exists. Binding a node's in-band error output is OPTIONAL: a flow that reads it nowhere compiles.Nothing to do — but see the two patterns below.

Gate the dependent write — the strong form. A conditional edge means a poisoned value never reaches the database:

- from: hash
  to: insert_user
  condition: { op: CEL, cel: 'error == ""' }
  adapter: { password_hash: phc_hash }

(The consumer's join then needs the skip-tolerant shape.)

Or surface it in the output compose — the minimum form, fine for a terminal node whose result nothing else consumes:

error: { cel: 'inputs.hash.error != "" ? "signup failed: " + inputs.hash.error : ""' }

on_error: edges are for real node failures (a crash, a timeout, an explicit NodeResponse.error) — not for in-band error fields, which never reach them.

Authoring mock errors

Errors from the mocks: section of a flow.yaml — the per-node fixtures described in Mock nodes while authoring a flow. The first group is raised by axiom flow validate and axiom flow compile; the second refuses to publish anything mocked.

Compile and validate

ErrorCauseFix
mocks.<alias>.output does not match the node's output message "<Message>": <detail>The fixture does not fit the node's declared output message — an unknown or misspelled field, or a value of the wrong type. <detail> names the offending field path.Correct the field in the fixture. For an Instance node, write the facade output shape you wire against, not the underlying generic port's.
mocks.<alias>: mock references unknown node alias "<alias>"The mocks: key doesn't match any alias in nodes:.Key the mock by the node's alias exactly as it appears in nodes: and edges:.
mocks.<alias>: a mock needs exactly one of output: (inline response), file: (fixture path), or error: (typed failure)The entry sets none of the three fields, or more than one.Keep exactly one. Use error: for a failure, output:/file: for a response.
mocks.<alias>.error.code: unknown error code "<code>" — one of USER, TIMEOUT, TRANSPORT, PANIC, CANCELLEDAn error: mock names a code that is not a typed node error code.Use one of the five codes. Only TRANSPORT and TIMEOUT are retryable.
mocks.<alias>.file: cannot read fixture <path>: <detail> / fixture <path> is not valid JSONThe file: path (resolved relative to the flow.yaml) is missing or does not parse.Fix the path, or regenerate the fixture with axiom flow mock add <alias> --from-execution <id>.
mocks are not supported with pipeline_mode (unary flows only) — a pipeline node's response is a frame stream, not a single fixtureThe flow sets pipeline_mode: true and declares mocks.Remove the mocks: section from the pipeline flow.
mocks.<alias>: only package nodes can be mocked (this node is a subflow or virtual boundary node) / facade boundary nodes are pass-throughs and cannot be mocked / a subflow node cannot be mocked hereThe alias is @flow_input/@flow_output or a subflow node.Mock the real node next to the facade boundary; for a subflow, declare the mock inside the subflow's own flow.yaml.
mocks.<alias>.output: encoded fixture is <n> bytes, over the <max>-byte payload ceilingThe fixture exceeds the platform payload cap.Trim the fixture to the fields the flow actually reads.

Publish

StatusBodyCauseFix
422cannot publish a flow whose saved source declares <n> authoring mock(s): a published flow must run its real nodes; remove the mocks: section from flow.yaml, re-save (axiom flow save), and publish thatThe saved flow document still declares mocks:.Delete the section, run axiom flow save again, and publish the new document.
422cannot publish a flow compiled with authoring mocks (<n> mocked node(s)): a published flow must run its real nodes; remove the mocks: section from flow.yaml (or recompile with axiom flow compile --no-mocks), re-save, and publish thatThe compiled artifact's graph embeds fixtures.Recompile with axiom flow compile --no-mocks and publish the artifact that produces.

Publish never strips mocks silently — going live is always an explicit recompile.

HTTP API errors when invoking a flow

Errors from POST /invocations/v1/flows/invoke and its streaming variant. For the full request/response contract see the HTTP API reference.

Authentication, rate limiting, and quotas

StatusBodyCauseFix
401{"error":"unauthorized"}Missing, malformed, revoked, or expired credentials on any authenticated route.Send a valid key in the Authorization: Bearer header — see Create and manage API keys.
429{"error":"rate limit exceeded"}Your tenant exceeded the per-tenant invocation rate limit (per-second burst). The response carries a Retry-After: 1 header.Back off and retry after the indicated delay.
429{"error":"quota_exceeded", "retry_after_seconds": ...}Your tenant exhausted one of its daily quotas — invocations or executions. The message names which; retry_after_seconds counts down to the reset at midnight UTC.Wait for the daily reset, or ask the operator to raise your tenant's limit.
429{"error":"auth_flow_throttled", "retry_after_seconds": 6}The flow is declared an auth surface (auth_surface: true in its flow.yaml) and this caller has made too many credential attempts against it. The budget is per (flow, client IP) — not per tenant — so one attacker cannot lock out your other users or your other flows. A refused attempt spends none of your execution or invocation quota.Back off and retry after retry_after_seconds. If real users are hitting this, they are behind a shared egress IP; that is the trade-off the mark buys, and the limit refills continuously.
403{"error":"tenant_suspended"}Your tenant has been disabled by the operator. All invocations and resumes are rejected.Contact the operator.
503{"error":"quota_unavailable", "retry_after_seconds": 5}The quota service could not be reached, so the request was rejected as a safety measure.Transient — retry after the indicated delay.
429{"error":"sandbox_quota_exhausted"}Anonymous landing-page sandbox only: this guest session has used all of its live runs. Carries no Retry-After — waiting does not restore the budget.Sign up for an account.
429{"error":"sandbox_budget_exhausted"}Anonymous landing-page sandbox only: the whole sandbox has used its daily run budget. Carries no Retry-After. Reads, inspection, and debug are unaffected — only live runs.Sign up for an account, or try again after the UTC-midnight reset.
503{"error":"sandbox_unavailable", "retry_after_seconds": 5}Anonymous landing-page sandbox only: the sandbox's quota counters could not be reached. The sandbox fails closed — unlike a normal tenant, whose metering fails open — so an anonymous surface can never run unmetered.Transient — retry after the indicated delay.

Structured invoke errors

Invoke failures with a known cause return a structured body:

{
  "error": "payload_too_large",
  "message": "payload exceeds hard cap: actual=17825792 max=16777216",
  "max_bytes": 16777216,
  "actual_bytes": 17825792
}
Statuserror classCauseFix
413payload_too_largeThe input exceeds the 16 MiB hard cap (max_bytes/actual_bytes populated).Shrink the input payload.
503upstream_unavailableThe flow could not be queued; a platform dependency was temporarily unavailable. retry_after_seconds: 5.Transient — retry after the indicated delay.
503blob_storage_unavailableA large input payload could not be stored; a platform dependency was temporarily unavailable. retry_after_seconds: 5.Transient — retry after the indicated delay.

Failures that don't classify return 500 with {"accepted": false, "error_message": "<detail>"}.

Timeouts

With "wait": true, if the execution does not complete within the timeout (default 30 seconds), the response is still 202 — the execution keeps running:

{
  "accepted": true,
  "execution_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "error_message": "timeout waiting for flow result (30s)"
}

Set "timeout_seconds" to wait longer, or look the execution_id up later in execution history. On the streaming endpoint, a timeout ends the stream with a final frame whose error is "timeout waiting for pipeline result"; a flow that fails mid-stream ends with a final frame whose error carries the flow run error described above.

A request that is accepted and delivered but whose execution runs to a failure (for example, the input did not match the flow's start-node schema) returns HTTP 202 with "result":{"success":false,...} and surfaces the worker's diagnostic in both result.error_message and the top-level error_message — so a synchronous caller can tell a delivered failure apart from a timeout or transport error without polling execution history.

HTTP API errors when invoking a single node

Errors from the per-node JSON endpoint POST /invocations/v1/nodes/{owner}/{pkg}/{version}/{node} (also accepts a node id in place of the name path):

StatusBodyCauseFix
404{"error_message":"node not found or not yet deployed"} (or node not found: <detail> for a name path that doesn't resolve)The node id or {owner}/{pkg}/{version}/{node} path doesn't match a published, deployed node.Check the package name, version, and node name against the marketplace listing.
400{"error_message":"invalid JSON for <InputType>: <detail>"}The JSON body doesn't match the node's input message.Match the input message's field names and types.
400{"execution_id":"...","error_message":"invalid value for <InputType>: field \"<name>\" carries enum ordinal <n>, which names no declared value ..."}An enum field is set to an ordinal that is not a declared value of its enum — typically a payload recorded before the field was an enum (or against a different schema version).Re-record the payload against the node's current schema.
422{"error_message":"<detail>"}The node ran and its handler returned an error — error_message is the handler's own message.Fix the input, or the node code.
502{"error_message":"could not connect to node"} / "calling node: <detail>" / "sending to node: <detail>"The platform could not reach the node's container.Usually transient (cold start) — retry; if it persists, check the node's deployment.

The binary-protobuf variant POST /invocations/v1/nodes/invoke validates its base64 payload against the node's current input schema before forwarding: bytes whose wire shape doesn't match (an undeclared field number, or a wire type conflicting with the declared kind), or any payload whose node schema can't be resolved, return HTTP 400 with {"success":false,"error_message":"payload does not match the node's current input schema: <detail>"}.

HTTP API errors when pre-warming a flow

Errors from POST /invocations/v1/flows/warm (see Pre-warm a flow). Authentication, rate-limit, and quota failures return the same bodies as invoking a flow above — a warm debits the same daily invocation quota an invoke does. Pre-warm adds these ownership and availability cases:

StatusBodyCauseFix
400{"error":"invalid_request","message":"..."}graph_id is missing (for a non-app-key credential) or doesn't match [A-Za-z0-9_-]+.Pass a valid graph_id. An app key may omit it — the warm then expands to the key's allowlist.
429{"error":"warm_rate_limited","message":"...","retry_after_seconds":20}This app key beaconed faster than its per-key warm cap (default 1 warm / 20 s, burst 3).Honour Retry-After; the generated client's auto-beacon already backs off.
403{"error":"forbidden","message":"not authorized to warm this flow"}The authenticated tenant does not own graph_id.Warm a flow your tenant owns; a foreign graph_id is never accepted, even to pre-warm it.
404{"error":"not_found","message":"flow not found"}graph_id doesn't resolve to any flow.Check the id against the flow's Use via API dialog.
503{"error":"warm_unavailable","message":"...","retry_after_seconds":5}The warm dispatcher isn't configured on this deployment.Transient/deployment-specific — retry after the indicated delay, or invoke the flow directly instead.

Execution history and debug API errors

Errors from the execution-history and debugging endpoints (used by Debug a flow):

  • Execution history and event requests return 404 with {"error":"execution not found"} when the execution id doesn't exist for your tenant.
  • GET /invocations/v1/executions/{id}/checkpoints/{checkpoint_id} returns 404 with {"error":"checkpoint_not_found_or_expired","detail":"checkpoint may have been evicted by TTL policy (default 30d)"} — checkpoints expire after a retention window (default 30 days), so debug a recent execution instead.
  • Agent memory operations return standard statuses: 404 when a memory entry or session is not found, 400 for invalid arguments, 403 for permission denied, 401 for unauthenticated requests, and 429 when resource limits are exhausted.

All endpoints are tenant-scoped: a valid id belonging to another tenant behaves exactly like a nonexistent one (404), never 403 — see Sandboxing and tenancy.