Error reference¶
One envelope, one code table, every surface
Every REST error is the same JSON shape, every gRPC error maps onto the same taxonomy, and every SDK turns both into the same typed exception hierarchy. This page is the single table to check when a call fails.
Normative sources
core-engine/src/api/error.rs (the envelope), core-engine/src/api/v1.rs and core-engine/src/api/auth.rs (where each code is raised), core-engine/src/grpc/mod.rs (gRPC status mapping), and .ai/specs/integration/2026-07-02-grpc-contracts.md §6 (the normative gRPC mapping).
The envelope¶
Every non-2xx REST response is:
{
"code": "QUERY_INVALID",
"message": "human-readable description",
"request_id": "0197c1a2-...-..."
}
codeis a stable, machine-checkable string (&'static strin the server).messageis human-readable; for validation failures it carries a JSON pointer to the offending field (e.g./where/severity: expected an operator object like {"$eq": ...}).request_idcorrelates with server logs; it is echoed from the request-id middleware and is the same UUID as thex-request-idresponse header. It is omitted only in the rare case no request id was available.
Every response, error or not, carries the x-request-id header, minted fresh (UUIDv7) by the outermost middleware layer for every request.
Error codes¶
| Code | HTTP status | Typical cause |
|---|---|---|
UNAUTHORIZED | 401 | Missing or invalid x-api-key (or bearer token where accepted) |
NOT_FOUND | 404 | No record, edge, job, trace, clarification, or person with that id/scope |
INTERNAL | 500 | Unhandled engine failure; details are logged server-side, never leaked in the message |
QUERY_INVALID | 400 | Query-DSL validation failure (/v1/query, /v1/snapshot); message carries a JSON pointer |
BAD_CURSOR | 400 | Cursor invalid, tampered, foreign-tenant, or plan-mismatched |
COMPARE_INVALID | 400 | /v1/compare request validation failure; message carries a JSON pointer |
INVALID_ID | 400 | A client-supplied logical id was nil |
INVALID_PROPERTY | 400 | A property value failed conversion (e.g. malformed {"$bytes": ...} base64) |
INVALID_INTERVAL | 400 | A validTime interval was malformed (e.g. end <= start) |
EMPTY_BATCH | 400 | POST /v1/records called with an empty records array |
VALIDATION | 400 | Miscellaneous request-shape failures (e.g. upsert without id; unrecognized clarification state/severity; missing connector for an API-key externalId lookup) |
INVALID_CONTENT | 400 | /v1/ingest: neither/both of content/contentBase64, or malformed content |
PLAN_INVALID | 400 | /v1/generate: empty question, or temperature outside [0, 2] |
MODEL_NOT_ALLOWED | 400 | /v1/generate model is not in the operator allowlist |
UNKNOWN_MODEL | 400 | Named embedding model is not registered |
VECTOR_INVALID | 400 | Evidence-planner vector clause failed validation |
ALREADY_EXISTS | 409 | A client-supplied id already has a version |
TOMBSTONED | 409 | The target record/edge/entity is already tombstoned |
TYPE_COLLISION | 409 | An edge type collides with an existing edge between the same nodes |
INGEST_ERASED_CONTENT | 409 | Content digest was retired by GDPR erasure; re-ingestion is permanently refused |
CONNECTOR_NAMESPACE_MISMATCH | 403 | A connector token acted outside the namespace it synced |
INSUFFICIENT_EVIDENCE | 422 | /v1/generate recall produced no evidence spans; pass allowUngrounded to proceed anyway |
TENANT_BUDGET_EXCEEDED | 429 | The tenant's token cap (daily/monthly) is exhausted |
UNIMPLEMENTED | 501 | Feature not configured (e.g. vector.text without an embedding endpoint, or /v1/generate with no [llm] configured) |
PROVIDER_ERROR | 502 | The LLM or embedding provider returned a permanent failure |
EMBEDDING_FAILED | 502 | The embedding provider failed or returned no vector |
PROVIDER_UNAVAILABLE | 503 | Transient provider failure, retries exhausted |
CLARIFY_UNPARSEABLE_ANSWER | 422 | A free-form clarification answer did not normalize |
CLARIFY_ALREADY_ANSWERED | 409 | Clarification is already bound and policy disallows further responses |
CLARIFY_INVALID_STATE | 409 | Answer attempted against a clarification in an incompatible state |
RESPONDER_ASSERTION_FORBIDDEN | 403 | A non-clarify-audience caller tried to assert a third-party responder or rights |
ALIAS_ATTACH_FORBIDDEN | 403 | Caller is not the clarify audience (only path allowed to attach verified aliases) |
ALIAS_AMBIGUOUS | 409 | The verified email matches zero or more than one Entra-verified person |
ALIAS_CONFLICT | 409 | The person already carries a different value for that alias kind |
ALIAS_EVIDENCE_INVALID | 422 | Alias evidence failed re-verification (email mismatch, no aad_oid, alias not verified) |
Where each surface raises them¶
| Endpoint group | Codes you'll see |
|---|---|
/v1/query, /v1/snapshot | QUERY_INVALID, BAD_CURSOR, UNAUTHORIZED, UNIMPLEMENTED |
/v1/compare | COMPARE_INVALID, BAD_CURSOR, UNAUTHORIZED |
/v1/records (POST/PUT/GET/DELETE), /v1/relationships | EMPTY_BATCH, INVALID_PROPERTY, INVALID_INTERVAL, INVALID_ID, VALIDATION, UNAUTHORIZED, NOT_FOUND, ALREADY_EXISTS, TOMBSTONED, TYPE_COLLISION, CONNECTOR_NAMESPACE_MISMATCH |
/v1/ingest | INVALID_CONTENT, UNAUTHORIZED, INGEST_ERASED_CONTENT |
/v1/generate | PLAN_INVALID, MODEL_NOT_ALLOWED, QUERY_INVALID, UNKNOWN_MODEL, VECTOR_INVALID, INSUFFICIENT_EVIDENCE, TENANT_BUDGET_EXCEEDED, UNIMPLEMENTED, PROVIDER_ERROR, EMBEDDING_FAILED, PROVIDER_UNAVAILABLE |
/v1/trace/{id} | UNAUTHORIZED, NOT_FOUND |
/v1/clarifications* | VALIDATION, UNAUTHORIZED, NOT_FOUND, RESPONDER_ASSERTION_FORBIDDEN, CLARIFY_ALREADY_ANSWERED, CLARIFY_INVALID_STATE, CLARIFY_UNPARSEABLE_ANSWER |
/v1/person-aliases/verified-match | VALIDATION, UNAUTHORIZED, ALIAS_ATTACH_FORBIDDEN, NOT_FOUND, ALIAS_AMBIGUOUS, ALIAS_CONFLICT, ALIAS_EVIDENCE_INVALID |
| Any handler's catch-all | INTERNAL (details logged server-side only) |
Full per-endpoint status/code tables are in REST API.
Validation errors carry a JSON pointer¶
QUERY_INVALID and COMPARE_INVALID messages are not free text, they name the exact field:
{
"code": "QUERY_INVALID",
"message": "/where/severity: expected an operator object like {\"$eq\": ...}",
"request_id": "..."
}
This is the same pointer format the query language fixture corpus pins, and the same format MCP tools echo back for their findRecords/generate/compareSnapshots validation failures (as a tool-level {"error": "query_invalid", "message": "..."}, not a transport-level error, see MCP tools).
gRPC status mapping¶
gRPC has no envelope; it maps the same taxonomy onto standard Status codes (grpc-contracts spec §6):
| Condition | gRPC status |
|---|---|
QUERY_INVALID / COMPARE_INVALID (validation failure) | INVALID_ARGUMENT |
| Tenant/token failures (missing, malformed, expired, wrong audience) | UNAUTHENTICATED |
| Scope violations (e.g. cross-tenant job access) | PERMISSION_DENIED |
| Budget-partial results | OK, with a partial flag on the response body, never an error |
Lease conflicts (stale seq, superseded lease) | ABORTED |
| Record not found | NOT_FOUND |
| Record already exists | ALREADY_EXISTS |
| Update against a tombstoned record | FAILED_PRECONDITION |
| Feature not configured | UNIMPLEMENTED |
| Engine internal failure | INTERNAL (request id in server logs) |
Malformed worker_id/id fields, bad UTF-8 in query_json | INVALID_ARGUMENT |
See gRPC API › Error mapping for how this plays out per-RPC.
How the SDKs surface errors¶
Both SDKs parse the envelope into a typed exception hierarchy rooted at a common base carrying {code, status, requestId} (or request_id in Python):
| Class | Trigger |
|---|---|
QueryInvalidError | 400 QUERY_INVALID (message carries the JSON pointer) |
ValidationError | other 4xx (BAD_CURSOR, INVALID_INTERVAL, EMPTY_BATCH, COMPARE_INVALID, ...) |
AuthError | 401 UNAUTHORIZED |
NotFoundError | 404 |
ConflictError | 409 (TOMBSTONED, TYPE_COLLISION, ALREADY_EXISTS, INGEST_ERASED_CONTENT, ...) |
UnimplementedError | 501 |
ServerError | 5xx INTERNAL and transport failures (status 0, code TRANSPORT) |
The TypeScript SDK additionally maps gRPC status codes onto this same hierarchy via fromGrpcError: UNAUTHENTICATED → AuthError, INVALID_ARGUMENT → QueryInvalidError/ValidationError, NOT_FOUND/ALREADY_EXISTS/ABORTED → NotFoundError/ConflictError, UNIMPLEMENTED → UnimplementedError, DEADLINE_EXCEEDED/UNAVAILABLE → ServerError.
See TypeScript SDK › Errors and Python SDK › Errors for the exact class shapes and retry semantics (only idempotent calls retry, and only on 5xx/transport failures, never on any 4xx, including all of the codes above).
Related¶
- REST API - per-endpoint status/code tables
- gRPC API - RPC-level error mapping and auth failure modes
- Query language - the grammar behind every
QUERY_INVALID - MCP tools - tool-level error shapes for the agent surface
- TypeScript SDK / Python SDK - typed exception hierarchies and retry policy