Query language¶
JSON query DSL, v1
Every read surface (REST /v1/query, /v1/snapshot, gRPC Query/Snapshot, and the MCP findRecords/semanticSearch tools) compiles the same JSON document into one logical plan. This page is the field-by-field grammar; the fixture corpus is the grammar of record.
Normative sources
.ai/specs/core-engine/2026-07-02-query-language.md, the AST in core-engine/src/query/ast.rs, and the pinned fixture corpus at core-engine/tests/fixtures/query_corpus.json (every valid and invalid shape, run against core and both SDKs in CI). Where this page and the corpus disagree, the corpus wins.
Shape at a glance¶
{
"v": 1,
"find": "RISK",
"where": {
"severity": { "$gte": 7 },
"$or": [
{ "status": { "$eq": "open" } },
{ "$and": [{ "owner": { "$exists": true } }] }
]
},
"atValidTime": "2025-05-01T00:00:00Z",
"atTxTime": "2025-06-01T00:00:00Z",
"expand": [{ "type": "OWNED_BY", "direction": "out", "depth": 1 }],
"vector": { "text": "supply chain delays", "model": "m1", "k": 20, "minScore": 0.5 },
"limit": 100,
"cursor": "opaque",
"trace": false
}
Deserialization is strict (deny_unknown_fields): an unknown top-level field, an unknown operator, or the wrong type at any position is rejected with a JSON pointer to the exact spot, not a generic 400.
Top-level fields¶
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
v | integer | no | 1 | Grammar version. Anything other than 1 is QUERY_INVALID at /v. |
find | string | yes | , | Label to scan. Empty string is rejected at /find. |
where | object | no | , | Predicate tree. See Predicates. |
atValidTime | µs integer or RFC3339 string | no | now | See Temporal coordinates. |
atTxTime | µs integer or RFC3339 string | no | now | See Temporal coordinates. |
expand | array | no | [] | Graph traversal steps. See Expand. |
vector | object | no | , | Semantic scoring clause. See Vector. |
limit | integer | no | 100 | 1..=1000. See Limit and cursor. |
cursor | string | no | , | Opaque continuation token from a previous page. |
trace | boolean | no | false | Emit a per-stage execution trace. |
The executor's logical plan runs the stages in a fixed order: TemporalResolve → Scan|IndexScan → Filter → Expand → VectorScore → Paginate → Trace.
Predicates (where)¶
Every predicate is an operator object keyed by property path, never a bare scalar:
Operators¶
| Operator | Meaning | Operand |
|---|---|---|
$eq | Equal | any scalar/array/object |
$ne | Not equal | any scalar/array/object |
$gt | Strictly greater | comparable value |
$gte | Greater or equal | comparable value |
$lt | Strictly less | comparable value |
$lte | Less or equal | comparable value |
$in | Membership | array, max 256 elements |
$exists | Property presence | boolean |
A property may carry multiple operators in one object (implicit AND): {"severity": {"$gte": 3, "$lte": 8}}.
Logical blocks: $and / $or¶
$and and $or take an array of predicate objects, non-empty, and nest up to depth 8:
{
"find": "RISK",
"where": {
"$or": [
{ "status": { "$eq": "open" } },
{ "$and": [{ "sev": { "$gte": 7 } }, { "owner": { "$exists": true } }] }
]
}
}
Only $and and $or are recognized logical keys; anything else starting with $ at that position (e.g. $nor) is unknown operator at that pointer.
Dotted paths¶
v1 supports one level of dotted-path predicates into Object values: "data.owner". Deeper paths are deferred (spec OQ-1) until schema inference tracks nested shapes.
Type semantics¶
Comparisons are type-strict against the engine's Value enum: Int and Float coerce against each other; comparing Str against Int is a validation error when the label's inferred schema knows the type, otherwise it is a runtime type mismatch that skips the row (and is counted in stats.typeMismatches, visible when trace: true).
Temporal coordinates¶
atValidTime and atTxTime each accept either a unix-microseconds integer or an RFC3339 string:
Omitting both means "current state." See The tritemporal model for what each axis means and worked examples of pinning one or both. An unparseable RFC3339 string is rejected at the field's pointer:
{ "code": "QUERY_INVALID", "message": "/atValidTime: invalid RFC3339 timestamp: ...", "request_id": "..." }
Expand¶
expand walks the graph from each primary result. Two forms:
| Field | Values | Default |
|---|---|---|
type | edge type string | all types |
direction | out | in | both | out |
depth | integer, 1..=3 | 1 |
Ceilings: at most 4 expand entries per query; depth outside 1..=3 is rejected at /expand/{i}/depth; more than 4 entries is rejected at /expand ("at most 4 expand entries allowed"). An unrecognized shape (bad direction value, or a stray field like dpeth) fails serde's untagged-enum match and reports /expand/{i}: data did not match any variant.
Expanded neighbors come back in the response's related array, grouped by which expand entry produced them, they are never merged into the primary records list.
Vector¶
vector adds semantic scoring, fused with any structured filter:
{ "find": "RISK", "vector": { "text": "supply chain delays", "model": "m1", "k": 20, "minScore": 0.5 } }
| Field | Type | Required | Notes |
|---|---|---|---|
text | string | exactly one of text/near | Embedded server-side; requires a configured embedding endpoint (501 UNIMPLEMENTED otherwise). |
near | array of float | exactly one of text/near | Pre-computed query vector. |
model | string | yes | Registered embedding model id. |
k | integer | yes | Neighbors to score, at least 1. |
minScore | float | no | Similarity floor, [0, 1]. |
alpha | float | no | Per-query fusion balance override, [0, 1] (see the confidence-decay design in Architecture). |
Validation:
- Supplying both
textandnear→QUERY_INVALIDat/vector("not both"). - Supplying neither →
QUERY_INVALIDat/vector("required"). k: 0→/vector/k("at least 1").minScoreoralphaoutside[0, 1]→/vector/minScoreor/vector/alpha.- An unknown field (e.g.
temperature) →/vector/temperature: unknown field.
Scores surface as score on each primary record and each related node when a vector clause is present. Primary results are then ordered by score descending rather than key order.
Vector queries do not paginate
vector results are k-bounded and similarity-ordered. Supplying a cursor alongside vector is QUERY_INVALID at /cursor, and vector responses never emit nextCursor.
Limit and cursor¶
limit: integer, 1..=1000, default 100. Outside range →QUERY_INVALIDat/limit("limit must be 1..=1000").cursor: an opaque, HMAC-signed continuation token from a previous page'snextCursor. It encodes the scan position, a scope hash (tenant-checked on resume), and a plan hash (the resumed query must be identical apart from the cursor). A tampered, foreign-tenant, or mismatched-plan cursor is rejected asBAD_CURSOR.
Trace¶
"trace": true asks the executor to attach a per-stage execution trace to the response (see stats in the REST API response shape: path, bitmapCandidates, rowsScanned, rowsFilteredOut, typeMismatches, rowsReturned, durationUs).
Ceilings reference¶
| Ceiling | Value | Configurable |
|---|---|---|
limit max | 1000 | no (v1 fixed) |
where nesting depth ($and/$or) | 8 | no |
$in elements | 256 | no |
expand entries | 4 | no |
expand depth | 1..=3 | no |
These are server ceilings, not tenant-configurable in v1. The MCP tool surface layers its own, tighter ceilings on top (findRecords clamps expand depth to 2 and limit to the server's row cap, and drops cursor entirely), see MCP tools.
Worked examples from the corpus¶
Valid shapes
{ "find": "RISK", "where": { "severity": { "$gte": 3, "$lte": 8 } } }
{
"find": "RISK",
"where": {
"$or": [
{ "status": { "$eq": "open" } },
{ "$and": [{ "sev": { "$gte": 7 } }, { "owner": { "$exists": true } }] }
]
}
}
Invalid shapes and their errors
| Query | Pointer | Message contains |
|---|---|---|
{"find": "RISK", "fnid": "typo"} | /fnid | "unknown field" |
{"find": "RISK", "where": {"severity": {"$gtee": 7}}} | /where/severity/$gtee | "unknown operator" |
{"find": "RISK", "where": {"$nor": [...]}} | /where/$nor | "unknown operator" |
{"v": 2, "find": "RISK"} | /v | "unsupported grammar version" |
{"limit": 10} (no find) | / | "missing field" |
{"find": ""} | /find | "must not be empty" |
{"find": "RISK", "where": 5} | /where | "expected an object" |
{"find": "RISK", "where": {}} | /where | "empty predicate" |
{"find": "RISK", "where": {"severity": 5}} | /where/severity | "operator object" |
{"find": "RISK", "where": {"$and": {"a": {"$eq": 1}}}} | /where/$and | "expected an array" |
{"find": "RISK", "where": {"$or": []}} | /where/$or | "at least one" |
{"find": "RISK", "where": {"status": {"$in": "open"}}} | /where/status/$in | "expected an array" |
{"find": "RISK", "where": {"owner": {"$exists": "yes"}}} | /where/owner/$exists | "expected a boolean" |
{"find": "RISK", "expand": [{"type": "X", "depth": 0}]} | /expand/0/depth | "1..=3" |
{"find": "RISK", "expand": [{"type": "X", "depth": 4}]} | /expand/0/depth | "1..=3" |
{"find": "RISK", "expand": ["A","B","C","D","E"]} | /expand | "at most 4" |
{"find": "RISK", "expand": [{"type": "X", "direction": "sideways"}]} | /expand/0 | "data did not match any variant" |
{"find": "RISK", "vector": {"text": "a", "near": [0.1], "model": "m1", "k": 5}} | /vector | "not both" |
{"find": "RISK", "vector": {"model": "m1", "k": 5}} | /vector | "required" |
{"find": "RISK", "vector": {"text": "a", "k": 5}} | /vector | "missing field" |
{"find": "RISK", "vector": {"text": "a", "model": "m1", "k": 0}} | /vector/k | "at least 1" |
{"find": "RISK", "limit": 0} | /limit | "1..=1000" |
{"find": "RISK", "limit": 1001} | /limit | "1..=1000" |
{"find": "RISK", "atValidTime": "May 1st 2025"} | /atValidTime | "invalid RFC3339" |
{"find": "RISK", "trace": "yes"} | /trace | "expected a boolean" |
The full, current set (60+ valid, 40+ invalid) lives in core-engine/tests/fixtures/query_corpus.json and is run against core and both SDKs in CI, treat it as the source of truth over this table.
Query DSL by transport¶
| Transport | How the DSL travels |
|---|---|
| REST | POST /v1/query and POST /v1/snapshot bodies (see REST API) |
| gRPC | QueryRequest.query_json / SnapshotRequest.query_json, raw bytes (see gRPC API) |
| MCP | findRecords and semanticSearch tool parameters, a constrained subset (see MCP tools) |
Related¶
- REST API -
/v1/query,/v1/snapshot,/v1/compareendpoints - gRPC API -
Query/SnapshotRPCs carrying this DSL as bytes - MCP tools -
findRecords/semanticSearchsubset and ceilings - The tritemporal model - what
atValidTime/atTxTimemean - Error reference - the full error code table