Custom statuses
Rename the lifecycle, add a review stage, and define your own transition graph.
The default lifecycle is PENDING → IN_PROGRESS → DONE. Most teams want at least one
more stage, and different words. Both are supported, because no command hardcodes a
literal status — each binds to a role.
Renaming, with no other change
version: 1
statuses: [TODO, DOING, SHIPPED]That is the whole change. Roles default to the first / second / last entries, so
ready is TODO, claimed is DOING, done is SHIPPED, and every command keeps
working:
tasks ready # looks for TODO
tasks claim wire-auth # moves it to DOING
tasks status wire-auth SHIPPEDAdding a review stage
version: 1
statuses: [TODO, DOING, REVIEW, SHIPPED]With four statuses the role defaults still land correctly — ready: TODO,
claimed: DOING, done: SHIPPED — and the default transition rule gives you exactly the
chain you would draw:
TODO ⇄ DOING ⇄ REVIEW ⇄ SHIPPEDOnly SHIPPED satisfies a dependency. A task sitting in REVIEW does not unblock
its dependents, which is usually what you want: review is not done.
Roles that are not first / second / last
Sometimes the natural word order does not match the role order. Say you want a triage stage before the ready pool:
version: 1
statuses: [TRIAGE, TODO, DOING, SHIPPED]
roles:
ready: TODO
claimed: DOING
done: SHIPPEDNow tasks ready ignores TRIAGE entirely — items sit there until someone moves them
to TODO with tasks status, and only then enter the pool.
ready → claimed must be legal
The config is refused at load time if roles.ready → roles.claimed is not a
legal transition, because tasks claim could never succeed. Here TODO → DOING is adjacent, so the default rule covers it. If it were not, you would
need an explicit transitions entry.
A custom transition graph
The default rule — step forward or back by exactly one — is uniform and unopinionated. When you need something else, list every legal pair:
version: 1
statuses: [TODO, DOING, REVIEW, SHIPPED]
roles:
ready: TODO
claimed: DOING
done: SHIPPED
transitions:
- [TODO, DOING] # claim
- [DOING, TODO] # release
- [DOING, REVIEW] # hand off for review
- [REVIEW, DOING] # changes requested
- [REVIEW, SHIPPED] # approved
- [SHIPPED, DOING] # reopentransitions REPLACES the default rule
It does not layer onto it. Anything you leave out becomes illegal, including
backward moves you never thought about. Omit [DOING, TODO] above and tasks release fails with exit 4 forever.
Walk the whole graph when you write one. A quick checklist:
- Can you claim? (
ready → claimed) — enforced at load time. - Can you release? (
claimed → ready) — nothing enforces this; you will find out at the firsttasks release. - Can you reopen? (
done → claimed) — otherwise a mistakenly-completed task is stuck. - Can you get back out of every intermediate stage?
What a graph buys you
Explicit transitions let you express things the step rule cannot. Skipping review for a trivial change, for instance:
transitions:
- [TODO, DOING]
- [DOING, TODO]
- [DOING, REVIEW]
- [DOING, SHIPPED] # fast path, no review
- [REVIEW, DOING]
- [REVIEW, SHIPPED]
- [SHIPPED, DOING]Or a strictly one-way lifecycle with no reopen at all — though think twice, since agents do mark things done in error and reopening is the cheapest recovery.
What not to add
BLOCKED and CANCELLED are not special-cased, and usually should not be statuses:
- Blocking is already expressed by
depends_on, and unlike a status it says what the block is. ABLOCKEDstatus is a note to a human that no query can act on. - Cancellation is
tasks delete. The task is archived, keeps its id reserved, and stops satisfying dependencies — which is the behaviour you actually want.
If you want them anyway, nothing stops you: add them to statuses and give them
transitions.
Migrating an existing tree
Renaming a status does not rewrite existing task files. Tasks still carrying the old
name become unknown-status errors in tasks validate, and --fix will not repair them —
guessing the mapping is not mechanically safe.
Add the new names alongside the old
statuses: [PENDING, TODO, IN_PROGRESS, DOING, DONE, SHIPPED]Ugly, but every existing task stays valid while you migrate.
Move each task across
tasks list --all --status PENDING --json | jq -r '.tasks[].id' |
while read -r id; do tasks status "$id" TODO; doneMind the transition rule while both sets are in the list — you may need explicit
transitions entries to make the sideways moves legal.
Drop the old names
Trim statuses back to the new list, set roles explicitly, and confirm:
tasks validateFor a small tree, a search-and-replace across .tasks/**/*.md followed by
tasks validate is faster and perfectly legitimate — frontmatter is machine-owned,
not machine-only.