# The TQ query language


TQ is the S-expression query language cairn evaluates over the [task graph](/cairn/concepts/the-task-graph). A query is one source form, or a source threaded through pipeline steps with `->`. It is read with `*read-eval*` disabled and walked, never evaluated, then lowered onto SQL over the task projection plus a few in-memory folds. Every result renders to text before it leaves the runner, so nothing but a string crosses the tool boundary, and every failure is a structured `Query error:` or `Parse error:` rather than a backtrace.

Two tools run TQ: [`task_query`](/cairn/reference/tools#task-query) reads, and [`task_query_write`](/cairn/reference/tools#task-query-write) reads and writes. The language is the same on both; one dynamic gate decides whether a mutation form is allowed.

## Synopsis

```text
QUERY  := SOURCE | (-> SOURCE STEP...)
SOURCE := (all) | (active) | (dormant) | (current)
        | (node "<substr>") | (query "<name>")
        | (schema) | (views) | (fields) | (edges)
        | (define! "<name>" QUERY) | (undefine! "<name>")   # write surface only
STEP   := (<keyword> ARG...)
```

A query is exactly one S-expression. The bare `->` head opens a pipeline whose first element is a source and whose rest are steps applied left to right. A step is a keyword form like `(:follow :phase-of)`; a bare keyword such as `:ids` is rejected — every operator is a form. A source carrying the wrong arity or an empty string argument errors rather than returning an empty set.

## Read versus write

The `!`-suffix marks every mutation. `task_query` binds the gate off, so any `!`-form — a `(:set-status! ...)` step or a `(define! ...)` source — is refused:

```text
Query error: Mutations need the task_query_write surface; task_query is read-only.
```

`task_query_write` binds the gate on and accepts them. The gate keys on the `!`-suffix and the step's `:mutation` kind, not on which tool is registered, so a write can never slip through the read surface. Inside set-operation operands and inside `(define! "name" Q)` the gate is forced off regardless of surface: you combine and define over selections, you do not mutate inside them. A mutation form within an operand is refused even under `task_query_write`. See [Reads, writes, and capabilities](/cairn/concepts/reads-writes-and-capabilities).

## Sources

A source is a nullary producer of a node-set. A node is a slug paired with its hydrated properties. The four broad sources exclude the reserved `@`-prefixed namespace (the `@cairn` view node); real task slugs are date-prefixed, so nothing a user created is dropped, and `(node ...)` and `(current)` still address reserved nodes.

| Form | Yields | Notes |
| --- | --- | --- |
| `(all)` | Every task. | Excludes the reserved namespace. |
| `(active)` | Tasks with status `open`, `active`, or `blocked`. | The live half of the status enum. |
| `(dormant)` | Tasks with status `completed` or `abandoned`. | The settled half. |
| `(current)` | The current task. | Errors when no task is selected: `This query needs a current task; select one first.` |
| `(node "substr")` | Tasks whose slug contains the substring. | Case-insensitive; a non-empty pattern is required. |
| `(query "name")` | The named view resolved to its node-set. | An unknown name errors and enumerates the available views. |
| `(schema)` | The source/step/write grammar as a node-set. | Reflective; see below. |
| `(views)` | The named views, built-in and user, as a node-set. | Reflective. |
| `(fields)` | The queryable fields and their value types. | Reflective. |
| `(edges)` | The edge vocabulary and each type's class. | Reflective. |
| `(define! "name" Q)` | Records `Q` as a named view; returns `Q`'s tasks. | Write surface only. |
| `(undefine! "name")` | Removes a user view, restoring any built-in; returns nothing. | Write surface only. |

`recent` is a **view**, not a source: reach it with `(query "recent")`. There is no `(recent)` source form. The list of resolvable view names lives in [Views](/cairn/reference/views).

## Steps

A step transforms or shapes the running node-set. Transformers map a node-set to a node-set and compose; shapers terminate the pipeline into a final value (a projection, a group, a slug list, or a count); mutations write and return their input so they keep composing. A shaper must be last — a transformer after a terminal value errors with `This step expects a set of tasks, but the previous step produced a final value.`

**Transformers** map a node-set to a node-set, so they chain.

### `(:follow EDGE)`

The tasks one `EDGE` hop forward. Structural edges (`phase-of`, `forked-from`) traverse the parent foreign key; lateral edges (`depends-on`, `related`) traverse the edges table. The edge is validated first, so an unknown type errors even over an empty input — the edge is a property of the query, not of the data.

```lisp
(-> (node "auth") (:follow :phase-of) (:ids))
```

### `(:back EDGE)`

The tasks one `EDGE` hop backward — the inverse of `:follow`. From a forked child, `(:back :phase-of)` reaches its parent.

```lisp
(-> (node "phase-3") (:back :phase-of) (:ids))
```

### `(:where PRED)`

The tasks satisfying predicate `PRED`. A predicate that references a non-local field auto-enriches the set on demand; a predicate over a base field reads it directly without enrichment. See [Predicates](#predicates).

```lisp
(-> (active) (:where (= :status "blocked")) (:ids))
```

### `(:sort FIELD [:asc|:desc])`

The node-set ordered by `FIELD` — numeric when the field holds numbers, lexical otherwise. The direction is optional and defaults to `:desc`; pass `:asc` to ascend. A direction that is neither `:asc` nor `:desc` is a structured error with a suggestion, not a silently ignored argument.

```lisp
(-> (active) (:sort :updated-ts))         ; most-recent first — the default
(-> (active) (:sort :slug :asc) (:ids))   ; A→Z by slug
```

```text
Query error: :sort needs :asc or :desc; did you mean :asc?
```

### `(:take N)`

The first `N` tasks. `N` must be an integer; a negative `N` clamps to zero.

```lisp
(-> (active) (:sort :obs-count) (:take 10))
```

### `(:enrich)`

Add counts and promoted metadata (`:obs-count`, `:edge-count`, and any metadata keys) to each node, so fields beyond the hydrated base become readable and printable.

```lisp
(-> (current) (:enrich))
```

### `(:union Q)`

Tasks in the pipeline or in sub-query `Q`, deduplicated by slug. `Q` must itself yield a node-set, evaluated read-only.

```lisp
(-> (active) (:union (dormant)) (:count))
```

### `(:intersect Q)`

Tasks in both the pipeline and sub-query `Q`.

```lisp
(-> (all) (:intersect (active)) (:count))
```

### `(:minus Q)`

Pipeline tasks not in sub-query `Q`.

```lisp
(-> (all) (:minus (active)) (:ids))
```

### `(:or-else Q)`

The pipeline if it holds any task, else sub-query `Q`. Used by the `plan` view to fall back from a root's children to its siblings.

```lisp
(-> (current) (:follow :phase-of) (:or-else (active)))
```

### `(:closure EDGE...)`

The forward transitive closure over one or more edge types, cycle-safe and bounded to depth 5. The starting nodes are excluded unless a cycle leads back to one. Each edge is validated eagerly. The `knowledge` view is `(:closure :phase-of :depends-on :related)`.

```lisp
(-> (current) (:closure :phase-of :depends-on :related) (:enrich))
```

**Shapers** end the pipeline in a final value and must come last. The text below each shaper is exactly what the runner returns.

### `(:select FIELD...)`

Project the named fields; at least one field is required. Every selected field is validated against the [field vocabulary](/cairn/reference/edges-statuses-and-fields), so an unknown field is a structured error with a nearest-match suggestion rather than a silently dropped column:

```lisp
(-> (all) (:select :updated-at))
```

```text
Query error: Unknown field :updated-at; did you mean :updated-ts?
```

A projected field that is not already on the node — a count or a promoted metadata key — is enriched on demand, so it shows its value instead of nothing. The result is rectangular: every selected field prints on every row, an absent value as the empty-set glyph `∅`, and a [timestamp field](/cairn/reference/edges-statuses-and-fields#fields) as its raw universal-time integer followed by the decoded UTC date.

```lisp
(-> (node "auth") (:select :display-name :status))
```

```text
1 task:
- 2026-04-02-auth-rework  display-name=Auth rework  status=active
```

```lisp
(-> (node "rate-limit") (:select :slug :obs-count :updated-ts :parent))
```

```text
1 task:
- 2026-04-05-rate-limit  slug=2026-04-05-rate-limit  obs-count=4  updated-ts=3984388200 (2026-04-05)  parent=∅
```

### `(:group-by FIELD)`

Bucket the node-set by `FIELD` value, largest bucket first.

```lisp
(-> (active) (:group-by :status))
```

```text
2 groups:

## active (2)
- 2026-04-02-auth-rework [Auth rework] (active)
- 2026-04-05-rate-limit (active)

## blocked (1)
- 2026-03-30-token-store (blocked)
```

### `(:ids)`

The slugs only, one per line, under a count line.

```lisp
(-> (active) (:ids))
```

```text
3 tasks:
- 2026-03-30-token-store
- 2026-04-02-auth-rework
- 2026-04-05-rate-limit
```

The count line pluralizes: a one-task result reads `1 task:`, and an empty result is the single line `No matching tasks.`

### `(:count)`

The cardinality, as a bare number with no surrounding text.

```lisp
(-> (all) (:count))
```

```text
2
```

**Mutations** are transformers that also write. A mutation records one event per real task through the durable [event log](/cairn/concepts/events-projection-and-reconcile) and returns its input set, so `(:count)` after it reports the size of the set it ran over — equal to the number of real tasks touched when no synthesized nodes are present. A synthesized node — a grammar node from a reflective source — carries no task id and is passed over, never minting a phantom, yet stays in the returned set. Events are append-only and their projection effects converge, so re-running a mutation is safe. Every mutation requires the `task_query_write` surface.

### `(:set-status! STATUS)`

Set each task's status. `STATUS` must be one of `open`, `active`, `completed`, `abandoned`, `blocked`.

```lisp
(-> (active) (:set-status! "completed") (:count))
```

```text
3
```

### `(:set! :key "value")`

Set a metadata field on each task. The key is a field keyword; it refuses `:slug` and `:status` (status has its own step).

```lisp
(-> (current) (:set! :objective "ship it"))
```

### `(:link! :edge "target")`

Add an edge from each task to the constant `target` slug. `:edge` is one of `phase-of`, `depends-on`, or `related` — the same closed enum the [`task_link`](/cairn/reference/tools) tool takes. A `depends-on` or `related` link lands in the edges table; a `phase-of` link is structural and folds into the parent foreign key rather than the lateral table, exactly as the [edges reference](/cairn/reference/edges-statuses-and-fields) describes. A task equal to the target is skipped — a task does not edge to itself.

```lisp
(-> (node "phase-2") (:link! :depends-on "phase-1"))
```

### `(:unlink! :edge "target")`

Remove the edge from each task to `target`. `:edge` is one of `phase-of`, `depends-on`, or `related`, matching `(:link!)`.

```lisp
(-> (node "phase-2") (:unlink! :depends-on "phase-1"))
```

## Predicates

A predicate is the argument to `(:where ...)` and to quantifiers. It is a form of `(slug props)` and composes with boolean combinators.

| Form | Meaning |
| --- | --- |
| `(= LHS v)` | `LHS` equals literal `v`. `LHS` is a field keyword or `(count TRAV)`. |
| `(has :field)` | `:field` is present and non-null on the node. |
| `(matches :field "substr")` | `:field`'s value contains the case-insensitive substring. |
| `(> LHS n)` | `LHS` is greater than real number `n`. |
| `(< LHS n)` | `LHS` is less than `n`. |
| `(>= LHS n)` | `LHS` is at least `n`. |
| `(on :ts "YYYY-MM-DD")` | timestamp field `:ts` falls on the given UTC day. |
| `(since :ts "YYYY-MM-DD")` | `:ts` is on or after the given UTC day. |
| `(before :ts "YYYY-MM-DD")` | `:ts` is before the given UTC day. |
| `(and P...)` | Every sub-predicate holds. |
| `(or P...)` | Some sub-predicate holds. |
| `(not P)` | `P` does not hold. |
| `(all TRAV P)` | `P` holds for every task one `TRAV` hop away. |
| `(any TRAV P)` | `P` holds for some task one `TRAV` hop away. |
| `(none TRAV P)` | `P` holds for no task one `TRAV` hop away. |

In a quantifier, `TRAV` is `(:follow EDGE)` or `(:back EDGE)`. The traversal's edge is validated eagerly, so an ill-typed quantifier errors even over an empty focus set. Over a focus node with no neighbours, `all` and `none` are vacuously true and `any` is false.

Numeric comparisons are type-checked. The left side of `>`, `<`, or `>=` must be a numeric field (one of `created-ts`, `updated-ts`, `status-ts`, `obs-count`, `edge-count`) or `(count TRAV)`, and the operand a real number; comparing a text field numerically is an error, not a silent empty match. `(count TRAV)` is the cardinality of a relative traversal, usable wherever a numeric left side is.

```lisp
(-> (node "release") (:where (> (count (:follow :depends-on)) 1)) (:ids))
```

The full field vocabulary and which fields are textual versus numeric is in [Edges, statuses, and fields](/cairn/reference/edges-statuses-and-fields).

### Dates and timestamps

`created-ts`, `updated-ts`, and `status-ts` are `:timestamp` fields: CL universal-time, an integer count of seconds since `1900-01-01 UTC`. They order as integers and read as dates. The `on` / `since` / `before` predicates window the clock by a calendar day so a date intent is one predicate, not a reverse-engineered range. Each parses a strict `YYYY-MM-DD` operand at UTC midnight; `on` matches the 24-hour day `[day, day+1)`, `since` matches `≥ day`, and `before` matches `< day`.

```lisp
(-> (active) (:where (on :updated-ts "2026-06-24")) (:ids))
(-> (active) (:where (since :created-ts "2026-06-01")) (:count))
```

A date predicate is gated to timestamp fields, and a malformed date is a structured error rather than a silent empty result:

```text
Query error: on needs a timestamp field; :status is text.
Query error: "not-a-date" is not a YYYY-MM-DD date.
```

Because a timestamp orders as an integer, the numeric predicates compare it too. An **integer** operand to `>`, `<`, or `>=` against a timestamp field is read as Unix seconds and lifted onto the same universal-time clock the write path uses, so a Unix-epoch literal lands where you mean it instead of below every value. `(< :updated-ts 2500000000)` compares against the lifted instant, not the raw integer.

### Unknown names suggest a fix

Every dispatched name — a field, a predicate, a step, a source, an edge type — is validated against its vocabulary, and an unknown one is a structured error carrying the nearest match within a small edit distance, or the full list when nothing is close. The query never silently misfires on a typo.

```text
Query error: Unknown predicate matchez; did you mean matches?
Query error: Unknown step :zelect; did you mean :select?
Query error: Unknown source recent; known: active, all, current, dormant, edges, fields, node, query, schema, views.
```

## Reflective forms

`(schema)`, `(fields)`, `(edges)`, and `(views)` are sources that render the language's own vocabularies as node-sets in the same carrier task queries produce, so every step composes over them. The slugs are bare names, never date-prefixed, so they never collide with a task, and a task traversal over a grammar node is an honest empty set rather than an error. Each list is generated from the registry the interpreter dispatches on, so the description cannot drift from behaviour.

| Source | Each node carries |
| --- | --- |
| `(schema)` | `:category` (`source`/`step`/`predicate`/`write`); `:kind` on steps (`transformer`/`shaper`/`mutation`) and predicates (`leaf`/`combinator`/`quantifier`); `:signature` (the machine-readable argument contract) on steps and predicates; and `:doc`. |
| `(fields)` | `:origin` (`declared` or `metadata`), `:type` (`text`, `number`, or `timestamp`), and on a timestamp field `:unit` and `:epoch`. |
| `(edges)` | `:class` (`structural` or `lateral`). |
| `(views)` | `:origin` (`builtin` or `user`) and `:source` (the view's TQ text). |

`(schema)` lists predicates alongside sources and steps, so the predicate vocabulary — including the date predicates and their signatures — is discoverable from the one call. There is no `(help)` source; `(schema)` is the canonical entry point to the grammar.

Probe the grammar with the algebra:

```lisp
(-> (schema) (:where (= :kind "shaper")) (:ids))
(-> (schema) (:where (= :category "predicate")) (:select :kind :signature))
(-> (fields) (:where (= :type "timestamp")) (:ids))
(-> (edges) (:where (= :class "structural")) (:ids))
(-> (views) (:where (= :origin "builtin")) (:ids))
```

The first lists the shapers by their bare names, sorted with the rest of the carrier:

```text
4 tasks:
- count
- group-by
- ids
- select
```

The second reads the predicate vocabulary with its kinds and signatures — for example the three date predicates share the signature `field-ts lit-date`, so `(-> (schema) (:where (= :signature "field-ts lit-date")) (:ids))` recovers `before`, `on`, and `since`.

Reflective sources run on the read surface — they describe, never mutate — and stay total. Use `(:select ...)` to project a field, for example `(-> (schema) (:where (= :category "step")) (:select :doc))`.

## Worked examples

Each program is one full query. Read-surface examples run under either tool; write-surface examples require [`task_query_write`](/cairn/reference/tools#task-query-write). The text fence under each query is the runner's actual output.

```lisp
(-> (active) (:where (not (has :parent))))
```

```text
2 tasks:
- 2026-04-02-auth-rework [Auth rework] (active)
- 2026-04-05-rate-limit (active)
```

Read. Active tasks with no parent — the `active-roots` view: every live root in the graph. With no `(:ids)` or `(:select ...)` shaper the result is the node-set itself, one `- slug [display] (status)` line per task; `:parent` is a base field, so the predicate reads it without enrichment and the lines carry no `obs=`/`edges=` counts. Add `(:enrich)` before the implicit render to print them.

```lisp
(-> (current) (:follow :phase-of) (:ids))
```

```text
3 tasks:
- 2026-04-02-auth-rework-phase-1
- 2026-04-02-auth-rework-phase-2
- 2026-04-02-auth-rework-phase-3
```

Read. The phases directly under the current task.

```lisp
(query "plan-frontier")
```

Read. The phases of the current plan that are not done and whose every `depends-on` dependency is settled — the ready work. `(query "frontier")` is an alias.

```lisp
(-> (node "auth") (:where (matches :description "oauth")) (:ids))
```

Read. Tasks whose slug contains `auth` and whose description mentions `oauth`.

```lisp
(-> (active) (:union (dormant)) (:count))
```

```text
2
```

Read. The size of the whole task set, by the status partition.

```lisp
(-> (current) (:closure :phase-of :depends-on :related) (:enrich))
```

Read. The transitive neighbourhood of the current task across every edge — the `knowledge` view.

```lisp
(-> (active) (:set-status! "completed") (:count))
```

```text
3
```

Write. Complete every active task and report how many were touched; re-running over the now-empty active set touches nothing and returns `0`.

```lisp
(-> (node "phase-2") (:link! :depends-on "phase-1"))
```

Write. Add a `depends-on` edge from each `phase-2` task to `phase-1`. The link step returns its input set, so the rendered output is the touched `phase-2` nodes.

```lisp
(define! "blocked-work" (-> (active) (:where (= :status "blocked"))))
```

Write. Record a user view named `blocked-work`; it then resolves on the read surface via `(query "blocked-work")` and shadows any built-in of the same name. `(undefine! "blocked-work")` removes it.

## Related

- [Tools](/cairn/reference/tools#task-query) — the `task_query` and `task_query_write` tool surface that runs TQ.
- [Views](/cairn/reference/views) — the built-in named views, each with its TQ source text.
- [Edges, statuses, and fields](/cairn/reference/edges-statuses-and-fields) — the closed enums TQ predicates and steps validate against.
- [Reads, writes, and capabilities](/cairn/concepts/reads-writes-and-capabilities) — why the `!`-gate is one dynamic boundary.
