Cloud Engineer Lab
Cloud Engineer Lab
Cloud Engineer Lab
Cloud Engineer Lab
© 2026
Build a Multi-Agent System: Planner, Worker, and Reviewer

Build a Multi-Agent System: Planner, Worker, and Reviewer

A single agent grading its own work shares its own blind spots. Splitting planning, execution, and review into three passes is what actually catches mistakes.

16 min read
Share

Ask a single agent to write a script, then ask that same agent, in the same turn, to check its own script for bugs, and you've asked one context to be both the author and the auditor. It usually says the code looks fine. Of course it does: the same reasoning that produced the bug is the reasoning being asked to catch it. If a model missed a division-by-zero case while writing a function, there's no strong reason it will suddenly notice that gap two sentences later just because you asked it to "double-check."

This is the actual case for a three-role multi-agent system: not that three agents are smarter than one, but that a Planner, a Worker, and a Reviewer each see the task from a genuinely different vantage point, with the Reviewer specifically built to never share the Worker's blind spot. This is a hands-on build of that exact pattern, plan a task, execute it, review it against the plan, retry on rejection, all with real code.

The one sentence to remember

The value of a separate Reviewer isn't a second opinion for its own sake, it's a differently-prompted pass, in a clean context, checking the work against criteria the Worker never got to grade itself on. Self-review inside one context tends to rubber-stamp its own reasoning; an independent review pass doesn't have that reasoning to protect.

This is the hands-on companion to Multi-Agent AI Systems: When One Agent Is Not Enough. That article covers the general supervisor-worker architecture and, just as importantly, when not to reach for it. This one builds a specific, narrower shape of that pattern: a Planner that decomposes a goal, a Worker that executes one step at a time, and a Reviewer that grades each result before the next step is allowed to start.


The Architecture

Goal: a task stated in plain language
Planner: breaks it into ordered steps, each with acceptance criteria
Worker: executes one step, produces a result
Reviewer: checks the result against that step's acceptance criteria
Rejected: feedback goes back to the Worker, bounded retries
Approved: move to the next step, or finish

Three roles, three separate contexts, on purpose

The Planner never executes anything. The Worker never grades its own output against the original acceptance criteria, it only sees the step it was asked to do. The Reviewer never writes code or performs the task, it only judges a result against a spec it didn't write and didn't execute. Collapsing any two of these into one context is exactly what reintroduces the self-review blind spot this whole architecture exists to avoid.


Why Split Into Three Roles Instead of One

Multi-Agent AI Systems names three conditions where multiple agents actually earn their cost: context pollution, genuine parallelism, and real specialization. This pattern is a direct instance of specialization, and it's worth being precise about which specialization is actually happening.

RoleWhat it's specialized forWhat it deliberately doesn't do
PlannerDecomposing a goal into ordered, checkable stepsNever executes a step or judges a result
WorkerProducing one concrete result per stepNever decides whether its own result is good enough
ReviewerJudging a result against criteria it didn't writeNever fixes the problem it finds

The Reviewer's value comes from what it doesn't know, not what it knows

The Reviewer is deliberately handed a clean context: the step's acceptance criteria and the Worker's result, nothing else. It never sees the Worker's reasoning, its false starts, or its internal justification for why a shortcut was fine. That absence is the entire point, a reasoning trail is exactly what makes self-review too forgiving.


The Contract Between the Three Roles

Before writing a single prompt, the roles need a shared, structured contract, the same principle covered under Agent Communication in Multi-Agent AI Systems, made concrete here as the actual data each role passes to the next.

json
{
  "step": {
    "id": "step-2",
    "description": "Write a Pester test for Get-LowDiskDevices covering the zero-free-space edge case",
    "acceptance_criteria": [
      "Test file uses Pester v5 syntax (Describe/It/Should)",
      "Covers a device reporting 0 bytes free without throwing",
      "Covers a device above the 10% threshold, expected to be excluded from results"
    ]
  },
  "result": {
    "step_id": "step-2",
    "artifact": "<the actual code or text the Worker produced>",
    "notes": "Assumptions the Worker made while executing this step"
  },
  "verdict": {
    "step_id": "step-2",
    "approved": false,
    "feedback": "The zero-free-space case isn't covered. Add an It block asserting no exception is thrown when FreeSpace is 0."
  }
}

