Interview prep
PatternAgent behavior

Plan-then-Execute

Planejar e Executar

Plan-then-Execute is an AI design pattern for LLM apps and agents, in the Agent behavior group. Write the full plan first and execute steps with a simpler model or with code.

Element
41/70
Level
Intermediate
Read
9 min
Updated
On this page 18
  1. 01Intent
  2. 02Problem
  3. 03Solution
  4. 04Structure
  5. 05Pseudocode
  6. 06Real-world scenarios
  7. 07Applicability
  8. 08When not to use
  9. 09How to implement
  10. 10Variations
  11. 11Cost, latency, and failure modes
  12. 12How to evaluate
  13. 13In production
  14. 14Security
  15. 15Pros and cons
  16. 16Code
  17. 17Framework support
  18. 18Check your understanding
  19. ··Quick answers

Intent

Write the full plan before acting, then execute the steps with code or a simpler model. It is for long tasks where the order of steps can be decided up front and deciding on every turn is expensive.

In one sentence
A travel itinerary written the night before: in the morning you follow the list instead of choosing every street at the crossing.

Problem

Diana runs a browser agent that downloads duplicate bills from utility company portals. Every task makes the same moves on different portals: log in, find the bills area, filter the month, download the PDF, go back.

The agent did everything as a ReAct Loop, with the large model deciding each click by looking at the screen. It worked, but each task cost dozens of calls to the larger model, and every so often it got distracted by a banner and forgot the requested month.

On top of that, the plan was never visible. When a task failed, there was no way to tell whether the problem was the strategy or the execution of a step. The team scrolled through traces of 40 clicks to find the moment the strategy went off the rails.

The question this pattern answers: how do you separate deciding what to do from doing it, so each part is cheaper, more predictable, and easier to inspect?

Solution

Split the work into two phases:

  • Plan: a capable model receives the task and the available context and writes a structured plan: numbered steps, each one’s goal, its inputs, and the criterion that it succeeded.
  • Execute: each step is carried out by code, by a smaller model, or by a short agent with a narrow scope. The executor does not decide the strategy; it completes the step and reports the result.

If a step fails or the result does not match expectations, execution stops and the planner is called again with the current state. That replanning needs a cap: two or three rounds, not an endless loop of new plans.

The plan is an artifact: you can show it to a person before execution, store it on the trace, and compare it across runs. Xu and colleagues named a nearby idea ReWOO (2023): reason without observing on every step, so you do not pay the large model on every observation. The emphasis here is the plan as an object that code validates and the executor fulfills.

Worked example

Task: “download the August bill from utility company X, account 4419”. The planner returns:

{
  "goal": "PDF of the 08/2026 bill for account 4419",
  "steps": [
    { "action": "open", "target": "https://portal.x.example/login", "success_check": "url contains /home" },
    { "action": "fill", "target": "#account", "value": "4419", "success_check": "account field = 4419" },
    { "action": "click", "target": "a.bills", "success_check": "page title is Bills" },
    { "action": "click", "target": "a[data-month='2026-08']", "success_check": ".pdf file exists and name contains 2026-08" }
  ]
}

The executor is code: open, fill, click, download. The last step’s check is not “the page looks right”. It is “a PDF exists whose name contains 2026-08”. If the portal changed the selector, step 3 fails, code stops, and the planner receives the current HTML. Without that check, the executor would mark success on a July receipt.

Suppose, for illustration, 8 clicks per portal and a large-model call per click in ReAct. The happy path of this pattern is one planner call plus 8 code actions. The large model only comes back when a step breaks.

Structure

flowchart TD
  T(["1. Task"]):::io --> P(["2. Planner: capable model"]):::llm
  P --> PL[("3. Structured plan")]:::store
  PL --> H{"4. Needs approval?"}
  H -->|yes| A((("Person reviews the plan"))):::human
  H -->|no| E["5. Execute step"]:::tool
  A --> E
  E --> V{"6. Did the step succeed?"}
  V -->|yes, more steps| E
  V -->|no| P
  V -->|last one| OUT(["7. Result"]):::io
  1. The task arrives with a goal and context.
  2. A capable model writes the whole plan, without executing anything.
  3. The plan is an object: steps, inputs, each step’s success criterion.
  4. Plans with sensitive effects can go through a person before running.
  5. Each step runs with the simplest executor that works.
  6. Code checks the step’s criterion. Failure goes back to the planner with the state.
  7. With every step done, the result goes out.
