Navigation
Copy page

Built-in and user-defined views

On this page

A view is a named query: a label bound to a piece of TQ source text that resolves to a set of tasks when you call (query "name"). cairn ships ten built-in views, expressed in TQ itself. They are the language's shipped vocabulary — plan-frontier, leaf-tasks, stale-phases, and the rest — and you read them on the read surface through task_query or define your own through task_query_write.

Each built-in is data, not tool surface. The view's body is ordinary TQ, so you can read the definition with (-> (views) (:where (= :origin "builtin")) (:select :source)), copy it, and adapt it. A view defined under your own name shadows the built-in of the same name; remove the user view and the built-in reappears.

Reading a view

Resolve any view by name with the (query "name") source:

text
task_query  (query "plan-frontier")

The result renders as a task list — one line per task, slug first, then display name, status, and any counts the view enriched:

text
2 tasks:
- 2026-06-20-write-the-parser (open) obs=3 edges=2
- 2026-06-20-wire-the-cli (open) obs=1 edges=1

(query "name") is a read. A view defined on the write surface still resolves on the read surface, because reading a view is a read regardless of how the view was created. Naming a view that does not exist is a structured error that enumerates every resolvable name — both built-in and user-defined:

text
Query error: Unknown named query plan-fronteir. Available: active-roots, orphans, leaf-tasks, stale-phases, plan, plan-frontier, frontier, recent, busy, hub-tasks, knowledge.

The ten built-in views

The table lists every shipped view with its TQ source text exactly as cairn defines it. frontier is an alias — its body is (query "plan-frontier") — so the eleven accepted names resolve to ten distinct queries.

View TQ source Yields
active-roots (-> (active) (:where (not (has :parent)))) Live tasks with no parent — the roots of every open plan.
orphans (-> (all) (:where (= :edge-count 0))) Tasks with no edges in any direction: unparented, unlinked, unforked.
leaf-tasks (-> (active) (:where (= (count (:follow :phase-of)) 0))) Live tasks with no phase children — the bottom of the plan tree.
stale-phases (-> (active) (:where (any (:back :phase-of) (or (= :status "completed") (= :status "abandoned"))))) Live phases whose parent has already settled, a sign the plan moved on without them.
plan (-> (current) (:follow :phase-of) (:or-else (-> (current) (:back :phase-of) (:follow :phase-of))) (:enrich)) The phases of the current task's plan, enriched with counts. Current-scoped.
plan-frontier (-> (query "plan") (:where (and (not (or (= :status "completed") (= :status "abandoned"))) (all (:follow :depends-on) (or (= :status "completed") (= :status "abandoned")))))) The ready subset of plan: unsettled phases whose every depends-on dependency is settled.
frontier (query "plan-frontier") Alias for plan-frontier.
recent (-> (active) (:sort :updated-ts) (:take 20)) The twenty most recently touched live tasks.
busy (-> (active) (:sort :obs-count) (:take 20)) The twenty live tasks with the most observations.
hub-tasks (-> (active) (:sort :edge-count) (:take 20)) The twenty live tasks with the most edges — the connectors in the graph.
knowledge (-> (current) (:closure :phase-of :depends-on :related) (:enrich)) The transitive neighborhood of the current task across all three edge types, cycle-safe and depth-bounded.

Three of these views read the (current) source — plan, frontier/plan-frontier, and knowledge — so they resolve against the current task pointer and error when no task is selected. The rest range over (active) or (all) and resolve the same way in every session. The TQ sources table defines each producer.

Built-in source forms in TQ

Each built-in is a complete TQ program, and every piece it draws on is documented in the TQ language reference. The sources are (active), (all), and (current). The steps are (:where), (:follow), (:back), (:sort), (:take), (:or-else), (:closure), and (:enrich). Inside a predicate they reach across edges with the (any TRAV P) and (all TRAV P) quantifiers and count with the (count TRAV) field-expression. plan-frontier composes over plan by name, and frontier composes over plan-frontier by name, so a view defined in terms of another view resolves transitively.

A view defined in terms of itself is a structured error, not an unbounded loop. Define a view named self whose body is (query "self") and resolving it reports the cycle rather than recursing — self in the error text is the offending view name:

text
Query error: View self is defined in terms of itself.

recent is a view, not a source

recent is a view name, so you read it with (query "recent"). (recent) is not a source form. The query language has exactly ten sources — (all), (active), (dormant), (current), (node "substr"), (query "name"), (schema), (views), (fields), (edges) — and recent is not among them. Calling (recent) as if it were a source is an unknown-source error that lists the ten known sources:

text
Query error: Unknown source recent; known: active, all, current, dormant, edges, fields, node, query, schema, views.

The same holds for every other view name: (plan), (orphans), and (busy) are not sources. Reach a view only through (query "name").

Defining your own views

define! and undefine! are write forms — they record an event and are accepted only on the task_query_write surface. The read surface refuses them:

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

define!

text
task_query_write  (define! "blocked-phases" (-> (active) (:where (= :status "blocked"))))

(define! "name" Q) validates Q read-only, records its source text as a view.define event, and returns Q's tasks — so define! is interchangeable with the query it names. The view name must be a non-empty string. The query Q is evaluated read-only even on the write surface: you define over a selection, you do not mutate inside the definition. After the call, (query "blocked-phases") resolves on either surface.

undefine!

text
task_query_write  (undefine! "blocked-phases")

(undefine! "name") records a view.undefine event and returns no tasks. It removes the user view of that name. If a built-in of the same name exists, that built-in becomes visible again.

Shadowing a built-in

A user view of a built-in's name shadows it in the one resolver. Define orphans over (dormant), and (query "orphans") now returns settled tasks instead of edgeless ones; undefine! it and the built-in (-> (all) (:where (= :edge-count 0))) resolves again:

text
task_query_write  (define! "orphans" (dormant))
task_query        (query "orphans")          # now resolves to your definition
task_query_write  (undefine! "orphans")
task_query        (query "orphans")          # the built-in is back

A view is a fold of view.define/view.undefine events, so a rebuild reconstructs your views exactly. See events, projection, and reconcile for how the view vocabulary replays from the durable log.

Listing every view

The (views) source produces every resolvable view as a queryable node-set. Each view node carries an :origin of builtin or user and a :source field holding its TQ text, so you can filter and project the catalogue with the algebra itself:

text
task_query  (-> (views) (:where (= :origin "builtin")) (:ids))
text
task_query  (-> (views) (:where (= :origin "user")) (:select :source))

A built-in shadowed by a user view of the same name reports once, as user(views) reflects what resolves, not what ships.