Task Graph (beads)
beads is godotz.ai’s persistent DAG task scheduler. It tracks work from high-level epics down to atomic steps, stores every state transition in a Dolt-versioned backend, and emits lifecycle hooks that agents can intercept. The CLI is bd.
Why a Task Graph
Plain in-context TODO lists vanish when a session ends. beads persists state across sessions, machines, and agent restarts. If a worker crashes mid-task, bd retry resumes from the last completed step — no work is duplicated.
The DAG model also prevents two agents from starting the same task concurrently. Dependency edges are the contract; beads enforces them.
Hierarchy
epic
└── task
├── step 1
├── step 2
└── step 3
| Level | Description | Typical owner |
|---|---|---|
| epic | A shippable feature or milestone | Human operator |
| task | A discrete unit of work within an epic | Orchestrator agent |
| step | An atomic action within a task | Worker agent |
An epic completes only when all its tasks complete. A task completes only when all its steps complete or are explicitly skipped.
bd CLI Reference
# Create
bd epic create "ship auth v2"
bd task create --epic auth-v2 "implement JWT refresh"
bd step create --task jwt-refresh "write token rotation logic"
# Run
bd run --task jwt-refresh # run all steps in dependency order
bd run --step token-rotation-logic # run a single step
# Status
bd status # tree view of all active work
bd status --epic auth-v2 # subtree for one epic
bd log --task jwt-refresh # step-by-step execution log
# Recovery
bd retry --task jwt-refresh # retry failed steps only
bd skip --step token-rotation-logic # mark step skipped, unblock dependents
bd cancel --epic auth-v2 # cancel all in-progress work for epic
# History
bd diff HEAD~1 # diff last two graph states (Dolt)
bd log --graph # full commit history of graph state
DAG Execution
Steps within a task form a directed acyclic graph. beads resolves execution order by topological sort and runs independent steps in parallel up to the configured maxConcurrency.
step-a ──┐
├── step-c ──── step-e (terminal)
step-b ──┘
└── step-d (terminal)
Given this graph:
step-aandstep-bstart immediately (no deps)step-cstarts when bothstep-aandstep-bcompletestep-dstarts whenstep-bcompletesstep-estarts whenstep-ccompletes
Steps step-d and step-e may run concurrently.
# bd.config.yaml
execution:
maxConcurrency: 4 # max parallel steps per task
stepTimeoutSeconds: 300 # step killed after 5 min if no heartbeat
retryPolicy:
maxAttempts: 3
backoffSeconds: [10, 30, 90]
Dolt Embedded Backend
beads stores its graph in a Dolt database — a MySQL-compatible relational database with git semantics. Every bd write operation creates a Dolt commit.
# bd under the hood
bd task create "implement JWT refresh"
# → INSERT INTO tasks (id, title, status, epic_id, ...) VALUES (...)
# → CALL dolt_commit('-Am', 'create task: implement JWT refresh')
This gives the task graph full version history:
bd diff HEAD~1 # what changed in the last operation
bd diff main..branch # compare branches (useful for parallel epic work)
bd checkout <commit> # restore graph to any historical state
Dolt runs embedded within the beads process — no separate server required for single-node deployments. Multi-node deployments use Dolt SQL Server mode with replication.
Prefix Conventions
Task and step IDs use a prefix convention to encode context at a glance:
| Prefix | Meaning | Example |
|---|---|---|
feat/ | Feature work | feat/auth-refresh |
fix/ | Bug fix | fix/token-expiry-edge-case |
chore/ | Non-functional change | chore/update-deps |
doc/ | Documentation | doc/api-reference |
test/ | Test-only change | test/jwt-integration |
infra/ | Infrastructure | infra/redis-cluster |
exp/ | Experiment, may not land | exp/rl-optimizer |
Agents use these prefixes to filter relevant tasks when scanning status:
bd status --filter feat/ # show only feature tasks
bd status --filter exp/ # show only experiments
Hooks
beads fires lifecycle hooks at state transitions. Hooks are shell commands or HTTP endpoints configured per-event:
# bd.config.yaml
hooks:
on_task_start:
- cmd: "ntfy publish omp-tasks 'Task started: {{task.title}}'"
on_step_complete:
- cmd: "bd log --step {{step.id}} >> .omc/logs/steps.log"
on_task_complete:
- http: "http://langfuse:3000/api/events"
body: '{"event": "task_complete", "taskId": "{{task.id}}"}'
on_step_fail:
- cmd: "ntfy publish omp-alerts 'FAIL: {{step.id}} — {{step.error}}'"
- cmd: "bd retry --step {{step.id}} --delay 30"
on_epic_complete:
- cmd: "gh pr create --title '{{epic.title}}' --body '{{epic.summary}}'"
Hook variables use {{field}} templating. Available variables per event:
| Variable | Available on |
|---|---|
step.id, step.title, step.error | step events |
task.id, task.title, task.status | task events |
epic.id, epic.title, epic.summary | epic events |
Hooks run synchronously before the state transition is committed. A hook that exits non-zero blocks the transition and fires on_hook_fail (no infinite recursion — hook failures do not re-fire the failed hook).
Human Checkpoints
A step can be marked type: checkpoint to pause execution and wait for human approval before continuing:
# task definition
steps:
- id: implement
type: exec
cmd: "claude-code run implement.md"
- id: human-review
type: checkpoint
message: "Review the implementation diff before deploy"
deps: [implement]
- id: deploy
type: exec
cmd: "bd run deploy.sh"
deps: [human-review]
bd status # shows "WAITING: human-review"
bd approve --step human-review
# → deploy step now starts
Checkpoints are stored in the Dolt graph and survive restarts — the wait is durable, not in-process.
Integration with the Agent Runtime
The OMC harness integrates with beads through the bd CLI available in every agent session. Orchestrators decompose epics and create tasks; workers claim steps and run them.
Orchestrator:
bd task create --epic $EPIC "implement feature"
bd step create --task $TASK "write code"
bd step create --task $TASK "write tests" --deps write-code
bd run --task $TASK
Worker (claimed a step):
# step is "in_progress" in the graph
# ... do the work ...
bd step complete --step $STEP_ID
bd run is idempotent: running it on an already-completed task is a no-op. Running it on a partially-complete task resumes from where it stopped.
Related
- Architecture Overview — beads at L2 in the stack
- Orchestration — how swarm YAML drives bd
- Observability — Langfuse hook integration