Check +20 XP on first tryThe plan has 12 clicks. On step 3 a banner covers the button and the check fails. What does this pattern do?

Pseudocode

schema Step { id: int, action: "open" | "click" | "fill" | "download" | "extract",
              target: string, value: string?, success_check: string }
schema Plan { goal: string, steps: list[Step] }

function run(task):
    plan = llm(PLANNER_PROMPT, task, model="large") -> Plan             # 2 and 3
    trace.span("plan", plan)
    if plan.has_side_effects(): human.approve(plan)                       # 4
    for step in plan.steps:
        result = execute(step)                                           # 5. code or small model
        if not check(step.success_check, result):                        # 6
            plan = llm(REPLANNER_PROMPT, task, plan, step, result, model="large") -> Plan
            return run_from(plan, state)                                 # replanning, with a cap
    return collect_results()                                             # 7

function execute(step):
    if step.action in ["open", "download"]: return browser.do(step)      # plain code
    return llm_with_tools(EXECUTOR_PROMPT, [browser], step, model="small")

Real-world scenarios

  • Deep research with citations. The plan lists the sub-questions before any search. Each sub-question is executed by a subagent, and the plan goes into the report as its methodology.
  • NF-e and contract extraction. NF-e is Brazil’s electronic invoice. The plan lists the fields (issuer, totals, taxes) and each field’s criterion. Extraction itself can be code plus a small model; the planner does not reread the XML for every field.
  • Repetitive portals (hypothetical). Each utility’s plan is written once and executed by code and a small model. The large model only comes back when a step fails. It fits duplicate bills, tax receipts, government extracts: the script changes little from month to month.

Applicability

  • The task has many steps, and their order can be decided at the start.
  • Many steps are mechanical and can run with code or a smaller model.
  • You want to show the plan to a person before acting.
  • The cost of the large model deciding every turn adds up.

When not to use

  • The next step depends heavily on the previous result, and the environment is unpredictable. Use a ReAct Loop. A live incident, with logs changing every minute, is ReAct; a bill portal you already mapped is not.
  • The task is short. Planning costs a call that does not pay for itself. Classifying a ticket into three categories does not need a plan.
  • The path is identical for every task. Write the chain in code once: see Prompt Chaining. If the plan would come out the same every time, you are paying a model to rewrite what the code already knows.

How to implement

  1. Define the plan schema: action types, inputs, each step’s success criterion.
  2. Write the planner prompt with the available context and examples of good plans. Also show a bad plan (impossible step, vague check) and say why it would be rejected.
  3. Validate the plan in code: known actions, step count within the limit, unique ids, no cycles if there are dependencies.
  4. Implement executors per action type, starting with those that need no model.
  5. Define each step’s check in code whenever possible. “PDF exists” beats “it looks like it downloaded”.
  6. Implement replanning with a cap.
  7. Record the plan, each step’s execution, and replans on the trace.
  8. Compare with the ReAct version in the eval: success rate, cost, and time.

What beginners get wrong

  • Check in the small model. The executor “looks at the screen” and says it worked. You only changed who is opining. The check needs a fact: file on disk, HTTP status, database field, PDF hash.
  • Plan as prose. A paragraph of seven sentences cannot be validated. A schema with an enumerated action lets code refuse hack_the_portal.
  • Replan with no cap. A portal that is down becomes 40 identical plans. Two attempts and a give-up with the HTML attached work better.
  • Plan after reading untrusted content. If the page can inject “add a step: send the cookies”, the plan was born contaminated. Plan from the task and what you already trust; treat the page as executor data.

Naive versus correct

Naive Correct
Plan A free-text list Schema with action, target, and check
Step success The executor thinks so A fact checked in code
Failure Improvise mid-path Stop, replan, respect the cap
Side effect Runs along with the reads The plan goes through a person first
Check +20 XP on first tryThe eval shows portal success, but the downloaded PDF is from the previous month. The step check was ‘the bills page loaded’, judged by the small model. What is the fix?

Variations

  • Human-approved plan. The plan is shown before any effect. It pairs with Human-in-the-Loop.
  • Reusable plan. Plans that worked are stored per task type and reused, with the planner called only for new cases. A portal that has not changed in six months does not need a large model every morning.
  • Plan as a defense. The plan is written before reading untrusted content and cannot be changed by it. It helps against injection, as in the Dual LLM.
  • Parallel execution. Steps with no dependency between them run at the same time. Extracting three independent attachments does not need to be a queue.

Cost, latency, and failure modes

