How to Build a Durable Coding-Agent Scheduler with Beads

A practical guide to adding durable task state, pull-based workers, deterministic CI, checkpoints and safer automation to a coding-agent scheduler.

The authority split between a human, Beads, Agentflow, coding-agent sessions and GitHub

After using the scheduler across a number of tasks and areas of work, a recurring problem became clear: the machine state could be correct whilst the next action for a person was unclear.

For example, a writer could complete the safe part of a task, record the available evidence and leave an environment-specific check open. The dependent reviews would remain blocked, as intended. A status update which only returned the blocked task identifiers still left me to find their titles, owners and missing evidence before I could decide what to do next.

I saw the same coordination gap elsewhere. Planning decisions lived in chat before a branch or pull request existed. Model sessions stayed open to watch external checks. Earlier work was spread across Codex, Claude Code, Copilot and several artefact folders. Longer-running workers needed a reliable way to resume after context compaction.

My original coding-agent scheduler still has four stages: shape the goal, split it by ownership, run bounded workers and gate the result. This follow-up explains how I added durable task state with Beads, pull-based workers, deterministic CI checks, bounded checkpoints and stricter launch controls. It also includes a small working setup which you can adapt without my private Agentflow tooling.

Why I added durable task state

The original design relied on Markdown handoffs, Git branches, GitHub issues and pull requests. Those are useful once a piece of work is ready for Git, but some important decisions happen earlier:

  • a research task may have no repository;
  • a review must remain blocked until its writer supplies the required evidence;
  • a worker may discover a new dependency before it has anything to commit; and
  • a controller may restart before a pull request exists.

I added Beads as the durable dependency graph. Beads is an issue tracker designed for coding agents. Its current CLI can calculate ready work, represent dependencies, atomically claim a task and operate without Git.

GitHub still handles delivery. Each part of the workflow now has a narrower job:

Component What it is authoritative for
Human Goal approval, exceptions, material decisions and merge authorisation
Beads Approved work, dependencies, assignments, review stages, evidence and decisions
Agentflow Routing policy, handoff checks, security controls, deterministic watchers and telemetry
Codex, Claude Code and Copilot One bounded execution or review context
GitHub Human-facing issues, pull requests, hosted checks and integration
A human approves work and decisions. Beads holds goals, dependencies and evidence. Agentflow routes ready work to short-lived Codex, Claude Code and Copilot sessions. Selected work moves through GitHub pull requests and checks.
Beads holds the work graph, Agentflow applies the workflow rules, provider sessions handle bounded tasks, and GitHub remains the delivery path. Open the full-size diagram or download the editable Excalidraw source.

With the graph in place, I can restart a provider session and reconstruct the approved work from the root task. A worker receives one task, its inputs and its acceptance evidence instead of the controller’s previous conversation.

Build a minimal coding-agent workflow with Beads

You can reproduce the useful foundation with the public Beads CLI. The example below creates one writing task and a read-only review which remains blocked until the writing task closes. At this stage, you launch the provider sessions yourself; Beads supplies the coordination state.

On macOS, install Beads with Homebrew and confirm that the bd command is available:

brew install beads
bd --version

The Beads installation documentation lists the current options for other operating systems.

Start with a small project structure

Keep the workflow instructions close to the work:

your-project/
├── AGENTS.md
├── .agents/
│   └── skills/
│       └── project-domain/
│           └── SKILL.md
├── .beads/
├── src/
└── tests/

Each part has a specific purpose:

  • AGENTS.md defines ownership, review and merge rules for every coding agent;
  • SKILL.md holds project-specific architecture, commands and evidence requirements;
  • .beads/ is generated and managed by Beads, so agents should not edit it by hand; and
  • src/ and tests/ are example ownership boundaries which tasks can narrow further.

In an established Git repository, initialise Beads without replacing agent instructions or installing hooks:

cd /path/to/your-project
bd init \
  --stealth \
  --skip-agents \
  --skip-hooks \
  --non-interactive

Stealth mode keeps the Beads data out of normal Git tracking. Remove --skip-agents or --skip-hooks only after you have reviewed what the generated integration would add to the repository.

Add a short coordination contract to AGENTS.md:

## Coding-agent workflow

- Work on one approved Beads task at a time.
- Claim the task before editing and stay inside its stated file ownership.
- Writers may edit only the paths named in the task.
- Reviewers are read-only and record findings as evidence.
- Run the task's acceptance checks before closing it.
- Record changed files, checks and remaining risks in the close reason.
- Never merge without explicit human authorisation.

