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.

StructureHow it worksGood forCost
Single promptEverything asked at onceShort, well-specified tasksCheapest; fails silently on multi-step work
Plan then executeModel writes the full plan first, then the loop runs itPredictable tasks where you want to review the planTwo calls plus N; plan may not survive contact
Interleaved ReActThink, act, observe, repeatExploratory tasks where each result changes the next moveMost calls; hardest to audit
Fixed workflowThe developer writes the steps; the model fills them inKnown processes with fixed orderFewest 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 -> answer

Plan-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 why field. 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.
💡
If the sequence of steps is the same on every run, you do not have an agent — you have a workflow. Write it as ordinary code with model calls at the points where judgement is genuinely needed. It is cheaper, testable and debuggable, and you lose nothing.

FAQ

Plan then execute, or interleaved?
Plan first when you can predict the steps and want human review of the plan, or when you want to parallelise independent steps. Interleave when the next action depends on what the last one returned — which is most research and investigation tasks.
How many steps is too many?
If a plan exceeds roughly six to eight steps, the goal is probably still too large. Split the task in your own code, or give the agent a sub-goal, rather than asking one plan to cover everything.

Guardrails, budgets and stopping conditions Multi-agent patterns

Last refreshed 2026-09-18.