Navigation
Copy page

Edges, statuses, and fields

On this page

cairn's task graph is built from three closed vocabularies: the edge types that connect tasks, the statuses a task moves through, and the fields a query reads. Each is a small fixed set defined in one place in the source, so a tool, a query predicate, and a stored CHECK constraint can never disagree about what is legal. This page is the catalogue of those sets. It also covers the reflective sources — (edges), (fields), (schema), (views) — that let a query read the catalogue back from the running store.

A field absent from the typed set is not an error: it is open metadata, set with task_set_metadata and read as text. The closed sets below are the ones the language understands well enough to validate, classify, and type-check.

Edge types

An edge connects two tasks. cairn stores edges two ways, and which way an edge is stored decides how a query traverses it.

A lateral edge lives in the edges table as an explicit row from a source task to a target task. A structural edge names the task fibration — a child's single parent — and folds into the child's parent_task_id, never the edges table. A query traverses a lateral edge by joining the edges table and a structural edge by walking the parent foreign key. The surface verb is the same (:follow / :back); the storage and the cardinality differ. The task graph page covers why the two are stored apart.

Edge type Class Accepted by tools Notes
phase-of structural yes A phase belongs to its parent plan. Folded into the child's parent_task_id; a task has at most one parent. Default edge_type for task_fork.
depends-on lateral yes This task waits on its target. The ordering edge the frontier reads to decide readiness.
related lateral yes A loose association with no ordering or ownership meaning.
forked-from structural no A structural alias of the parent pointer that the traversal layer recognizes; in the store it is indistinguishable from phase-of (both fold into parent_task_id). It is never the edge a fork writes — a fork records phase-of by default — and never a value you pass as edge_type.

The tool-accepted vocabulary

task_link, task_sever, and task_fork accept exactly three edge_type values: phase-of, depends-on, related. The check is case-insensitive — Depends-On is accepted and lowercased — and anything outside the set is refused before the write:

text
"blocks" is not a valid edge type; expected one of phase-of, depends-on, related.

blocks and blocked-by look like they should work, and they read cleanly, but they are not part of the tool-accepted vocabulary. Internally cairn projects both onto depends-on while preserving the original name in a tag; that projection is an implementation detail of how a raw edge is normalized, not a value the tools advertise. Pass depends-on directly.

Traversal classes

A query traversal classifies its edge keyword the same way before it runs, and an unrecognized edge is a structured error rather than a silently empty result — the edge is a property of the query, not of the data. The error carries the nearest known type, or the full list when nothing is close:

text
Unknown edge type spawned-by; known: :depends-on, :forked-from, :phase-of, :related.

Note the traversal vocabulary is wider than the tool vocabulary: a query may traverse forked-from (and phase-of) because both name the structural fibration, even though only phase-of is a tool-accepted edge_type. The mutation steps :link! / :unlink! narrow back to the three tool-accepted edge_type values (phase-of, depends-on, related), mirroring the write tools.

Statuses

Every task carries exactly one of five statuses. The set is closed and the SQLite CHECK constraint mirrors it, so an invalid status cannot reach the store.

Status Half Meaning
open active Created, not yet started.
active active In progress.
blocked active Stalled on an external condition; still live work.
completed dormant Finished and settled.
abandoned dormant Settled without completion; will not be resumed.

The status enum partitions into an active half (open, active, blocked) and a dormant half (completed, abandoned). This is the partition the query sources read: (active) yields the live half and (dormant) yields the settled half. A plan's frontier is then ready precisely when a phase is not dormant and all of its depends-on targets are. The partition is checked at load time against the full enum, so a status can never be live and settled at once, nor fall outside both halves.

task_update_status lowercases its status argument and validates it against the set; anything else is refused:

text
"done" is not a valid status; expected one of open, active, completed, abandoned, blocked.

completed and abandoned are not terminal in the data — a task is reopened by setting it active again, and the reopen flag on task_update_status is shorthand for exactly that. They are terminal only in the sense that the dormant half drops out of the active sources until you move it back.

Fields

A field is a named, typed value a query reads off a task. Eleven fields are declared with a type; the type is what lets a numeric predicate refuse a textual field instead of comparing garbage, and a date predicate refuse anything but a clock.

Field Type Source
slug text The task's date-prefixed identifier; the node's own name.
status text The current status (see above).
description text The freeform description set at creation.
depot text The depot the task belongs to.
parent text The slug of the structural parent, or absent for a root.
display-name text A human-readable label; defaults to the slug.
created-ts timestamp Creation time, universal-time seconds.
updated-ts timestamp Last-touched time, universal-time seconds.
status-ts timestamp Time of the last status change.
obs-count number Number of observations recorded on the task.
edge-count number Number of edges incident to the task (lateral plus structural).

