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. |
predict.busy | 429 | A predict over very many candidate values did not fit in memory beside other such predicts already running. Nothing failed; retry after Retry-After seconds. See large predicts. |
predict.too_large | 400 | A predict over so many candidate values that it could never fit in this server's memory for predicts, even alone. See large predicts. |
response_too_large | 413 | The response would be over the 10 MB response limit for synchronous responses, so it was withheld; the request itself was fine. The operation ran, so a write it made has been applied: don't blindly retry it. Job results are not capped. The message names the limit and the response's actual size. Ask for fewer rows with a smaller limit (or fewer fields with select), or page through the results with offset. On builds without this answer the connection closes with no status at all (behind a proxy, a 502); the response limit says which builds have it. |
row_limit_exceeded | 403 | A write would pass a row cap: the free tier's, or a licence's own. Not transient, so don't retry it; data.license names the licence state. See quotas. Available since v2.11.8; v2.11.7 and earlier answer 429. |
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.
Grouped _aggregate: groupBy and its refusals
_aggregate with groupBy computes its aggregates per group. Each group's
values are exactly what the ungrouped _aggregate answers with that group in
where, companions included, so one grouped call replaces a _batch of
per-group calls:
{ "from": "deals", "groupBy": "stage", "aggregate": ["$f", "amount.$sum"] }
The answer is a page of rows, one hit per group:
{ "offset": 0, "total": 3, "hits": [
{ "stage": "lost", "$f": 3, "amount.$sum": 12.0, "amount.$sum.samples": 2 },
{ "stage": "won", "$f": 3, "amount.$sum": 30.0, "amount.$sum.samples": 2 },
{ "stage": "open", "$f": 2, "amount.$sum": 0.0, "amount.$sum.samples": 0 } ] }
groupBytakes a field of the table, or an array of them: a String, Int, Long, Decimal, Boolean, Date or Timestamp field. Several keys group by the combinations that occur, not each key separately. A key is written as_querywrites the same field.- Rows with no value for a key form a group whose key is
null. totalis the number of groups.limitandoffsetpage the groups; with nolimit, every group is returned.- Groups come most rows first, ties by key ascending.
orderBytakes agroupByfield (ascending), an aggregate key (descending), or{"$asc": key}/{"$desc": key}. A missing value (anullkey, or an aggregate over no values) sorts last in either direction. - A group whose rows have no value for an aggregated field answers what the
ungrouped
_aggregateanswers over no values: a sum of0.0, and a mean, minimum and maximum of"NaN"(a JSON string, since NaN is not a JSON number), with.samples0. Check.samplesbefore reading the value. This is the one place_aggregateand_sqldiffer:_sql'sGROUP BYanswersNULLfor both the sum and the average over no values, as SQL does.
Refused with 400 and the code aggregate.groupby_unsupported:
- a Text key: Text is indexed by its tokens, so a row would belong to several
groups. Group by a String field, or use
_sql. - a multi-valued key (an array or set), a vector key, and any other non-scalar type.
- a key through a link (
company.segment): not supported yet._sqlgroups by the path:SELECT company.segment, count(*) FROM deals GROUP BY company.segment.
Also refused:
- more than 10,000 groups:
400aggregate.too_many_groups. A group-by on a near-unique key is a scan; narrow thewhere, group by a coarser field, or page the rows with_query. - grouping more rows than this server allows for one aggregate (about a quarter
of its memory):
400aggregate.too_large. The rows are counted before any is grouped. Narrow thewhere, or run per-group aggregates in a_batch. - an aggregate named like a
groupByfield ({"stage": "$f"}grouped bystage):400, since a hit holds both. groupByon a legacytype: "table"(engine v1):501operation.unsupported. Use_sqlthere:
SELECT stage, count(*) AS n FROM deals GROUP BY stage
Available since: v2.11.9. On v2.11.8, groupBy is refused with 400. On
v2.11.7 and earlier, a collection ignores groupBy and returns the total over all
matching rows with 200, which looks like an answer. A legacy type: "table" refuses it.
503 write.deadline: the write ran past its time and was not committed
A write request (insert, batch, _modify, _delete, a schema change, or an env/release/ref admin write) that runs longer than the server's request timeout is answered with 503 and data.code: "write.deadline". The commit that was in progress is refused: nothing from it is applied, before or after the answer. Retry the request, split it into smaller ones, or submit it as an async job (POST /api/v2/jobs/β¦), which has no request timeout. The time counts from when the request body has been fully received, so a slow upload does not use up the write's time.
A multi-operation _modify whose operations commit one by one says in the message how many of its operations were committed before the deadline.
Available since: v2.11.8. On v2.11.7 and earlier, a write that outlived the timeout was answered with a bare 503 while it kept running and committing afterwards.
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. A403with the coderow_limit_exceededis a different thing, a row cap rather than a key problem: see quotas.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. -
Every write refused on a self-hosted server. A server checking its own keys (
KEY_VALIDATION=LOCAL, as the Docker image does) with noREAD_WRITE_APIKEY, or with it set to the empty string, has no write key and refuses every write. Its startup log saysREAD_WRITE_APIKEY is not set. Set the variable and restart. With neitherREAD_WRITE_APIKEYnorAPIKEYset, the server refuses to start; to run without keys, setDISABLE_API_KEY_AUTH=trueandDISABLE_WRITE_API_KEY_AUTH=true. An emptyx-api-keyheader is never a key.Available since: v2.12.0. Up to v2.11.9 a server with an unset key generated one at startup and printed it in its log, and that key could write when only
APIKEYwas set. -
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.
A predict over very many candidate values: 429 predict.busy and 400 predict.too_large
A predict scores every candidate value of its target and keeps some state for each one until the answer is built. Predicting a column with hundreds of thousands of distinct values (a near-unique number such as a price or an amount, an id, a free-text field) therefore needs a lot of memory: roughly 2β3 KB per candidate value. The server admits such a predict by its estimated memory, so one request can never take the server down for everyone.
429 predict.busy, with aRetry-Afterheader: the predict fits on its own, but not beside other large predicts that are running now. It waited for them, in arrival order (up to 30 seconds by default), and they did not finish in time, or too many others were already waiting. Retry the same request. Nothing was changed.400 predict.too_large: the predict could never fit, even alone. The message names the number of candidate values and the estimate.
To make a large predict small, do one of:
- For a number (an amount, a price): use
_estimatefor one value, or store a range bucket beside the number when you write the row ("amountBand": "1000-5000") and predict the bucket. A near-unique number makes a poor predict target anyway: each value occurs about once, so its prior is flat and the exact answer is close to a guess (see near-unique targets). - Predict a lower-cardinality field.
- Narrow the candidates with a
whereon the predicted field itself, or predict through a link whose target table is smaller. - Optimize the table. A table with many unmerged segments costs more per candidate.
- Use a server with more memory. A self-hosted server sets how much of its heap large predicts may hold together with the system property
aito.predict.heapPercent(default75), the wait withaito.predict.admissionWaitMs(default30000), how many large predicts may wait at once withaito.predict.maxWaiters(default4; more are answered429at once), and the size below which a predict is not counted withaito.predict.heavyFloorBytes(default 256 MB).
Ordinary predicts, up to well over a hundred thousand candidate values, are not affected. A query that runs several large predicts, and the members of one _batch, share one allowance: they run one after another and never refuse each other.
- On
/api/v1the same two answers carry the same codes, statuses andRetry-After. - Over the PostgreSQL interface,
predict.busyis SQLSTATE53000(insufficient resources, worth retrying) andpredict.too_largeis54000(program limit exceeded, a retry will not help). - In an async job, a large predict that was busy when the job ran leaves the job failed. Its
/resultanswers429 predict.busywithoutRetry-After, because fetching the result again returns the same answer: resubmit the job instead.
Available since: v2.11.8. On v2.11.7 and earlier, two very large predicts at the same time can run the server out of memory.
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.
A path continues through a self-link (a column whose link is this.<key>,
such as prev β this.id) like any other link: prev.user.tags and
prev.prev.weekday resolve; see Multi-hop paths. On
v2.11.1 and earlier such a path is refused the same way, naming prev.
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.
A link between a v1-engine and a v2-engine table
Available since: v2.11.9. Earlier builds refuse the same queries with a message that does not name the cause: an empty field list ("its fields are: "), "field not found", "could not be opened", or an internal class name.
Error: 400, e.g.
path 'context.user': hop 1 'context' links 'impressions' (v1 engine) to 'contexts' (v2 engine),
and a link cannot cross engines. Migrate 'impressions' to v2: POST /api/v2/jobs/data/_modify
with {"operations": [{"migrate": "impressions", "engine": "v2"}]}
Cause: one table of a linked pair has been migrated to the v2 engine and the other has
not. A link does not cross engines, in either direction, so a query that follows it is
refused. That covers a path through the link (where, select, relate, a recommend
goal), and on the v1 API also a predict, recommend or match of the link value itself,
where the v1 engine opens the linked table. Queries that do not follow the link keep
working.
Fix: migrate the table the message names, or migrate all linked tables together in one
_modify request. The jobs/ form returns at once, so a
large table does not hit the request timeout.
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" } } }
Content after the body
Available since: v2.11.8. On v2.11.7 and earlier, content after a body was ignored.
A request body is one JSON value. Anything but whitespace after it, such as stray text or a second copy of the body, is refused with a 400 (json.malformed on v2) instead of being answered as its first value. In a v2 NDJSON stream, a line with content after its object refuses the whole request, and nothing is written.
Duplicate keys
Error: json.malformed: Duplicate field 'columns' (line X, column Y)
A JSON object with the same key twice is refused by the schema routes (PUT /schema, PUT /schema/{table}, PUT /schema/{table}/{column}, _copy, _plan, _apply, _migrate, and their v1 counterparts). (A query with a duplicate key is refused too, as query.invalid with "duplicate field".) It applies at any depth: columns twice, the same column name twice, or type twice in one column. A JSON reader keeps only one of the values, so such a schema used to be accepted with columns silently missing; it is now a 400 that names the key. Send each key once, and merge column sets before sending:
// Wrong β "columns" twice: only the second set would have been kept
{ "type": "collection", "columns": { "id": { "type": "Int" } }, "columns": { "name": { "type": "String" } } }
// Correct
{ "type": "collection", "columns": { "id": { "type": "Int" }, "name": { "type": "String" } } }
Available since: v2.11.8 for the schema routes. Earlier builds accept a duplicate key in a schema body and keep the last value.
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.