The project skill can stay small at first:

---
name: project-domain
description: Architecture and validation guidance for this project.
---

# Project domain

## Architecture
Describe the important directories and boundaries.

## Validation
List the exact commands a worker must run.

## Evidence
State what a completion report must contain.

Create a writer and dependent reviewer

Run these commands from the project root and keep the same terminal open, because the three task identifiers are stored in shell variables:

root_id=$(bd create "Improve the login flow" \
  --type epic \
  --priority 1 \
  --description "Approved outcome and boundaries" \
  --acceptance "All child tasks are complete" \
  --silent)

writer_id=$(bd create "Implement the retry fix" \
  --type task \
  --priority 1 \
  --parent "$root_id" \
  --description "Change only src/login/ and its tests" \
  --acceptance "Regression test passes twice" \
  --silent)

review_id=$(bd create "Review the retry fix" \
  --type task \
  --priority 1 \
  --parent "$root_id" \
  --description "Read-only review of the change and evidence" \
  --acceptance "A finding or approval is recorded" \
  --silent)

bd dep add "$review_id" "$writer_id"
bd ready --parent "$root_id" --json

The final command should return the writer task and omit the blocked review. Claim the first ready task atomically:

BD_ACTOR=writer-a bd ready \
  --parent "$root_id" \
  --unassigned \
  --claim \
  --json

Pass the returned task identifier to a fresh Codex, Claude Code or Copilot session. This is the minimum useful handoff:

Work only on Beads task <task-id>.

1. Read AGENTS.md and .agents/skills/project-domain/SKILL.md.
2. Run: bd show <task-id> --long
3. Stay inside the task's file ownership and acceptance criteria.
4. Run the required validation.
5. If acceptance is met, close the task with:
   bd close <task-id> --reason "Changed: <files>; Checks: <results>; Risks: <remaining risks>"
6. If acceptance is not met, leave it open and report the exact blocker.

Do not merge or start another task.

When the writer closes its task, Beads releases the review:

bd ready --parent "$root_id" --json

Claim it with a reviewer identity, then use the same handoff. The task description and AGENTS.md keep this session read-only:

BD_ACTOR=reviewer-a bd ready \
  --parent "$root_id" \
  --unassigned \
  --claim \
  --json

You now have the smallest useful pull-based workflow: durable state, one atomic claim at a time, explicit ownership and a review dependency. Run this loop manually on a few tasks and note where you still have to translate state or relay messages. Those repeated chores are the best candidates for a small wrapper or watcher; a second provider or dashboard can wait until it solves a problem you have actually seen.

Workers now pull one ready task

The original controller launched every writer and reviewer, relayed corrections, watched CI and decided what each worker should do next. Its context grew during routine state changes which required no judgement.

The replacement is a staged graph:

approved goal
  -> ready code task
  -> writer records evidence
  -> review becomes ready
  -> accepted correction returns to the writer
  -> deterministic CI watcher records the result
  -> controller makes the integration decision

The public example above proves the core transition. My private Agentflow CLI adds filters and launch policy around it. The following command illustrates that interface; it is not a public download or an installer for this article.

agentflow worker pull \
  --root <approved-root> \
  --stage code \
  --actor <worker-name> \
  --capability shell-write \
  --once

A pull is restricted to one approved root, stage and capability set. Assigned work is considered before shared unassigned work, and the claim happens once. The worker exits when there is no matching task instead of using model requests to poll.

Closing the writing task releases its reviewers. If a review finds a reproducible defect, the controller records whether it accepts, rejects, defers or deduplicates the finding. An accepted correction goes back to the original writer, and the original reviewer gets the first opportunity to check it again.

The controller now spends less of its context relaying routine changes. I bring it back in for disagreements, exceptions and the final judgement.

Pending CI no longer needs a model session

In the early workflow, an agent could spend several requests checking whether a pull request was still pending. Nothing useful was being inferred during those checks.

Agentflow now observes authorised pull-request checks through a deterministic watcher:

agentflow ci watch \
  --bead <ci-task> \
  --pr <pull-request-number> \
  --repo <owner/repository> \
  --writer <writer-task>

When the checks pass, the watcher closes the CI task. A failure creates one deduplicated diagnosis task. The diagnosis worker remains read-only; when product code needs to change, a separate fix is routed to the original writer.

