/pattern/plan-and-execute/

01 · Single AgentPlanner-ExecutorPlan then ActTask Planning

Plan-and-Execute.
Plan once. Execute the list.

An agent first generates a complete plan and then executes the steps sequentially or in a controlled manner.

When to reach for it

  • The overall goal can be clearly broken down into testable subtasks.
  • Execution needs to be traceable and centrally controllable.
  • Cost or latency matters; you want to minimize LLM calls.

When it backfires

  • The environment is highly dynamic and the plan might fundamentally change based on early tool-call results.
  • The task is exploratory and the next step depends on the previous result.

The tradeoff

Better structure and testability compared to ReAct, but carries the risk of the agent clinging to outdated or unsuitable plans.

The mental model

A shape you can draw on a napkin.

Plan first — in one LLM call — then execute the steps you wrote down. The shape of the work is fixed before any tool runs.

PLANEXECUTEPlannerExecutorAnswer
Walk it through

A real run, step by step.

Task"Compare the pricing of our three biggest competitors and write a markdown summary."
1 / 6
Plan"1. Identify the three biggest competitors in our segment. 2. Fetch each pricing page. 3. Extract the entry tier price. 4. Render a markdown comparison."
Step 1market_research(segment="b2b-saas") → ["AcmeCorp", "BetaInc", "GammaCo"]
Step 2parallel.fetch_page([acmecorp.com/pricing, beta.com/pricing, gamma.co/pricing])
Step 3extract_entry_price(pages) → { Acme: €29, Beta: €35, Gamma: €19 }
Step 4render_markdown(prices) → "| Vendor | Entry tier |..."
AnswerMarkdown comparison delivered. Total: 1 planning call + 4 tool calls + 0 reasoning steps between them.
In code

The loop is already built in.

LangGraphpython
class State(TypedDict):
    goal: str
    plan: list[str]
    done: list[tuple[str, str]]
    result: str | None

def make_plan(state):
    plan = planner_llm.invoke(state["goal"]).steps
    return {"plan": plan}

def run_step(state):
    step = state["plan"][0]
    output = executor.invoke(step, context=state["done"])
    return {
        "plan": state["plan"][1:],
        "done": state["done"] + [(step, output)],
    }

graph = StateGraph(State)
graph.add_node("planner", make_plan)
graph.add_node("executor", run_step)
graph.add_edge("planner", "executor")
graph.add_conditional_edges(
    "executor",
    lambda s: "executor" if s["plan"] else END,
)
Pitfalls

Three ways this pattern will hurt you.

Stale plan

An early step returns something the planner didn't predict — an empty list, a 404, a different schema. The plan still runs, with the wrong inputs.

Fix · Add a replan trigger. Validate each step's output against the planner's assumption; if invalidated, hand control back to the planner.

Cascading errors

Step 2 misreads step 1's output. Step 3 builds on the misread. By step 4, the agent is solving a different problem.

Fix · Pydantic schemas on every step boundary. Cheap validators are worth more than expensive replanning.

Planner overreach

The planner writes an ambitious 12-step plan for a task that probably needed three. The cost of executing the plan dwarfs what you saved over ReAct.

Fix · Constrain the plan length in the planner prompt. Reward terseness; penalise speculative steps.

Framework support

Where Plan-and-Execute is native.

LangGraphplan-and-execute prebuiltNative
CrewAINative
Microsoft Agent FrameworkNative
OpenAI Agents SDKbuildable as a two-phase agentAdaptable

Search

Search patterns, frameworks, and pages.