Navigation
Copy page

Tasks, observations, and handoffs

On this page

A cairn graph is built from three nouns. A task is a node: a date-prefixed slug with a status, a description, edges to other tasks, open metadata, and a stream of observations. An observation is the cheapest write you can make, one line of freeform text appended to a task. A handoff is a resumable summary: a scaffolded document whose one-line summary is the first thing the next session reads. These three are all you write. Everything else cairn shows you, from the frontier to the timeline, is computed from them.

Each noun maps to a verb you call and an event the call records. That is the shape of the whole system: a small, closed vocabulary of writes, each one durable, each one replayable. Understanding what each noun is, and what it is not, tells you when to reach for which.

The task: a node with a date-prefixed slug

A task is identified by a slug minted from the name you give it. task_create over the name wire up auth yields a slug of the form <today>-wire-up-auth (the UTC date stamp followed by the slugified name) and returns Created <slug>.. The date namespace is system-owned. If you hand task_create a name that already carries a leading YYYY-MM-DD- prefix, it strips that before stamping today's, so the prefix never doubles and minting the same name twice in a day is idempotent. A name that recovers nothing descriptive is rejected before any event is recorded.

The slug is the address. Every other write names a task by slug or operates on the current one. A task carries:

  • a status — one of open, active, completed, abandoned, blocked, moved with task_update_status (<task> is now <status>.);
  • a description — the freeform line set at creation;
  • edges — typed links to other tasks (phase-of, depends-on, related), added with task_link and removed with task_sever;
  • metadata — open key/value pairs set with task_set_metadata (Set <key> on <task>.);
  • observations — the append-only text stream described below.

task_get renders all of this for one slug: status, description, parent, children, edges, metadata, and the five most recent observations. Past five, it appends a one-line pointer to the rest — read a task's full observation history with timeline(full=true, types=observation). None of it is a file you edit. The task is a projection of the events recorded against its slug, so the rendered node is always whatever the log replays to. See Events, projection, and reconcile.

Creating the first task adopts it as current when no current task is set; after that, task_create leaves the pointer alone. Forking with task_fork always switches the pointer to the child. The difference matters when you are building a plan, and it is covered in The current task pointer.

The observation: the cheapest write, the heartbeat

An observation is one line of freeform text appended to a task. You call observe with text, it records an observation event, and it returns Observed on <task>.. That is the entire transaction. observe changes nothing but the observation stream and leaves your focus where it was.

This cheapness is the point. Because an observation costs nothing, the design assumes you record one whenever you learn something: a dead end you ruled out, the file and line where the real bug lives, a decision and the reason behind it. They are the running record of why the task moved the way it did. The store indexes them for full-text search, so an observation written today is what a future session retrieves with task_search when it asks "did anyone already look at this?" See The cairn-method for the discipline and task_search for the retrieval side.

An observation resolves to the current task unless you pass task_id to scope it elsewhere. Because it does not move the pointer, you can annotate a neighboring task mid-flow and stay on your own.

The handoff: a resumable summary, summary first

A handoff is a document that lets a fresh session pick up where you stopped. You call handoff with a one-line summary; it mints a path under the task scratchpad, writes a skeleton document, records a handoff.create event, and returns Handoff scaffolded for <task> at <path>. The skeleton is deterministic: frontmatter, a State snapshot computed from the task, and empty Recent work and Next steps sections. The tool never drives an authoring turn. It hands you a valid file and a path; you read the scaffold and overwrite it with the rich body.

The summary is load-bearing: a resuming session reads it first and can often act on it without opening the file, so its precision matters more than the body's. It is also what task_bootstrap surfaces in its handoffs: block when you orient on the task.

Two surfaces write handoffs, and they are not the same:

  • The handoff tool scaffolds the skeleton and records the event. It is deterministic and never authors prose.
  • The /handoff command, under a kli host, is an authoring turn: it composes the one-line summary, calls the tool to scaffold and record, reads the scaffold, then overwrites it with a thorough document — task status, critical references, recent changes in file:line form, learnings, artifacts, graph state, and numbered next steps.

The tool gives you the durable record and the file; the command gives you the writing. Either way the summary comes first. See Slash commands for the /handoff authoring command and the handoff tool for the scaffold it builds on.

Three nouns, three events

Every write you make against the graph reduces to one of these nouns appending one event:

Noun Verb Event recorded Returns
Task task_create task.create Created <slug>.
Task task_fork task.create + task.fork Forked <child> from <parent> (<edge>).
Task task_link task.link Linked <src> -> <target> (<edge>).
Task task_sever task.sever Severed <src> -> <target> (<edge>).
Task task_set_metadata task.set-metadata Set <key> on <task>.
Task task_update_status task.update-status <task> is now <status>.
Observation observe observation Observed on <task>.
Handoff handoff handoff.create Handoff scaffolded for <task> at <path>

Reads — task_get, timeline, task_search, task_query, and task_bootstrap — record no event. They project the log; they do not extend it. task_bootstrap in particular looks like a write because it can move the current pointer, but it records nothing: it switches focus and reads back state. The boundary between what extends the log and what merely reads it is enforced by capability, not by which tools are visible. See Reads, writes, and capabilities.

Why this shape

The three nouns are deliberately uneven in cost. A task is a commitment: it has a slug, a status, a place in the graph. An observation is nearly free, one line with no ceremony, because the friction of recording what you learned is the thing that loses knowledge between sessions. A handoff sits in between: more than a note, less than a meeting, a single document whose summary a tired or truncated reader can act on without opening it.

Keeping the write vocabulary this small is what makes a cairn graph durable and replayable. There is no rich task object to migrate, no handoff schema to version. There is a log of task.create, observation, handoff.create, and a handful of edge and status events, and the task you see is what that log folds to.