DEV Community

Cover image for Six Planning Patterns for AI Agents: When to Use Fixed Workflows, ReAct Loops, and Tree Search
mech.app
mech.app

Posted on Originally published at mech.app

Six Planning Patterns for AI Agents: When to Use Fixed Workflows, ReAct Loops, and Tree Search

Most teams copy ReAct loops or chain-of-thought prompts because they saw them in a demo. Then they discover their agent burns tokens on trivial tasks or fails to complete complex ones. The problem is not the pattern. The problem is choosing a pattern before understanding the task.

Planning patterns define how an agent breaks a goal into steps, decides which tools to call, and handles failures. Six patterns cover most production use cases: fixed workflows, ReAct loops, chain-of-thought reasoning, tree search, plan-and-execute, and hybrid approaches. Each has a sweet spot. Each has failure modes.

Fixed Workflows: Deterministic Tasks with Known Steps

A fixed workflow is a hardcoded sequence of steps. The agent executes them in order. No branching. No dynamic planning. Just a script with LLM calls at specific points.

When to use it:

  • The task always follows the same structure (generate report, send email, log result)
  • You need predictable cost and latency
  • Debugging requires clear step boundaries
  • Compliance or audit trails matter

Architecture:
The orchestrator owns the control flow. Each step is a function that may call an LLM, a tool, or both. State passes from step to step as a dictionary or typed object.

def fixed_workflow(user_query: str, context: dict) -> dict:
    # Step 1: Extract entities
    entities = llm_call("extract_entities", user_query)

    # Step 2: Fetch data for each entity
    data = []
    for entity in entities:
        result = tool_call("database_lookup", entity)
        data.append(result)

    # Step 3: Summarize findings
    summary = llm_call("summarize", data)

    # Step 4: Format output
    return {"summary": summary, "raw_data": data}
Enter fullscreen mode Exit fullscreen mode

Failure modes:

  • Brittle when task requirements change
  • Cannot adapt to unexpected tool results
  • Wastes steps if early results make later steps unnecessary

Circuit breakers:
Set timeouts on each step. Log step entry and exit. If a step fails, decide whether to retry, skip, or abort. Do not let one slow tool block the entire workflow.

ReAct Loops: Dynamic Exploration with Tool Feedback

ReAct (Reasoning + Acting) gives the agent a loop: observe the current state, reason about what to do next, act by calling a tool, then observe the result. The loop continues until the agent decides it has enough information.

When to use it:

  • The number of steps is unknown
  • Tool results determine the next action (API returns a list, agent decides which item to inspect)
  • The task requires exploration (research, debugging, multi-hop question answering)

Architecture:
The orchestrator runs a loop. Each iteration sends the conversation history and available tools to the LLM. The LLM returns either a tool call or a final answer. Tool results append to the history.

def react_loop(user_query: str, tools: list, max_steps: int = 10) -> str:
    history = [{"role": "user", "content": user_query}]

    for step in range(max_steps):
        response = llm_call_with_tools(history, tools)

        if response.is_final_answer:
            return response.content

        # Agent chose a tool
        tool_result = execute_tool(response.tool_name, response.tool_args)
        history.append({"role": "assistant", "tool_call": response.tool_name})
        history.append({"role": "tool", "content": tool_result})

    return "Max steps reached without final answer"
Enter fullscreen mode Exit fullscreen mode

Failure modes:

  • Infinite loops when the agent repeats the same tool call
  • Token bloat as history grows
  • No backtracking when the agent explores a dead end

Circuit breakers:
Track tool call sequences. If the agent calls the same tool with the same arguments twice in a row, inject a system message: "You already tried that. Try a different approach or provide your best answer." Set a token budget. When history exceeds it, summarize early steps or truncate.

Chain-of-Thought: Reasoning Before Acting

Chain-of-thought prompts the LLM to explain its reasoning before producing an answer. It does not change the control flow. It changes what the model outputs.

When to use it:

  • The task requires multi-step logic (math, planning, legal reasoning)
  • You want to inspect the agent's reasoning for debugging or compliance
  • The model tends to jump to conclusions

Architecture:
Add a system prompt that instructs the model to think step-by-step. Parse the reasoning from the final answer. Optionally, log reasoning separately for observability.

Failure modes:

  • Verbose reasoning increases latency and cost
  • The model may confabulate reasoning that sounds plausible but is wrong
  • Reasoning does not guarantee correct tool use

Circuit breakers:
Validate the final answer with code. If the task involves arithmetic, re-compute the result. If the task involves rules, check the answer against a rule engine.

Tree Search: Optimization and Backtracking

Tree search explores multiple paths in parallel or sequentially, scores each path, and picks the best one. It is expensive but powerful when the task has a clear objective function.

When to use it:

  • The task has multiple valid solutions and you need the best one (code generation, route planning, resource allocation)
  • You can score intermediate states (unit tests pass, cost is below threshold)
  • Latency and cost are acceptable

Architecture:
The orchestrator maintains a tree of states. Each node represents a partial solution. The agent expands nodes by generating possible next steps. A scoring function evaluates each node. The orchestrator prunes low-scoring branches.

State to persist:

  • Node ID, parent ID, depth
  • Partial solution (code, plan, configuration)
  • Score (test pass rate, estimated cost, user feedback)
  • Tool calls made so far

Pruning strategy:
Keep the top N nodes at each depth. Discard nodes with scores below a threshold. If a node reaches the goal, stop expanding that branch but continue others to find a better solution.

