
Autonomous Epic Delivery with Orchestrator + Worker Agents
A pattern where an orchestrator agent drives a whole GitHub backlog to done on Bedrock AgentCore — pausing on zero compute until a human is truly needed.
Autonomous Epic Delivery with Orchestrator + Worker Agents
Most "AI coding agent" demos stop at a single issue: you
@mention a bot, it opens a pull request, you review, you merge. That's useful, but it's still you driving — one issue at a time, waiting on the agent, then getting pulled back in to dispatch the next one.I wanted something further along that curve. I wanted to hand an agent a whole feature vision, have it break the work into a tracked backlog, and then let a self-driving loop implement, review, and merge the entire thing — pausing only when it genuinely hit a decision that was mine to make.
This is the pattern I settled on. It's built entirely on pieces I've written about before — Claude Code hosted on Amazon Bedrock AgentCore , dispatched by GitHub webhooks into Step Functions — but the interesting part is how they compose into a loop that runs for hours without a human and without an ever-growing context window.
Two phases: plan with a human, deliver without one
The pattern splits cleanly into two phases.
Phase 0 — Planning (human + agent, interactive). You sit down with a local Claude Code session in plan mode and describe the feature set you want. The agent explores the codebase, researches options, and proposes a structure: milestones → epics → child issues, each with acceptance criteria and explicit
blocked-by dependencies. On your approval, it creates the GitHub milestones, the native parent/child sub-issue links , and a GitHub Projects board to visualize the backlog.The backlog is the contract. Everything downstream is driven off issues, labels, and dependency edges — not off the planning chat. This matters more than it looks: it's what lets the delivery phase be stateless (more on that below).
Phase 1 — Autonomous delivery (agents, hands-off). You kick off the loop once. From then on the system drives issues to done without waiting for you between items, and only stops when all work is delivered or the only remaining work is blocked on a human decision.
The loop
There are two roles, and — this is the fun part — they can be the same agent binary. A hosted Claude Code runtime is both the orchestrator and the workers; they differ only in the prompt they're dispatched with.

