ReAct, Plan-and-Execute, Reflexion, router, supervisor, handoff

What it is

Six named agent control-flow patterns. They are not six alternatives to choose between: they answer different questions, and a real system usually composes two or three.

Two axes organise them:

                  WHO decides the next step?
                  model                        code
              ┌──────────────────────┬──────────────────────┐
  one         │  ReAct               │  Router              │
  actor       │  Reflexion           │  Chain / workflow    │
              ├──────────────────────┼──────────────────────┤
  several     │  Supervisor          │  Plan-and-Execute    │
  actors      │  Handoff             │  (plan by model,     │
              │                      │   execution by code) │
              └──────────────────────┴──────────────────────┘
PatternLoop shapeUse when
ReActthink → act → observe → repeatThe path is unknown; adaptation matters
Plan-and-Executeplan once → execute steps → replan on failureThe path is mostly knowable; cost matters
Reflexionact → evaluate → critique → retryOutput quality is checkable and improvable
Routerclassify → dispatch to one handlerRequests fall into distinct known types
Supervisora coordinator delegates to specialistsSub-tasks need different tools or contexts
Handoffcontrol transfers, context resetsThe conversation genuinely changes domain

What this is confused with: "agentic" as a synonym for "uses an LLM in a loop." The useful distinction is who decides the next step. If your code decides, you have a workflow, and you should say so, because workflows are cheaper, faster, testable and debuggable. Reach for a model-driven loop only when the sequence genuinely cannot be determined in advance, and be able to say why.

The problem it solves

A single LLM call cannot do anything that requires acting on the world and reacting to the result: look something up and then decide what to look up next, try an approach and recover when it fails, or work through a task whose length is not known in advance.

The naive fix is one big prompt with all the information, which fails for the reasons on the context rot page, and cannot act at all.

The naive second fix is an unconstrained loop, which produces the failure modes on the agent failure modes page: infinite loops, hallucinated tool calls, and a cost that no one bounded.

These patterns are the space between those two, and the reason to know them by name is that each has a characteristic failure and a characteristic cost, so naming the pattern tells you what to watch.

Mechanics

ReAct: reason, act, observe

The default agent loop, and the one most frameworks implement.

def react(task: str, tools: dict, max_steps: int = 15) -> str:
    ctx = [SYSTEM, format_tools(tools), task]
    for step in range(max_steps):
        response = model(ctx)                       # THOUGHT + ACTION
        if response.is_final:
            return response.text
        result = tools[response.tool].run(response.args)   # OBSERVATION
        ctx += [response, result]
    raise StepLimitExceeded(step)
Thought: I need the customer's most recent order to answer this.
Action: lookup_orders(customer_id="C-8841", limit=1)
Observation: {"order_id": "O-4471", "status": "shipped", "carrier": "DHL"}
Thought: It shipped. The question is about delivery date, so I need tracking.
Action: track_shipment(carrier="DHL", order_id="O-4471")
Observation: {"eta": "2026-08-06", "last_scan": "Toronto sort facility"}
Thought: I have what I need.
Final: Your order shipped and is expected 6 August; it is currently in Toronto.

Strength: genuinely adaptive. Step 2 was chosen because of what step 1 returned.

Weakness, and it is structural: the context grows with every step, so cost is quadratic in step count and quality degrades as the window fills with observations. Fifteen steps at 2,000 tokens of observation each is roughly 240,000 cumulative input tokens. This is the problem sub-agent isolation exists to solve.

The characteristic failure is the loop: the model repeats a failing action because the observation did not change its assessment. Bound max_steps, and detect repetition explicitly:

recent = [(r.tool, r.args) for r in ctx if r.is_action][-3:]
if len(recent) == 3 and len(set(recent)) == 1:
    ctx += ["That action has failed three times. Try a different approach or stop."]

Plan-and-Execute: decide the sequence once

def plan_and_execute(task: str, tools: dict) -> str:
    plan = model(PLANNER_PROMPT + task, schema=Plan)     # a LIST of steps
    results = []
    for step in plan.steps:
        try:
            results.append(execute(step, tools))
        except StepFailed as e:
            # Replan from here, with what we learned. Do NOT abandon.
            plan = model(REPLAN_PROMPT + context(plan, results, e), schema=Plan)
            continue
    return model(SYNTHESIS_PROMPT + format(results))