Acceptance criteria belong to the Planner, not the Worker

If the Worker is allowed to write its own acceptance criteria, it will, unsurprisingly, write criteria its own output already satisfies. The criteria have to be fixed at planning time, before the Worker has produced anything to grade, or the Reviewer ends up checking the Worker's work against the Worker's own definition of done.


The Planner

The Planner's only job is decomposition: turn a goal into an ordered list of steps, each with acceptance criteria specific enough that a Reviewer with no other context could actually judge a result against them.

python
import json
import anthropic
 
client = anthropic.Anthropic()
 
PLANNER_SYSTEM_PROMPT = """You are a planning agent. Break the user's goal into an \
ordered list of concrete steps. Each step needs acceptance criteria specific enough \
that someone with no other context could judge a finished result against them. \
Respond with JSON only, matching this shape:
{"steps": [{"id": "step-1", "description": "...", "acceptance_criteria": ["...", "..."]}]}
"""
 
def create_plan(goal: str) -> dict:
    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=2048,
        system=PLANNER_SYSTEM_PROMPT,
        messages=[{"role": "user", "content": goal}],
    )
    return json.loads(response.content[0].text)

Vague criteria produce a Reviewer that can't actually reject anything

"Write good tests" isn't acceptance criteria, it's a wish. A Reviewer handed that has nothing concrete to check a result against and will end up approving almost anything. Push the specificity into the Planner: "covers the zero-free-space edge case without throwing" is something a Reviewer can actually verify against a result. This is the same discipline as writing a real evaluation rubric, covered in full in LLM Evaluation, just applied per-step instead of per-release.


The Worker

The Worker executes exactly one step at a time. It never sees the full plan, only the current step and, on a retry, the Reviewer's feedback from the previous attempt.

python
WORKER_SYSTEM_PROMPT = """You are an execution agent. You will be given one step \
and its acceptance criteria. Produce the artifact that satisfies them. If feedback \
from a previous attempt is included, address it directly. Respond with JSON only:
{"artifact": "...", "notes": "..."}
"""
 
def execute_step(step: dict, feedback: str | None = None) -> dict:
    prompt = f"Step: {step['description']}\nAcceptance criteria:\n"
    prompt += "\n".join(f"- {c}" for c in step["acceptance_criteria"])
    if feedback:
        prompt += f"\n\nYour previous attempt was rejected. Feedback: {feedback}"
 
    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=2048,
        system=WORKER_SYSTEM_PROMPT,
        messages=[{"role": "user", "content": prompt}],
    )
    return json.loads(response.content[0].text)

Scope the Worker's tool access to exactly this step

If a step involves reading a file, calling an API, or touching a real system, give the Worker only the tools that specific step needs, not the full toolset every other step in the plan might use. This is the same least-privilege reasoning from AI Agent Identity: a Worker mid-way through "write a test file" has no legitimate reason to hold a tool that can execute PowerShell against a production device. When a step genuinely is "run a real command," the layered architecture for doing that safely, an allowlisted action menu, scoped credentials, and audit logging, is covered in full in Build an AI Agent That Can Safely Execute PowerShell Commands.


The Reviewer

The Reviewer is the piece that makes this whole architecture worth the extra API calls. It gets the step's acceptance criteria and the Worker's result, in a fresh context that never saw how the Worker arrived at that result.

python
REVIEWER_SYSTEM_PROMPT = """You are a strict reviewer. You will be given a step's \
acceptance criteria and a result. Check the result against each criterion \
individually. Approve only if every criterion is clearly met. Be specific about \
what's missing when you reject. Respond with JSON only:
{"approved": true, "feedback": "..."}
"""
 
def review_step(step: dict, result: dict) -> dict:
    prompt = (
        f"Acceptance criteria:\n"
        + "\n".join(f"- {c}" for c in step["acceptance_criteria"])
        + f"\n\nResult to review:\n{result['artifact']}"
    )
    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=1024,
        system=REVIEWER_SYSTEM_PROMPT,
        messages=[{"role": "user", "content": prompt}],
    )
    return json.loads(response.content[0].text)

This is an LLM-as-judge, so it inherits every LLM-as-judge failure mode

