The Four-Agent Pipeline
How to build a team of AI agents that ships a complete software feature — from idea to final review — overnight, while you sleep.
1.Why a Pipeline Beats a Pile of Agents
Only one thing separates a pile of scattered agents from a real team: the handoff. The moment each agent writes its output where the next one can read it, you get a chain in which every link builds on the one before it — instead of starting from scratch each time.
Most people use AI agents as "lone snipers": they fire a reviewer here, a test generator there, by hand, one at a time, and none of them knows what the previous one did. The result is that you become the bottleneck, because you have to keep taking one agent's output and handing it to the next.
The fix is a four-stage pipeline whose links connect to one another automatically:
One trigger, four stages, and a finished feature by morning. You fire the command before bed, and over your coffee you read only the final verdict.
2.The Root Problem: Context-Window Pollution
Every language model has a Context Window: a limited working memory that holds all the inputs and outputs of the current conversation. That memory is neither infinite nor free.
Now picture a single agent trying to do everything. Its window gradually fills with a jumble of mixed data: planning notes, intermediate code snippets, tool outputs, test results, review remarks, and even dead ends that led nowhere. The heavier that accumulation gets:
- The model forgets decisions it made an hour ago.
- Its code suggestions slowly drift away from the codebase's established patterns.
- Every extra token in the history both costs money on the next turn and crowds out room for the file you actually need.
- And quality drops — quietly and steadily.
The fix is not "starting over" — every fresh start throws away all your accumulated understanding. The fix is to split the work across four specialists, each working in a clean, narrow, focused window. None has to hold the whole picture in mind; each knows only as much as its own stage demands.
In Claude Code, these specialists take the form of a Subagent: a specialized instance of Claude that runs in its own independent context window, with a custom system prompt and a restricted list of tools. The main (parent) agent delegates the work to the subagent, the subagent does it in isolation, and at the end it returns only the summary to the parent.
3.The Core Idea: Specialization + the Handoff File
At the heart of this pattern is the handoff file: each agent writes its output somewhere the next agent can pick it up. A shared folder — say .pipeline/ — plays the role of a "shared workbench":
| Agent | Reads from | Writes to |
|---|---|---|
| Planner | The project's code | .pipeline/spec.md |
| Coder | spec.md | .pipeline/changes.md |
| Tester | changes.md + spec.md | .pipeline/test-results.md |
| Reviewer | All of the above + git diff | .pipeline/review.md |
That's the whole thing: four subagents, one command, and one shared folder for the handoffs. Even the orchestrator that runs these four agents one after another is nothing more than a "slash command".
Handoff files have three advantages: 1) Transparency — you can see exactly what each stage decided; 2) Inspectability — you can correct any stage's output yourself before continuing; 3) Isolation — no agent has to keep the previous agent's full history in its window; it reads only that structured summary.
4.The Overall Architecture
Before we get into the details of each agent, it's worth seeing the file structure all at once. In Claude Code, every subagent is a Markdown file with a "YAML frontmatter" block at the top, and the body of the file becomes that agent's system prompt:
.claude/
├── agents/
│ ├── planner.md # Agent 1 (model: opus)
│ ├── coder.md # Agent 2 (model: sonnet)
│ ├── tester.md # Agent 3 (model: sonnet)
│ └── reviewer.md # Agent 4 (model: opus)
└── commands/
└── ship.md # Orchestrator: the /ship command
.pipeline/ # shared workbench (handoff files)
├── spec.md
├── changes.md
├── test-results.md
└── review.md
We match each agent's model to the nature of its work. The stages that set the "quality ceiling" — planning and reviewing — run on the stronger model (Opus), while the execution stages that work against a clear spec — coding and testing — run on the more balanced, cheaper model (Sonnet). This simple matching keeps quality up and costs down at the same time.
5.Agent 1 — The Planner
The Planner never writes code. Its job is to turn a vague feature request into a clear, precise spec — a spec the Coder can follow without guessing.
Create the file .claude/agents/planner.md:
---
name: planner
description: Turns a feature request into an implementation spec. Use as the first stage of the feature pipeline.
tools: Read, Grep, Glob, Write
model: opus
---
You are a planning specialist. You do NOT write implementation code.
Given a feature request:
1. Read the relevant parts of the codebase to understand current patterns.
2. Write a spec to `.pipeline/spec.md` containing:
- Files to create or modify, with exact paths
- The interface or function signatures needed
- Edge cases the implementation must handle
- Which existing patterns to follow (name the file to copy from)
3. Flag anything ambiguous as an OPEN QUESTION at the top of the spec.
Keep the spec tight. The Coder reads this and nothing else, so leave
no gaps and invent no requirements that weren't asked for.
Why does it run on Opus?
Because this stage sets the quality ceiling for everything that comes after it. A vague spec produces vague code no matter how strong the Coder is. So investing in quality right here pays off more than anywhere else.
If the Planner hits any ambiguity, instead of guessing it flags it as an "OPEN QUESTION" at the top of the spec. This is a control gate: as long as the spec has an open question, the pipeline halts and waits for your decision — rather than spending hours coding against a wrong assumption.
6.Agent 2 — The Coder
The Coder reads the spec and writes the implementation. It neither plans nor reviews its own work; it just builds exactly what the spec says.
Create the file .claude/agents/coder.md:
---
name: coder
description: Implements the spec at .pipeline/spec.md. Use as the second stage of the feature pipeline, after the planner.
tools: Read, Write, Edit, Grep, Glob, Bash
model: sonnet
---
You are an implementation specialist.
1. Read `.pipeline/spec.md` in full. If it has OPEN QUESTIONS, stop and
surface them instead of guessing.
2. Implement exactly what the spec describes. Follow the patterns it
names. Do not add features it didn't ask for.
3. Write a short summary to `.pipeline/changes.md`: which files changed,
what each change does, and anything the Tester should focus on.
You write code that matches the repo. You do not refactor unrelated
code or "improve" things outside the spec's scope.
Why does it run on Sonnet?
Implementation against a clear spec is exactly the "balanced cost-and-quality" work that Sonnet excels at. With the Planner having already done the hard decision-making upstream, the Coder only has to execute faithfully.
The handoff note in changes.md is precisely what lets the Tester target exactly the right surface instead of testing blind.
7.Agent 3 — The Tester
The Tester sees what changed, writes tests that prove the feature works, and then runs them.
Create the file .claude/agents/tester.md:
---
name: tester
description: Writes and runs tests for changes described in .pipeline/changes.md. Third stage of the feature pipeline.
tools: Read, Write, Edit, Grep, Glob, Bash
model: sonnet
---
You are a test specialist.
1. Read `.pipeline/changes.md` to see what was built and where.
2. Read the changed files and the spec at `.pipeline/spec.md`.
3. Write tests covering: the happy path, the edge cases the spec named,
and at least one failure case. Match the repo's test framework.
4. Run the tests. If any fail, write the failures to
`.pipeline/test-results.md` and STOP. Do not fix the code yourself.
5. If all pass, note that in `.pipeline/test-results.md`.
You test behavior, not implementation details. A failing test means
the pipeline pauses for the Reviewer, not that you patch around it.
If the Tester were allowed to "patch" its own broken code, the line between "building" and "proving" would blur, and it might hide a bug instead of surfacing it. A failing test means the pipeline pauses for the Reviewer, not that the Tester works around it. The Tester's job is to assess behavior, not implementation details.
8.Agent 4 — The Reviewer
The last gate. The Reviewer reads everything the pipeline produced and renders a verdict before anything reaches your main branch.
Create the file .claude/agents/reviewer.md:
---
name: reviewer
description: Final review of the full pipeline output. Fourth and last stage before human sign-off.
tools: Read, Grep, Glob, Bash
model: opus
---
You are a senior reviewer. You are read-only. You do not edit code.
1. Read the spec, the changes summary, and the test results from
`.pipeline/`.
2. Run `git diff` to see the actual changes.
3. Assess: does the code match the spec? Are the tests meaningful or
superficial? Any security, performance, or correctness issues?
4. Write a verdict to `.pipeline/review.md`:
- VERDICT: SHIP / NEEDS WORK / BLOCK
- For NEEDS WORK or BLOCK, list exactly what to fix and where.
Be the last line of defense. If the tests are green but the code is
wrong, say BLOCK. Green tests are not the same as correct behavior.
Why read-only?
Read-only tools mean the Reviewer cannot paper over problems by editing; it can only judge. It's exactly this separation of roles that guarantees the independence of judgment. And the philosophical point of this agent: a green test is not the same as correct behavior. If the tests pass but the code is wrong at its foundation, the verdict must be BLOCK.
9.The Orchestrator: One Command to Run Them All
Now the very piece that turns four separate agents into a pipeline: a slash command that calls them one after another, each one picking up the handoff file the previous agent wrote.
Create the file .claude/commands/ship.md:
Run the full feature pipeline for: $ARGUMENTS
Execute these stages in order. Do not skip ahead. After each stage,
confirm the handoff file exists before starting the next.
1. Delegate to the `planner` subagent with the feature request above.
Wait for `.pipeline/spec.md`.
2. If the spec has OPEN QUESTIONS, stop and show them to me. Otherwise
delegate to the `coder` subagent. Wait for `.pipeline/changes.md`.
3. Delegate to the `tester` subagent. Wait for `.pipeline/test-results.md`.
If tests failed, stop and show me the failures.
4. Delegate to the `reviewer` subagent. Show me `.pipeline/review.md`.
Report the final verdict. Do not merge anything. Leave the branch for
my morning review.
Then a single line kicks off the whole chain:
/ship add rate limiting to the login endpoint
Here $ARGUMENTS is a "placeholder": it captures whatever text you type after the command name and slots it into the prompt. So /ship add rate limiting... means that whole sentence is handed to the Planner as the feature request.
The .claude/commands/ folder is now considered the "legacy" format. The currently recommended format is .claude/skills/<name>/SKILL.md, which supports the same slash invocation (/name) and, on top of that, can be called autonomously by the model itself. Fortunately, the command-line tool still accepts both formats, so the example above keeps working; but for new projects, the Skill format is the more future-proof choice.
10.A Full Worked Example in .NET / C#
So far we've seen the pattern. Now let's follow that same example command — /ship add rate limiting to the login endpoint — on a real ASP.NET Core project and watch exactly what each agent produces. This example lays the output of all four stages side by side for one concrete feature.
Stage 1 — Planner output: spec.md
The Planner reads the existing code, works out that the login endpoint is a Minimal API in Program.cs, and writes this spec:
# Spec: Rate limiting for the login endpoint
## Goal
Protect POST /login from brute-force by limiting attempts per client IP.
## Files to modify
- `src/Api/Program.cs` (register limiter + apply policy)
## Approach (follow built-in ASP.NET Core rate limiting)
- Use `AddRateLimiter` with a named policy "login".
- Partition by client IP (Connection.RemoteIpAddress).
- Fixed window: PermitLimit = 5, Window = 1 minute, QueueLimit = 0.
- RejectionStatusCode = 429 (Too Many Requests).
- Call `app.UseRateLimiter()` before endpoint mapping.
- Apply `.RequireRateLimiting("login")` to the /login endpoint only.
## Edge cases
- Null RemoteIpAddress (proxied/local) -> fall back to "unknown" key.
- Successful logins still count toward the limit (brute-force protection).
- 429 response must include a Retry-After header.
## Existing pattern to follow
- Endpoint style mirrors the existing POST /register in Program.cs.
## OPEN QUESTIONS
- (none)
Stage 2 — Coder output: implementation + changes.md
The Coder implements the spec to the letter, drawing on the built-in rate-limiting middleware in .NET 7+:
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
// --- Rate limiting: named "login" policy, partitioned by client IP ---
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.AddPolicy("login", httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.Connection.RemoteIpAddress?.ToString()
?? "unknown",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 5,
Window = TimeSpan.FromMinutes(1),
QueueLimit = 0
}));
// Add a Retry-After header on rejection.
options.OnRejected = async (ctx, token) =>
{
if (ctx.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retry))
ctx.HttpContext.Response.Headers.RetryAfter =
((int)retry.TotalSeconds).ToString();
await ctx.HttpContext.Response.WriteAsync(
"Too many login attempts. Try again later.", token);
};
});
var app = builder.Build();
app.UseRateLimiter(); // must come before endpoint mapping
app.MapPost("/login", (LoginRequest req) =>
{
// ... existing authentication logic ...
return Results.Ok(new { token = "..." });
})
.RequireRateLimiting("login");
app.Run();
public record LoginRequest(string Username, string Password);
public partial class Program { } // exposes Program to the test project
# Changes
## Program.cs
- Registered `AddRateLimiter` with a fixed-window "login" policy
(5 requests / 1 minute), partitioned by client IP.
- Added `OnRejected` to emit a Retry-After header + 429 body.
- Applied `.RequireRateLimiting("login")` to POST /login only.
- Added `public partial class Program {}` so the test host can boot it.
## For the Tester to focus on
- The 6th request from the same IP within 60s must return 429.
- The first 5 must NOT return 429.
- The 429 response should carry a Retry-After header.
Stage 3 — Tester output: xUnit tests + test-results.md
The Tester uses WebApplicationFactory to write an integration test covering the happy path, the edge case, and the failure case:
using System.Net;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;
public class LoginRateLimitTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public LoginRateLimitTests(WebApplicationFactory<Program> factory)
=> _factory = factory;
private static readonly LoginRequest Payload =
new("[email protected]", "wrong-password");
[Fact] // happy path: first 5 attempts are allowed
public async Task Allows_First_Five_Attempts()
{
var client = _factory.CreateClient();
for (int i = 1; i <= 5; i++)
{
var res = await client.PostAsJsonAsync("/login", Payload);
Assert.NotEqual(HttpStatusCode.TooManyRequests, res.StatusCode);
}
}
[Fact] // failure case: the 6th attempt is blocked with 429
public async Task Blocks_Sixth_Attempt_With_429()
{
var client = _factory.CreateClient();
for (int i = 1; i <= 5; i++)
await client.PostAsJsonAsync("/login", Payload);
var blocked = await client.PostAsJsonAsync("/login", Payload);
Assert.Equal(HttpStatusCode.TooManyRequests, blocked.StatusCode);
}
[Fact] // edge case: rejection carries a Retry-After header
public async Task Rejection_Includes_RetryAfter_Header()
{
var client = _factory.CreateClient();
for (int i = 1; i <= 6; i++)
await client.PostAsJsonAsync("/login", Payload);
var res = await client.PostAsJsonAsync("/login", Payload);
Assert.True(res.Headers.Contains("Retry-After"));
}
}
# Test results
$ dotnet test
Passed! - Failed: 0, Passed: 3, Skipped: 0, Total: 3
- Allows_First_Five_Attempts ............ PASS
- Blocks_Sixth_Attempt_With_429 ......... PASS
- Rejection_Includes_RetryAfter_Header .. PASS
All green. Pipeline continues to the Reviewer.
Stage 4 — Reviewer output: review.md
The Reviewer runs git diff and assesses the real quality — not merely whether the tests are green:
# Review
VERDICT: NEEDS WORK
The implementation matches the spec and tests are meaningful
(they assert behavior, not internals). However:
1. [Correctness] IP partitioning trusts RemoteIpAddress directly.
Behind a reverse proxy (nginx/Cloudflare) every request shares the
proxy IP, so ALL users would share one bucket. Fix: enable
ForwardedHeaders middleware and read X-Forwarded-For, OR document
that this limiter sits in front of the proxy.
2. [Design] An in-memory fixed-window limiter does not survive a
restart and is per-instance only. For multi-instance deploys, note
that a distributed store (e.g. Redis) is needed. Acceptable for now
if single-instance — but say so in the spec.
3. [Minor] Consider SlidingWindow over FixedWindow to avoid the
burst-at-window-boundary problem (10 requests across 2 windows).
Action: address (1) before merge. (2) and (3) can be follow-ups.
Notice that all the tests were green, yet the Reviewer did not return SHIP. It caught a real security flaw — the rate-limit bucket being shared behind a proxy — that no test could see. This is exactly the "last line of defense" we described in Section 8: a green test is not the same as correct behavior.
11.Pitfalls, Costs, and Best Practices
1) The token cost is real
Each subagent has its own context window; for that reason, subagent-heavy workflows can use roughly seven times the tokens of a single-thread session. There's no separate bill, but the token volume is higher. So use the pipeline where it's worth it — full features, not one-line fixes.
2) Keep a human in the loop
The pipeline is deliberately built so that it does not merge; it leaves the branch untouched for your morning review. The stopping gates — open questions, failing tests, a BLOCK verdict — intentionally hand the work back to you. That's a feature, not a shortcoming.
3) Start small
First build only the Planner and Coder and run them as a two-stage chain. Once that flow feels solid, add the Tester and the Reviewer too. By the time all four agents are in place, you'll kick off a feature before bed and read its verdict over your morning coffee.
4) Restrict the tools
Give each agent only the tools it needs. The Reviewer is read-only; the Planner has no need for Bash. Restricting tools both raises security and makes the agent's behavior more predictable.
Claude Code also has an experimental capability called "Agent Teams," which is off by default and must be turned on with the CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS environment variable. It takes multi-agent coordination one step further; but the manual pattern in this booklet is more fundamental, more transparent, and better suited to learning.
12.Generalizing the Pattern Beyond Code
The important point is that this pattern isn't just good for code. The structure of "planner → executor → reviewer, with handoffs through a shared file" is a general orchestration pattern. The same logic could:
- Run a content team: strategist → writer → editor → SEO reviewer.
- Drive a research team: question definition → source gathering → synthesis → fact-checking.
- Automate a support team: ticket triage → draft reply → tone review.
There are managed cloud platforms that solve the "handoff" problem for you: a "Coordinator" routes work between the agents, passes context from one to the next, and keeps a shared brief — the same architecture as in this booklet, without you wiring the handoff files yourself. In return, the advantage of this booklet's manual implementation is full transparency and control: you know exactly what each stage does, and you can inspect and correct it before every handoff.
13.Summary and a Path to Gradual Adoption
What separates "a pile of agents" from "a pipeline" is that very handoff: four specialists writing to shared files, one orchestrator running them one after another, and each stage building on the one before it instead of starting from scratch.
| Step | Action | Expected result |
|---|---|---|
| 1 | Build only the Planner + Coder | A stable two-stage chain |
| 2 | Add the Tester | Automatic assurance of behavior |
| 3 | Add the Reviewer | A quality gate before merge |
| 4 | Build the /ship command | A single trigger for the whole pipeline |
Once all four agents are wired up, you'll launch a feature before bed and read its verdict with your morning coffee. The key to everything is that one simple phrase: specialization, plus a transparent handoff.
1) Create the .claude/agents/ folder • 2) Add the four agent files with the right model • 3) Put the ship command in .claude/commands/ (or the newer Skill format) • 4) Try it on a small feature • 5) Restrict each agent's tools to the minimum needed.
Educational booklet — rewritten and expanded from a thread on building a four-agent team in Claude Code.
Expanded edition with supplementary explanations, multiple examples, and a worked .NET/C# example — Raderon AI Laboratory.