Task states
Every Task carries a status field validated against a fixed set of values. Writes that would move a Task to a non-adjacent state are rejected. This page describes each state, the allowed transitions between them, and how agent and human actions differ in what they can trigger.
The state machine
There are seven states. Two are terminal.
| State | Meaning |
|---|---|
backlog | Staged for planning. The dispatch cron ignores Tasks in this state, even if a bot is assigned. Default at creation. |
todo | Ready to start. A bot-assigned Task in todo is eligible for dispatch on the next cron tick. |
in_progress | Actively being worked — either a bot is running or a human has started. |
blocked | Work cannot continue without external input or a dependency resolving. Not a terminal state — a Task in blocked can return to in_progress. |
awaiting_approval | A human review gate. The Task is paused until someone approves or rejects. The frontend renders Approve / Reject controls in the Task's thread. |
completed | Terminal. The Task is done. completed_at is set. If the Task was completed by a bot, output_md may be populated. |
cancelled | Terminal. The Task will not be done. Cancelling a parent cascades to all non-terminal descendants. |
Allowed transitions:
backlog → todo, cancelled
todo → in_progress, blocked, cancelled, completed
in_progress → blocked, awaiting_approval, completed, cancelled
blocked → in_progress, cancelled
awaiting_approval → in_progress, completed, cancelled
completed → (terminal — no exits)
cancelled → (terminal — no exits)
Same-status writes are idempotent. Setting status: 'todo' on a Task already in todo is a no-op for that field.
Tasks cannot be created in awaiting_approval, completed, or cancelled. New Tasks must start in backlog, todo, in_progress, or blocked.
Transitions
Who moves a Task between states, and how:
Most transitions happen through two paths: a direct field update (you edit the status in the UI), or a bot tool call (update_task, complete_task, cancel_task, or comment_on_task with a set_status side-effect).
A few transitions carry specific logic:
backlog → todo— must be done explicitly. The cron will not auto-promote Tasks frombacklog.in_progress → awaiting_approval— typically triggered by a bot that has finished its work but needs a human to sign off before the Task closes.awaiting_approval → in_progress(approved) — triggered when a user posts a comment withset_decision: 'approved'. This moves the Task toin_progressby default. The caller may pass an explicitset_statusto override this (e.g., go directly tocompleted). The decision, decision date, and the comment body (asdecision_reason) are recorded on the Task.awaiting_approval → cancelled(rejected) — triggered when a user posts withset_decision: 'rejected'. Cancellation cascades to all non-terminal descendants.in_progress / todo → completed— humans can mark a Task complete directly without waiting for a bot. Bots callcomplete_task, which optionally writesoutput_md.
Dispatch signal. When a human changes status, assignee, body_md, start_at, due_at, or event_at, the system bumps dispatch_signal_at. The cron uses this to detect that something changed since the last bot run and re-dispatches the assigned bot.
Owner vs. executor
Two concepts that look similar but carry different responsibilities:
Owner (assignee) is the person or bot accountable for the Task. Whoever is set as assignee appears on the Task card, receives dispatch calls if they're a bot, and is the named responsible party if the Task is overdue.
Executor is whoever is doing the work right now. For bot-assigned Tasks, the executor is the bot during its dispatch run. For human-assigned Tasks, the executor is the person.
A Task can be reassigned mid-flight. If a bot is working and the Task is reassigned to a human (bot → user), the dispatch signal is not bumped — the bot stops on its next natural exit, and the human takes over. If a Task is reassigned to a different bot (bot → bot), dispatch_signal_at is bumped immediately so the new bot picks it up.
Assigning a Task to no one (null) removes it from all dispatch queues and "My tasks" views. The Task stays in whatever status it was in.
Blocked, paused, abandoned
Three exception paths worth distinguishing:
blocked is a first-class state for Tasks that cannot move forward — a dependency is unresolved, a piece of information is missing, or an external action is needed. Move the Task back to in_progress once the blocker is cleared. Blocked Tasks assigned to a bot are not dispatched.
If a bot is soft-deleted or has accept_delegated_work disabled, the dispatch cron sets its assigned Tasks to blocked automatically. Reassign or enable the bot to resume.
Paused (no formal state) — Tasks that are todo but haven't been picked up are effectively waiting. There is no distinct paused state. If you want to hold a Task without it appearing in the active queue, set it to backlog.
cancelled is a permanent close — no exit. If a Task was cancelled by mistake, create a new one. The same applies to completed — there is no undo in the current version.
Sub-tasks inherit cancellation: cancel_task on a parent runs a cascade that sets every non-terminal descendant to cancelled in a single transaction. Already-terminal descendants are left alone.