A Reviewer built this way can be too lenient, too harsh, or inconsistent between runs, the exact grading problems covered in LLM Evaluation. Two things keep it honest here: criteria specific enough to check individually rather than judge holistically, and, where a criterion can be checked deterministically, a plain code check beats a model's opinion every time. If "the function doesn't throw on zero free space" can be verified by actually running the test, run the test. Reach for the Reviewer's judgment only for the parts a script genuinely can't verify, whether an explanation is clear, whether an edge case was reasoned about correctly, not for anything a simple try/except could answer more reliably.


The Orchestrator Loop

The orchestrator is plain code, not another agent, it doesn't need judgment, it needs to reliably run the same sequence every time.

Ask the Planner for a plan, once, up front

python
plan = create_plan(goal)

The plan is fixed for the rest of the run. If a step turns out to be impossible given what the Worker discovers while executing it, that's a signal to stop and escalate, not a license for the Worker to quietly redefine the step.

For each step, run the Worker, then the Reviewer

python
def run_plan(goal: str, max_retries: int = 2) -> list[dict]:
    plan = create_plan(goal)
    completed = []
 
    for step in plan["steps"]:
        feedback = None
        for attempt in range(1, max_retries + 2):
            result = execute_step(step, feedback)
            verdict = review_step(step, result)
 
            if verdict["approved"]:
                completed.append({"step": step, "result": result})
                break
 
            feedback = verdict["feedback"]
 
            if attempt == max_retries + 1:
                return escalate(step, result, verdict, completed)
 
    return completed

Feed a rejection's feedback back into the next attempt, don't start over blind

The Worker's second attempt gets the Reviewer's specific feedback, not just a bare "try again." A Worker retrying without knowing what actually failed tends to either repeat the same mistake or overcorrect into a different one.

Bound the retries, and give up loudly rather than silently

escalate() is a function you write, not a fallback that quietly ships an unapproved result. At minimum it should log the step, the last rejected result, the Reviewer's final feedback, and everything completed so far, then hand the whole thing to a human. This is the same retryable-versus-not distinction from Failure Handling in Multi-Agent AI Systems: a step that fails review twice in a row isn't a transient glitch worth a third automatic try, it's a sign the step itself, or the criteria written for it, needs a person to look at it.


A Full Trace

Tracing one goal through the whole loop, "write a PowerShell function that flags devices under 10% free disk space, and prove it handles the zero-free-space edge case":

StepWhat happened
PlannerProduces three steps: write Get-LowDiskDevices, write a Pester test for it, update the changelog. Step 2's criteria explicitly require covering zero free space without throwing.
Worker, step 1, attempt 1Writes the function. It divides FreeSpace by TotalSize to get a percentage, with no check for TotalSize being zero.
Reviewer, step 1, attempt 1Rejects. Feedback: "No handling for a device reporting zero total size, this would throw a divide-by-zero."
Worker, step 1, attempt 2Adds a guard clause, treats zero total size as unmeasurable rather than dividing.
Reviewer, step 1, attempt 2Approves.
Worker, step 2, attempt 1Writes a Pester test covering the normal case and the above-threshold case, but not zero free space specifically.
Reviewer, step 2, attempt 1Rejects. Feedback matches the earlier JSON example: the zero-free-space case isn't covered.
Worker, step 2, attempt 2Adds the missing It block.
Reviewer, step 2, attempt 2Approves.
Worker, step 3Updates the changelog. Approved on the first attempt.

Notice where the value actually showed up

Both rejections caught a real gap, one in the code, one in the test meant to catch problems in that code, and neither gap was something the Worker flagged about its own output. A single agent asked to "write the function and test it yourself" had already written the version with the missing guard clause once. Nothing about asking it to review its own work in the same breath was likely to surface that gap; a separate pass, with nothing invested in the first version being right, is what actually did.


Guarding Against Runaway Loops

A retry loop with no ceiling is a cost incident waiting to happen

Every rejected attempt is a full Worker call plus a full Reviewer call. A step that's genuinely impossible, or acceptance criteria that quietly contradict each other, will happily consume retries forever if nothing stops it. max_retries in the orchestrator above isn't a style choice, it's the difference between a bounded, budgeted run and one that keeps calling the API until someone notices the bill.

Three limits are worth setting explicitly, not left as implicit defaults:

