PlanAndExecute
from loomflow.architecture import PlanAndExecute, Plan, PlanStep, StepResultPlanAndExecute 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
| Type | int |
| Default | 8 |
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
| Type | str | None |
| Default | None (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
| Type | str | None |
| Default | None (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
| Type | str | None |
| Default | None (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
| Type | str | Model | None |
| Default | None (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:
- Plan. One LLM call. The output is parsed into a
Plan(seeplanner_prompt). Emitsplan.planner_started/plan.createdevents. - Execute. Walks
plan.stepsin 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. - Synthesize. One LLM call combining step outputs into the
final answer (event
plan.synthesizer_started, thenplan.completed). Writes tosession.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.
Related types
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: strOne 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: strThe 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.