Strength: the plan is a reviewable artifact. You can show it to a user for approval, log it, cache it, and execute independent steps in parallel. And the planner sees the task without the accumulated observation debris, so it plans from a clean context.

Weakness: the plan is made without knowing what the steps will return. A plan that assumes an order exists fails when it does not, and without a replanning path the whole run fails.

Cost comparison, which is the usual reason to choose it:

6-step task, ~2,000 tokens of observation per step:
  ReAct:              ~7 model calls, cumulative input ~58,000 tokens
  Plan-and-Execute:   1 plan call + 6 executions + 1 synthesis
                      cumulative input ~19,000 tokens

Roughly 3x cheaper, because each execution step carries only its own context rather than the accumulated history.

The right default: plan-and-execute with replanning on failure. It gets most of ReAct's adaptability at a third of the cost, and it produces an artifact you can show a human.

Reflexion: critique and retry

def reflexion(task: str, max_attempts: int = 3) -> str:
    attempt, feedback = None, ""
    for _ in range(max_attempts):
        attempt = model(TASK_PROMPT + task + feedback)
        critique = evaluate(attempt)                  # tests, a rubric, or a judge
        if critique.passes:
            return attempt
        feedback = f"\nPrevious attempt: {attempt}\nProblems: {critique.issues}"
    return attempt        # best effort; return it and flag low confidence

The whole pattern depends on evaluate being real. With a genuine external signal (unit tests pass, the SQL executes, the JSON validates against a schema, a compiler accepts it) Reflexion works well. With an LLM critiquing its own output and no external signal, it is much weaker: the model's critique correlates with its generation, so it tends to approve its own errors.

Reflexion with unit tests as the evaluator (code generation):
  attempt 1 passes:  61%
  by attempt 3:      84%          <- large, real gain

Reflexion with self-critique only (open-ended writing):
  attempt 1 rated acceptable:  72%
  by attempt 3:                76%   <- marginal, and 3x the cost

Use Reflexion where a cheap external verifier exists. Where it does not, prefer best-of-N with a separate ranker, which at least uses an independent signal.

Router: classify then dispatch

ROUTES = {
    "billing":   BillingHandler(tools=[lookup_invoice, issue_refund]),
    "technical": TechnicalHandler(tools=[search_docs, check_status]),
    "account":   AccountHandler(tools=[lookup_user, update_settings]),
}

def route(request) -> str:
    kind = classifier(request.text)        # a small model, or a real classifier
    return ROUTES[kind].handle(request)

Underrated, and it is the cheapest useful pattern. A classification call plus one specialised handler is far cheaper than one agent with the union of every tool, and it solves a real problem: an agent with 40 tools has a large tool-definition prefix in every request and picks the wrong tool more often than one with 6.

The characteristic failure is the ambiguous request that belongs to two routes, or to none. Handle it explicitly with a fallback route and a confidence threshold rather than letting the classifier guess.

Supervisor and handoff: two ways to involve several agents

Supervisor keeps a coordinator in charge; specialists are called and return.

def supervisor(task):
    ctx = [SUPERVISOR_SYSTEM, task]
    while True:
        decision = model(ctx, schema=Delegation)
        if decision.done:
            return decision.answer
        result = SPECIALISTS[decision.agent].run(decision.subtask)
        ctx += [decision, result.summary]       # SUMMARY, not the transcript

Handoff transfers control; the previous agent is gone.

def handoff_loop(request, agent=TriageAgent):
    while True:
        response = agent.run(request)
        if response.handoff_to:
            agent = AGENTS[response.handoff_to]
            request = response.handoff_context     # explicit, minimal
            continue
        return response.text

The distinction that matters: supervisor accumulates, handoff resets. Supervisor keeps one context that grows with each specialist's summary, so it can synthesise across them and it is subject to context rot. Handoff starts fresh, which is cleaner and loses everything not explicitly passed.