While checks are pending, the watcher records their state without keeping a model session open. I use ordinary commands for ready-work queries, known wait predicates, file fingerprints, worktree state, metadata indexing and usage summaries. I start an agent when the result needs diagnosis or judgement.

Task identifiers needed a useful status report

Beads uses compact identifiers because they work well as coordination keys. Returning one to a person without any explanation caused the problem at the start of this post.

I added an explanation command:

agentflow beads explain <task-id>

The output includes the title, stage, state, reason, assignee and one suggested next request:

Title: Validate the environment-specific behaviour
Stage: code
State: blocked
Reason: hosted evidence is required before review can start
Assignee: writer-a (claimed; live session not established)
Next request: Run the hosted test or record an approved exception.

The wording about the live session is intentional. A claimed task proves ownership in the graph. It does not prove that a provider process is still running. In the same way, a visible terminal pane says nothing about review evidence or merge permission.

The same graph works for folders without Git

Some of the work I schedule produces research, reports, diagrams or a website rather than a software repository. Pretending that every output had a branch and pull request made the completion reports misleading.

Beads supports Git-free operation, so I use an embedded graph in an ordinary directory for those jobs. The controller is the only graph writer. Concurrent workers receive disjoint output paths, and overlapping edits run in sequence.

For example:

researcher owns research/source-ledger.md
diagram worker owns diagrams/
controller owns README.md and the final synthesis

Git-backed work still uses separate worktrees created from an exact base commit. The worktree helper records dirty and merge state and refuses to retire a checkout which contains uncommitted or unmerged work. Both modes use the same ownership rule, but they report completion in terms appropriate to the project.

A private provenance index for earlier work

Useful context was scattered across local Codex, Claude Code, Copilot and VS Code histories. There were also selected research and report folders that I wanted to find again.

I considered copying that content into the task graph, but it would have widened the privacy boundary and created another source of truth. Agentflow now keeps a private local provenance index. Its model-free refresh records a limited set of structural information:

  • provider and source identifier;
  • hashed workspace provenance;
  • a safe locator and content fingerprint;
  • file size, modification time and completeness signals; and
  • parent relationships when the source format supplies them.

Raw prompts, responses, reasoning, tool inputs, credentials, logs and transcript bodies are excluded from Beads. A separate bounded summary can record a goal, outcome, decisions, evidence, blockers and unresolved work. The summary is tied to the source fingerprint and becomes stale when that source changes.

Selected artefact folders are registered explicitly with include and watch patterns. Their content stays in its original location; the index records where it came from and whether it has changed.

Search uses SQLite FTS5 over those bounded records and returns the provenance, freshness and privacy fields with each result. The search exists, but its retrieval quality has not yet been proven. I have a fixed-question benchmark planned to test whether the correct prior result appears in the top three.

Dashboards help with navigation

I also tested whether a terminal manager or graph dashboard should become the centre of the workflow.

Native provider views still suit short, bounded subagents. I use an observable external session when a job is long-running, crosses providers, owns a separate worktree or is likely to need a human decision. Mardi Gras provides a convenient view of the Beads graph.

I use these views to find a session or task which may need attention. The task dependencies still determine readiness, recorded evidence supports validation, and the integration gate holds merge permission.

Smaller context packages worked better than a model switch

One broad document review spent its first budget ingesting a 56-page artefact before it reached the substantive review. The retry completed after I supplied indexed text and renders of only the pages that needed visual inspection.

Other attempts failed because the worker lacked a required shell tool, received too much unrelated material or delegated a review which had to be independent. Those failures led to several preflight checks:

  • an input manifest with exact files and byte counts;
  • provider and capability checks before launch;
  • required project skills resolved from the worker’s real directory and pinned by path and hash;
  • an explicit writable output boundary;
  • a budget, retry limit and stop condition; and
  • no forwarded transcript when the worker can read the original source.

I still choose between providers and models, but I compare their usage only within the same task class. A metadata scan and an architecture review are not meaningful cost comparisons.

For each class I record the accepted findings or changes, retries, elapsed time, completed evidence and the provider-reported usage. The authenticated provider dashboard remains the authority for its own allowance and billing.

Security review found tests which could pass for the wrong reason

More automated launching and imported agent assets increased the impact of a bad assumption. The security work was valuable because it found controls which appeared to pass without proving the intended behaviour.

