# Edges, statuses, and fields


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`](/cairn/reference/tools#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`](/cairn/reference/tq-language#steps) / `:back`); the storage and the cardinality differ. The [task graph](/cairn/concepts/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`](/cairn/reference/tools#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`](/cairn/reference/tools#task-link), [`task_sever`](/cairn/reference/tools#task-sever), and [`task_fork`](/cairn/reference/tools#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!`](/cairn/reference/tq-language#steps) / `: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)`](/cairn/reference/tq-language#sources) yields the live half and `(dormant)` yields the settled half. A plan's [frontier](/cairn/concepts/plans-phases-and-the-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`](/cairn/reference/tools#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](/cairn/reference/tq-language#dates-and-timestamps) `on` / `since` / `before` accept, letting `(on :updated-ts "2026-06-24")` express a calendar-day intent directly. When projected with [`:select`](/cairn/reference/tq-language#steps), 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!`](/cairn/reference/tq-language#steps) 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`](/cairn/reference/tq-language#steps)), 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](/cairn/reference/tq-language) 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 | `:class` — `structural` 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](/cairn/reference/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.

## Related

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