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'"
  }
}
  • kind is always the literal string "error" β€” the cheapest check for "did this request fail" (independent of the HTTP status).
  • data.code is a stable, machine-readable code (a dotted namespace) β€” branch on this in an SDK. Codes are contractual; the message wording is not.
  • data.message is 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):

codeHTTPCause
query.invalid400The query shape is wrong β€” a bad from, an unrecognised structure.
request.invalid400A value failed validation (bad type, out-of-range argument).
proposition.unsupported400A $-operator isn't applicable to the named field.
query.empty_population400A _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.malformed400The request body isn't valid JSON.
not_found404A referenced table (the from, or a linked table) doesn't exist.
operation.unsupported501A 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".
internal500An unexpected server error. Report it.

Data-write endpoints (_delete, _modify, .../batch, .../stream, .../import) and schema endpoints add:

codeHTTPCause
data.bad_request400A write was rejected (e.g. a vector dimension mismatch, or on_conflict on a keyless table).
import.failed Β· table.exists Β· schema.not_collection400Import problems: the import failed, the target table already exists, or it isn't a collection.
write.contention409Concurrent 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_failed400The schema operation was rejected (the message says why).
not_found404The 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, _predict hits, filtered _query) return the v1-compatible body { "offset": …, "total": …, "hits": [ … ] } β€” no kind field (an absent kind means 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 of kind β€” 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-key missing, or a key the server does not know: 401, on every route and method (see 401 / 403).
  • content-type: application/json missing or different: 415, whose message starts Content-Type must be application/json and names the header to send. curl -d sends application/x-www-form-urlencoded unless you say otherwise, so this is the usual cause. On /api/v2 the body is the v2 error envelope with the code unsupported_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/DELETE schema, POST .../_batch). Reads succeed, writes 403 β€” use a read-write key. On the Docker image, the read-only key is APIKEY and the read-write key is READ_WRITE_APIKEY; a write with the read-only key returns 403 with the code forbidden.

    Available since: v2.11.2. Up to v2.11.1 a missing key answered 400 on a GET and 405 "method not allowed, supported methods: GET" on a POST/PUT/DELETE; an unknown key 403 or 405; and a read-only key on a write 401.

  • 403 / 401 on an env path that reads fine on master is almost always an addressing bug β€” a dot instead of /env/, or a gateway rewriting the path. Confirm the env exists with GET /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 409 alone. Other 409s 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 on write.contention.
  • Back off with jitter. Treat Retry-After as the minimum wait, then grow the wait on each attempt and add a random component (e.g. full jitter: wait a random time between Retry-After and 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, not productName.
// 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:

typeunknown field in wherewhy
"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 ignoredthe schema is fixed, so an unknown name is a mistake
"collection" (flexible)200 with total: 0the 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 $match for 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 limit to bound the result set.
  • Use more specific where conditions.
  • After heavy writes, optimize the 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 search retrieval over the resolved history instead of predict (see Support Triage).
  • Reserve predict for 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.