Filtering, sorting & projection
The everyday shape of a query: take a step's rows, keep the ones that match, order them, and project the fields you want.
The examples on this page run against an analyze step whose output is an entities array (stepbook q analyze …):
{
"entities": [
{ "name": "Ada Lovelace", "type": "Actor", "score": 0.91, "tags": ["math", "compute"] },
{ "name": "Hypatia", "type": "Actor", "score": 0.62, "tags": ["math"] },
{ "name": "Acme Corp", "type": "Organization", "score": 0.40, "tags": [] },
{ "name": "Babbage Ltd", "type": "Organization", "score": 0.77, "tags": ["compute"] }
]
}from: entities
where:
all:
- { field: type, eq: Actor }
- { field: score, gte: 0.5 }
sort: [{ field: score, dir: desc }]
limit: 3
select: [name, score]Reads as: out of entities, keep ones whose type equals Actor AND whose score ≥ 0.5; sort by score descending; take the top 3; project name and score. That compiles to JSONata (what --explain prints) and runs:
((((analyze.entities)[(type = "Actor" and score >= 0.5)])^(>score))[[0..2]]).{"name": name, "score": score}[
{ "name": "Ada Lovelace", "score": 0.91 },
{ "name": "Hypatia", "score": 0.62 }
]from is a path within the step's output, which is why the JSONata is rooted at analyze.entities. The result shown is the faithful query value — what --json and the raw view return. The table view (and the dashboard grid) wrap bare scalars in a value column for display, but the value itself is whatever JSONata produced.
where operators
Each leaf condition names a field plus exactly one operator:
| Operator | Meaning |
|---|---|
eq / ne | equals / not equals |
gt / gte / lt / lte | numeric or lexicographic comparison |
between: [min, max] | inclusive range (min ≤ field ≤ max) |
in: [a, b, c] | field's value is one of the listed values |
has: <value> | field (an array) contains this value |
matches: <regex> | regex test against the field's string value |
exists: true | field is present (or false for absent) |
Nest with all, any, not. Each condition is exactly one of these (or a leaf), so to combine them — e.g. "(Actor or Organization) and not deleted" — nest them rather than putting two at the same level:
where:
all:
- any:
- { field: type, eq: Actor }
- { field: type, eq: Organization }
- not: { field: status, eq: deleted }The in operator can also take a sub-query that derives its set from another step — see Cross-step queries.
select shapes
select: name # one field
select: [name, type] # list of fields
select: { who: name, kind: type } # alias map (rename on the way out)In the alias-map form, a value can also be a computed op over a field — one of upper / lower / trim (string), round / abs (number), or count (array length):
select:
name: { upper: name } # ADA LOVELACE
tagCount: { count: tags } # array length
score: { round: score } # 9.4 → 9
who: name # plain fields still work alongside(There's no generic length — it's type-ambiguous, so use count for arrays.)
sort shapes
sort: score # ascending by score
sort: [{ field: score, dir: desc }] # descending by score
sort: [type, { field: score, dir: desc }] # type asc, then score desclimit
limit: <integer> keeps the first N rows after sorting.
distinct
distinct: true dedupes the result rows (deep-equality), here or in aggregate mode:
from: entities
select: type
distinct: true # the set of distinct `type` values$distinct((analyze.entities).type)["Actor", "Organization"](A single-field result is a plain array. The table view renders it as a one-column value table; --json and the raw view give you the array directly.)