AWS Builder Center
Autonomous Epic Delivery with Orchestrator + Worker Agents

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.
This loop shows how the agent works


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 {
number
blockedBy(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:
  1. An orchestrator persona — a versioned system prompt that encodes the loop: read the backlog, dispatch small slices, pause, review, merge, repeat.
  1. A  gh -free done-check — a  curl / git -only command the monitor loop can run verbatim to detect "no epic in-flight."
  1. Long-wait support — let the pause shape use a single long  Wait  rather than clamping to short poll intervals.
  1. A dev/prod phase flag — one explicit signal that gates destructive and merge actions.
  1. Merge authority scoped by phase — the orchestrator merges in  development , a human merges in  production .
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  Wait  lets 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 / production  switch 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:
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article