The orchestrator
Its job is to be a project manager, not an engineer. It reads the backlog, works epics in dependency order, and dispatches each ready slice to a worker (by commenting the agent mention on the issue). "Ready" means an open epic with no open blocker — and the GitHub GraphQL API hands that to it directly. Each issue's
blockedBy connection returns every blocking issue with its state, so one query over the open epics is enough to compute the ready-set:graphql
repository(owner: "<owner>", name: "<repo>") {issues(first: 100, states: OPEN) {nodes {numberblockedBy(first: 50) { nodes { number state } }}}}
An epic is ready when its
blockedBy list contains zero OPEN nodes — which covers both "never had a blocker" and "every blocker is now closed." (Within an epic, the worker handles child-issue ordering.)One thing to not reach for here: the built-in
no:blocked-by search filter you'd use in the Projects board UI. It matches issues that have no blocking relationship at all, so once a blocker closes the link still exists and the newly-unblocked epic stays filtered out — the exact work you now want to pick up. That's a real limitation of the board's saved-filter syntax, but it's a non-issue for programmatic dispatch: the GraphQL state field lets you tell "blocked by an open epic" apart from "blocked by an epic that's since closed," which is precisely the distinction the search filter can't express.It dispatches at the epic level, not the individual issue — one worker owns a whole epic and delivers it as a single PR. That's a deliberate shift from an earlier version where every child issue got its own worker and its own PR: correct, but it buried me in dozens of tiny PRs to review. One PR per epic keeps the review surface proportional to the feature, not the task breakdown. The wall-clock ceiling still bites, though — a hosted run can't always finish a large epic in one shot — so the worker pushes a draft PR the moment its first change type-checks and keeps committing to that same branch as each child issue lands. If it hits the ceiling mid-epic, the orchestrator re-dispatches the epic and the next run continues on the existing branch.
Then the orchestrator pauses while the workers run. When it wakes, it reviews each resulting PR, merges the green ones, re-dispatches any epic whose worker didn't finish (it hit the ceiling — the next run picks up its branch), and leaves
needs-review issues alone. It repeats until the backlog is drained.Bias to action is the governing rule. The orchestrator and workers make reasonable calls autonomously and document them on the issue or PR rather than stopping to ask. A human can always request changes later.
needs-review is reserved for decisions that are genuinely a human's to make — product direction, an irreversible external action, an ambiguity no sensible default resolves.The workers
Each worker is a single dispatch against one epic. It creates a feature branch for the epic (never commits to
main ), works the epic's child issues in dependency order, type-checks, and pushes a draft PR early — then keeps committing to that same branch as each child lands. The PR body carries auto-closing Closes #<child> lines for the issues it completes, so a single merge closes the finished children (and the epic once they're all done). It fixes its own CI failures, documents any non-obvious choice as a comment, and — if it hits a real decision point — labels the issue needs-review , states the options and its recommendation, leaves the PR as a draft, and ends.The human (you)
Between kickoff and completion you do nothing unless an issue is labeled
needs-review . That label is the loop's signal back to you. You answer in the issue comment, remove the label, and the orchestrator picks it up on its next wave. It's the same escalation convention whether the loop is fully autonomous or gated (see dev vs. prod below).Pausing without holding compute
The orchestrator can't just sit in a
sleep — that would burn a compute-hour doing nothing while three workers grind through their issues. This is where AWS Step Functions earns its keep.At the end of a wave, the orchestrator emits a small monitor instruction: "re-invoke me when no open issue is still in-flight." The runtime hands that back to a Step Functions state machine, which enters a
Wait → check → Choice loop:- During the
Wait, no runtime compute is held. The AgentCore microVM is reclaimed at its idle threshold, so an orchestrator polling every 15 minutes for hours costs essentially nothing.
- The check runs in the same session — the same workspace clone, the same disk. When it passes, the orchestrator is re-invoked; otherwise the loop waits another interval.
The Step Functions
Wait state supports a single wait of up to 99,999,999 seconds — roughly 3.17 years — while holding no compute. So "pause a few hours, then continue" is one Wait , not a busy poll loop. You have two useful shapes:- Timed wait — "sleep N seconds, then re-invoke." A direct single
Wait.
- Condition poll — wake as soon as the workers are done, using an infrequent check so you're not hammering the API.
The done-signal itself is worth calling out because it's the crux of the whole loop. Each dispatched run adds an
agent-working label to the epic it's working when it starts and removes it when it ends — whether it finished the epic or stopped at the ceiling. So the authoritative "it's my turn to act" condition is simply: no open epic carries the agent-working label. That's a single curl against the GitHub REST API — no gh CLI auth needed inside the check environment, which matters because the check runs in a minimal shell.Why the context window never explodes
Here's the property I'm proudest of, and it falls out of the design rather than being bolted on.
A naive orchestrator would keep one long conversation across every wave — dispatch, wait, review, dispatch, wait, review — and accumulate a giant, expensive context window full of stale PR diffs. This architecture avoids that by construction, with no trimming logic.
Every re-invoke spawns a fresh agent process with an empty conversation. There's no
--resume . What survives a wait is the workspace git clone on disk and the durable state in GitHub — not a transcript. So each wave's context is bounded by that wave's own work, never the sum of all prior waves. And the multi-hour Wait holds zero compute and zero context.The design principle that follows: treat every wave as stateless and re-derivable from durable external state (GitHub), not from a transcript. Three techniques make that cheap:
Technique | Effect |
|---|---|
GitHub is the state store — keep it compact. The orchestrator reconstructs "where am I?" from the epic checklist, the sub-issue rollup, the blocked-by graph, and a short delivery-ledger comment — a few hundred tokens regardless of wave count. | Cross-wave state cost is O(1), not O(waves). |
Delegate heavy reading down, not up. The orchestrator reads conclusions — a worker's one-line "merge-ready" or "blocked because X" — not artifacts. Diffs, CI logs, and file dumps stay in the worker's context. | The biggest per-wave saving: the orchestrator window never fills with PR diffs. |
Tiny follow-up prompt, lean on prompt caching. The only thing crossing the wait boundary is a pointer — "resume the loop; read the ledger on epic #NNN" — not a state dump. The stable system prompt is identical each wave, so it's a natural prompt-cache hit. | Cheap cold restarts. |
This also makes the loop robust: a wave that crashes loses nothing the next cold wave can't rebuild from the issue and PR trail.
Development mode vs. production mode
There has to be a clear, explicit break between the initial-build period and production, because the right amount of autonomy is different in each. I gate it with a single
PROJECT_PHASE value — development or production — that every run reads at the top of its turn. | Development (default) | Production |
|---|---|---|
Breaking schema/API changes | Fine — prioritize speed | Require a migration + human review |
Deleting shared resources | Fine | Forbidden without explicit sign-off |
Merge to main | Orchestrator merges green, on-scope PRs autonomously | A human reviews and approves every PR — the orchestrator never merges main itself |
Autonomy | Full — decide and document | Human approves anything destructive or outward-facing |
In
development , the loop is fully autonomous end to end: workers open PRs and the orchestrator merges the green, on-scope ones, so the epic drains without you. In production , the merge step becomes a human gate. Workers still implement and push PRs. The orchestrator still reviews, runs the full merge bar — green CI, a valid auto-close keyword, an on-scope diff — and does everything up to the merge. But instead of merging, it marks the PR ready for human review, posts its merge-readiness verdict as a comment, and labels the issue needs-review . You review the diff and merge. On its next wave, the orchestrator sees the merged work, watches the dependents unblock, and continues.The point is that a human merge is just another gate the loop waits on, exactly like a
needs-review question — so you keep the throughput of autonomous planning, implementation, CI-fixing, and review-prep while making the one irreversible, outward-facing action a deliberate human decision.Enforce the gate; don't just document it. Two layers work well together: the orchestrator prompt branches on
PROJECT_PHASE and simply never calls the merge command in production ; and GitHub branch protection on main (require a PR plus at least one human approving review) makes an autonomous merge impossible even if the prompt-level rule is somehow bypassed. Belt and suspenders for the one action you most want gated.What it takes to stand this up
If you've already got a hosted coding agent, a webhook dispatch pipeline, and a compute-free pause/re-invoke loop, the remaining work is orchestration policy, not new infrastructure:
- An orchestrator persona — a versioned system prompt that encodes the loop: read the backlog, dispatch small slices, pause, review, merge, repeat.
- A
gh-free done-check — acurl/git-only command the monitor loop can run verbatim to detect "no epic in-flight."
- Long-wait support — let the pause shape use a single long
Waitrather than clamping to short poll intervals.
- A dev/prod phase flag — one explicit signal that gates destructive and merge actions.
- Merge authority scoped by phase — the orchestrator merges in
development, a human merges inproduction.
The load-bearing parts — a coding agent on AgentCore, a webhook dispatch pipeline, a pause/re-invoke loop that holds no compute, and a reliable done-signal — are the hard infrastructure. Once those exist, the autonomous epic loop is a few days of prompt-and-glue work on top.
Takeaways
- Make the backlog the contract. If every wave can rebuild its state from GitHub issues, labels, and dependency edges, the delivery loop can be stateless — and stateless is what keeps it cheap and crash-proof.
- Pause on zero compute. A Step Functions
Waitlets a long-lived orchestrator sleep for hours between waves without holding a microVM. This is the difference between "affordable" and "why is my bill like this."
- Delegate reading down, not up. The orchestrator should read conclusions, not artifacts. That single discipline is what keeps its context window flat no matter how many waves run.
- Gate the irreversible action, automate the rest. A
development/productionswitch lets you run fully hands-off while building and insert a human merge gate the moment the project is real — without giving up any of the autonomy in the steps before merge.
Related reading
This pattern is the capstone on a series about running Claude Code as cloud infrastructure. If you want the pieces underneath it:
- @agent in Your GitHub Issues, No Runner Required: Webhooks + Step Functions + Bedrock AgentCore — the dispatch pipeline that turns an issue comment into an agent run.
- Host Claude Code on Bedrock AgentCore with the AgentCoreApplication CDK Construct — the runtime both the orchestrator and workers run on.
- How I Used Claude Code to Make an Autonomous Coding Harness Reliable — hardening the worker side so unattended runs don't wedge.
- Persist Claude Code Turns in Bedrock AgentCore Memory — where cross-session state lives when you do want it.
- I Stopped Running Claude Code on My Laptop. Here's What I Do Instead. — the "why run it in the cloud at all" backstory.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article