When a convoy is the right tool, and two convoy recipes, each run end to end on a small Node.js notes
API: two source files, four tests, a CLAUDE.md, and Claude Code as the agent runtime. The output is what the CLI
printed in real runs, trimmed where marked with ….
Experimental: the convoy engine (opencastle convoy) is experimental
and may change. Every command and flag is on the CLI page.
Convoy, /batch or /fleet?
Side by side
Claude Code's /batch and Copilot CLI's /fleet also run several agents at once,
inside a session. A convoy runs from the terminal, outside any session. Where the three differ, as each
tool's documentation describes it in October 2026:
Yes, written into the plan; a task starts when the ones it waits for are done
The plan
Shown once for your approval
Decided by the main agent during the session; it can start from a plan you accepted
Checked in code and saved as a YAML spec you can edit, write yourself and run again
Isolation
A git worktree per unit
Not documented
A git worktree per task; tasks that share files never run at the same time
Review and checks
Each unit runs its tests
Not documented
A read-only review per task, except a small change outside sensitive paths (planned tasks: by complexity); your checks run once on the merged result
The result
Each unit publishes its own change
Not documented
One branch, each task merged onto it in turn
An interrupted run
Not documented
Not documented
opencastle convoy resume carries on from .opencastle/convoy.db
Models
Not documented
A low-cost model by default, or the custom agent you name with @
Each agent's tier on Claude Code, with reviews on the economy tier
Agent runtime
Claude Code
Copilot CLI
Claude Code, Copilot, Cursor, OpenCode or Codex
Not documented means the tool's page does not say, not that the tool cannot do it.
Which to use
/batch for a mechanical change across many files, where every piece looks
alike and none needs another: a rename, a codemod, a move from JavaScript to TypeScript.
/fleet when you work in Copilot CLI and want a change with several parts
done faster in the session you are in, while you watch. GitHub notes it does not help a request that is
sequential by nature.
A convoy for a feature split across the API, the UI and the data, where order matters
and you want one branch to review. Also for work you want to leave running, resume after a stop, and see
the time and cost of, or run on Cursor, OpenCode or Codex.
None of them for most everyday changes. /oc:implement-feature keeps the work
in your session, and the Team Lead hands off only what spans several areas.
Plan and run a feature
Introduction
You describe a feature in one sentence. OpenCastle plans it without changing anything, shows
you the plan and asks once. The agents then work in git worktrees of their own, in parallel where
the plan allows, and the result lands on a branch for you to review and merge. Your checkout is
never touched.
The run quoted here added pagination and DELETE /notes to the notes API: two tasks,
planned in 2 minutes 59 seconds and run in 5 minutes 28 seconds, for $2.63 in all.
Before you start
Node.js 22.5 or later, and Git
A git repository with at least one commit
An agent CLI, installed and signed in: Claude Code (claude), GitHub Copilot (copilot), Cursor (cursor-agent), OpenCode (opencode) or Codex (codex). The convoy uses the one for the assistant opencastle init sets up — see Runtime
Your project's checks as package.json scripts — typecheck, lint, test, build — if you want them run at the end
1
Step 1: Initialize
Run the initializer in the repository. It finds the CLAUDE.md, so it compiles for
Claude Code, and that is the runtime the convoy will use.
Terminal
$ cd ~/notes-api
$ npx opencastle init
🏰 OpenCastle v1.0.0
Compiles your AI assistant config for every assistant you use
Scanning repository…
Found assistant config:
• Claude Code (CLAUDE.md)
Will compile for:
→ Claude Code
Integrations detected:
chrome-devtools
Set this up? [Y/n] y
Configuring project...
…
✓ Merged into your existing CLAUDE.md
your content is above the managed block and is never overwritten
…
Commit what it wrote. Each task's worktree starts from your last commit, so anything uncommitted
is not there for the agents.
Terminal
$ git add -A
$ git commit -m "Add OpenCastle"
2
Step 2: Describe the task
Say what you want, with the details that matter to you. Planning changes no code. This request
was sized small, so the tasks were planned straight from it; larger work gets a PRD in
.opencastle/prds/ first. With --yes, one more session reviews the plan and
the run starts without asking.
Terminal
$ npx opencastle convoy "Add pagination to GET /notes: limit and offset query parameters \
(defaults 20 and 0, limit capped at 100, a negative or non-numeric value is a 400 with \
a JSON error). Add DELETE /notes, which removes every note and answers 204. Cover both \
with tests." --yes
npx opencastle convoy — planning, read-only
Runtime: Claude Code — configured by npx opencastle init
✓ Sized low — planned straight from the request, without a PRD
✓ 2 tasks planned
✓ Plan checked
✓ Plan reviewed
Plan: Notes pagination delete — 2 tasks, up to 1 at a time
TASK AGENT DEPENDS ON FILES
add-get-notes-pagination developer – src/notes.js, src/s…
add-delete-notes-endpoint developer add-get-notes-pagination src/notes.js, src/s…
Branch convoy/notes-pagination-delete · runtime claude · then once: npm run test
Spec .opencastle/convoys/notes-pagination-delete.convoy.yml
Planned in 2m 59s · 3 sessions · 334K tokens · $0.66
The table is the plan: who does what, after what, touching which files. Below it are the branch,
the runtime, the checks that run once at the end, found in package.json, and what
planning spent. (This output is from an earlier build; the current one plans the same request in
under 30 seconds, for about $0.22.)
Without --yes, the command stops here and asks. Enter runs the plan. n,
or no answer at all — as in a script — starts nothing and prints the command for later:
Run it? [Y/n] n
Not started. To run it later: npx opencastle convoy run .opencastle/convoys/<name>.convoy.yml
Tip:--dry-run plans and writes the spec, then stops. The spec is plain
YAML: edit it and run it with npx opencastle convoy run, as in
the second recipe.
3
Step 3: The run
Each task starts as soon as the tasks it depends on are done, in a worktree of its own. When its
agent finishes, the work is committed there, gets the review its complexity sets — usually one
read-only review, a panel of three for the largest — and is merged onto the run's branch. Here
the second task waits for the first, because they change the same files. The test suite ran
once, at the end, on the merged result.
Convoy: Notes pagination delete — 2 tasks
Models: by agent tier — premium opus, standard sonnet, economy haiku (and reviews)
Live view: http://127.0.0.1:4300
Branch convoy/notes-pagination-delete (from main) · 2 tasks · up to 1 at once
▶ [add-get-notes-pagination] developer
✓ [add-get-notes-pagination] (3m 57s) · $1.40
▶ [add-delete-notes-endpoint] developer
✓ [add-delete-notes-endpoint] (1m 30s) · $0.56
Gates:
✓ npm run test
──────────────────────────────────────
✓ Convoy done in 5m 28s — 2/2 tasks done
Gates: 1/1 passed
Spent: 3.1M tokens · $1.97
Convoy: convoy-1791045784848
Branch: convoy/notes-pagination-delete (from main)
Review: git diff main...convoy/notes-pagination-delete
Merge: git merge convoy/notes-pagination-delete
Log: .opencastle/logs/convoys/convoy-1791045784848.ndjson
The developer agent is standard tier, so both tasks ran on sonnet; both reviews ran
on haiku and passed. The suite went from 4 tests to 12.
Tip:--verbose streams each agent's output.
4
Step 4: Watch it
On a terminal the run starts the read-only Observability dashboard (Live view: above):
every run's tasks, what they wait for and spent, reviews, checks, retries, a failed task's reason and
the event timeline. To open it at any time:
Terminal
$ npx opencastle convoy dashboard
The overview, over seven real runs on copies of this project. Open each of them at
opencastle.dev/dashboard.
5
Step 5: Review and merge
The work is on convoy/notes-pagination-delete. Your own branch has only the new spec in
.opencastle/convoys/, uncommitted. Read the change, then merge it, or push the branch and
open a pull request.
npx opencastle convoy with nothing after it says where the last run stands and the
one thing to do next:
🚚 Convoy (experimental)
✓ Notes pagination delete — done
2/2 done
Branch convoy/notes-pagination-delete
Next: npx opencastle convoy "<task>"
every task is done; start another
Time and cost
Seven runs on this project with Claude Code, all on earlier builds; read them as data points, not a
promise. The first three asked for the same change.
What was asked
Planned
Tasks
Planning
Running
Tests
Pagination and DELETE /notes (this recipe)
sized low, no PRD, 3 sessions
2, one at a time
2m 59s · $0.66
5m 28s · $1.97
4 → 12
The same
PRD, fixed once, 7 sessions; a test task folded in
3, one at a time
7m 25s · $1.67
6m 10s · $2.13
4 → 20
The same
PRD, 5 sessions; tests as tasks of their own
4, up to 2 at once
5m 41s · $1.56
4m 34s · $2.22
4 → 19
“Add tags to notes: … Reject invalid notes with 400 … Document every endpoint in README.md …”
The model matters most. A developer task on sonnet cost $0.48–$1.40;
on Opus, the account default before tasks ran by tier, $2.01–$2.60.
Planning is now much shorter. The current build plans the pagination request in
24–28 seconds for $0.22, and a feature that needs a PRD in under 2 minutes.
Fewer tasks are not always faster. Tasks that change the same files run one after
the other.
When something fails
A task failed
A failed attempt runs again, told why it failed: once in a spec you write, up to twice for
most planned tasks, as their complexity sets. If it runs out of retries, the
tasks that depend on it are skipped and the rest carry on. The summary lists each task that is
not done with its reason, and the dashboard shows the end of its output. Fix the cause, then
npx opencastle convoy resume.
A review blocked a task
If it has a retry left, the task runs again, given the reviewer's issues, as in the second
recipe. A review that cannot run or gives no verdict is recorded as skipped, never as a pass.
The checks failed at the end
A planned spec gives one agent one attempt to fix the merged result. If the checks still fail,
the run ends gate-failed with the failing command and its first line of output; the
branch has the work so far.
A task could not be merged
A conflict re-runs the task once on top of the work it collided with. If it still cannot merge,
the task fails and the summary names the branch its work is kept on.
No runtime found
The command stops before planning and names the CLI to install. To use another one, pass
--adapter.
Write a spec, stop it, resume it
Introduction
When you already know the tasks, skip planning and write the spec yourself. This one adds search
and an export to the notes API, after opencastle init as in
the first recipe. Partway through, the run was stopped with Ctrl+C and then
resumed: everything is recorded in .opencastle/convoy.db, so resume carries
on with what is not done and keeps what is. The same command picks up a run that failed, or one that
was killed outright.
1
Step 1: Write the spec
Give each task an id, an agent, the files it may change and a
prompt; depends_on orders them. Keep code and its tests in the same task.
Tasks with no files in common run at the same time.
search.convoy.yml
name: Notes search and export
branch: convoy/search-export
gates:
- npm test
tasks:
- id: search
agent: developer
description: Search notes by text
files: [src/notes.js, test/notes.test.js]
prompt: |
Add searchNotes(query) to src/notes.js: a case-insensitive match on title or body,
returning matching notes in creation order; an empty or whitespace query returns [].
Add tests for it in test/notes.test.js and run `npm test`.
- id: export
agent: developer
description: Export notes as JSON Lines
files: [src/export.js, test/export.test.js]
prompt: |
Create src/export.js exporting toJsonLines(notes): one JSON object per line, no trailing
newline, empty string for no notes. Add test/export.test.js covering empty, one and many
notes, and run `npm test`.
- id: routes
agent: developer
description: Wire search and export into the server
depends_on: [search, export]
files: [src/server.js, test/server.test.js]
prompt: |
In src/server.js add GET /notes/search?q=<text> (uses searchNotes; a missing q is 400 with
a JSON error) and GET /notes/export (uses toJsonLines, content-type application/x-ndjson).
Add tests in test/server.test.js and run `npm test`.
Tip: a spec you write gets no checks unless it lists them under
gates; the planner adds them from package.json. Keep files
to paths and directories: a pattern such as src/*.js is refused.
2
Step 2: Check it
--dry-run runs the same checks a run does and shows the order, the runtime, the branch
and the gates. It starts nothing and records nothing.
Terminal
$ npx opencastle convoy run search.convoy.yml --dry-run
Convoy: Notes search and export — 3 tasks, up to 4 at once
TASK AGENT WAITS FOR FILES
search developer – src/notes.js, test/notes.test.js
export developer – src/export.js, test/export.test.js
routes developer search, export src/server.js, test/server.test.js
Runtime Claude Code — configured by npx opencastle init
Branch convoy/search-export
Gates npm test (once, after the tasks)
Dry run — nothing started, nothing recorded. To run it: npx opencastle convoy run search.convoy.yml
3
Step 3: Run it
Terminal
$ npx opencastle convoy run search.convoy.yml
Convoy: Notes search and export — 3 tasks
Runtime: Claude Code — configured by npx opencastle init
Models: by agent tier — premium opus, standard sonnet, economy haiku (and reviews)
Live view: http://127.0.0.1:4300
Branch convoy/search-export (from main) · 3 tasks · up to 4 at once
▶ [search] developer
▶ [export] developer
✓ [search] (1m 2s) · $0.48
✓ [export] (1m 49s) · $0.73
▶ [routes] developer
search and export ran side by side; routes started once both
were merged.
4
Step 4: Press Ctrl+C
The first Ctrl+C stops the agents and starts no new task. The task that was running goes back to
the queue, the run is marked interrupted, and the command exits with code 130. A second Ctrl+C
quits at once.
■ SIGINT — stopping: killing running agents, no new tasks. Press Ctrl+C again to quit at once.
──────────────────────────────────────
■ Convoy interrupted in 2m 19s — 2/3 tasks done
• routes (pending): stopped; runs again on resume
Spent: 2.0M tokens · $1.20 (est.)
Convoy: convoy-1791044097553
Branch: convoy/search-export (from main)
Review: git diff main...convoy/search-export
Merge: git merge convoy/search-export
Log: .opencastle/logs/convoys/convoy-1791044097553.ndjson
Resume with: npx opencastle convoy resume
The two finished tasks are already merged onto the branch, and your checkout is as it was. A
session stopped mid-way never reports what it used, so its tokens are estimated and the run's
totals are marked est.
5
Step 5: See where it stands
Terminal
$ npx opencastle convoy
🚚 Convoy (experimental)
! Notes search and export — interrupted
2/3 done, 1 pending
Branch convoy/search-export
Next: npx opencastle convoy resume
runs what is not done — failed, interrupted and skipped tasks — and keeps the rest
--dry-run lists what resuming would run, and starts nothing:
Terminal
$ npx opencastle convoy resume --dry-run
Resuming Notes search and export (convoy-1791044097553) — interrupted, 2/3 tasks done
Would run 1 of 3 task(s):
routes — developer [pending]
Dry run — nothing started, nothing recorded.
6
Step 6: Resume
It continues the newest run that is not done, on the same branch and the same runtime, with the
spec as it was when the run started. Only routes ran.
Terminal
$ npx opencastle convoy resume
Resuming Notes search and export (convoy-1791044097553) — interrupted, 2/3 tasks done
Runtime: Claude Code — the runtime this run started on
Models: by agent tier — premium opus, standard sonnet, economy haiku (and reviews)
Live view: http://127.0.0.1:4300
Branch convoy/search-export (from main) · 1 task(s) to run
▶ [routes] developer
⚠ [routes] changed files outside its list: .opencastle/KNOWN-ISSUES.md
⟳ [routes] review blocked, retry 1/1: Remove change to .opencastle/KNOWN-ISSUES.md — not in allowed files list (src/server.js, test/server.test.js)
▶ [routes] developer attempt 2/2
✓ [routes] (1m 44s) · $1.32 (est.)
Gates:
✓ npm test
──────────────────────────────────────
✓ Convoy done in 3m 56s — 3/3 tasks done
Gates: 1/1 passed
Spent: 3.7M tokens · $2.53 (est.)
Convoy: convoy-1791044097553
Branch: convoy/search-export (from main)
Review: git diff main...convoy/search-export
Merge: git merge convoy/search-export
Log: .opencastle/logs/convoys/convoy-1791044097553.ndjson
The first attempt also edited a file outside the task's list; the review blocked it, and the second
attempt, told why, passed. Spent covers both halves of the run.
Tip: resume also re-runs failed tasks, with their retries back, and the tasks a
failure skipped. To change what a task does, edit the spec and start it again with
npx opencastle convoy run: resume replays the spec the run started with.
Where the work lands. Never the branch you have checked out
concurrency
4
Tasks at once; --concurrency overrides it
on_failure
continue
continue skips only what depends on a failed task; stop starts nothing new
adapter
the one opencastle init set up
The runtime, for this spec
gates, gate_retries
none, 0
Commands run once at the end, and how many fix attempts a failure gets
defaults.timeout, timeout
30m
Per task
defaults.max_retries, max_retries
1
Attempts after the first
defaults.review, review
auto
auto passes a writer's change, and one of at most 10 lines in 2 files, without a reviewer — never one in auth/, security/, migrations/ or rls/, or from the security or data agent; fast always reviews; panel asks three reviewers; none skips
defaults.model, model
by the agent's tier
The model, for every task or for one
defaults.permission_mode
acceptEdits
What the agents may do: default, acceptEdits, auto, dontAsk, bypassPermissions or plan
The planner writes the same format; open any spec in .opencastle/convoys/ for another
example. How the engine reads it is in
ARCHITECTURE.md.