Error codes
data.svc returns 66 distinct code values on its main API surface, 21 more on the worksheet
surface, and one constant code — admin — on the admin plane. This page is the complete list. Branch on code,
not on the status: several codes carry a status that is chosen at response time from the
underlying error.
Envelopes
Section titled “Envelopes”Two JSON shapes, and one route class that is not JSON at all.
| Surface | Body |
|---|---|
Main API (/data/rest/*, /data/anon/rest/*, /data/schema/*, /data/actions/*, /data/x/*, /data/stateflow*, /data/bulk, /data/cache, /data/contentstore/list) | {"errors":[{"code":"<slug>","message":"<message>","details":{...}}]} — errors is always an array; code and details omitted when empty. GraphQL and the bulk read carry a path on each entry |
Worksheets (/data/worksheets/*) | {"error":{"code":"<area>.<slug>","message":"…","details":{…},"request_id":"…"}} — error is an object |
Admin plane (/data/admin/data/*, /data/superadmin/*) | {"errors":[{"code":"admin","message":"<message>"}]} — the constant code admin, no details |
| Authentication rejection (any protected route) | Plain text Unauthorized, Content-Type: text/plain; charset=utf-8 |
details is populated on these request-level codes:
| Code | details |
|---|---|
bulk_failed | operations — the partial per-operation result array |
rate_limited | profile, limit, remaining, retry_after (seconds) |
action_method_not_allowed | allowed — array of verbs |
method_not_allowed | allowed — a single verb string, not an array |
file_storage_unconfigured | entity |
file_downloader_unconfigured | entity, id |
search_text_not_supported | entity, audit |
search_dsl_not_supported | entity, got — the Go type of the rejected filter — and audit |
and on every failure the engine raises (query_failed, create_failed, update_failed,
delete_failed, restore_failed, search_query_failed, the bulk read’s per-query errors and the
bulk write’s op_failed), where it says where the failure came from:
| Key | Present when |
|---|---|
hook, phase | A hook raised it — the hook’s name and the phase it ran in |
tier | The access hook refused it |
field, rule, code, failures[] | Validation refused it — every accumulated failure, not only the first |
backend, sqlstate, constraint, table, column | The database refused the statement; the message is the database’s own sentence, never its DETAIL line |
gate | The query compiler’s accessgate or safetyguard refused it |
code: reference_restrict, reference, parent_entity, child_entity, count | A delete was refused because child rows still reference the row — see relations |
code: version_mismatch, expected, current, column | The row’s concurrency token did not match the one the caller sent — see concurrency |
code: precondition_required / precondition_unsupported / precondition_invalid / lock_requires_transaction | The request asked for something the entity or the call cannot honour; the message says what |
code: lock_unsupported, backend | The backend has no such lock |
code: lock_timeout / deadlock, retryable: true | A lock wait ran out, or the database broke a deadlock — retry the request |
The message is the innermost cause alone. The engine’s wrapper chain
(exec: execute mutation: postgres: exec: …, hook validate (BeforeValidate): …) is what the
service log shows and never reaches a client.
How the status is chosen
Section titled “How the status is chosen”Six codes — query_failed, create_failed, update_failed, delete_failed,
restore_failed, search_query_failed — name the operation that failed, not the reason.
The status carries the reason, and is derived from the engine error:
| Engine error | Status |
|---|---|
| Access rule or row-level rule denied the operation | 403 |
| Row or entity not found | 404 |
A unique or foreign-key constraint refused the statement; a delete refused by a reference’s restrict rule; a lock wait that ran out or a broken deadlock | 409 |
| The row’s concurrency token did not match | 412 |
| Field, entity or business-constraint validation failed — including a reference to a missing parent | 422 |
| The entity requires a concurrency token and the write carried none | 428 |
| A precondition the entity cannot check, or a lock outside a transaction or on a backend without locks | 400 |
| Anything else | 500 |
Classification is by error type first. A substring fallback covers the paths that still return
untyped errors: access denied / not authorised / not authorized / forbidden → 403,
not found → 404, validation / invalid / is required → 422, everything else → 500.
Tenancy and routing
Section titled “Tenancy and routing”| Code | Status | Meaning | Likely cause |
|---|---|---|---|
no_tenant | 403 | No tenant key was resolved into the request context | Missing or unroutable tenant headers, or the tenant is not configured on this deployment |
missing_tenant | 400 | The :tenant path parameter is required on this superadmin route | Superadmin URL built without the tenant segment |
invalid_tenant | 400 | The :tenant value must be the bare tenant name, not a full four-part key | Customer, product and environment come from the request, not the path |
bad_path | 400 | A required path segment is empty — entity, id, action name, or entity plus action | Request against a route shape the handler does not recognise |
Request body and parameters
Section titled “Request body and parameters”| Code | Status | Meaning | Likely cause |
|---|---|---|---|
bad_json | 400 | The body is not valid JSON, or the bulk envelope failed to parse | Malformed payload |
body_read | 400 | The body could not be read off the wire | Truncated request, client disconnect, oversized body |
bad_params | 400 | Action parameters failed to bind | Unreadable body, or invalid JSON in an action call |
missing_id | 400 | An update needs a primary key and none was supplied | PUT/PATCH /data/rest/{entity} with no key in the body and no /id/{id} in the path |
id_mismatch | 422 | The primary key in the body contradicts the one in the path | The key is immutable; send it consistently or omit it |
bad_expression | 400 | The ?expression= program failed to compile | Syntax error in the response-shaping expression |
bad_precondition | 400 | If-Match or the body’s expect is not a concurrency token | A version that is not a number, a timestamp that is not RFC 3339 |
precondition_conflict | 400 | If-Match and expect are both present and disagree | Send one, or the same token in both |
expression_runtime | 500 | The expression compiled but failed at evaluation, or exceeded its time ceiling | Reference to a field absent from the result, or an illegal operation |
Expression semantics are on request flags.
Definition and row lookup
Section titled “Definition and row lookup”| Code | Status | Meaning | Likely cause |
|---|---|---|---|
entity_not_found | 404 | The entity is not in the tenant’s composed schema | Typo, or the definition was never loaded for this tenant and product |
row_not_found | 404 | No row with that id | Wrong id, or the row is soft-deleted and therefore invisible to reads |
missing_entity | 400 | No entity was supplied, as a path segment or as ?entity= | Bulk import/export without ?entity=, or a schema route without the entity segment |
stateflow_not_found | 404 | No stateflow registered under that name | Typo, or the flow is not in the composed schema |
missing_name | 400 | The stateflow name path segment is empty | Detail route called with no name |
row_not_found after a successful DELETE is expected: delete is soft by default. See
traits.
Reads, writes and search
Section titled “Reads, writes and search”| Code | Status | Meaning | Likely cause |
|---|---|---|---|
query_failed | 400 / 403 / 404 / 409 / 422 / 500 | A read failed in the engine | Access denial, missing row, invalid filter, a lock outside a transaction, database error |
create_failed | 403 / 404 / 409 / 422 / 500 | A create failed | Validation failure, a reference to a missing parent (reference_missing), unique violation (409), access denial |
update_failed | 400 / 403 / 404 / 409 / 412 / 422 / 428 / 500 | An update failed | final / readonly / writeonce violation, validation failure, a stale concurrency token (412), rejected state transition, access denial |
delete_failed | 400 / 403 / 404 / 409 / 412 / 422 / 428 / 500 | A delete failed | Child rows still reference the row (409 reference_restrict), a stale concurrency token (412), access denial |
restore_failed | 403 / 404 / 422 / 500 | A soft-delete restore failed | Most often restore: entity %q has no soft-delete columns — the entity does not carry the trait |
search_query_failed | 403 / 404 / 422 / 500 | A structured search failed | Unfilterable field, bad operator, access denial |
search_body_parse_failed | 400 | The search body is not decodable JSON | Malformed body on POST /data/rest/{entity}/search |
search_text_not_supported | 501 | Free-text search is not implemented | Sending query instead of a filter object; no search client is wired |
search_dsl_not_supported | 501 | A filter supplied as a string is not interpreted | Send filter as a JSON object |
Operators and filter shapes are on query DSL; the per-rule failure messages are on validations and stateflows.
Bulk envelope, import and export
Section titled “Bulk envelope, import and export”| Code | Status | Meaning | Likely cause |
|---|---|---|---|
empty_envelope | 400 | The operations array is empty or absent | Envelope posted with no operations. A body carrying query or queries is a bulk read; for now it is routed there as an alternate to the QUERY verb |
bulk_failed | 422 | The bulk transaction rolled back | Any operation failed; details.operations carries the partial results |
op_failed | — | Per-operation code inside the response body, not a status | Carried in the failing operation’s errors[] entry, with path set to [<operation index>] and meta.status set to failed |
bad_format | 400 | Unsupported import/export format | ?format= is not csv, json or jsonl (ndjson is accepted as an alias for jsonl) |
import_failed | 500 | A bulk import failed mid-stream | Malformed source rows, type coercion failure, or a database error |
The whole envelope runs in one transaction: one failure rolls everything back. The 422 body still carries the per-operation array for diagnostics — treat it as a report, not as a partial success.
The bulk read (QUERY /data/rest) refuses a malformed batch with a 400 before any query runs:
| Code | Status | Meaning |
|---|---|---|
query_missing | 400 | Neither query nor queries, or an empty queries object |
bad_queries | 400 | queries is not a JSON object of name → query |
too_many_queries | 400 | More than 25 queries; details.max, details.got |
bad_query_name | 400 | A query name that is not an identifier; details.name |
reserved_query_name | 400 | A query named batch or queries; details.name |
duplicate_query_name | 400 | A query name used twice; details.name |
query_parse_failed | 400 | The single query did not parse |
query_kind_refused | 400 | The single query is a mutation, not a read |
query_filter_unparsed | 400 | The single query carried a where the parser dropped |
query_param_missing | 400 | The single query names a $variable that was not supplied |
In the batch form the last four are per-query errors[] entries on a 200, with path naming
the query. See REST — bulk read.
Actions and query hashes
Section titled “Actions and query hashes”| Code | Status | Meaning | Likely cause |
|---|---|---|---|
action_not_found | 404 | No action or query hash registered under that name | Typo, or the definition is not part of the tenant’s composed schema |
action_method_not_allowed | 405 | The action declares no section for this verb | An Allow header lists the accepted verbs and details.allowed repeats them |
action_failed | 400 / 403 / 404 / 409 / 422 / 500 | The action executed and errored | SQL error (500), a lock the backend cannot take (400 lock_unsupported), a lock wait that ran out (409 lock_timeout), a constraint violation (409). The status and details follow the engine error, as for the CRUD codes |
registry_unavailable | 500 | The query-hash registry could not be resolved | Registry provider not wired, or tenant configuration incomplete |
Parameter binding refusals surface as action_failed with messages naming the parameter:
unknown parameter, missing required parameter, value not in enum.
References, concurrency and locks
Section titled “References, concurrency and locks”These ride in details.code under the operation’s own code, the way validation rule codes do.
details.code | Status | Meaning | Where |
|---|---|---|---|
reference_missing | 422 | A write points a reference at a parent row that does not exist; one entry per bad reference under details.failures[] with its field | Relations |
reference_restrict | 409 | A delete was refused because child rows still reference the row; details.reference, parent_entity, child_entity, count | Relations |
version_mismatch | 412 | The row carries another concurrency token; details.expected, current, column | Concurrency |
precondition_required | 428 | The entity requires a token on every update and delete | Concurrency |
precondition_unsupported | 400 | A token was sent for an entity with no concurrency strategy | Concurrency |
precondition_invalid | 400 | The token is not the kind the entity’s strategy checks | Concurrency |
lock_requires_transaction | 400 | A row or named lock outside a transaction | Concurrency |
lock_unsupported | 400 | The backend has no such lock; details.backend | Concurrency |
lock_timeout | 409 | The lock wait ran out; details.retryable: true | Concurrency |
deadlock | 409 | The database broke a deadlock; details.retryable: true | Concurrency |
Scripted endpoints
Section titled “Scripted endpoints”| Code | Status | Meaning | Likely cause |
|---|---|---|---|
endpoint_not_found | 404 | No endpoint at that path — including one that exists but is disabled | A disabled endpoint deliberately answers 404, not 403, so its existence is not disclosed |
endpoint_path_missing | 404 | The dispatch catch-all matched with an empty path | Request to the script root with no endpoint segment |
method_not_allowed | 405 | The endpoint declares one verb and the request used another | Allow header and details.allowed name the expected verb |
unauthenticated | 401 | The endpoint requires a principal and none was resolved | Missing or expired token. This 401 is JSON — unlike the middleware’s |
forbidden | 403 | The endpoint’s auth gate rejected the principal | Principal lacks what the endpoint requires; also returned for an unknown auth mode in the declaration |
bad_body | 400 | The body is unreadable, not JSON, or over 1 MiB | Body shape does not match the endpoint’s declaration |
script_error | 500 | The script failed to compile, or threw at runtime | Any uncaught error in the endpoint body; also a file: script whose bytes were never inlined |
script_engine_missing | 500 | The endpoint registry was built without a script engine | Deployment wiring bug. An unsupported language: does not land here — it is refused at registration, so the endpoint answers endpoint_not_found |
See scripted endpoints.
| Code | Status | Meaning | Likely cause |
|---|---|---|---|
file_storage_unconfigured | 501 | No file-storage backend is registered | Any multipart create |
file_downloader_unconfigured | 501 | No downloader is registered | Any ?download=true read |
multipart_parse | 400 | The multipart body could not be parsed | Bad boundary or truncated upload |
multipart_empty | 400 | Multipart parsed but produced no form | Upload with no file and no fields |
multipart_bad_payload | 400 | A file part targets an undeclared field, targets a non-file field, carries more than one file for a field, or could not be read | Wrong field names, or an array upload against a single-file field |
download_needs_id | 400 | Download requires the /id/{id} form | ?download=true used on a list path |
missing_field | 400 | Download requires ?field= | Field name omitted |
field_not_found | 404 | The field does not exist on the entity | Typo |
field_not_file | 400 | The field is not file-typed | Download requested against a scalar |
field_empty | 404 | The file field exists but holds no reference on this row | Nothing was ever uploaded |
field_unexpected_shape | 500 | The stored reference is present but not a string | Data written outside the API, or a storage-format change |
download_failed | 500 | The byte stream could not be fetched | Storage outage, expired credentials, deleted object |
contentstore_list_failed | 500 | The content-store catalogue could not be read | Backing store unavailable |
Access, roles and rate limiting
Section titled “Access, roles and rate limiting”| Code | Status | Meaning | Likely cause |
|---|---|---|---|
requires_admin | 403 | The route requires admin or superadmin | Role matching is case-insensitive, and superadmin also satisfies admin |
requires_superadmin | 403 | The route requires superadmin strictly | Plan apply, retention and lock force-release are superadmin-gated even for tenant admins |
rate_limited | 429 | The rate-limit bucket for this tenant, user and profile is exhausted | Anonymous traffic is rate-limited too |
A rate_limited response carries Retry-After plus X-RateLimit-Limit,
X-RateLimit-Remaining and X-RateLimit-Reset.
One further code, authz_denied (403), exists in the catalogue but data.svc never returns it:
it is raised only on alias-mounted CRUD routes, and data.svc mounts none. Where it does apply it
fires only when an authorizer is registered — with none registered the check fails open and the
request proceeds. It is not a substitute for
access rules, which deny by default.
Entity-level and row-level denials never carry these codes — they arrive as the CRUD code with a 403 status. See row-level security and field protection.
Schema export and cache
Section titled “Schema export and cache”| Code | Status | Meaning | Likely cause |
|---|---|---|---|
export_render_failed | 500 | An export failed to render | A schema construct the exporter cannot represent, or a template failure |
cache_clear_failed | 500 | Clearing the caller’s tenant schema cache failed | Cache backend unavailable |
invalidate_failed | 500 | Superadmin cache invalidation for a target tenant failed | Cache backend unavailable, or the tenant does not resolve |
list_tenants_failed | 500 | The superadmin tenant listing failed | Configuration client unavailable |
See schema export and caching.
Engine and service
Section titled “Engine and service”| Code | Status | Meaning | Likely cause |
|---|---|---|---|
engine_unavailable | 500 | The per-tenant engine bundle could not be resolved | Tenant configuration not loaded yet, definitions failed to compile, no datastore declared, or the database is unreachable |
engine_unexpected_type | 500 | The resolved engine does not implement the interface this surface needs | Deployment wiring bug |
engine_unavailable is the most common boot-order failure. The wrapped message names the
failing step. See schema sources and
datastores.
GraphQL
Section titled “GraphQL”GraphQL does not use the JSON error envelope. Field-level failures return 200 with the error
in the errors array and the field set to null — including access denials, validation failures
and constraint violations. Two transport-level rejections are plain text, not JSON:
| Condition | Status | Body |
|---|---|---|
Any verb other than POST | 405 | method not allowed |
| Unparseable request body | 400 | invalid JSON body |
no_tenant (403) and engine_unavailable (500) are still returned in the JSON envelope,
because they are raised before the handler resolves. Introspection is not served: __schema and
__type validate but have no resolver, so they come back as cannot resolve field. See
GraphQL.
The bulk read (QUERY /data/rest, or POST /data/rest?_method=query) follows the same convention:
a per-query failure returns 200 with an errors[] entry carrying path: ["<query name>"] and
data.<name> set to null, while the other queries in the batch still return their rows. Only a
malformed batch is a 400 — duplicate_query_name, reserved_query_name (batch, queries)
or bad_query_name (names follow GraphQL alias rules: ^[_A-Za-z][_0-9A-Za-z]*$).
Worksheets
Section titled “Worksheets”The worksheet surface has its own catalogue and its own envelope. Statuses come from a fixed table, not from message classification.
| Code | Status | Meaning |
|---|---|---|
worksheet.not_found | 404 | No worksheet with that id, or it has been evicted |
worksheet.expired | 410 | The TTL passed; the worksheet is inside the grace window |
worksheet.cpet_violation | 403 | The worksheet belongs to a different tenant |
worksheet.invalid_ttl | 400 | Requested TTL is outside the allowed range |
worksheet.memory_exceeded | 507 | The worksheet exceeded its memory budget |
file.not_found | 404 | No staged file with that id |
file.uri_unreachable | 502 | A file referenced by URI could not be fetched |
file.uri_access_denied | 403 | The remote store refused the fetch |
file.format_invalid | 400 | Format unrecognised or unparseable; also when the multipart form has no file part |
file.size_exceeded | 413 | Over the file-size, file-count, row or column limit |
file.encoding_error | 400 | The character encoding could not be decoded |
file.sheet_selection_required | 400 | A multi-sheet workbook needs an explicit sheet |
table.name_conflict | 409 | A table with that name already exists |
table.not_found | 404 | No such table in the worksheet |
table.union_incompatible | 400 | Column sets or types do not line up |
column.not_found | 404 | No such column on the table |
query.syntax_error | 400 | The SQL failed to parse |
query.timeout | 504 | The query exceeded its time budget |
query.execution_error | 400 | The query parsed but failed at execution |
invalid_request | 400 | Invalid JSON body on a worksheet call |
rate_limit_exceeded | 429 | Worksheet rate limit; sends Retry-After: 60 |
Limits and the values behind each code are on worksheet limits.
Admin and superadmin plane
Section titled “Admin and superadmin plane”Every handler under /data/admin/data/* and /data/superadmin/* returns
{"errors":[{"code":"admin","message":"<message>"}]} — the same errors[] array as the main
API, but the code is always the constant admin and there is no details. Only the auth gates
in front of them use per-condition codes, so requires_admin, requires_superadmin,
missing_tenant and invalid_tenant do appear on these routes — but nothing the handler
itself raises carries anything other than admin. Branch on the message.
| Status | Message shapes |
|---|---|
| 400 | plan_id query parameter required, plan id missing in path, missing :resource segment, claimer_id is required, confirmations: empty, entity path segment missing, invalid confirm_max_risk %q (use low / medium / high), body parse failures |
| 401 | Always missing tenant identity |
| 403 | tenant key missing from request context |
| 404 | plan %q not found, entity <name> not found in schema |
| 409 | Applying a plan that was refused at generation time. The plan is stored so the id resolves, but it can never be applied — regenerate it |
| 500 | Wrapped operational errors; engine resolution failures are prefixed engine_unavailable: inside the message string, not carried as a code |
Plan apply is the exception to the table: it answers 207 whenever the result is anything other
than fully applied, and carries a partial result object rather than an error body —
{plan_id, status, steps_applied, steps_failed, steps_skipped, failed_step?, halted_step?, error?}. It falls back to 500 with the plain {"errors":[…]} shape only when the apply produced no
result at all. See migrations.
What is not a wire code
Section titled “What is not a wire code”Three families of identifier look like error codes and are not.
Per-rule validation codes. REQUIRED, READONLY, FINAL, WRITE_ONCE, MIN, MAX,
MIN_LENGTH, MAX_LENGTH, LENGTH, PATTERN, the format-checker codes (EMAIL, URL,
URL_PROTOCOL, UUID, ULID, CUID, NANOID, SEMVER, PHONE, IP, HOSTNAME,
MACADDRESS, PORT, JSON, DATAURI, CREDITCARD, LATITUDE, LONGITUDE, MIMETYPE,
FILESIZE_MIN, FILESIZE_MAX, CONTAINS, NOTCONTAINS, STARTSWITH, ENDSWITH, NOTIN,
DIVISIBLEBY, BEFORE, AFTER, PAST, FUTURE, PATTERN_UNKNOWN, VALIDATION_UNKNOWN), and
the defaults ENTITY_VALIDATE, ENTITY_VALIDATE_ERR, BUSINESS_CONSTRAINT,
BUSINESS_CONSTRAINT_ERR, POINTCUT_ERR, POINTCUT_ABORT are the rule’s identity, not a wire
code: the response carries the CRUD code, the rule’s message, and the rule’s own code under
details.code (and per failure under details.failures[]).
Schema-compile codes. ENTITY_NOT_FOUND, FIELD_NOT_FOUND, TYPE_MISMATCH,
INVALID_ENUM_VALUE, RELATION_NOT_FOUND, FIELD_NOT_QUERYABLE, UNSUPPORTED_OPERATION,
DUPLICATE_ENTITY, DUPLICATE_FIELD, SHADOW_PRODUCT_FIELD and INVALID_TRANSITION are declared
constants with no emitter — nothing in the tree raises one. The schema front-end the deployable
service uses reports load failures as plain wrapped errors naming the failing step, not as coded
ones. A schema that fails to load never serves traffic, so a client sees nothing from this family
either way. Do not branch on these names.
The legacy data-* catalogue. A large set of codes spelled data-<something> exists in the
tree and is dead: no handler emits any of them. Do not build client branching on that list.