Failure modes:

  • Exponential growth if branching factor is high
  • Poor scoring function leads to premature pruning of good paths
  • No backtracking if all branches fail

Circuit breakers:
Set a maximum tree depth. Set a maximum number of nodes. If the tree exceeds limits, return the best partial solution found so far.

Plan-and-Execute: Upfront Planning with Execution Monitoring

The agent generates a full plan before executing any step. Then it executes the plan, monitoring for failures. If a step fails, the agent may replan or abort.

When to use it:

  • The task is complex but the structure is predictable (multi-step data pipeline, compliance workflow)
  • You want to review the plan before execution
  • Execution is expensive (API calls, database writes)

Architecture:
Step 1: The LLM generates a plan as a list of steps with tool calls and arguments.
Step 2: The orchestrator validates the plan (check tool names, argument types).
Step 3: The orchestrator executes each step, logging results.
Step 4: If a step fails, the orchestrator asks the LLM to replan from that point.

Failure modes:

  • The plan is wrong but execution proceeds anyway
  • Replanning is slow and may produce a worse plan
  • The agent does not adapt to unexpected tool results

Circuit breakers:
Validate the plan against a schema. If the plan includes unknown tools or invalid arguments, reject it and ask for a new plan. Set a maximum number of replanning attempts. If replanning fails repeatedly, abort and return an error.

Hybrid Patterns: Mixing Fixed and Dynamic Steps

A hybrid pattern combines fixed workflows with dynamic planning. For example, a fixed workflow handles authentication and data fetching, then a ReAct loop explores the data, then a fixed step formats the output.

When to use it:

  • The task has predictable phases but variable steps within each phase
  • You want the reliability of fixed workflows for critical steps
  • You want the flexibility of dynamic planning for exploratory steps

Where to draw the boundary:
Draw the boundary at the orchestrator level. The orchestrator knows which steps are fixed and which are dynamic. It calls the appropriate executor for each step.

def hybrid_workflow(user_query: str) -> dict:
    # Fixed: Authenticate and fetch data
    auth_token = authenticate()
    raw_data = fetch_data(auth_token)

    # Dynamic: Explore data with ReAct
    insights = react_loop(
        f"Analyze this data: {raw_data}",
        tools=["filter", "aggregate", "visualize"],
        max_steps=5
    )

    # Fixed: Format and send report
    report = format_report(insights)
    send_email(report)

    return {"status": "complete", "report": report}
Enter fullscreen mode Exit fullscreen mode

Failure modes:

  • Complexity increases as you add more phases
  • Debugging requires understanding both fixed and dynamic behavior
  • State management becomes tricky if dynamic steps modify state that fixed steps depend on

Circuit breakers:
Isolate state between phases. Pass only the necessary data from one phase to the next. If a dynamic phase fails, decide whether to skip it and continue with default values or abort the entire workflow.

Pattern Selection Table

Pattern Best For Cost Latency Debuggability Failure Risk
Fixed Workflow Predictable tasks Low Low High Low
ReAct Loop Exploration, multi-hop Medium-High Medium-High Medium Medium
Chain-of-Thought Reasoning tasks Medium Medium High Medium
Tree Search Optimization High High Low Low
Plan-and-Execute Complex, reviewable Medium Medium High Medium
Hybrid Mixed predictability Medium Medium Medium Medium

Observability and State Management

Every planning pattern needs observability. Log each step, tool call, and decision. Persist enough state to reconstruct what happened.

What to log:

  • Step number and timestamp
  • Tool name and arguments
  • Tool result (truncate large payloads)
  • LLM prompt and response (sample, do not log every token)
  • Decision points (why did the agent choose this tool?)

What to persist:

  • Conversation history (for ReAct)
  • Plan (for plan-and-execute)
  • Tree nodes (for tree search)
  • Intermediate results (for all patterns)

How to prune:
Set retention policies. Keep full logs for recent runs. Summarize or delete logs older than 30 days. For tree search, persist only the winning path after execution completes.

Security Boundaries

Planning patterns expose different attack surfaces.

ReAct loops: An attacker may craft a query that causes the agent to call expensive tools repeatedly. Limit tool calls per session. Rate-limit tool execution.

Tree search: An attacker may trigger exponential tree growth. Set hard limits on tree depth and node count.

Plan-and-execute: An attacker may inject malicious steps into the plan. Validate the plan against a whitelist of allowed tools and argument patterns.

Hybrid patterns: Each phase has its own attack surface. Apply the appropriate mitigations to each phase.

Technical Verdict

Use fixed workflows when:

  • The task structure is known and stable
  • You need predictable cost and latency
  • Compliance or audit trails matter

Use ReAct loops when:

  • The task requires exploration
  • Tool results determine the next action
  • You can tolerate variable cost and latency

Use tree search when:

  • You need the best solution, not just a good one
  • You can score intermediate states
  • Cost and latency are acceptable

Use plan-and-execute when:

  • The task is complex but structured
  • You want to review the plan before execution
  • Execution is expensive

Use hybrid patterns when:

  • The task has predictable and exploratory phases
  • You want the best of both worlds

Avoid:

  • Copying a pattern because it is popular
  • Using ReAct for tasks that fit a fixed workflow
  • Using tree search without a good scoring function
  • Mixing patterns without clear phase boundaries

Start from the task. Pick the simplest pattern that fits. Add complexity only when the task demands it.

Source Links

Top comments (0)