Skip to content

The query AST ​

A StepQL query is a single object. Every field is optional; which ones you set decide what the query does. This page breaks down each field — what it accepts and where the full rules live.

Shape at a glance ​

yaml
from: <path> # base path into the step output ($ if omitted)
join: { step, on, select, type } # graft another step's columns onto each row
where: <condition> # leaf comparison(s), optionally nested with all/any/not
groupBy: <field> | [<field>, …] # group rows by one or more fields
aggregate: count | { op, field } | { alias: spec, … } # count/sum/avg/min/max
having: <condition> # filter the grouped/aggregated rows
select: <fields> # project — single field, list, or alias map
sort: <field-or-list> # asc by default; { field, dir } for desc
limit: <integer>
distinct: true # dedupe the result rows
jsonata: <string> # escape hatch (mutually exclusive with structured fields)

The order you write the keys in doesn't matter — the compiler emits stages in a fixed order (see Evaluation order below).

The fields ​

from ​

The base path into the queried step's output — a dotted path like entities or data.items. Omit it and the base is the whole step output ($). Every other field operates on the rows from selects.

yaml
from: entities # or data.items; omit for the whole step output

join ​

Graft another step's columns onto each row before filtering and projection, so later fields can use them. Shape: { step, on, select, type }. Full semantics — key matching, left vs inner, collisions — in Cross-step queries.

yaml
join: { step: classify, on: id, select: [label], type: left }

where ​

Keep only rows that match a condition. A condition is either:

  • a leaf — { field, <operator> }, exactly one operator per leaf, or
  • a group — all, any, or not, each nesting more conditions.

The operator catalog and nesting rules are in Filtering.

yaml
where:
  all:
    - { field: type, eq: Actor }
    - { field: score, gte: 0.5 }

groupBy ​

Collapse rows into groups by one field, or a list of fields for a composite key. Setting it puts the query in aggregate mode.

yaml
groupBy: type # or [type, status] for a composite key

aggregate ​

Reduce each group (or the whole set) to a value: count, { op, field } for sum/avg/min/max, or an { alias: spec, … } map for one column per alias. Setting it puts the query in aggregate mode. See Grouping & aggregation.

yaml
aggregate: { n: count, total: { op: sum, field: score } }

having ​

Filter grouped rows by their aggregate columns — the aggregate-mode counterpart to where, with the same condition grammar. Only valid in aggregate mode. See Multiple aggregates, and having.

yaml
having: { field: n, gt: 1 } # references an aggregate column

select ​

Project the output: a single field, a list of fields, or an alias map that renames columns and can apply computed ops (upper, round, count, …). See select shapes.

yaml
select: { who: { upper: name }, tags: { count: tags } }

sort ​

Order rows: a field name (ascending), { field, dir: desc } for descending, or a list to sort by several keys in turn. See sort shapes.

yaml
sort: [{ field: score, dir: desc }]

limit ​

An integer cap on the number of rows, applied after sort.

yaml
limit: 3

distinct ​

true dedupes the result rows by deep equality. Works in both modes. See distinct.

yaml
select: type
distinct: true

jsonata ​

The escape hatch: a raw JSONata string, run verbatim. Mutually exclusive with every structured field above — set jsonata or the structured fields, never both. See Raw JSONata.

yaml
jsonata: '$sort(entities, function($a, $b) { $a.score < $b.score }).name'

Two modes ​

A query is in aggregate mode when it sets groupBy and/or aggregate; otherwise it's in row mode. The mode decides which fields apply and the order they run in — having exists only in aggregate mode, and grouping happens after where but before projection.

Evaluation order ​

Whatever order you write the keys, the compiler emits the stages like this:

  • Row mode: base → where → sort → limit → select → distinct
  • Aggregate mode: base → where → groupBy/aggregate → having → sort → limit → distinct

(base is from applied to the queried step, with any join grafted on first.)

The schema ​

The AST is one Zod schema (QueryAst), which is also exported as a JSON Schema (queryAstJsonSchema) from @stepbook/query — so the same definition drives the compiler, runtime validation, and editor tooling. An invalid combination (a where leaf with no operator, jsonata alongside structured fields) is rejected up front rather than producing a broken query.

Released under the MIT License.