Error Catalog
Actions never throw — each returns a typed result, either success with a payload or a typed error. That uniform contract is what lets the engine apply consistent retry, rollback, and HTTP/GraphQL mapping across every connector.
Error categories
Section titled “Error categories”Each error falls into a category that drives engine behaviour:
| Category | Meaning | Retried? |
|---|---|---|
| Transient | Temporary — timeout, rate-limit, connection, upstream-unavailable, lock conflict. | Yes |
| Permanent | Won’t succeed on retry — bad input, auth failure, not-found, parse error, bad config. | No |
| Infrastructure | Platform-level — file I/O, storage, backup failure. | No |
| Business | Flow-level — step failure, flow error, human-task, staging, share. | No |
The engine retries Transient errors per the step’s retry: policy; once attempts run out, a retry-exhausted error wraps the last failure.
HTTP and GraphQL mapping
Section titled “HTTP and GraphQL mapping”A representative slice of the error codes and how they surface:
| Code | HTTP | GraphQL |
|---|---|---|
timeout |
504 | TIMEOUT |
authentication-error |
401 | UNAUTHORIZED |
permission-denied |
403 | FORBIDDEN |
invalid-parameters |
400 | BAD_USER_INPUT |
invalid-configuration |
400 | BAD_USER_INPUT |
not-found |
404 | NOT_FOUND |
parse-error |
400 | BAD_USER_INPUT |
storage-error |
500 | INTERNAL_ERROR |
step-invocation-error |
500 | INTERNAL_ERROR |
staging-error |
500 | STAGING_ERROR |
compensation-error |
500 | INTERNAL_ERROR |
retry-exhausted |
500 | INTERNAL_ERROR |
ambiguous-binding |
400 | (no GraphQL code — see below) |
binding-missing |
400 | (no GraphQL code — see below) |
compensation-error is the rollback-failed code — it surfaces when a saga compensation itself fails, which is the situation you most want surfaced loudly.
ambiguous-binding and binding-missing fire only on a direct connector/entity invoke that binds via on= — never at flow-YAML step or run level. ambiguous-binding is raised when a bare on=<env> resolves more than one connection of the connector’s type; binding-missing is its sibling for when on= resolves none. Both carry category permanent, so neither is retried.
These two are the only rows in the table above that are not platform AppError codes — they are structured per-result ErrorEntry codes attached to a failed connector result, so they never travel through the AppError→GraphQL-code mapping and no code path emits BAD_USER_INPUT for them. What each surface actually returns:
- REST and MCP — the kebab code above, plus a machine-readable
hint.candidatesblock:aliasesforambiguous-binding(the validon=<env>/<alias>values),envsandconnectionsforbinding-missing. The HTTP status is 400 because a code with no specific status folds tomax 400. - GraphQL connector invoke — no GraphQL error at all. The mutation returns a successful
InvokeConnectorPayloadwithsuccess: falseanderrorsas bare message strings; code, category and hint are dropped. Checksuccess, don’t look for an error extension. - GraphQL entity operations — an
EntityErrorin the payload carryingcode: "ambiguous-binding"(the kebab wire code, not aSCREAMING_CASEGraphQL code) andcategory: "permanent".
A step naming an unbound alias is a compile-time error instead (V1315); a run passing an undeclared environment is a separate, declared-set membership error.
Error classes (for connectors)
Section titled “Error classes (for connectors)”In practice ~95% of connector failures are a single ConnectorError whose message determines its class. These classes are what a connector declares in its <name>.connector.yaml:
| Class | Category | Retryable | Typical message |
|---|---|---|---|
validation |
Permanent | No | "{Field} is required" |
not-found |
Permanent | No | "{Entity} '{id}' not found" |
auth |
Permanent | No | "Authentication failed", "Invalid API key" |
timeout |
Transient | Yes | operation exceeded its time limit |
rate-limit |
Transient | Yes | HTTP 429, "Rate limit exceeded" |
connection |
Transient | Yes | "Connection refused", "Connection reset" |
unavailable |
Transient | Yes | HTTP 502/503/504, "overloaded" |
api-error |
Permanent | No | "{API} error ({code}): {message}" |
parse |
Permanent | No | "Invalid JSON", "Failed to parse" |
config |
Permanent | No | "{Field} is required when {Condition}" |
locked |
Transient | Yes | "Database locked", "File is locked" |
quota |
Permanent | No | "File too large", "Max results exceeded" |
storage |
Infrastructure | No | "Path does not exist", S3/disk errors |
How this shows up while authoring
Section titled “How this shows up while authoring”A failed run reports the error code and category in its log and in the REST/GraphQL response body. Transient failures retry automatically; permanent ones fail fast so you fix the flow rather than waiting through retries. See Common Pitfalls for the most frequent permanent (validation / invalid-parameters) cases and their fixes.