Plans, phases, and the frontier
On this page
A plan in cairn is a graph of tasks, not a document. There is no plan.md that an agent reads, edits, and re-reads until it drifts from the work. The plan is the root task, the phases are its children, and the ordering between phases is an edge type the query language understands. Because the plan is structure rather than prose, cairn can answer a question a markdown file never could: of everything left to do, which tasks are ready right now. That answer is the frontier, and it is the single question an agent asks at the top of every working session.
This matters because the reader resuming a plan is usually a fresh context. A flat checklist forces that fresh context to re-derive what blocks what; a graph carries the dependencies as data, so the next task is computed, not remembered.
A plan is a root task with phase children
The plan is a task — a node in the task graph — that you grow phases beneath. You create the root with task_create, then break it into phases with task_fork. Each fork records a phase-of edge from the child up to the parent, so the plan's shape lives in the graph the moment you build it.
task_create takes one required argument, name, and mints the slug from it; the slug carries today's date as a prefix.
task_create(name="Migrate the auth service to the new token format")
-> Created 2026-06-22-migrate-auth-service.
task_fork carves a phase off whatever task it forks from (the parent slug; it defaults to the current task), and its edge_type defaults to phase-of. The parent argument is from — there is no parent or parent_id.
task_fork(name="Add the new token codec", from="2026-06-22-migrate-auth-service")
-> Forked 2026-06-22-add-the-new-token-codec from 2026-06-22-migrate-auth-service (phase-of).
A phase is a phase because of its phase-of edge, not because of anything in its description. phase-of is a structural edge: in the store it is folded into the parent foreign key, so every task has at most one parent and the phase backbone is always a tree. That single-parent rule is what lets the plan view start from one task and walk straight down to its phases. See The task graph for why structural and lateral edges are stored differently.
A phase is meant to be an independently verifiable unit of work — something that can move from open to completed on its own, with its own acceptance, while the rest of the plan stays put. Phases that bundle three unrelated outcomes are hard to mark done and harder to resume into. Forking one phase per checkable result keeps the frontier honest.
depends-on edges order the phases
Phase-of says what belongs to the plan. It says nothing about what comes first. Ordering is a separate, lateral edge: depends-on, added with task_link.
task_link(target_id="2026-06-22-add-the-new-token-codec", edge_type="depends-on", task_id="2026-06-22-cut-over-the-verifier")
-> Linked 2026-06-22-cut-over-the-verifier -> 2026-06-22-add-the-new-token-codec (depends-on).
Read A depends-on B as "A cannot proceed until B is settled." A depends-on edge is lateral — it lives in the edges table, not the parent key — so a phase can depend on any number of others without disturbing the phase-of tree. The two edge types compose: phase-of builds the plan's spine, depends-on threads ordering across it. The closed edge vocabulary accepted by task_link is phase-of, depends-on, and related; see Edges, statuses, and fields.
Keeping the backbone and the ordering on different edge types is deliberate. You can restructure the plan (re-parent a phase) without touching the dependency order, and you can re-order work without re-parenting anything. Neither operation forces a rewrite of a prose plan, because there is no prose plan to rewrite.
The frontier is the ready subset
Two named views answer the two questions a plan poses. plan returns the whole plan scoped to where you are standing. plan-frontier returns the subset of that plan that is ready to work.
The plan view is current-scoped: it reads from the current task pointer and walks one hop down the phase-of backbone to the phases, falling back to your sibling phases when the current task is itself a phase.
(-> (current) (:follow :phase-of) (:or-else (-> (current) (:back :phase-of) (:follow :phase-of))) (:enrich))
plan-frontier filters plan down to the tasks that are both unfinished and unblocked:
(-> (query "plan")
(:where (and (not (or (= :status "completed") (= :status "abandoned")))
(all (:follow :depends-on)
(or (= :status "completed") (= :status "abandoned"))))))
Read that predicate as the frontier rule, in two clauses:
- Not yet settled. The task's own status is not
completedorabandoned. A settled task is behind you, not ahead. (completedandabandonedare the dormant half of the status enum;open,active, andblockedare the active half — see Edges, statuses, and fields.) - Every dependency settled. Walking each
depends-onedge forward, all the tasks it points to arecompletedorabandoned. A phase with one unfinished dependency is not on the frontier yet — its turn comes when that dependency settles.
A task with no depends-on edges trivially satisfies the second clause: all over an empty set is true. So the frontier of a fresh plan is exactly its unblocked entry points. As you complete phases, the frontier advances to the phases they unblocked. You never compute this by hand; you ask for it.
frontier is an alias — the view frontier is defined as (query "plan-frontier"), so either name returns the same ready subset.
The shape of working a plan
With the plan in the graph, an agent's session has a fixed shape, and the shape is the point. The loop is a frontier query, a completion, and a re-query: ask which tasks are ready, settle one of them, and ask again. Nothing in between edits a list. The loop runs over task_query.
The frontier query inherits plan's enrichment, so each line carries its inline counts — obs=N edges=N — after the status:
task_query(query="(query \"plan-frontier\")")
-> 2 tasks:
- 2026-06-22-add-the-new-token-codec (open) obs=0 edges=1
- 2026-06-22-write-the-migration-guide (open) obs=2 edges=0
A completion is a single task_update_status against any task on the frontier:
task_update_status(status="completed", task_id="2026-06-22-add-the-new-token-codec")
-> 2026-06-22-add-the-new-token-codec is now completed.
The re-query is the same call as before, and the phase that depended on the one just settled now appears — the frontier moved without anyone editing a list. This is the same heartbeat the cairn-method wraps in research, plan, implement, and validate discipline. For the step-by-step version on a toy plan, see Plan and resume.
When a plan graph is overkill
A plan graph earns its keep when work outlives one context: many phases, real ordering between them, and a resume that has to recompute what is ready. For a single task you will finish in one sitting, that machinery is friction. Create the task, observe against it, and skip the phases — a one-node "plan" has a frontier of one, which is just the task itself. Reach for task_fork and depends-on when the ordering is something a future context would otherwise have to reconstruct from memory.
Related
- Plan and resume — build the root, fork phases, order them with
depends-on, and resume the plan - The task graph — the two stores and the structural-vs-lateral edge split
- The current task pointer — why
planandplan-frontierare current-scoped - Views — the built-in views
plan,plan-frontier,leaf-tasks, andstale-phaseswith their TQ source text - The TQ language — the query language the frontier rule is written in