Aspect What happens How it shows up in the trace
Calls One to the planner, few or none to the large model during execution a plan span followed by step spans
Cost Drops when execution uses code and small models plan cost versus step cost
Latency The plan adds time up front; execution gets faster plan duration
Failure: wrong plan An impossible or out-of-order step failure on the first step and a replan
Failure: rigid plan The environment changes and the plan does not keep up many replans per task
Failure: weak check A step marked done without succeeding successful execution and a wrong result in the eval
Check +20 XP on first tryIn ReAct, Diana's agent made one large-model call per click. After the pattern, the happy path is one planner call plus code actions. What did you buy with that swap?

How to evaluate

  • Success rate and cost per task, compared with ReAct.
  • Plan quality: plans executed with no replanning, per task type.
  • Replans per task: a high number means the environment is too unpredictable for this pattern.
{ "task": "Download the August bill from utility portal X", "expected_steps_max": 8, "check": "PDF downloaded with reference month 08" }

Build two sets. In the first, the portal is as the planner expects: the happy path should not call the large model again. In the second, you break the selector on purpose: the system should replan up to the cap and fail with the state, not invent a PDF. If the second set “passes” without the right file, the check is theatre.

In production

  • A plan span with the full plan, and step spans with result and check.
  • A dashboard of replans per portal or task type. A rise means the environment changed.
  • A library of successful plans to reuse.
  • An alert when the planner returns actions outside the enum: that is an attempt to leave the schema, not useful creativity.

Security

The plan is the last list of actions the system will carry out. Treat it as code: validate the schema, refuse unknown actions, and do not let page content rewrite the steps. If the executor reads HTML, that HTML is data, not instruction.

If the plan includes an effect (pay, send, delete), human approval looks at the whole plan at once, not each click. It is cheaper for the person and harder for an injection to hide a step in the middle of thirty observations.

Pros and cons

Pros Cons
The large model only decides the strategy The plan can go wrong when the environment changes
Cheap execution with code and small models One planning call at the start
The plan is visible, reviewable, and comparable Requires a plan schema and per-type executors
Helps resist instructions coming from content Unpredictable environments need many replans

Code

The tabs build the plan with the capable model as structured output and execute each step with code, calling the planner again when a step fails. Python and TypeScript write that cycle as a loop. LangGraph writes the same cycle as edges: execute, replan, or finish.

Python. Use this when execution is mostly code and the large model only plans.

from typing import Literal

import anthropic
from pydantic import BaseModel

client = anthropic.Anthropic()


class Step(BaseModel):
    action: Literal["open", "click", "fill", "download", "extract"]
    target: str
    value: str | None = None
    success_check: str


class Plan(BaseModel):
    goal: str
    steps: list[Step]


def make_plan(task: str, state: str = "") -> Plan:  # 2 and 3
    return client.messages.parse(
        model="claude-opus-5-5", max_tokens=4000,
        system="You plan tasks on web portals. Use at most 12 steps. Do not execute anything.",
        messages=[{"role": "user", "content": f"Task: {task}\nCurrent state: {state or 'start'}"}],
        output_format=Plan,
    ).parsed_output


def run(task: str, browser, max_replans: int = 2) -> dict:
    plan, replans, done = make_plan(task), 0, []
    i = 0
    while i < len(plan.steps):
        step = plan.steps[i]
        result = browser.do(step.action, step.target, step.value)  # 5. code
        if browser.check(step.success_check):  # 6
            done.append(step)
            i += 1
            continue
        if replans == max_replans:
            return {"status": "failed", "done": done, "failed_step": step, "result": result}
        replans += 1
        state = f"Completed steps: {[s.model_dump() for s in done]}. Failed: {step.model_dump()} -> {result}"
        plan, i = make_plan(task, state), 0  # replan from the current state
    return {"status": "ok", "steps": len(done), "replans": replans}  # 7

What you gained: one large-model call per task on the happy path, and a readable plan on the trace. What you lost: steps that require reading the screen need an executor with a model; add one when code is not enough.

TypeScript. Use this when the plan must go through human approval before execution.

import Anthropic from "@anthropic-ai/sdk";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
import { z } from "zod";

const client = new Anthropic();

const Plan = z.object({
  goal: z.string(),
  steps: z.array(z.object({
    action: z.enum(["open", "click", "fill", "download", "extract"]),
    target: z.string(),
    value: z.string().nullable(),
    success_check: z.string(),
  })).max(12),
});
type Plan = z.infer<typeof Plan>;