Use handoff when the conversation genuinely changes domain (triage to billing, and billing does not need triage's reasoning). Use supervisor when the answer requires combining specialists' outputs.

The handoff failure mode is the lost detail: the user said something in turn 2 that the new agent needs and did not receive. The mitigation is an explicit, schema'd handoff payload rather than a free-text summary.

A worked example: 40 tools, 22 seconds, $0.31 a request

A DevOps assistant: query metrics, read logs, check deploys, describe Kubernetes resources, open incidents, page on-call. Built as a single ReAct agent with all 40 tools.

Baseline:

tool definitions in every request:  11,400 tokens
mean steps per request:             9.2
mean cumulative input tokens:       142,000
p50 latency:                        22s
cost per request:                   $0.31
task success:                       67%
wrong-tool selections:              18% of tool calls

Two problems visible in those numbers. The 11,400-token tool prefix is paid on every one of 9.2 steps, so 105,000 of the 142,000 tokens are tool definitions re-sent. And an 18 percent wrong-tool rate is what happens when a model chooses among 40 similar options.

Change 1: a router. Requests were classified into four domains, each with 6 to 11 tools.

DOMAINS = {
    "observability": [query_metrics, search_logs, get_traces, ...],       # 9 tools
    "deployment":    [list_deploys, get_deploy_status, rollback, ...],    # 7
    "kubernetes":    [describe_pod, get_events, exec_command, ...],       # 11
    "incident":      [create_incident, page_oncall, update_status, ...],  # 6
}
tool definitions per request:  11,400 -> 2,900 tokens   (-75%)
wrong-tool selections:         18% -> 6%
cost per request:              $0.31 -> $0.12
task success:                  67% -> 74%

A classification call plus a smaller tool set improved success by 7 points, which was unexpected. Fewer options is not merely cheaper; the model chooses better among 9 than among 40.

Change 2: plan-and-execute instead of ReAct, since most requests followed recognisable shapes.

class Plan(BaseModel):
    steps: list[Step] = Field(max_length=8)
    rationale: str

class Step(BaseModel):
    tool: str
    args: dict
    depends_on: list[int] = []      # enables PARALLEL execution
mean cumulative input tokens:  142,000 -> 38,000
p50 latency:                   22s -> 9s      (independent steps run in parallel)
cost per request:              $0.12 -> $0.05
task success:                  74% -> 71%     <- DROPPED 3 points

Cheaper and faster and slightly worse. The failures were requests where the right second step depended on the first step's output, which the planner could not know. A plan that says "check pod status, then read logs for the failing pod" cannot name the failing pod in advance.

Change 3: replanning on failure, which is the fix rather than reverting.

for i, step in enumerate(plan.steps):
    try:
        results.append(execute(step))
    except (StepFailed, PreconditionUnmet) as e:
        remaining = plan.steps[i:]
        plan = replan(task, done=results, failed=step, error=e, remaining=remaining)
        # continue with the NEW plan from this point
task success:                  71% -> 88%
mean replans per request:      0.4
cost per request:              $0.05 -> $0.06
p50 latency:                   9s -> 10s

Seventeen points of success for 20 percent more cost. Replanning recovers exactly the cases where the plan was made without information only execution could provide, which was the entire gap.

Change 4: Reflexion on the one sub-task with a real verifier. Generating PromQL queries was error-prone, and a PromQL query either parses and returns data or it does not.

def generate_promql(intent: str) -> str:
    feedback = ""
    for _ in range(3):
        q = model(PROMQL_PROMPT + intent + feedback)
        ok, err = prometheus.validate(q)          # a REAL external check
        if ok:
            return q
        feedback = f"\nPrevious: {q}\nError: {err}"
    raise CannotGenerateQuery(intent)
PromQL first-attempt validity:   64%
after up to 3 attempts:          97%
task success (overall):          88% -> 93%

Reflexion applied to one narrow sub-task with a genuine verifier gave 5 points overall. They had considered applying it to the whole agent loop and did not, because there was no external signal for "was this a good incident response."

Final:

                          single ReAct   final (router + plan/execute + replan + reflexion)
tool defs per request        11,400          2,900
cumulative input tokens     142,000         41,000        (-71%)
p50 latency                    22s             10s        (-55%)
cost per request             $0.31           $0.06        (-81%)
task success                   67%             93%        (+26 points)
wrong-tool selections          18%              5%

Four patterns composed, none of them used everywhere. The router handles dispatch, plan-and-execute handles the main loop, replanning handles the adaptive gap, and Reflexion handles the one sub-task with a checkable output.

The transferable lesson is the ordering of the diagnosis. They did not choose an architecture; they measured what was expensive (tool prefix re-sent 9 times), what was wrong (wrong-tool selection among 40 options), and what was failing (plans made without execution-time information), and each measurement pointed at a specific pattern. The patterns are answers to questions, and the work is asking the right question.

Production evidence

ReAct (Yao et al., 2022) introduced the interleaved reasoning-and-acting format and is the basis of most framework agent loops. Its own paper notes the context-growth problem and does not solve it.

Plan-and-Solve (Wang et al., 2023) and the LangChain Plan-and-Execute implementation formalised separating planning from execution, with the stated motivation being cost and the ability to inspect the plan.

Reflexion (Shinn et al., 2023) reported large gains on tasks with verifiable outcomes (HumanEval coding, ALFWorld) and the paper is explicit that the quality of the evaluator determines the benefit. Subsequent work on self-correction without external feedback (Huang et al., 2023, "Large Language Models Cannot Self-Correct Reasoning Yet") found that self-critique alone frequently makes things worse, which is the caveat to carry.

OpenAI's Swarm and the Agents SDK implement handoff as a first-class primitive, where one agent transfers control and context to another. LangGraph implements supervisor and handoff topologies over an explicit state graph, and CrewAI and AutoGen implement supervisor-style delegation.

Anthropic's "Building Effective Agents" makes the argument this page opens with: most production systems are better served by composable workflow patterns (routing, chaining, parallelisation) than by an autonomous agent loop, and agents should be reserved for cases where the sequence genuinely cannot be predicted. That guidance from a model vendor, against their own commercial interest in more tokens, is worth weighing.

The debate

Agent or workflow? Ask who decides the next step. If your code can decide, it should: workflows are cheaper, faster, deterministic, testable and debuggable, and a model-driven loop buys adaptability you may not need. My position: default to a workflow, and require a specific justification for a model-driven loop. The justification is usually "the number and order of steps depends on data we only see at runtime," and if you cannot state it in that form you probably want a workflow.

ReAct or plan-and-execute? Plan-and-execute with replanning, as the default. It costs roughly a third as much because each step carries its own context rather than the accumulated history, it produces an inspectable plan, and independent steps run in parallel. Pure ReAct wins when nearly every step depends on the previous result, which is rarer than it feels. Pure plan-and-execute without replanning is the worst of the three, because it is brittle in exactly the situations that motivated an agent.

Is Reflexion worth 3x the cost? Only with a genuine external verifier. Tests, a compiler, a schema validator, a query parser: yes, and the gains are large (61 to 84 percent on code in the published results). Self-critique with no external signal: the evidence says it is marginal at best and sometimes harmful, because the critique is generated by the same model whose errors it is meant to catch. Best-of-N with a separate ranker is the better spend when no verifier exists.

How many tools should one agent have? Fewer than people give them. The worked example saw wrong-tool selection fall from 18 percent to 6 percent by splitting 40 tools across four routed domains, and success rise 7 points. Somewhere around 10 to 15 tools is where selection quality starts degrading noticeably, and the fix is routing rather than better tool descriptions.

Supervisor or handoff? Handoff when the domain genuinely changes and the new agent does not need the old context: it is cleaner and cheaper. Supervisor when the answer requires combining specialists' work. The failure mode of handoff is the lost detail, so the payload should be a schema rather than free text, and the failure mode of supervisor is context growth, so specialists should return summaries rather than transcripts.

Are multi-agent topologies worth it at all? For context isolation and parallelism, yes, and those are mechanical benefits you can measure. For "specialised agents collaborate better," the evidence is much weaker, and I would be sceptical of a design whose justification is that each agent has an expert persona. See sub-agent isolation.

Follow-up Q&A

"How do you decide between an agent and a workflow?"

Who decides the next step. If your code can determine the sequence, write a workflow: cheaper, faster, deterministic, testable, and it produces stack traces. Use a model-driven loop when the number and order of steps depends on data you only see at runtime, and be able to state the justification in that form. Most systems described as agents are workflows with a model in one node, and describing them accurately makes them easier to reason about.

"ReAct or plan-and-execute?"

Plan-and-execute with replanning, as a default. ReAct's context grows with every observation, so cost is quadratic in step count and quality degrades as the window fills; plan-and-execute carries only each step's own context, is roughly 3x cheaper on a six-step task, produces an inspectable plan, and can run independent steps in parallel. The gap is that plans are made without execution-time information, and replanning on failure closes it: in one case that was 17 points of task success for 20 percent more cost.

"When does Reflexion help?"

When a cheap external verifier exists: tests, a compiler, a schema validator, a query parser. Published results show large gains on code (61 to 84 percent across three attempts). With self-critique and no external signal, the gains are marginal and there is published evidence that self-correction alone can make reasoning worse, because the critique comes from the same model as the error. Where no verifier exists, best-of-N with a separate ranker uses an independent signal and is the better spend.

"How many tools is too many?"

Selection quality degrades noticeably somewhere past 10 to 15. In one case, splitting 40 tools across four routed domains took wrong-tool selection from 18 percent to 6 percent and raised task success 7 points, as well as cutting the tool-definition prefix by 75 percent. Fewer options is not just cheaper; the model chooses better. The fix is routing rather than writing better descriptions for 40 tools.

"Supervisor or handoff?"

Handoff transfers control and resets context, so it is right when the domain genuinely changes and the next agent does not need the previous reasoning. Supervisor keeps a coordinator that accumulates specialists' summaries, so it is right when the answer requires combining them. Handoff's failure is the lost detail, which is mitigated by making the payload a schema rather than free text; supervisor's failure is context growth, which is mitigated by returning summaries rather than transcripts.

"An agent is looping. What do you do?"

Bound the steps, which should already be true. Then detect repetition explicitly: if the last three actions are identical, inject an observation saying so and asking for a different approach, because the model repeating an action means the observation did not change its assessment and nothing in the loop will change that on its own. Then look at whether the tool is returning a useful error: a tool that returns an empty result for both "no match" and "malformed query" gives the model no way to distinguish retry from rethink.

Common misconceptions

"Agentic means better." It means the model decides the control flow, which is a cost and a debugging burden you take on to buy adaptability. If your code can decide, it should.

"ReAct is the agent pattern." It is the most common one and the most expensive, because its context grows with every observation. Plan-and-execute with replanning gets most of the adaptability for a third of the cost.

"Reflexion improves any output." It improves output where an external verifier can score it. With self-critique alone, published work finds the gains marginal and sometimes negative.

"More tools makes an agent more capable." Past roughly 10 to 15, selection accuracy degrades and the tool-definition prefix is paid on every step. Routing to smaller tool sets improved both cost and success in the worked example.

"Multi-agent means the agents collaborate." The measurable benefits are context isolation and parallelism. Claims that specialised personas produce specialised expertise are much weaker, and a design justified that way deserves scrutiny.

Interview delivery note

Say this verbatim: "The question that separates these is who decides the next step. If my code can decide, it should, because workflows are cheaper, deterministic and debuggable. Model-driven loops are for when the number and order of steps depends on data you only see at runtime, and I would default to plan-and-execute with replanning rather than ReAct, because it is about a third of the cost and the plan is an artifact you can show a human." The organising question plus a committed default.

The senior-versus-staff separator is knowing that fewer tools improves accuracy, not just cost. A senior engineer names the patterns correctly. A staff engineer measures that wrong-tool selection was 18 percent among 40 tools and 6 percent among 9, that success rose 7 points from routing alone, and concludes that the fix for tool sprawl is routing rather than better descriptions. That is a non-obvious, measurable claim.

The second signal is being sceptical of Reflexion without a verifier, and being able to say why: the critique is generated by the same model whose errors it is meant to catch, and there is published evidence that self-correction without external feedback degrades reasoning. Recommending best-of-N with a separate ranker instead shows you are reasoning about signal independence rather than pattern-matching on a paper title.

Further reading

  • Yao et al., "ReAct: Synergizing Reasoning and Acting in Language Models" (2022).
  • Shinn et al., "Reflexion: Language Agents with Verbal Reinforcement Learning" (2023), read alongside Huang et al., "Large Language Models Cannot Self-Correct Reasoning Yet" (2023).
  • Anthropic, "Building Effective Agents," for the workflow-before-agent argument and the composable patterns.
  • LangGraph documentation on supervisor and handoff topologies, and OpenAI's Agents SDK handoff primitive.