The first macOS network-isolation probe counted any connection failure as a successful block. It could therefore pass when the laptop was simply offline. The corrected test first confirms that a process outside the sandbox can reach the same literal-IP endpoint. The sandboxed attempt passes only when the failure is attributable to the confinement policy. Offline, refused, timed-out and ambiguous results now fail closed.

Timeout handling had a similar problem. Killing the parent process did not guarantee that its children had stopped. The launcher now owns a process group, sends termination and kill signals to that group and reaps the processes.

Imported skills, plugins and MCP packages have an approval record containing their source, revision, content hash, entry point, reviewer and declared capabilities. Verification rejects unexpected symlinks, special files, path traversal, modified content and additional capabilities. A failed asset is quarantined before launch.

These are macOS-specific version-one controls. They do not make arbitrary autonomous code safe, and the hardened launch path refuses to run when the required confinement mechanism is unavailable.

Long jobs get a bounded checkpoint and wait contract

I first considered using the complete provider transcript as resume state. In practice, it carried far more context than the task needed, could include sensitive data and left the next worker to infer what had actually finished.

Agentflow checkpoints use a closed set of fields:

task
phase
next action
completed evidence
blocker
changed files
last check
remaining risk
session hash

Credentials, transcript excerpts and unbounded logs are rejected. Those fields let a worker resume a bounded task after context compaction whilst keeping the checkpoint much smaller than a conversation archive.

Long-running commands have a separate deterministic wait contract:

progress predicate
success predicate
failure predicate
maximum silent interval
global deadline
cleanup owner

The last observed predicate and cleanup owner are retained when a session stops. That has been implemented and tested locally; I still need a representative set of end-to-end waits before making a reliability claim about it.

What has been verified so far?

I now separate implementation facts, observations and ongoing measurements.

Working in my current setup

  • Staged Beads work, scoped pulls, correction routing, deterministic CI observation, human-readable task explanation and Git-free coordination are implemented.
  • Checkpoints, wait contracts, exact-base worktrees, history indexing, artefact registration, usage-by-task-class, macOS isolation and imported-asset verification are implemented.
  • Provider handoff checks and project-owned skill resolution remain launch requirements.

This describes the behaviour I have exercised in my own environment. I have not treated it as evidence for every hosted check, user interface or operating environment.

Observed during project work

  • genuine incomplete scope kept downstream reviewers blocked;
  • review findings still needed controller decisions because some were factual defects, some were preferences and some duplicated earlier findings;
  • static and rendered checks missed behaviour which required runtime, hosted or visual validation;
  • a smaller indexed context package rescued the document review which exhausted its first budget; and
  • pending CI state could be observed without leaving a model session running.

Still being measured

  • whether Beads improves quality, speed or cost;
  • whether visible sessions and dashboards reduce intervention time;
  • attention-state accuracy over enough live sessions;
  • checkpoint recovery after repeated context compaction;
  • historical-search relevance against a fixed question set;
  • concurrent-worktree success across enough tasks;
  • provider and model yield within stable task classes; and
  • whether thinner domain adapters reduce input without losing correctness or evidence.

I am collecting these measurements now and do not yet have enough comparable runs to publish a result.

How I would add these changes incrementally

If you built the smaller scheduler from the first article, I would add the new pieces in this order:

  1. Keep the original goal, ownership, orchestration and integration stages.
  2. Add a dependency graph when approved state begins to outlive a chat session.
  3. Make every status report translate task IDs into a title, blocker, owner and next request.
  4. Let writers and reviewers claim one ready task from an approved root.
  5. Move CI polling and known wait predicates into deterministic commands.
  6. Use exact-base worktrees for Git projects and explicit path ownership elsewhere.
  7. Add bounded checkpoints before relying on long-running sessions.
  8. Index provenance before deciding whether any source content needs a summary.
  9. Add isolation and imported-asset checks before allowing broader automated execution.
  10. Compare provider routes only after collecting like-for-like evidence.

I would stop after each addition until a repeated failure justifies the next one. The original four-stage workflow is still enough for smaller jobs.

The controller in my current setup reads the durable graph, handles decisions which need judgement and leaves known state changes to ordinary commands. I find the workflow easier to inspect because I no longer have to reconstruct it from several open sessions. I am continuing to measure its effect on output quality, elapsed time and cost.

Beads and the private CLI examples can change, so inspect current command help before adapting them.