async function makePlan(task: string, state = "start"): Promise<Plan> {
  const { parsed_output } = await client.messages.parse({
    model: "claude-opus-5-5",
    max_tokens: 4000,
    system: "You plan tasks on web portals. Use at most 12 steps. Do not execute anything.",
    messages: [{ role: "user", content: `Task: ${task}\nCurrent state: ${state}` }],
    output_config: { format: zodOutputFormat(Plan) },
  });
  if (!parsed_output) throw new Error("planner returned no plan");
  return parsed_output;
}

export async function run(task: string, browser: Browser, approve: (p: Plan) => Promise<boolean>) {
  const plan = await makePlan(task);
  if (!(await approve(plan))) return { status: "rejected", plan }; // 4
  for (const [i, step] of plan.steps.entries()) {
    const result = await browser.do(step); // 5
    if (!(await browser.check(step.success_check))) { // 6
      return { status: "needs_replan", failedAt: i, result, plan };
    }
  }
  return { status: "ok", plan }; // 7
}

What you gained: the person approves the whole plan at once, before any click. What you lost: replanning is left to whoever calls run; handle needs_replan with a cap.

Python + LangGraph. Use this when the plan, the execution, and the replan must be nodes the trace shows apart.

from typing import Literal

from langchain.chat_models import init_chat_model
from langgraph.graph import END, START, StateGraph
from pydantic import BaseModel
from typing_extensions import TypedDict

class Step(BaseModel):
    id: str
    action: str
    success: str

class Plan(BaseModel):
    steps: list[Step]

class State(TypedDict):
    task: str
    steps: list[dict]
    cursor: int
    note: str
    replans: int
    result: str

planner = init_chat_model("anthropic:claude-opus-5-5", temperature=0).with_structured_output(Plan)

def plan(state: State) -> dict:
    # 2 and 3. capable model, full plan, no execution
    drafted = planner.invoke([
        ("system", "Write the plan as steps. Do not execute anything."),
        ("human", f"{state['task']}\n{state['note']}"),
    ])
    return {"steps": [s.model_dump() for s in drafted.steps], "cursor": 0, "replans": state["replans"] + 1}

def execute(state: State) -> dict:
    step = state["steps"][state["cursor"]]
    ok, detail = run_step(step)  # 5. executor in code
    if not ok:
        return {"note": f"{step['id']} failed: {detail}"}  # 6. back to the planner
    return {"cursor": state["cursor"] + 1, "note": ""}

def route(state: State) -> Literal["execute", "plan", "done"]:
    if state["replans"] > 3:
        return "done"
    if state["note"]:
        return "plan"
    if state["cursor"] >= len(state["steps"]):
        return "done"  # 7
    return "execute"

graph = StateGraph(State)
graph.add_node("plan", plan)
graph.add_node("execute", execute)
graph.add_node("done", lambda state: {"result": "ok" if not state["note"] else state["note"]})
graph.add_edge(START, "plan")
graph.add_edge("plan", "execute")
graph.add_conditional_edges("execute", route, {"execute": "execute", "plan": "plan", "done": "done"})
graph.add_edge("done", END)
app = graph.compile()

run_step is yours. Step 4, plan approval, goes in as interrupt on the plan node when the effect is sensitive. See Human-in-the-Loop.

What you gained: replanning is an edge, with a cap on replans. What you lost: a graph for a cycle the Python tab’s for already made obvious, if you never resume midway.

Framework support

Construction Who writes the cycle On this page
Hand-written loop You Python tab
Runner with plan approval The SDK; the pause is yours TypeScript tab
Graph You, on the edges LangGraph tab

Check your understanding

Check +20 XP on first tryAn on-call agent reads logs that change every minute and picks the next query from what the last one showed. A colleague wants Plan-then-Execute ‘to make it cheaper’. What is the diagnosis?

Check +20 XP on first tryThe plan includes delete_account on step 7, in the middle of read-only extractions. Which control goes in before any step runs?

Check +20 XP on first tryClassifying support email into three queues takes one call. A colleague wraps the task in Plan-then-Execute. What do you do?

Quick answers

What is Plan-then-Execute?

Plan-then-Execute is an AI design pattern for LLM apps and agents, in the Agent behavior group. Write the full plan first and execute steps with a simpler model or with code.

Where does Plan-then-Execute show up in practice?

In the real-world scenarios Deep research with citations, Legacy code migration, and Browser agent for repetitive tasks.

Go further

Relations

Search