Skip to content

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 …):

json
{
  "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"] }
  ]
}
yaml
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}
json
[
  { "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:

OperatorMeaning
eq / neequals / not equals
gt / gte / lt / ltenumeric 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: truefield 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:

yaml
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

yaml
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):

yaml
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

yaml
sort: score # ascending by score
sort: [{ field: score, dir: desc }] # descending by score
sort: [type, { field: score, dir: desc }] # type asc, then score desc

limit

limit: <integer> keeps the first N rows after sorting.

distinct

distinct: true dedupes the result rows (deep-equality), here or in aggregate mode:

yaml
from: entities
select: type
distinct: true # the set of distinct `type` values
$distinct((analyze.entities).type)
json
["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.)

Released under the MIT License.