Skip to Content

PlanAndExecute

from loomflow.architecture import PlanAndExecute, Plan, PlanStep, StepResult

PlanAndExecute writes a plan first, then works through it one step at a time, then combines the step results into an answer. Use it for tasks you can break into known reasoning steps up front, where re-sending a growing conversation on every turn (as ReAct does) would cost more. Based on Wang et al. 2023 (Plan-and-Solve Prompting) and He et al. 2025 (Plan-then-Execute Pattern Implementation).

For the conceptual page see Plan-and-Execute.


Class signature

class PlanAndExecute: name: str = "plan-and-execute" def __init__( self, *, max_steps: int = 8, planner_prompt: str | None = None, executor_prompt: str | None = None, synthesizer_prompt: str | None = None, planner_model: str | Model | None = None, executor_model: str | Model | None = None, synthesizer_model: str | Model | None = None, ) -> None: ...

Constructor parameters

max_steps

Typeint
Default8

Hard cap on plan length. A planner sometimes produces a 30-step plan when 5 steps would do, so the architecture truncates the plan to max_steps. Must be >= 1; raises ValueError otherwise.

planner_prompt

Typestr | None
DefaultNone (uses built-in default)

Override the planner’s system prompt. The default teaches the model to emit a JSON list of steps, each a short imperative description. Provide your own when the default doesn’t fit your domain (e.g. domain-specific step shapes, custom output formats).

The reply is parsed leniently: a JSON list of strings or of {"description": ...} objects, or a {"steps": [...]} object, found even inside prose or a code fence. Without JSON, the numbered or bulleted lines are the steps, so a lead-in such as “Here is the plan:” isn’t one (if no line is marked, every line is a step). Blank steps are dropped.

executor_prompt

Typestr | None
DefaultNone (uses built-in default)

Override the per-step executor prompt. The default seeds each step with the original problem + the plan + prior step outputs, then asks for the step’s result.

synthesizer_prompt

Typestr | None
DefaultNone (uses built-in default)

Override the final synthesizer prompt. The default combines step outputs into a single answer. Customize when you want a different output shape (structured JSON, citations format, etc.).

planner_model / executor_model / synthesizer_model

Typestr | Model | None
DefaultNone (use the agent’s model)

Give one phase a different model, for example a strong model for planning and a cheaper one for the per-step calls. Accepts a model string or a Model instance. These role models are not wrapped with the agent’s retry policy, so wrap them yourself if you need retries. A model string is resolved with the agent’s secrets= backend, like the main model.

from loomflow.architecture import PlanAndExecute arch = PlanAndExecute(planner_model="claude-opus-4-7", executor_model="claude-haiku-4-5")

Methods

declared_workers

Returns {}. Single-agent.

run

Three-phase loop:

  1. Plan. One LLM call. The output is parsed into a Plan (see planner_prompt). Emits plan.planner_started / plan.created events.
  2. Execute. Walks plan.steps in order. Each step is a text-only model call seeded with the original problem, the plan and the prior step outputs. Per-step events: plan.step_started / plan.step_completed.
  3. Synthesize. One LLM call combining step outputs into the final answer (event plan.synthesizer_started, then plan.completed). Writes to session.output.

The budget is checked before the planner, before each step and before the synthesizer.

Empty plan terminates with session.output = "Planner produced no steps; cannot execute." and emits plan.empty_plan.


Plan

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

The planner’s output. Stored on session.metadata["plan"] (and the step results on session.metadata["step_results"]) for introspection.

PlanStep

class PlanStep(BaseModel): id: str description: str

One step of the plan: an id (step_1, step_2, … in plan order) and a short imperative description. Each step is a single text-only call; it does not run its own sub-architecture.

StepResult

class StepResult(BaseModel): step_id: str description: str output: str

The result of executing one PlanStep. The synthesizer receives list[StepResult] and produces the final answer.


Cost model

For an N-step plan: 1 (planner) + N (steps) + 1 (synthesizer) LLM calls. ReAct on the same task makes roughly N + 1 calls, but each one carries the growing message history. Plan-and-Execute is cheaper when message history is the dominant token cost.


Limitations

  • No sub-architecture invocation per step. Each step is a single text-only call, not a fresh ReAct (or any other architecture) invocation. Steps can’t call tools. For tool-heavy plans, use ReWOO.
  • No adaptive replanning. The plan is committed at start; there is no re-plan after a failed step. Failures surface as the step’s output text.

Example

from loomflow import Agent from loomflow.architecture import PlanAndExecute agent = Agent( "Research the topic and produce a structured brief.", model="claude-opus-4-7", architecture=PlanAndExecute(max_steps=5), ) result = await agent.run("State of vector databases in 2026.")

Or by string:

agent = Agent("...", model="claude-opus-4-7", architecture="plan-and-execute")

Source

loomflow/architecture/plan_and_execute.py

Plan-and-Execute vs ReWOO. Both are plan-then-execute architectures. PlanAndExecute steps are text-only model calls; ReWOO steps are tool calls with {En} placeholder substitution. Use ReWOO when the plan is “search → fetch → summarize”; use PlanAndExecute when each step is its own reasoning chunk.

Last updated on