LimitWhat it prevents
Retries per stepOne step's rejection loop from consuming the entire run's budget
Total steps per planA Planner that decomposes a simple goal into an unreasonably long plan
Wall-clock or token budget for the whole runA slow accumulation across many approved-but-expensive steps, not just an obvious runaway loop

The full discipline for tracing exactly where a run's time and cost went, not just that it went somewhere, is covered in AI Observability. At minimum, log every Worker attempt and every Reviewer verdict against the run, so a slow or expensive plan is diagnosable after the fact instead of a mystery.


Self-Review Versus an Independent Reviewer

Test this claim yourself before trusting it

Don't take "self-review misses things an independent pass catches" on faith. Run the same goal two ways: once as a single agent asked to write the code and then check its own work in the same conversation, once through the Planner, Worker, Reviewer loop above. Compare what each version actually catches. The gap is the entire argument for this architecture, and it's more convincing measured on your own task than read in an article.

Single agent, self-reviewPlanner, Worker, Reviewer
Context the check runs inSame conversation that produced the workFresh context, no visibility into the Worker's reasoning
What "acceptance criteria" meansWhatever the agent decides after the factFixed by the Planner before the Worker starts
Cost per taskOne API callAt minimum three, more with retries
Most likely failure modeApproves its own reasoning, misses shared blind spotsCosts more, and a badly specified criterion still slips through

Neither column is free of failure modes. The honest case for the three-role version isn't that it's failure-proof, it's that its likely failures are different from a single agent's, and specifically don't include "agreeing with itself."


When This Pattern Is Overkill

Apply the same test from the architectural article

A one-step task doesn't need a Planner, there's nothing to decompose. A task where correctness is trivially self-evident, formatting a date string, doesn't need an independent Reviewer either, a plain assertion would catch the same problem for a fraction of the cost. Reach for this shape when a wrong result would be genuinely hard to catch in the same breath that produced it, not by default because three agents sounds more rigorous than one.

If your task is...The right shape is probably...
A single, simple action with an obviously checkable resultOne agent call, or even a plain function, no review loop needed
Correctness that a deterministic check can verifyA single agent plus a code-based assertion, skip the LLM Reviewer entirely
Multi-step, with results that are genuinely hard to self-assess in the same context that produced themThe full Planner, Worker, Reviewer loop
Steps that are independent of each other and could run concurrentlyThe same loop, with independent steps parallelized, the genuine-parallelism condition from Multi-Agent AI Systems

The Bottom Line

A Planner, a Worker, and a Reviewer aren't three ways of asking the same question and hoping for a better average answer. They're three different jobs, decompose, execute, judge, deliberately kept in separate contexts so that the pass meant to catch mistakes never inherits the reasoning that produced them. The retry loop, the bounded attempts, and the escalation path aren't optional scaffolding around that idea, they're what turns "an independent review sounds good" into a system that actually stops before it burns an unbounded budget on a step that was never going to pass.

The exercise worth running before this touches anything real

Take one task you'd currently hand to a single agent with a "check your work" instruction bolted on. Run it through this loop instead, and read the Reviewer's first rejection, if there is one, closely. If it caught something specific and real, that's the pattern earning its cost on your own work, not just in a worked example.

One agent grading itself shares its own blind spots by construction. A Reviewer that never saw the reasoning has nothing to protect, and that absence is exactly what makes it useful.

CChetan Yamger

Written by

Chetan Yamger

Cloud Engineer · AI Automation Architect · Modern Workplace Consultant

Cloud Engineer, AI Automation Architect, and Modern Workplace Consultant based in Amsterdam, Netherlands. Specializing in scalable, secure enterprise solutions with Microsoft Azure, Intune, PowerShell, and AI-driven automation using ChatGPT, Gemini, and modern LLM technologies.

Cloud & Modern WorkplaceMicrosoft Intune & MDMAzure & Microsoft 365AI AutomationPrompt EngineeringPowerShell & Graph APIWindows AutopilotConditional Access & Zero TrustSCCM / MECM & MSIXVDI / WVDPower BINode.js & Next.js
Newsletter

Stay in the loop.
New articles, straight to you.

Deep-dive technical articles on Intune, PowerShell, and AI — no noise, no spam.

New article notifications
No spam, ever
Free forever

Discussion

Share your thoughts — your email stays private

Leave a comment

0/2000

Your email is used to prevent spam and will never be displayed.