Teach your agent to think before it acts. Five concepts — why complex tasks need a plan, routing requests before you plan, breaking goals into sub-tasks, running them as a dependency graph, and picking the right tools per step.
The 5 concepts of planning
Tap any concept to jump to its 5-card cluster.
Plan the whole goal before acting
A raw ReAct loop is great for 2–3 tool calls, but on a big goal — "research 3 competitors, compare pricing, draft a report" — it makes locally sensible but globally messy choices, backtracks, and loses the thread.
Planning is a layer on top of ReAct: the agent first analyzes the entire goal and produces a structured plan of sub-tasks with dependencies, then executes it. On benchmarks, ReAct-only completes complex tasks ~40% of the time; adding a planning step pushes that to ~80%.
Build IKEA without the instructions
BEFORE: Imagine assembling IKEA furniture without reading the manual — you open the box, see 47 pieces, and just start connecting things that look like they fit.
PAIN: You attach a panel upside down, realize at step 15 you skipped a bracket at step 3, and end up with leftover screws. That's a ReAct-only agent: each move is locally reasonable, the whole build is a mess, because it never made a picture of the finished thing.
MAPPING: Planning is reading the instructions first. The agent breaks the goal into ordered phases, marks what depends on what, then follows the plan tracking progress — turning "try things and hope" into "understand, plan, execute."
Planning sits on top of ReAct
- Analyze the goalRead the full request and decide it's complex enough to need a structured plan.
- Generate the planOne model call produces a list of sub-tasks, each with its dependencies — the "what to do".
- Execute per sub-taskReAct handles the "how" of each leaf task, one at a time or in parallel.
- Adapt if neededA plan is a hypothesis: if a sub-task fails or surprises you, re-plan rather than force the old plan.
Pseudocode
FUNCTION run(goal): IF is_simple(goal): RETURN answer_directly(goal) plan = model.make_plan(goal) # sub-tasks + deps FOR task IN plan.ready_tasks(): result = react_execute(task) plan.mark_done(task, result) IF result.failed: plan = re_plan(goal, plan) # adapt RETURN synthesize(plan.results)
Misconceptions
Planning is a layer on top of ReAct: analyze the goal, build a sub-task plan, then execute it. It roughly doubles completion rates on complex tasks.
The plan says "what"; ReAct handles "how". Adapt the plan instead of clinging to it.
Route before you plan
Intent classification is a lightweight first pass that decides what kind of request this is and whether it even needs planning. One model call returns an intent label, a complexity score, and the key entities.
Based on that, your code routes: a simple question goes straight to a direct answer, a single-step task goes to a ReAct loop, and a complex multi-step task triggers the full planning pipeline. It's a router, not a filter — nothing is blocked, everything just takes the cheapest correct path.
The ER triage nurse
BEFORE: Imagine a hospital where every patient — paper cut or chest pain — gets the same full workup: blood tests, MRI, specialist consult.
PAIN: The paper-cut patient wastes hours and thousands of dollars, while the chest-pain patient waits behind them. Without routing, "What's 2+2?" runs through decomposition, DAG creation, and tool discovery — burning tokens and adding seconds for a 200ms task.
MAPPING: The classifier is the triage nurse: paper cut → band-aid station (direct answer), sprained ankle → minor procedure (single tool call), chest pain → full workup (planning pipeline). Each request gets the right level of care.
One call, three fields, one route
- Classify intentLabel the request: simple_question, single_tool, multi_step_task, or creative_generation.
- Score complexityRate it low / medium / high, and extract the key entities from the text.
- Decide needs_planningCombine intent + complexity into a yes/no on whether to plan.
- Route the requestSend it down the matching path — direct answer, ReAct, or full planning.
Which path does this take?
IF intent = simple_question: → answer directly (one call) IF intent = single_tool: → run a short ReAct loop IF intent = multi_step_task OR complexity = high: → full planning pipeline IF parsing / intent is unclear: → default needs_planning = TRUE
Over-planning a simple task wastes a few hundred tokens; under-planning a complex one fails the whole task. So when in doubt, plan.
Misconceptions
Intent classification is a cheap router that decides whether to plan at all. It saves the planning tax on the 60–70% of requests that are simple.
Keep categories few, and default to planning when unsure — false negatives are the expensive ones.
Break the goal into sub-tasks
Task decomposition uses the model to turn a vague goal into a hierarchy of concrete sub-tasks, each with explicit dependencies. Each leaf task should be small enough to finish in 1–2 tool calls.
Three strategies: top-down (split the goal into phases, then phases into actions), bottom-up (start from atomic actions and group them), and iterative (plan only the next step, useful when later steps depend on earlier results). Top-down is the common default.
The project manager who breaks it down
BEFORE: A manager gets "Launch the new website" and just starts — no breakdown, no timeline. They bounce between copy, DNS, and the homepage, never finishing anything.
PAIN: An agent handed a raw complex goal has the same problem: it can't tell where to start, how to track progress, or when it's done. It makes headway on one aspect, then loses that context switching to another.
MAPPING: A good manager splits "launch" into concrete tasks — finalize mockups, migrate content, configure DNS, load-test, go-live — each specific with clear inputs and outputs. Decomposition does the same: the model generates a tree where each leaf is a 1–2 tool-call action.
Goal to task tree
- Split into phasesThe model breaks the goal into 3–5 high-level phases.
- Expand each phaseDecompose phases into specific leaf actions (one per competitor, etc.).
- Add dependenciesMark which tasks must finish before others can start.
- Assign toolsTag each leaf with the tools it will need (search, extract, compare).
Pseudocode
plan = model.decompose(goal) # plan is a list of tasks, each: # id, description, # dependencies: [other ids], # tools: [tool names] FOR task IN plan: assert task.is_atomic() # 1-2 tool calls assert task.deps_exist() # no dangling ids # aim for 5-10 leaf tasks total — # too many = coordination overhead.
Misconceptions
Decomposition turns a goal into a hierarchy of atomic, dependency-linked sub-tasks. Right granularity is a 1–2 tool-call leaf.
Target 5–10 leaves that collectively cover the whole goal — not 15+ fragments.
Run sub-tasks as a dependency graph
Sub-tasks aren't all sequential — researching three competitors can happen at once, but the report can't start until all research is done. A DAG (Directed Acyclic Graph) captures exactly that: nodes are tasks, edges are dependencies (A→B means A must finish first).
The executor works in waves: start every task with no unmet dependencies in parallel; as each finishes, release the tasks it unblocked. "Acyclic" means no loops — a cycle (A needs B, B needs A) would deadlock, so you validate the graph before running it.
Cooking a multi-dish dinner
BEFORE: Imagine cooking roast chicken, mashed potatoes, salad, and dessert strictly one step at a time — peel potatoes, then wait; chop salad, then wait; preheat oven, then wait. Total: 3 hours.
PAIN: Sequential execution wastes time on independent work. If Asana and Monday.com each take 5 seconds, running them one after another costs 10s; running them together costs 5s. Across 8 sub-tasks, parallelism can cut wall-clock time 40–60%.
MAPPING: A DAG is your cooking schedule: it says what can happen at once (chop while the oven preheats) and what must wait (can't frost a cake before it bakes). The executor starts everything unblocked and releases waiting tasks as dependencies clear.
Execution in waves
- Find the rootsTasks with no dependencies start immediately, in parallel.
- Run a waveDispatch all currently-ready tasks together.
- Release the unblockedAs each task finishes, mark any task whose deps are now all satisfied as ready.
- Validate acyclicBefore running, a topological check catches cycles; if found, ask the model to regenerate the plan.
Pseudocode
WHILE not all_done(dag): ready = [ t FOR t IN dag IF deps_done(t) AND not t.done ] IF ready is empty: raise "cycle or stuck" run_in_parallel(ready) # one wave FOR t IN ready: mark_done(t) # cap parallelism (~3-5) to respect # API rate limits.
Misconceptions
A DAG runs independent sub-tasks in parallel waves while respecting dependencies. It saves ~35% wall-clock time and prevents out-of-order execution.
Validate the graph is acyclic, cap parallelism to your rate limit, and allow partial completion.
Give each sub-task only its tools
When you hand the model 20+ tool definitions on every call, two things break: tool-selection accuracy drops (noticeable above 5–7 tools), and you waste context on descriptions the model won't use.
Dynamic tool discovery picks a relevant subset at runtime — usually the 3–5 tools that match the current sub-task — and injects only those. Fewer, more relevant tools means faster, cheaper, and more accurate decisions.
The contractor's tool truck
BEFORE: Imagine a contractor who hauls their entire tool truck — plumbing, electrical, carpentry, landscaping — into every job.
PAIN: To find a wrench they sort through 200 tools; the clutter slows them and they sometimes grab the wrong one because too many look alike. Giving the model 20 tool defs (100–200 tokens each) is the same: 2,000–4,000 tokens of noise per call, and worse picks.
MAPPING: Discovery is the contractor sizing up the job first — "this is plumbing, I need pipe wrench, cutter, Teflon tape" — and grabbing just those. The agent matches the sub-task to its inventory and injects only the 3–4 relevant tools.
Match sub-task to tools
- Describe the sub-taskTake the current leaf task's description as the query.
- Search the inventoryRank tools by relevance — embedding similarity, or a category map.
- Select the top fewKeep the 3–5 best matches; drop the rest.
- Inject only thosePut just the selected tool definitions into this sub-task's prompt.
Embeddings or a category map?
IF tools < 5-7 total: → just send them all IF 5-15 tools, stable set: → category map (task type → tool subset) IF 15+ tools, evolving fast: → embedding similarity search # selection accuracy beats # availability: 4 right > 20 noisy
Embedding search adds 50–100ms and a vector store; a category map is faster and easier to debug for smaller sets.
Misconceptions
Dynamic tool discovery injects only the few tools relevant to each sub-task. It cuts token cost ~80% and lifts selection accuracy from ~85% to ~95%.
Send all tools under ~7; category-map mid-size sets; embed only large, evolving inventories.
Test the core insight
One question per concept. Tap to reveal the answer.
needs_planning: true. Over-planning a simple task wastes a few hundred tokens (cheap), but under-planning a complex task fails the whole task (expensive). False positives are cheap, false negatives are not.Open the full module on desktop
The desktop version has you build an intent classifier, a task decomposer that emits a dependency graph, and a wave-based DAG executor with dynamic tool discovery — wired together into a working planning agent against local Mistral/Ollama, in Python and Node.
Open M13 on Desktop → Planning · intent routing · decomposition · DAG · tool discovery · ~70 minModule 9 of 12 · Open Source Track · Agent Architectures