Task Decomposition
A ReAct loop is great at chaining 2–3 tool calls. Hand it a 10-step goal and it gets lost. Planning adds a layer that reads the whole goal first, breaks it into a dependency graph, and routes simple requests around the overhead. Five concepts, from "why plan" to "which tools to load."
The 5 concepts of planning
Tap any concept to jump to its 5-card cluster.
- 1Why Complex Tasks Need PlanningWhere ReAct loops fall apart, and what fixes them
- 2Intent ClassificationRoute the request before you spend on planning
- 3Task DecompositionTurning vague goals into atomic sub-tasks
- 4DAG ExecutionRunning parallel and sequential paths in waves
- 5Dynamic Tool DiscoveryLoading only the tools each sub-task needs
Why complex tasks need planning
A ReAct agent thinks one step at a time: observe → reason → act → repeat. For two or three tool calls that's plenty. Hand the same agent a goal like "research our top three competitors, compare their pricing, draft a summary report" and it starts to drift — researching one competitor twice, forgetting to gather all names first, drafting the report before the comparison is done.
Planning is a meta-strategy that runs before ReAct: read the entire goal, write down the phases and dependencies, and only then start tool-calling. On benchmarks with four-or-more required steps, ReAct alone completes ~40% of tasks. Add a planning layer and that jumps to ~80%. Same model, same tools — just one extra "think first" pass.
IKEA without the instructions
BEFORE: Imagine assembling an IKEA bookshelf without reading the manual. You open the box, see 47 panels and a bag of cams and screws, and start connecting things that look like they fit.
PAIN: You attach a side panel upside-down. At step 15 you realise you skipped a bracket from step 3 and have to dismantle three rows. You finish — eventually — with leftover screws and a wobble that means it's coming apart again next week.
MAPPING: A ReAct-only agent on a complex task is the same person. Each tool call looks locally reasonable, but the global ordering is wrong. Planning is reading the manual first: identify the phases ("gather names → research each → compare → draft"), note the dependencies, then build — not "try things and hope."
Plan, then ReAct
- Read the entire goalOne Claude call analyses the whole request — not just the next move. This catches order-of-operations issues a step-by-step loop would miss.
- Emit a structured planThe model returns JSON: a list of sub-tasks, each with an id and a list of dependencies. This is the plan the executor will run.
- ReAct executes each leafEach leaf task is small enough for the existing ReAct loop to handle in 1–2 tool calls. Planning does the strategy; ReAct does the tactics.
- Re-plan on surpriseIf a sub-task returns a result that invalidates the plan ("there are five competitors, not three"), regenerate the rest of the plan. Plans are hypotheses, not contracts.
When to add a planning layer
# Question: my agent is dropping tasks. Plan or not? IF goal needs > 4 tool calls OR phases have ordering constraints OR sub-tasks can run in parallel: ADD planning layer # this module ELIF goal is 1–3 tool calls AND path is mostly linear: USE raw ReAct # M12 is enough ELIF goal is a single direct answer: JUST CALL Claude # no agent at all # Rule of thumb: planning costs ~300 extra tokens # and ~500ms. It pays off whenever the alternative # is a 30% chance of restarting the whole task.
Planning is not free. The extra Claude call adds latency and cost. The break-even point is roughly four sub-tasks — below that, raw ReAct usually wins.
Misconceptions
Planning is a layer on top of ReAct, not a replacement. Planning decides what to do; ReAct decides how to do each item.
Use it when goals exceed ~4 steps, have ordering constraints, or contain parallelisable sub-tasks. For everything else, the M12 ReAct loop is enough.
Route before you plan
Intent classification is a small Claude call that runs first and answers two questions: what kind of task is this? and what are the key entities? It returns a JSON label like simple_question, multi_step_task, or creative_generation, plus a complexity score and an extracted entity list.
Your code uses that label to pick a path: simple_question goes straight to a Claude call (no planning), single tool goes to a one-shot ReAct, multi-step goes through the full planning pipeline. The classifier itself adds ~300–500ms and ~250 tokens — cheap insurance against burning the planning budget on questions that didn't need it.
The hospital triage nurse
BEFORE: Picture a hospital where every patient — paper cut, sprained ankle, chest pain — goes through the same full diagnostic workup: blood draw, MRI, specialist consult.
PAIN: The paper-cut patient wastes hours and thousands of dollars on procedures they don't need, while the chest-pain patient waits in line behind them. Throughput collapses for everyone.
MAPPING: The triage nurse looks at each arrival for thirty seconds and routes them: paper cut to the band-aid station, sprained ankle to minor procedures, chest pain to full workup. The intent classifier is the triage nurse for your agent — a quick assessment that sends each request down the right path instead of the most expensive one.
One Claude call, three fields
- Send a small classification promptSystem prompt lists the allowed intent labels and asks for JSON output. User message is the original request.
- Receive structured JSONThree fields back:
intent(label),complexity(low/medium/high), andentities(array of key nouns). - Map intent to strategyA simple lookup:
simple_question → direct,single_tool → ReAct,multi_step_task → full planning. - Default to plan on doubtIf parsing fails or confidence is low, route to planning. False positives (extra planning) are cheap; false negatives (skipped planning) cause failed tasks and retries.
In production, ~60–70% of requests are simple. Skipping planning on those alone often saves more tokens than the classifier ever spends.
Pseudocode
# Step 1: classify the request FUNCTION classify(user_message): prompt = "Return JSON with: intent, complexity, entities. " + "intents = [simple_question, single_tool, multi_step]" result = CALL Claude(system=prompt, user=user_message) RETURN parse_json(result) # Step 2: route on the intent label FUNCTION handle(user_message): c = classify(user_message) IF c.intent == "simple_question": RETURN direct_answer(user_message) ELIF c.intent == "single_tool": RETURN react_loop(user_message) ELSE: # multi_step OR parse failed plan = decompose(user_message, c.entities) RETURN dag_execute(plan)
Use Haiku for the classifier — it's fast, cheap, and accurate enough on a 3-label problem.
Misconceptions
The classifier is a router, not a filter. It doesn't block requests — it just sends each one down the cheapest path that can answer it.
Total overhead is small (~300ms, ~250 tokens), but it saves the full planning cost on the majority of requests that don't need a plan.
Goals into atomic sub-tasks
Decomposition is a Claude call whose job is to convert a vague goal into a hierarchy of concrete sub-tasks. Each leaf task should be small enough to finish in 1–2 tool calls. Each task carries an id, a description, and a list of dependencies (the ids of tasks that must finish first).
The output is JSON, not prose — you parse it directly into a graph the executor can run. Granularity matters: too coarse and a sub-task fails because it's secretly five tool calls; too fine and the agent drowns in coordination overhead. Production data shows the sweet spot is 5–10 sub-tasks per goal, each scoped to 1–2 tool calls.
The project manager's whiteboard
BEFORE: A project manager hears the goal "launch the new website" and dives in — writing copy, configuring DNS, designing the homepage — bouncing between tasks without finishing any.
PAIN: Three weeks in, the homepage is half-designed, the DNS is half-configured, and nobody knows what's actually blocking launch. There's no list. Just motion.
MAPPING: A good PM walks to the whiteboard first and writes: (1) finalise mockups, (2) migrate content, (3) configure DNS, (4) load test, (5) cutover. Each item is concrete, has a clear input and output, and can be assigned. Decomposition does the same for the agent — the JSON plan is the whiteboard.
Three decomposition strategies
- Top-downThe default. Claude breaks the goal into 3–5 phases, then breaks each phase into specific actions. Best when you understand the shape of the goal up front.
- Bottom-upStart from the available tools ("search", "extract", "format") and group them into phases. Useful when the toolkit is the constraint and the workflow is flexible.
- IterativePlan only the next step, run it, then plan the step after. Best when later steps genuinely depend on what earlier ones return ("research competitor pricing" only makes sense once you know who the competitors are).
- Validate the leavesAfter decomposition, a quick check: is each leaf achievable in 1–2 tool calls? If a leaf needs five tool calls, it's actually a phase — ask Claude to decompose it further.
Pseudocode
# Ask Claude for a structured plan FUNCTION decompose(goal, entities): prompt = "Break this goal into 5–10 sub-tasks. " + "Each leaf must be 1–2 tool calls. " + "Return JSON: tasks[id, description, deps]" raw = CALL Claude(system=prompt, user=goal) plan = parse_json(raw) # Validate before handing to executor FOR task IN plan.tasks: ASSERT task.id AND task.description FOR dep IN task.deps: ASSERT dep IN [t.id FOR t IN plan.tasks] IF has_cycle(plan): RETURN decompose(goal, entities) # retry RETURN plan
Always validate the JSON. Claude occasionally invents a dependency on a task id that doesn't exist — catch it before the executor deadlocks.
Misconceptions
Decomposition is a JSON-emitting Claude call. The output is a graph: nodes are sub-tasks, edges are dependencies.
Right granularity matters more than the algorithm. Start top-down with 5–10 leaf tasks of 1–2 tool calls each, validate the structure, and only re-decompose the leaves that turn out to be too big.
Run the plan in parallel waves
The decomposed plan is a Directed Acyclic Graph: nodes are sub-tasks, directed edges are dependencies, "acyclic" means no circular waits. The executor runs it in waves — on each wave, dispatch every task whose dependencies are satisfied, run them concurrently, then advance.
That's the entire trick. Three "research competitor X" tasks all depend only on "find competitors" — so they go in the same wave and run in parallel. The "draft report" task depends on "compare pricing" — so it waits its turn. Sequential for the same goal takes ~25 seconds; wave-based takes ~15. Same logic, same model, just no idle waiting.
Cooking dinner with a schedule
BEFORE: A novice cook making roast chicken, mashed potatoes, salad, and dessert one step at a time: peel potatoes, then wait. Chop salad, then wait. Preheat oven, then wait. Total elapsed: three hours.
PAIN: Most of the wall clock is spent doing nothing — ingredients sit while the cook focuses on a single task. The tasks aren't dependent on each other; the cook just lacks a schedule.
MAPPING: An experienced cook plans waves: oven preheats while potatoes peel, salad gets chopped while chicken roasts, frosting waits because the cake has to cool. The DAG executor is the cooking schedule — everything that can run together does, and only true dependencies introduce waits.
The wave loop
- Find ready tasksA task is ready when every id in its
depslist has statusDONE. The first wave is every task with no dependencies. - Dispatch in parallelRun all ready tasks concurrently with
asyncio.gather,Promise.allSettled, or your runtime's equivalent. Each task's input includes the results of its dependencies. - Mark and repeatWhen a task returns, mark it
DONE(orFAILED) and store its output. Loop back to "find ready tasks" until none remain. - Allow partial completionIf "research ClickUp" fails, mark it FAILED but keep going. The "compare pricing" task can still proceed with two of three results — better than aborting the entire run.
- Re-plan on cascade failureIf a failure blocks too many downstream tasks, hand the partial state back to the planner and ask for a revised plan.
Pseudocode
# Wave-based executor with partial completion FUNCTION dag_execute(plan): status = {t.id: "PENDING" FOR t IN plan.tasks} results = {} WHILE True: ready = [t FOR t IN plan.tasks IF status[t.id] == "PENDING" AND ALL(status[d] == "DONE" FOR d IN t.deps)] IF ready is empty: BREAK # done or stuck # Run this wave concurrently outputs = PARALLEL(react_loop(t, results) FOR t IN ready) FOR t, out IN zip(ready, outputs): IF out is exception: status[t.id] = "FAILED" ELSE: status[t.id] = "DONE" results[t.id] = out RETURN results, status
Each leaf task hands off to the M12 ReAct loop — that's where the actual tool calls happen. The DAG layer is pure orchestration.
Misconceptions
The executor is a wave loop, not a queue. On each iteration, dispatch every task whose dependencies are satisfied, then wait for the slowest one in the wave before advancing.
Cap parallelism to your API tier. Catch failures per-task instead of per-wave. Re-plan only when too many downstream branches are blocked.
Load only the tools each sub-task needs
If your agent has 20+ tools, do not put all 20 into every Claude prompt. Above ~5–7 tools, selection accuracy drops measurably — Claude starts picking plausible-but-wrong tools because their descriptions overlap. You also burn 100–200 tokens per tool, every call, on definitions the model never uses.
Dynamic tool discovery picks 3–5 relevant tools per sub-task and injects only those. Implementation is either embedding-based (semantic similarity between sub-task and tool descriptions) or category-based (a static map from task category to tool subset). For under ~15 tools, the static map wins on simplicity. Above that, embeddings scale better.
The contractor's tool truck
BEFORE: Picture a contractor who drags every tool they own into every house: plumbing tools, electrical tools, carpentry tools, landscaping tools. Two hundred tools in the kitchen.
PAIN: When they need a wrench they sort through 200 things to find it. Worse, they sometimes grab the pipe wrench when they wanted the basin wrench because both are similar at a glance. The clutter slows them down and causes mistakes.
MAPPING: A working contractor reads the job ticket first ("leaky tap") and brings the four tools that job needs. The agent does the same: read the sub-task description, look up the matching tools, inject only those into the Claude prompt. Less clutter → faster, cheaper, more accurate.
Two implementation styles
- Embedding-basedEmbed each tool's description as a vector at startup. At runtime, embed the sub-task description and pick the top 3–5 tools by cosine similarity. Flexible, handles new sub-tasks well, but adds 50–100ms per lookup and needs a vector store.
- Category-basedMaintain a static map:
data_retrieval → [search, db_query, api_fetch],analysis → [extractor, comparison, chart]. The classifier from cluster 2 already produces a category — reuse it. Simple, fast, but new tools need a config update. - Inject and hand offWhichever style, the output is a list of 3–5 tool definitions. Put them in the
toolsargument of the Claude call for that sub-task. Done. - Audit drift periodicallyLog which tools each category actually used. If
analysisconsistently never useschart, drop it from the map — you've trimmed more noise.
Tool descriptions matter more under discovery, not less — they're the search keys.
Pseudocode
# Style A: category-based (small toolkits) TOOL_MAP = { "data_retrieval": [search, db_query, api_fetch], "analysis": [extractor, comparison, chart], "content": [text_gen, formatter, emailer], } FUNCTION select_tools(subtask): category = classify_subtask(subtask) # Haiku call RETURN TOOL_MAP[category] # Style B: embedding-based (large toolkits) FUNCTION select_tools_emb(subtask, k=4): q_vec = EMBED(subtask.description) scored = [(COS_SIM(q_vec, t.vec), t) FOR t IN tool_index] RETURN top_k(scored, k) # Hand the trimmed list to the per-task ReAct call react_loop(subtask, tools=select_tools(subtask))
Misconceptions
Fewer relevant tools beat many available tools. Selection accuracy drops above ~5–7 tools per prompt, and every unused tool definition is dead context.
Start with a category map, log usage, and only graduate to embeddings when the toolkit grows past ~15 entries. Either way, treat tool descriptions as search keys.
One question per concept
Tap a card to reveal the answer.
simple_question, what happens — and why does the default fallback always lean toward "plan"?t1 (no deps). Wave 2: t2, t3, t4 in parallel (all unlocked by t1). Wave 3: t5 (waits for all three). Wall-clock time = sum of the slowest task in each wave, not the sum of all tasks — that's the parallelism win.web_search when it should pick db_query. Trim to 4 relevant tools and selection accuracy rises from ~85% to ~95%.Open the full module on desktop
The desktop version walks through every layer in code — intent classifier, JSON-mode decomposer with cycle validation, parallel DAG executor with retry, and dynamic tool discovery — in Python and Node.js.
Open M13 on Desktop → Full code · DAG executor build · ~70 minModule 13 of 30 · Track 4: Agent Architectures