Planning and task decomposition
Break a goal into steps a model can actually execute: plan-then-execute versus interleaved ReAct, replanning on failure, and when a fixed workflow wins.
Four ways to structure the same task
Decomposition is the decision of when the plan is produced and who produces it. The model is only one option. Choosing the structure badly is a bigger cost than choosing a weaker model, because a wrong structure cannot be fixed with a better prompt.
| Structure | How it works | Good for | Cost |
|---|---|---|---|
| Single prompt | Everything asked at once | Short, well-specified tasks | Cheapest; fails silently on multi-step work |
| Plan then execute | Model writes the full plan first, then the loop runs it | Predictable tasks where you want to review the plan | Two calls plus N; plan may not survive contact |
| Interleaved ReAct | Think, act, observe, repeat | Exploratory tasks where each result changes the next move | Most calls; hardest to audit |
| Fixed workflow | The developer writes the steps; the model fills them in | Known processes with fixed order | Fewest failures and the lowest cost |
plan-then-execute
goal -> [ plan: s1, s2, s3 ] -> run s1 -> run s2 -> run s3 -> answer
interleaved (ReAct)
goal -> think -> act -> observe -> think -> act -> observe -> answerPlan-then-execute gives you something valuable: a plan you can validate before spending money on it. Interleaved survives environments you cannot predict, at the price of never knowing how long the run will take.
A planner the loop can trust
A plan is data, so give it a schema and validate it. An unvalidated plan is a list of natural-language wishes, and the executor will discover at step four that step one produced nothing it can use.
PLANNER = """You are a planner. Produce 2-6 steps for the goal.
Each step names exactly one tool and its arguments.
Available tools: {tools}
Reply with JSON only:
{"steps":[{"id":1,"tool":"...","args":{...},"why":"..."}]}
"""
def make_plan(goal, tools, max_steps=6):
raw = model(PLANNER.format(tools=describe(tools)),
messages=[{"role": "user", "content": goal}],
response_format={"type": "json_object"})
plan = json.loads(raw)
steps = plan["steps"]
if not steps:
raise ValueError("empty plan")
if len(steps) > max_steps:
steps = steps[:max_steps]
for s in steps:
if s["tool"] not in tools:
raise ValueError(f"unknown tool: {s['tool']}")
schema = tools[s["tool"]].schema
validate(s["args"], schema) # reject before executing anything
return steps- Cap the number of steps. Models plan optimistically; a ten-step plan for a three-step job is a budget leak, not thoroughness.
- Require one tool per step. A step that says "analyse and then update" is two steps and cannot be retried independently.
- Validate the whole plan before running any of it. Executing steps one to three and then discovering step four is invalid wastes the writes.
- Ask for the reasoning as a short
whyfield. It costs almost nothing and makes the plan reviewable by a human. - Keep the goal pinned in every subsequent call, or later steps drift toward whatever the last tool returned.
Replanning, and knowing when not to
The plan is a default, not a contract. When a step fails, you have three choices: retry the same step, feed the error back and let the model revise the plan, or stop. Deciding which is a design decision you should make in code.
done, results = [], {}
for step in plan:
for attempt in range(2):
outcome = run(step, context=results)
if outcome.ok:
results[step["id"]] = outcome.data
done.append(step)
break
else:
# two failures: let the model see the errors and the completed work
plan = replan(goal=goal, completed=done, failed=step,
error=outcome.error, remaining_steps=plan[len(done):])
if plan is None:
return {"status": "failed", "reason": outcome.error, "partial": results}
return summarise(goal, results)- Replan with evidence: pass the completed steps, their results and the exact error. Replanning with the goal alone produces the same plan again.
- Limit the number of replans, not just the number of steps. A replan loop is a step loop wearing a disguise.
- Do not replan after every step. If each result changed the next action, you wanted the interleaved structure in the first place.
- Distinguish retryable failures from terminal ones. A 429 is retryable; "order not found" will not become true.
FAQ
Plan then execute, or interleaved?
How many steps is too many?
Related
Guardrails, budgets and stopping conditions Multi-agent patterns
Last refreshed 2026-09-18.