Common Query Errors (v2)
This page describes common errors on the v2 API and how to resolve them. A theme runs through v2: it fails loud. Conditions that a lenient API might turn into an empty result β an unsupported operator, an unknown config profile, a missing test set β return a 4xx with a message instead, because an empty result is indistinguishable from "no match" and hides the real problem.
The error response format
Every v2 error β from any endpoint, at any status β returns the same JSON envelope, so a client can parse errors one way across the whole API:
{
"kind": "error",
"data": {
"code": "not_found",
"message": "Unknown table 'invoicess'"
}
}
kindis always the literal string"error"β the cheapest check for "did this request fail" (independent of the HTTP status).data.codeis a stable, machine-readable code (a dotted namespace) β branch on this in an SDK. Codes are contractual; themessagewording is not.data.messageis a human-readable, single-line explanation β surface it, but don't parse it.
The HTTP status carries the coarse class (4xx client, 5xx server); the code carries the specific cause. For an SDK, the reliable pattern is: if the body has "kind": "error", read data.code; otherwise treat the body as a normal result.
Code catalogue
Query and read endpoints (_query, _search, _predict, _recommend, _relate, _aggregate, _estimate, _evaluate, _sql, _batch):
code | HTTP | Cause |
|---|---|---|
query.invalid | 400 | The query shape is wrong β a bad from, an unrecognised structure. |
request.invalid | 400 | A value failed validation (bad type, out-of-range argument). |
proposition.unsupported | 400 | A $-operator isn't applicable to the named field. |
query.empty_population | 400 | A _predict or _evaluate whose nested from selects no rows β there is nothing to learn from. A list endpoint (_query, _search, _recommend, _match, relate $patterns) answers the same case with 200, no hits and a query.empty_population warning. Available since v2.11.4; earlier builds answer request.invalid. |
json.malformed | 400 | The request body isn't valid JSON. |
not_found | 404 | A referenced table (the from, or a linked table) doesn't exist. |
operation.unsupported | 501 | A capability isn't implemented for this table/engine (e.g. a rep2-only operator on a legacy rep1 table). Distinct from 500 β "not supported here", not "we crashed". |
internal | 500 | An unexpected server error. Report it. |
Data-write endpoints (_delete, _modify, .../batch, .../stream, .../import) and schema endpoints add:
code | HTTP | Cause |
|---|---|---|
data.bad_request | 400 | A write was rejected (e.g. a vector dimension mismatch, or on_conflict on a keyless table). |
import.failed Β· table.exists Β· schema.not_collection | 400 | Import problems: the import failed, the target table already exists, or it isn't a collection. |
write.contention | 409 | Concurrent writes to the same table kept moving it until this write gave up. Nothing was written; retry after Retry-After seconds. See write contention. |
schema.create_failed Β· schema.column_update_failed Β· schema.refresh_failed Β· schema.plan_failed Β· schema.apply_failed | 400 | The schema operation was rejected (the message says why). |
not_found | 404 | The named table/column doesn't exist. |
A _delete / _modify where reads the collection's own fields. A field it
does not have β a link path such as cust.tier, or a typo β is a 400 whose
message names the field. Select the rows with a _query on the link path, then
change them by their own key.
Available since: v2.11.5. Earlier builds answered such a
where with a 200 that matched no row (or, under $not, every row).
Two things this table makes explicit and worth relying on: a typo'd table name is a 404, not a 500 (it reads as "you referenced something missing", not "we crashed"), and an unsupported operation is a 501, not a 500 β so a client can tell "this engine can't do that" apart from a genuine bug.
Failures that aren't domain-specific β an unauthorised key, a wrong method, a
quota refusal β carry the envelope too, with a code named after the status:
unauthorized (401), forbidden (403), method_not_allowed (405), conflict
(409), payload_too_large (413), unsupported_media_type (415), rate_limited (429), unavailable (503). A
refusal that carries extra machine-readable facts keeps them beside code and
message β a free-tier row-quota refusal, for instance, adds limit, table
and hint under data.
Available since: v2.7.0 for the 404s from the schema
and data endpoints (GET/DELETE /api/v2/schema/{table},
POST /api/v2/data/{table}/batch) and for the status-named codes above. On an
older build those specific failures answer with the flat v1 body
{"message": β¦, "status": β¦} instead of the envelope β so a client that must
support both should read data.code when kind === "error" and fall back to
the top-level message.
Success responses
For contrast, successful responses come in two shapes:
- Row results (
_search,_predicthits, filtered_query) return the v1-compatible body{ "offset": β¦, "total": β¦, "hits": [ β¦ ] }β nokindfield (an absentkindmeans rows). - Non-row results (
_aggregate,_estimate,_evaluate,_batch) use the same{ "kind": β¦, "data": β¦ }frame as errors β e.g.{ "kind": "aggregate", "data": { β¦ } }. So the presence of"kind": "error"β never just the presence ofkindβ is what marks a failure.
These shapes do not depend on how the table is stored. _estimate,
_aggregate and _evaluate run different implementations for a rep2
collection and a legacy rep1 table, but both answer in the {kind, data}
frame. Where the legacy path names a payload field the way v1 did, the v2 name
is added alongside it: _estimate returns both data.value and data.estimate
holding the same number, so data.value is always safe to read.
Available since: v2.7.0 for the engine-independent shape.
On an older build, _estimate/_aggregate/_evaluate against a legacy
type: "table" return the bare v1 body with no kind β which the
kind ?? "rows" rule reads as a page of rows with no hits, silently losing
the result.
401 or 415 on a POST: the two headers every POST needs
Every POST needs two headers, and each has its own status when it is missing:
x-api-keymissing, or a key the server does not know:401, on every route and method (see 401 / 403).content-type: application/jsonmissing or different:415, whose message startsContent-Type must be application/jsonand names the header to send.curl -dsendsapplication/x-www-form-urlencodedunless you say otherwise, so this is the usual cause. On/api/v2the body is the v2 error envelope with the codeunsupported_media_type.
If both headers are missing, the key is checked first, so the answer is 401; fix
the key, and the next request answers 415 until the content type is set.
Available since: v2.11.2. Up to v2.11.1 both mistakes answered the
same plain-text 405 "HTTP method not allowed, supported methods: GET" on a POST,
which named neither header (and a GET with no key answered 400).
Fix: send both headers on every POST:
curl -X POST 'https://shared.aito.ai/db/aito-demo/env/v2/api/v2/_query' \
-H 'x-api-key: yg4rTlXkqDzm4y8gPeY75HCKaNwfbTQ2si64ONTi' \
-H 'content-type: application/json' \
-d '{ "from": "products", "limit": 1 }'
Against a local Docker container, the URL is http://127.0.0.1:9005/api/v2/_query
and the key is your READ_WRITE_APIKEY (see Docker).
401 / 403 β authentication and environments
Almost every auth failure on v2 is one of two things: wrong addressing or a read key used for a write.
There is no env-scoped API key. The same key that reads or writes master reads or writes every environment. An environment is selected purely by URL path β never by a dot suffix, a request-body field, a query parameter, or a header:
β
/db/{db}/env/{name}/api/v2/_query (multi-tenant server)
β
/env/{name}/api/v2/_query (single-db server)
β /db/{db}/env.{name}/api/v2/_query (dot β not an env selector)
β body: { "env": "{name}", ... } (ignored)
The status says which of two things is wrong, on every route and method alike:
-
401 β fix your key.
"The required header 'x-api-key' is missing"(no key sent) or"The API key you provided is not recognised"(not a key of this database β a typo, a rotated key, or a key of another database). -
403 β this key can't do that.
"not authorized to perform this operation": a real key without the permission β almost always a read-only key on a write (PUT/DELETEschema,POST .../_batch). Reads succeed, writes 403 β use a read-write key. On the Docker image, the read-only key isAPIKEYand the read-write key isREAD_WRITE_APIKEY; a write with the read-only key returns403with the codeforbidden.Available since: v2.11.2. Up to v2.11.1 a missing key answered
400on aGETand405 "method not allowed, supported methods: GET"on aPOST/PUT/DELETE; an unknown key403or405; and a read-only key on a write401. -
403 / 401 on an env path that reads fine on
masteris almost always an addressing bug β a dot instead of/env/, or a gateway rewriting the path. Confirm the env exists withGET /db/{db}/api/v2/_envs, then address it with the slash form above.
501 β $patterns / relate on a legacy table
Error: 501 β relate $patterns "supported on collections only".
Cause: the v2 _relate $patterns (itemset mining) endpoint requires a rep2 CollectionDb. Tables uploaded through the v1 API are legacy rep1 TableDb, and the v2 endpoint returns 501 for them by design. Two ways forward:
- Mine now, no rebuild: hit the legacy endpoint
POST /api/v1/_relateβ it mines rep1 tables today. - Everything on v2: re-upload the table as a
"type": "collection"(see Schema Migration).
Collection written by an older binary format
Error: 409 β This collection was written in binary format N, but this Aito build reads format M. β¦, or Env '<name>' is stored in an older on-disk format and is being migrated in the background β¦ retry shortly. On builds up to v2.8.0 the same situation surfaced as a 500 carrying a raw internal error such as slice [1090930175543, β¦] out of bounds or ListSeqRepIoType expected 8 entries in OrderDir, got 6 β a message about the decoder, on a query that has nothing to do with formats.
Cause: the collection's on-disk state was written by an older Aito build whose binary format has since changed. master migrates at server start; an env branch migrates the first time you use it, so the first request against a not-yet-migrated branch starts the rebuild and is refused while it runs. A state that arrives mid-session β typically an env restored from an older backup β behaves the same way. See server upgrades and env branches.
Fix: retry. The migration is already running in the background and the same request succeeds once it finishes; a large branch takes longer. Use POST /api/v2/data/{table}/repair (once per collection in that env) when you would rather rebuild immediately than wait, or if a migration reported a failure. Only rep2 collections are affected; rep1 type: "table" tables have their own versioning and migrate at open.
Available since: v2.8.1 for the 409 and the migrate-on-use behaviour. A read-only instance cannot run the migration at all and answers 503 instead, naming that as the reason.
Write contention: 409 write.contention
Error: 409 with the code write.contention and a Retry-After header β a positional write re-anchored 64 times without committing: concurrent writes kept moving the table; retry the request.
Cause: a large write that resolves its rows first and commits second β a _delete that matches many rows, or a keyed upsert of many rows β met a table that other writers had changed in between, re-resolved, and lost the race again on every attempt. It happens when a steady stream of small writes lands on the same table during one large write. Nothing was written: the request either commits whole or not at all.
Fix: retry the same request. It is safe: a write.contention answer means nothing was committed, so the retry cannot apply the write twice.
- Key on the code, not on
409alone. Other409s mean something else: an env being migrated (retry later), a collection in an older binary format (needs a repair, not a retry), a release that already exists. Retry automatically only onwrite.contention. - Back off with jitter. Treat
Retry-Afteras the minimum wait, then grow the wait on each attempt and add a random component (e.g. full jitter: wait a random time betweenRetry-Afterand an exponentially growing cap). Under heavy write churn, many clients retrying on a fixed one-second beat keep colliding with each other. Give up after a few attempts. - If it keeps happening, pause the small writers to that table for the duration of the large write, or split the large write into smaller batches.
Over the PostgreSQL interface the same condition is SQLSTATE 40001 (serialization failure), which database drivers and ORMs already retry.
Available since: v2.11.5. Earlier builds answer this case with 500.
Unknown field
Error: Unknown field 'fieldName' in table 'tableName'
Cause: the field name doesn't exist in the table schema.
- Field names are case-sensitive β check the spelling.
- Verify available fields:
GET /api/v2/schema/{table}. - For linked fields use dot notation:
product.name, notproductName.
// Wrong β field doesn't exist
{ "from": "products", "where": { "productName": "coffee" } }
// Correct
{ "from": "products", "where": { "name": { "$match": "coffee" } } }
Strict tables reject it; flexible collections match nothing
Whether an unknown where field is an error or a silent empty result
depends on the schema policy of what you are querying:
type | unknown field in where | why |
|---|---|---|
"table" (strict) | 400 β no such field 'x' in the where clause. This is a strict (type:table) schema, so an unknown field is rejected rather than silently ignored | the schema is fixed, so an unknown name is a mistake |
"collection" (flexible) | 200 with total: 0 | the schema grows with the data, so an unknown name is simply a column no row has yet |
The flexible case is the one that costs debugging time: a typo returns a
perfectly valid empty result, indistinguishable from "nothing matched". If a
query on a collection returns total: 0 unexpectedly, check the field names
against GET /api/v2/schema/{table} before assuming the data is missing.
A dotted path is different on both kinds of table: a path that does not
resolve through the schema β a segment the linked table does not have, or a hop
through a field that is not a link β is a typo in the path, not a column no row
has yet, so it is refused with 400, naming the segment that failed and the
fields available at that hop:
where: no such path 'company_id.NOPE': 'NOPE' is not a field of 'company_id'; its fields are: company_id, industry, name.
The same holds for a misspelt column inside a $refs filter
({ "$refs.contacts.company_id": { "$exists": { "rol": "CTO" } } } names
rol and the referring table). select and orderBy already refused an
unresolvable path.
On v2.11.0 and earlier a path cannot continue through a self-link (a column
whose link is this.<key>, such as prev β this.id): prev.user.tags is
refused the same way, naming prev. Filter on the self-link's own value instead.
Since v2.11.2, a path traverses self-links; see
Multi-hop paths.
Available since: v2.10.3. On v2.10.2 and earlier an
unresolvable dotted path in where, or a misspelt column in a $refs
filter, returned 200 with total: 0.
Available since: v2.7.0 for the strict rejection to apply
to pre-existing tables β those created through the v1 path and later
queried through v2, i.e. the whole population of a v1βv2 migration. Up to
v2.6.1 an unknown field on such a table returned 200 total=0 even though
the same query on a freshly-created type:"table" was correctly rejected.
If you migrated against an older build and relied on that silence, expect
those queries to start failing loudly β which is the point: they were
returning wrong results, not empty ones.
Unknown table
Error: 404 with the code not_found. A query names the table in the
message ("invoicess not found"); the data and schema endpoints say
"No such table: invoicess":
{ "kind": "error", "data": { "code": "not_found", "message": "invoicess not found" } }
It is usually a misspelt name, or a query sent before the table was created. On a
fresh local container no table exists yet, so a copied _predict fails this way
until you create the table and insert rows.
- Table names are case-sensitive.
- List tables:
GET /api/v2/schema. - Ensure the table was created before querying.
Type mismatch
Error: Type mismatch: expected 'expectedType', got 'actualType'
Use the JSON type the column declares:
// Wrong β price is a number column
{ "from": "products", "where": { "price": "3.95" } }
// Correct
{ "from": "products", "where": { "price": 3.95 } }
Strings for Text/String, numbers for Int/Decimal, true/false for Boolean, arrays for Array/Set.
_similarity errors
POST /api/v2/_similarity exists in v2. Earlier versions of this page said it
returns 404; it does not. A mistake in the body is a 400 that names the
problem, for example missing 'from' (table name) for an empty body. A 404
with "code": "not_found" means the path itself is wrong, such as a typo in the
endpoint name.
A valid call names the table and a {field: text} probe to rank rows against:
POST /api/v2/_similarity
{
"from": "products",
"similarity": { "name": "lactose free milk" },
"select": ["name", "$score"],
"limit": 3
}
{
"offset": 0,
"total": 42,
"hits": [
{ "$score": 8.28, "name": "Pirkka lactose-free semi-skimmed milk drink 1l" },
{ "$score": 7.31, "name": "Valio eilaβ’ Lactose-free semi-skimmed milk drink 1l" },
{ "$score": 1.91, "name": "Pirkka Finnish nonfat milk 1l" }
]
}
To combine similarity with other conditions, rank by it inside an ordinary query
with orderBy, next to any where:
{
"from": "products",
"where": { "price": { "$lt": 3 } },
"orderBy": { "$similarity": { "name": "coffee" } },
"limit": 10
}
No test set on collection evaluate
Error: 400 β evaluate on a collection with neither test nor testSource.
Cause: unlike v1 tables, a v2 collection evaluate has no implicit default test set. Omitting both is rejected rather than silently sampling 100 rows, so you never measure something you didn't scope.
// Wrong β nothing scopes the test set
{ "evaluate": { "from": "products", "predict": "category" } }
// Correct β a deterministic 20% fold
{ "test": { "$index": { "$mod": [5, 0] } },
"evaluate": { "from": "products", "where": { "name": { "$get": "name" } },
"predict": "category" } }
See the Evaluation guide (v2).
Unknown inference profile
Error: 4xx β Unknown config.ai profile 'name'.
Cause: config.ai accepts fast (flat), and (v1), group (v2, the default), and high. An unrecognised name is rejected loudly, never silently ignored β so a typo can't quietly downgrade your inference.
// Wrong
{ "from": "products", "predict": "category", "config": { "ai": "groupp" } }
// Correct
{ "from": "products", "predict": "category", "config": { "ai": "group" } }
Unsupported computed column
Error: 4xx describing the unsupported expression.
Cause: a computed / derived column expression that the v2 engine doesn't support fails loud rather than resolving to an empty value. This is by design β a silent empty here would read as "no match" and mask the real cause.
Empty candidate set
Error: Empty candidate set / No matching rows.
Your filters matched nothing:
- Broaden the criteria, or verify the filtered values exist.
- Use
$matchfor fuzzy text instead of exact match:
// Exact match may return nothing
{ "from": "products", "where": { "name": "coffee" } }
// Fuzzy
{ "from": "products", "where": { "name": { "$match": "coffee" } } }
Empty $search / $knn. Multi-term $search and $knn/$nn match all query tokens (AND). A term that appears in no row empties the whole result β near: "organic milk" returns nothing if "organic" is in no name, and $search "return policy refund" returns nothing if no single row has all three. Use OR or single terms ("$search": "return OR refund"), or the ranked search block, which orders by relevance instead of requiring every token: { "from": "prompts", "search": { "field": "prompt", "text": "refund" } }. Note that $search/$knn apply only to tokenized Text columns, not exact String columns.
Unknown operator
Error: Unknown operator '$operatorName'
- Operators are case-sensitive and start with
$. - Common v2 operators:
$match(full-text),$has(Set/Array contains),$gt/$gte/$lt/$lte,$and/$or/$not,$numeric,$similarity,$get,$context,$why. - See the Query Operator Reference (v2).
Invalid JSON syntax
Error: Invalid JSON at line X, column Y
Usual suspects: missing commas, trailing commas, single quotes, unquoted strings, unbalanced brackets.
// Wrong β trailing comma
{ "from": "products", "where": { "name": { "$match": "coffee" }, } }
// Correct
{ "from": "products", "where": { "name": { "$match": "coffee" } } }
Query timeout
Error: Query timeout / Request timeout
- Add
limitto bound the result set. - Use more specific
whereconditions. - After heavy writes,
optimizethe table and warm it β see Warming Strategies (v2).
_predict returns a flat / uniform distribution
Not an error, but a common surprise: every candidate comes back at ~1/N (e.g. 0.02 across 50 values). The evidence isn't discriminating β usually a high-cardinality target with only a few examples per value and only diffuse text signal, so there isn't enough per-value evidence to move off the prior.
- For open-ended "which known answer / article / template", use ranked
searchretrieval over the resolved history instead ofpredict(see Support Triage). - Reserve
predictfor lower-cardinality targets (type, category, route, assignee) where each class has many examples. - Add
"$why"β if the factor tree shows only a base rate and no evidence factors, the evidence was below the detection threshold.
Values look wrong after an engine upgrade
If a column reads back implausibly β a boolean that's all-true, a text column that lost its analysis, links that resolve empty β the data may have been ingested by a build that predated the relevant fix. Some fixes are on the write path and do not repair already-stored data. Re-ingest the environment and re-check before assuming a live engine bug.
Related
- Limits & Timeouts (v2) β request size, the 15-minute request timeout, pagination,
_batchfail-fast semantics, and the429backpressure model. - Destructive operations & data-loss safety β why a dropped
/env/{name}/segment silently writes to production, and how to avoid it. - Query Operator Reference (v2) Β· Inference (v2) Β· Evaluation (v2)
- v1 Common Errors β the classic-API version.