Nested Loop Architecture
Learn how to build loops within loops in AI agent systems, including sequential, parallel, and conditional nesting patterns, state sharing between nested loops, and practical implementation examples.
Overview
The nested loop architecture extends the Single Loop Architecture by embedding loops within loops. The outer loop manages the high-level workflow -- planning subtasks, delegating work, and evaluating the aggregate -- while inner loops handle individual subtasks with their own execute-evaluate-iterate cycles. Each subtask converges independently before its result feeds into the outer loop's evaluation.
This pattern is visible across every production agent framework. CrewAI (github.com/crewAIInc/crewAI) orchestrates nested loops through its hierarchical task delegation: a manager agent runs the outer loop, while each crew member runs an inner loop with its own context and tools. LangGraph (github.com/langchain-ai/langgraph) builds nested loops as sub-graphs within a parent graph, where each sub-graph has its own state, nodes, and edges. OpenHands (github.com/All-Hands-AI/OpenHands) nests an inner execution loop (edit code, run tests, observe output) inside an outer task loop (receive task, plan steps, verify result).
The nesting depth is deliberate. Each level adds observability overhead and token cost, so the architecture demands clear interfaces between levels and per-loop iteration limits to prevent any single subtask from consuming all resources.
Concept: Loops Within Loops
In a nested loop system, each level operates with its own executor, evaluator, and feedback channel. The outer loop delegates subtasks to inner loops, collects their results, and evaluates the aggregate. This creates a tree of loops where each node has its own convergence criteria.
Outer Loop (Workflow Manager)
|
+-- Inner Loop 1 (Subtask: Research)
| execute --> evaluate --> feedback (repeat until quality met)
|
+-- Inner Loop 2 (Subtask: Draft)
| execute --> evaluate --> feedback (repeat until quality met)
|
+-- Inner Loop 3 (Subtask: Review)
execute --> evaluate --> feedback (repeat until quality met)
|
Outer: aggregate results --> evaluate --> feedback (repeat if needed)
Nesting Patterns
Sequential Nesting
Inner loops execute one after another, with each inner loop receiving context from the previous one. This is the most common pattern and maps directly to CrewAI's sequential process mode, where crew members complete their tasks in order and pass outputs downstream.
class SequentialNestedLoop:
def __init__(self, inner_loops: list[SingleLoop]):
self.inner_loops = inner_loops
def run(self, task: Task) -> Result:
context = task.initial_context
for loop in self.inner_loops:
subtask = Task(
description=loop.name,
input_data=context,
threshold=task.subtask_threshold
)
result = loop.run(subtask)
context = {"previous_output": result.output, "context": context}
return Result(task_id=task.id, output=context)
Parallel Nesting
Inner loops execute simultaneously, and results are merged afterward. This pattern maximizes throughput for independent subtasks and is what Cursor 2.0 achieves with up to 8 parallel agents, and what MetaGPT (github.com/geekan/MetaGPT, 45K+ stars) implements through its multi-role parallel execution.
class ParallelNestedLoop:
def __init__(self, inner_loops: dict[str, SingleLoop], merger):
self.inner_loops = inner_loops
self.merger = merger
def run(self, task: Task) -> Result:
subtasks = self._split_task(task)
with ThreadPoolExecutor() as executor:
futures = {
name: executor.submit(loop.run, subtask)
for name, (loop, subtask) in zip(self.inner_loops.keys(), subtasks)
}
results = {
name: future.result()
for name, future in futures.items()
}
merged = self.merger.merge(results)
return Result(task_id=task.id, output=merged)
Real-world example: MetaGPT runs parallel inner loops for different software engineering roles -- Product Manager writes requirements, Architect designs the system, Project Manager breaks down tasks, and Engineers write code. Each role runs its own inner loop with specialized prompts and tools. The outer loop collects all role outputs and runs a QA agent to verify consistency.
Conditional Nesting
Inner loops are activated only when specific conditions are met. This prevents unnecessary work and token spend on subtasks that do not apply to the current task.
class ConditionalNestedLoop:
def __init__(self, conditions: dict[str, Callable], loops: dict[str, SingleLoop]):
self.conditions = conditions
self.loops = loops
def run(self, task: Task) -> Result:
results = {}
for name, condition in self.conditions.items():
if condition(task):
subtask = self._create_subtask(task, name)
results[name] = self.loops[name].run(subtask)
else:
results[name] = self._default_result(name, task)
return Result(task_id=task.id, output=results)
Real-world example: SWE-Agent (github.com/princeton-nlp/SWE-Agent, 15K+ stars) uses conditional nesting in its research-oriented approach. Given a GitHub issue, it conditionally activates different inner loops: a search loop if the issue references unfamiliar code, an edit loop if the fix location is known, and a test loop if the issue includes reproduction steps. Each conditional activation saves tokens by skipping irrelevant subtasks.
OpenHands (github.com/All-Hands-AI/OpenHands) implements a similar conditional pattern. The outer loop classifies the task and activates specific inner execution loops based on the classification: a CodingAgent loop for code modifications, a BrowsingAgent loop for web research, and a DataAnalysisAgent loop for notebook-based tasks.
State Sharing Between Nested Loops
There are three primary approaches:
| Approach | Description | Trade-off |
|---|---|---|
| Top-down context | Outer loop passes context to inner loops | Simple, but inner loops cannot influence outer state |
| Bottom-up results | Inner loops return results to outer loop | Clean separation, but limited cross-loop awareness |
| Shared state store | All loops read/write to a shared store | Maximum flexibility, but requires conflict management |
Top-Down Context: The CLAUDE.md Pattern
Claude Code (github.com/anthropics/claude-code) uses top-down context passing through CLAUDE.md files. The outer loop defines project-level conventions, coding standards, and quality gates. Each inner loop reads these constraints at the start of its iteration. Inner loops cannot modify the outer context, which keeps the architecture predictable.
# CLAUDE.md for a nested-loop project
## Outer Loop Rules
- Process tasks in priority order from tasks.json
- Max 3 outer iterations before accepting best-effort
- Budget cap: 100K tokens per task batch
## Inner Loop Rules (inherited by all subtask loops)
- All tests must pass before marking a subtask complete
- Run `mypy src/` and `pytest tests/ -v` after each edit
- Max 5 inner iterations per subtask
Shared State Implementation
For more complex scenarios where loops need bidirectional communication, a shared state store with scoped access prevents conflicts:
class NestedLoopState:
def __init__(self):
self._store: dict[str, Any] = {}
self._lock = threading.Lock()
def set(self, key: str, value: Any, scope: str = "global"):
with self._lock:
full_key = f"{scope}:{key}"
self._store[full_key] = {
"value": value,
"timestamp": time.time(),
"scope": scope
}
def get(self, key: str, scope: str = "global") -> Any:
with self._lock:
return self._store.get(f"{scope}:{key}")
def get_context(self, loop_id: str) -> dict:
with self._lock:
return {
k.split(":", 1)[1]: v["value"]
for k, v in self._store.items()
if v["scope"] in ("global", loop_id)
}
This scoped approach implements the InfoQ compression and retention principles: global state holds only essential constraints (compressed), while each loop retains its own working state. Cline (github.com/cline/cline) uses a similar pattern through its MCP protocol support, where shared context is managed through a server that both the outer and inner agent loops connect to.
Complete Nested Loop Example
Putting the patterns together, here is a complete nested loop implementation with state sharing, per-loop iteration limits, and divergence detection:
class NestedLoop:
def __init__(self, outer_loop: SingleLoop, inner_loop_factory, state: NestedLoopState):
self.outer_loop = outer_loop
self.inner_loop_factory = inner_loop_factory
self.state = state
def run(self, task: Task) -> Result:
for iteration in range(task.max_iterations):
plan = self._plan_subtasks(task)
subtask_results = {}
for subtask_def in plan:
inner_loop = self.inner_loop_factory.create(
subtask=subtask_def,
shared_state=self.state
)
result = inner_loop.run(subtask_def)
subtask_results[subtask_def.name] = result.output
aggregated = self._aggregate(subtask_results)
feedback = self.outer_loop.evaluator.evaluate(task, aggregated)
if feedback.passed:
return Result(task_id=task.id, output=aggregated)
return Result(task_id=task.id, output=aggregated, status="best_effort")
Configuration Parameters for Nested Loops
Every level in the nesting hierarchy needs its own configuration. The outer loop and each inner loop should have independent limits:
| Parameter | Outer Loop | Inner Loop | Rationale |
|---|---|---|---|
| max_iterations | 3-5 | 5-10 | Outer loop should converge quickly; inner loops get more attempts |
| token_budget | Proportional to task count | Proportional to subtask complexity | Budget splits across active loops |
| time_limit | 30-60 min total | 5-10 min per subtask | Proportional timeouts prevent resource starvation |
| quality_threshold | 0.85-0.95 | 0.8-0.9 | Outer loop enforces higher standards on the aggregate |
Real-World Implementations
CrewAI: Hierarchical Agent Orchestration
CrewAI (github.com/crewAIInc/crewAI) implements nested loops through its manager-worker pattern:
from crewai import Agent, Task, Crew, Process
researcher = Agent(role="Researcher", goal="Gather information", verbose=True)
writer = Agent(role="Writer", goal="Write documentation", verbose=True)
research_task = Task(
description="Research the API endpoints and their parameters",
agent=researcher
)
writing_task = Task(
description="Write API documentation based on research findings",
agent=writer
)
# Sequential nesting: research loop runs first, then writing loop
crew = Crew(
agents=[researcher, writer],
tasks=[research_task, writing_task],
process=Process.sequential,
max_iter=10 # Per-task inner loop limit
)
result = crew.kickoff()
Each task in CrewAI runs its own inner loop where the assigned agent iterates until the task is complete. The Crew manages the outer loop, delegating tasks and collecting results.
OpenHands: Multi-Stage Autonomous Engineering
OpenHands (github.com/All-Hands-AI/OpenHands) nests loops for end-to-end software engineering:
- Outer loop: Receives the task, plans a multi-step approach, executes each step through inner loops, and verifies the final result
- Inner loops: Each step (edit code, run tests, browse docs) runs its own execution loop with independent iteration limits
The conditional nesting pattern in OpenHands is particularly effective. A task like "fix the authentication bug in the login handler" triggers an edit inner loop (modify code), a test inner loop (verify the fix), and only conditionally a browsing inner loop (if the developer's research phase identifies unclear API semantics).
When to Use Nested Loops
Use nested loops when:
- A task naturally decomposes into sequential or parallel subtasks, each requiring its own quality control cycle
- Subtask quality directly determines the quality of the aggregate result (a weak research phase produces a weak final draft)
- You need independent convergence at different granularity levels
- The subtasks are complex enough to benefit from multiple iterations, not trivially executed in one shot
Use simpler architectures when:
- A single loop can handle the entire task (most coding tasks in Aider or Claude Code fit this pattern)
- Subtasks are so simple that a single iteration suffices -- the inner loop overhead is not justified
- The task is exploratory and the decomposition is not known in advance
Move to Multi-Level Task Loop when nesting goes deeper than two levels and cross-level communication becomes critical. Move to Multi-Agent Loop when different subtasks genuinely require different agent expertise rather than just different iteration budgets.
Best Practices
-
Isolate inner loop state by default; share only what is necessary -- use top-down context passing (like
CLAUDE.md) for constraints and bottom-up results for outputs. Reserve shared state stores for cases where bidirectional communication is genuinely required. -
Define clear interfaces between outer and inner loops -- each inner loop should receive a well-scoped task description and acceptance criteria, and return a structured result. This is what makes CrewAI and LangGraph composable.
-
Set per-loop iteration limits independently -- the outer loop should have a tight limit (3-5 iterations), while inner loops get more room (5-10). This prevents any single subtask from consuming all resources.
-
Propagate timeouts proportionally -- if the outer loop has a 30-minute budget and there are 5 subtasks, each inner loop should have a 5-minute timeout, not 30 minutes.
-
Use structured logging with full hierarchy -- every log entry should include the loop path (e.g.,
[outer:iteration-2] [inner:research:iteration-3]). This enables debugging by tracing exactly which loop and iteration produced a failure. -
Design the evaluator first -- as with single loops, the evaluator determines when the loop stops. In nested architectures, design both the inner evaluator (does this subtask meet its criteria?) and the outer evaluator (do the aggregated results meet the overall goal?) before implementing executors.