The six text fields are slug, status, description, depot, parent, display-name. The two number fields are obs-count and edge-count. The three timestamp fields are created-ts, updated-ts, and status-ts.

The type drives predicate type-checking: (> :obs-count 3) compiles because obs-count is numeric, while (> :status 1) is rejected as ill-typed before it touches a row:

lisp
(-> (active) (:where (> :status 1)))
text
Query error: > needs a numeric field; :status is text.

A timestamp is CL universal-time — an integer count of seconds since the epoch 1900-01-01 UTC. It is numeric-comparable, so it orders and compares as an integer ((> :updated-ts 0) is well-typed), and it is the only type the date predicates on / since / before accept, letting (on :updated-ts "2026-06-24") express a calendar-day intent directly. When projected with :select, a timestamp renders as its raw integer and its decoded UTC date — updated-ts=3984388200 (2026-04-05) — so the integer clock stays legible. The unit and epoch are not buried in this page: (fields) carries them on every timestamp field, below.

Any field not in this table is open metadata — a key written with task_set_metadata or the :set! step. It is read on demand and treated as text, so (matches :owner "mika") works against a metadata key cairn has never heard of; only the declared eleven get a numeric or timestamp type. The counts obs-count and edge-count, and any promoted metadata, are added by enrichment (:enrich), which a query inserts automatically when a step references a field that is not already local.

Reflective sources

The three vocabularies above are queryable from the running store, not only documented here. Four sources synthesize the catalogue into the same node-set carrier that task queries produce, so every TQ step composes over them for free. The names they yield are bare (never date-prefixed), so they never collide with a real task. Each source is generated from the registry the interpreter dispatches on, so the readout cannot drift from behavior.

Source Yields Per-node props
(edges) The edge vocabulary, one node per type :classstructural or lateral
(fields) The declared fields plus any live metadata keys :origin (declared / metadata), :type (text / number / timestamp), and :unit + :epoch on a timestamp
(views) The named views, built-in and user :origin (builtin / user), :source (TQ text)
(schema) The combinator grammar: every source, step, predicate, and write form :category (source / step / predicate / write), :kind (on steps and predicates), :signature (on steps and predicates), :doc

(edges) lists all four known edge types — the three tool-accepted plus forked-from — because the traversal surface understands all four. To recover only the tool-accepted lateral set, filter on the class:

lisp
(-> (edges) (:where (= :class "lateral")) (:ids))
text
2 tasks:
- depends-on
- related

The same algebra reads the fields. List every numeric field, or every timestamp field:

lisp
(-> (fields) (:where (= :type "number")) (:ids))
text
2 tasks:
- edge-count
- obs-count
lisp
(-> (fields) (:where (= :type "timestamp")) (:ids))
text
3 tasks:
- created-ts
- status-ts
- updated-ts

The clock is self-describing: project a timestamp field's :unit and :epoch instead of reverse-engineering them from raw values.

lisp
(-> (fields) (:where (= :type "timestamp")) (:select :type :unit :epoch))
text
3 tasks:
- created-ts  type=timestamp  unit=seconds  epoch=1900-01-01 UTC (CL universal-time)
- status-ts  type=timestamp  unit=seconds  epoch=1900-01-01 UTC (CL universal-time)
- updated-ts  type=timestamp  unit=seconds  epoch=1900-01-01 UTC (CL universal-time)

(fields) also lists the open metadata keys live in the store, each tagged :origin "metadata" (declared fields are :origin "declared"), so a query discovers the metadata vocabulary it can filter and project without reading any code — (-> (fields) (:where (= :origin "metadata")) (:ids)).

(schema) describes the grammar itself, including the reflective sources — it lists edges, fields, views, and schema among its source nodes, and it lists the predicate vocabulary (each predicate tagged :category "predicate" with its :kind and :signature) alongside the sources and steps. That makes the language self-documenting: an agent that has the (schema) source can discover the rest of the surface without leaving the query, which is the same loop these docs serve for a human reader. There is no (statuses) source; the status partition is documented above and surfaces through (active) / (dormant) rather than a reflective list.

  • Tools — the writers that consume these enums: task_link, task_fork, task_update_status, task_set_metadata.
  • The TQ language — the sources, steps, and predicates that read fields and traverse edges.
  • Views — the built-in named queries phrased over these vocabularies.
  • The task graph — why structural and lateral edges are stored differently.
  • Plans, phases, and the frontier — how phase-of, depends-on, and the status partition combine into readiness.