AI Agents LangGraph

Cycles and Self-Loops in LangGraph

Intermediate

Cycles and Self-Loops in LangGraph

In this topic, we explore cycles and self-loops in LangGraph and understand how they enable iterative, adaptive, and autonomous AI workflows. We discuss the difference between cycles and DAGs (Directed Acyclic Graphs), explain how cyclic execution works in LangGraph, and examine how loops can be created using both add_edge() and add_conditional_edges() .

We also cover self-loops, execution flow across repeated iterations, and how state changes throughout cyclic workflows. In addition, we explore common loop patterns used in modern AI agents, including ReAct loops, retry systems, reflection/self-correction workflows, multi-agent feedback cycles, and human-in-the-loop interactions.

The topic further explains fixed vs dynamic loops, conditional loop termination, iteration limits, recursion safety, and state-driven loop control using the Command object and routing logic. We also discuss advanced concepts such as nested cycles, subgraphs, multi-agent cyclic architectures, and parallel cyclic execution.

Finally, we review debugging techniques, visualization strategies, performance considerations, common mistakes such as infinite loops and missing END conditions, and best practices for building safe, maintainable, and efficient cyclic workflows in LangGraph.

Cycles and Self-Loops in LangGraph

What Are Cycles?

In LangGraph, a Cycle occurs when the execution flow returns to a previously visited node, creating a loop. Instead of flowing in a straight line from START to END, the graph can go back and repeat certain nodes multiple times. This is achieved by using conditional edges that route execution back to an earlier node.
Simple visualization:
START → Agent → Tools → Agent → Tools → Agent → END
           ↑_______________________|
                  (Cycle)
Cycles are essential for any workflow that requires iteration, repetition, or continuous reasoning.

What Is a Self-Loop?

A Self-Loop is a special type of cycle where a node routes back to itself directly.
graph.add_conditional_edges(
    "agent",
    lambda state: "agent" if continue_reasoning(state) else "END"
)
In this case, after the agent node finishes, it can immediately run again without going through any other node. Self-loops are commonly used for:
  • Repeated reflection / self-critique
  • Continuous monitoring
  • Polling-style operations
Note: True self-loops (node → itself) are less common than full cycles (Agent → Tools → Agent), but both are fully supported.

Why Cycles Matter in AI Agents

Cycles are what turn static workflows into thinking, reasoning agents. They enable:
  • Multi-step reasoning: The agent can think, act, observe results, and think again.
  • Tool usage loops: Keep calling tools until the task is solved (ReAct pattern).
  • Self-improvement: Generate → Critique → Improve → Repeat.
  • Adaptive behavior: Change strategy based on intermediate results.
  • Persistent problem solving: Continue working until success criteria are met.
Without cycles → We get simple chains.
With cycles → We get powerful, agentic systems that can handle complex, open-ended tasks.

Cycles vs DAGs (Directed Acyclic Graphs)

Aspect DAG (No Cycles) Graph with Cycles
Flow Linear or branching only Can loop back
Execution Runs once per node Nodes can execute multiple times
Use Case Simple pipelines, ETL jobs AI agents, reasoning loops
Complexity Easier to reason about More powerful but requires safety controls
Termination Guaranteed to end Must manually ensure termination
LangGraph Support Fully supported Fully supported (one of LangGraph’s most powerful features)
Key Insight:
Traditional LangChain chains are DAGs.
LangGraph shines because it supports cyclic graphs , which are necessary for building real AI agents.

How Cycles Work in LangGraph

A cycle in LangGraph is created when an edge routes execution back to a node that has already been visited. Unlike traditional linear chains, LangGraph allows the same node to execute multiple times during a single graph run. Each time a node runs, it receives the latest updated state, performs its work, and returns new updates. Execution flow during a cycle:
  1. Node A runs
  2. Edge (usually conditional) decides to go back to Node A
  3. Node A receives the updated state (including previous outputs)
  4. Node A runs again with fresh information
  5. The loop continues until a stopping condition is met
This pattern is what powers ReAct agents , reflection loops, retry mechanisms, and multi-step reasoning. Important: Cycles rely heavily on proper state design (especially message history and reducers) so the agent remembers what happened in previous iterations.

Creating Cycles with add_edge()

You can create simple cycles using fixed edges with add_edge() . This is less common but useful for fixed repeating patterns.
from langgraph.graph import StateGraph, START, END

graph = StateGraph(MessagesState)

graph.add_node("agent", agent_node)
graph.add_node("tools", tool_node)

graph.add_edge(START, "agent")

# Fixed cycle
graph.add_edge("agent", "tools")
graph.add_edge("tools", "agent")   # ← Creates the cycle

# You still need a way to exit the cycle
graph.add_conditional_edges(
    "agent",
    lambda state: "END" if task_complete(state) else "tools"
)
Note: Pure fixed cycles are rare because they have no built-in stopping condition. Most real cycles combine add_edge() with add_conditional_edges() .
This is the most common and powerful way to create cycles in LangGraph.
def route_after_agent(state: MessagesState) -> str:
    last_message = state["messages"][-1]
    
    # Stopping condition
    if last_message.tool_calls is None and "final answer" in last_message.content.lower():
        return "END"
    
    # Continue the cycle
    if last_message.tool_calls:
        return "tools"
    else:
        return "agent"          # ← Loop back to agent


graph = StateGraph(MessagesState)

graph.add_node("agent", agent_node)
graph.add_node("tools", tool_node)

graph.add_edge(START, "agent")

# Conditional routing that creates the cycle
graph.add_conditional_edges(
    "agent",
    route_after_agent,
    {
        "tools": "tools",
        "agent": "agent",      # Self-referential cycle
        "END": END
    }
)

graph.add_edge("tools", "agent")   # Tools always return to agent

This is the classic ReAct loop pattern.

Self-Loops in LangGraph

A self-loop occurs when a node routes directly back to itself without going through any other node.
def self_loop_router(state: MessagesState) -> str:
    """Agent reflects on its own output and improves it."""
    last_message = state["messages"][-1]
    
    if needs_improvement(state):
        return "agent"           # ← Self-loop
    else:
        return "END"


graph.add_node("agent", agent_node)

graph.add_conditional_edges(
    "agent",
    self_loop_router,
    {
        "agent": "agent",   # Self-loop
        "END": END
    }
)

This is the most common and powerful way to create cycles in LangGraph.
 
Use cases for self-loops:
  • Repeated self-critique / reflection
  • Iterative refinement (e.g., keep improving an answer)
  • Continuous monitoring or polling
Alternative (Modern) using Command:
from langgraph.types import Command

def agent_node(state):
    response = llm.invoke(...)
    
    if needs_improvement(state):
        return Command(
            update={"messages": [response]},
            goto="agent"           # Self-loop via Command
        )
    return Command(
        update={"messages": [response]},
        goto=END
    )

Execution Flow in Cyclic Graphs

In a cyclic graph, execution doesn’t follow a simple straight path. Instead, it can loop back multiple times before reaching END. Step-by-Step Execution Flow:
  1. Start — Graph begins at START and moves to the first node.
  2. Node Execution — A node (e.g., agent) receives the current state and runs.
  3. State Update — The node returns updates which are merged into the state.
  4. Edge Evaluation — LangGraph checks the outgoing edges (usually conditional).
  5. Routing Decision:
    • If the router returns a previous node → Cycle continues
    • If the router returns END → Execution stops
  6. Repeat — The graph goes back to step 2 with the updated state.
Visual Flow (ReAct-style cycle):
START 
  ↓
Agent (Iteration 1) → decides tool call
  ↓
Tools → returns result
  ↓
Agent (Iteration 2) → decides tool call
  ↓
Tools → returns result
  ↓
Agent (Iteration 3) → decides "final answer"
  ↓
END

Each time the agent node runs, it has access to all previous interactions thanks to state persistence.

State Updates Across Iterations

One of the most important concepts in cyclic graphs is how state evolves across multiple iterations. Because the same node can run many times, the state acts as long-term memory for the loop.
How State Updates Work in Loops:
from langgraph.graph.message import add_messages
from typing import Annotated

class AgentState(TypedDict):
    messages: Annotated[list, add_messages]   # Accumulates history
    iterations: int
    final_answer: str | None

def agent_node(state: AgentState):
    # The agent sees ALL previous messages
    response = llm_with_tools.invoke(state["messages"])
    
    return {
        "messages": [response],                    # Append new message
        "iterations": state.get("iterations", 0) + 1   # Update counter
    }

def tools_node(state: AgentState):
    tool_results = execute_tools(state["messages"][-1].tool_calls)
    
    return {
        "messages": [AIMessage(content=tool_results)]  # Add tool output
    }
Key Behavior:
  • Each iteration receives the full accumulated state from all previous iterations.
  • Reducers (like add_messages) automatically handle appending data.
  • You can track progress using custom fields (iterations, confidence, error_count, etc.).
Example: Tracking Iterations to Prevent Infinite Loops
def route_after_agent(state: AgentState):
    if state.get("iterations", 0) >= 15:
        return "END"                           # Safety stop
    
    last_message = state["messages"][-1]
    
    if last_message.tool_calls:
        return "tools"
    else:
        return "END"
Important Note:
Proper state design is critical in cyclic graphs. Poorly designed state can lead to:
  • Very large message histories (use summarisation)
  • Lost context between iterations
  • Infinite loops

Common Loop Patterns in LangGraph

Here are the most widely used loop patterns when building agents and workflows with cycles:

1. ReAct Loop (Reason + Act)

The most classic and popular pattern. The agent reasons , calls tools if needed, observes the result, and repeats until it can give a final answer.
def route_react(state: MessagesState):
    last_message = state["messages"][-1]
    
    if last_message.tool_calls:
        return "tools"
    else:
        return "END"


graph.add_edge(START, "agent")
graph.add_conditional_edges("agent", route_react)
graph.add_edge("tools", "agent")   # Loop back
Flow: Agent → Tools → Agent → Tools → ... → END

2. Retry Loop

Retries a failing or low-quality step a limited number of times.
def retry_router(state: MessagesState):
    attempts = state.get("attempts", 0)
    
    if attempts >= 4:
        return "fallback"
    if is_error(state) or confidence_low(state):
        return "agent"          # Retry
    return "END"


graph.add_node("agent", agent_node)
graph.add_conditional_edges("validator", retry_router)

# Update attempts counter in agent or validator node
return {"attempts": state.get("attempts", 0) + 1}

3. Reflection / Self-Correction Loop

The agent critiques its own output and improves it iteratively.

def reflection_router(state: MessagesState):
    critique = critique_llm.invoke(state["messages"])
    
    if "good" in critique.content.lower() or "excellent" in critique.content.lower():
        return "END"
    else:
        return "agent"   # Self-loop / cycle back for improvement

Flow: Generate → Critique → (Improve → Generate) → END

4. Tool Retry Pattern

Specifically retries failed tool calls (network issues, rate limits, invalid input, etc.).

def tool_retry_router(state: MessagesState):
    last_message = state["messages"][-1]
    
    if last_message.tool_calls and is_tool_error(state):
        return "tools"           # Retry same tool
    elif last_message.tool_calls:
        return "agent"           # Back to agent for new plan
    else:
        return "END"

5. Multi-Agent Feedback Loop

Multiple specialized agents collaborate and give feedback to each other.
def supervisor_router(state: MessagesState):
    last_message = state["messages"][-1]
    
    if "research_complete" in state:
        return "critic_agent"
    elif "critique_done" in state:
        return "writer_agent"
    else:
        return "research_agent"


graph.add_conditional_edges("supervisor", supervisor_router)

# Cycle: Research Agent → Critic → Writer → Supervisor
graph.add_edge("research_agent", "supervisor")
graph.add_edge("critic_agent", "supervisor")
graph.add_edge("writer_agent", "supervisor")

6. Human-in-the-Loop Cycle

Pauses execution and waits for human input, then continues or retries.
def human_router(state: MessagesState):
    if needs_human_approval(state):
        return "human_review"
    return "END"


graph.add_conditional_edges("agent", human_router)

# Compile with interrupt
app = graph.compile(
    checkpointer=MemorySaver(),
    interrupt_before=["human_review"]
)

# After human provides feedback:
app.invoke({"messages": [HumanMessage(content="Approved. Continue.")]}, config)

Fixed Loops vs Dynamic Loops

Fixed Loops

A fixed loop has a predetermined, unchanging path. The cycle is created using add_edge() and always follows the same sequence.
# Fixed loop example
graph.add_edge("agent", "tools")
graph.add_edge("tools", "agent")   # Always loops the same way

Dynamic Loops

A dynamic loop uses add_conditional_edges() so the decision to continue the loop or exit is made at runtime based on the current state.
def dynamic_router(state: MessagesState):
    if task_is_complete(state):
        return "END"
    elif should_use_tool(state):
        return "tools"
    else:
        return "agent"   # Continue loop dynamically

graph.add_conditional_edges("agent", dynamic_router)
graph.add_edge("tools", "agent")
Recommendation:
Use Dynamic Loops in almost all real-world agents because they are more intelligent and adaptable.

Conditional Loop Termination

Conditional Loop Termination means deciding when to break out of the cycle based on runtime conditions rather than a fixed number of steps.
def termination_router(state: MessagesState) -> str:
    last_message = state["messages"][-1]
    iterations = state.get("iterations", 0)
    
    # Multiple ways to terminate
    if iterations >= 12:
        return "END"
    elif "final answer" in last_message.content.lower():
        return "END"
    elif last_message.tool_calls:
        return "tools"
    else:
        return "agent"


graph.add_conditional_edges("agent", termination_router)
Good termination conditions include:
  • Task successfully completed
  • Confidence score above threshold
  • Maximum iterations reached
  • LLM explicitly says “final answer”
  • Human approval received

Returning END from Cycles

To exit a cycle, you must eventually return "END" from a router or node.
# Option 1: From router function
def router(state):
    if task_complete(state):
        return "END"                    # ← Exit cycle
    return "agent"

# Option 2: Using Command (cleaner)
from langgraph.types import Command

def agent_node(state):
    response = llm_with_tools.invoke(state["messages"])
    
    if is_final_answer(response):
        return Command(update={"messages": [response]}, goto=END)
    else:
        return Command(update={"messages": [response]}, goto="tools")

Important: Always map "END" in your path_map when using add_conditional_edges() .

Loop Counters and Iteration Limits

Tracking how many times a loop has run is one of the most important safety mechanisms in LangGraph.

class AgentState(TypedDict):
    messages: Annotated[list, add_messages]
    iterations: int = 0                     # Track loop count

def agent_node(state: AgentState):
    return {
        "messages": [llm_with_tools.invoke(state["messages"])],
        "iterations": state.get("iterations", 0) + 1
    }

def router(state: AgentState):
    if state.get("iterations", 0) >= 15:    # Safety limit
        return "END"
    if state["messages"][-1].tool_calls:
        return "tools"
    return "END"

Best Practice : Always include an iteration counter and a hard upper limit (typically 10–25 depending on the use case).

Infinite Loops and Safety Risks

Infinite loops are the #1 risk when working with cycles in LangGraph.
What causes them?
  • Router always returns the same node
  • No termination condition
  • State never changes in a way that satisfies the exit condition
  • LLM keeps asking for more tools without progress
Consequences:
  • High token usage & cost
  • Timeout errors
  • Stuck executions
  • Poor user experience
Prevention:
  • Always set a maximum iteration limit
  • Have multiple exit conditions (success + safety)
  • Monitor state changes

Maximum Recursion Limits

LangGraph (and Python) has built-in recursion limits to prevent stack overflow. Even though LangGraph uses a graph execution engine, very deep cycles can still hit recursion or stack limits in some environments. Solutions:
  1. Use a high but reasonable max_iterations (15–30)
  2. Prefer checkpointer + stream() for long-running graphs
  3. Use Command or Send where possible instead of deep recursion
# Safe approach
app = graph.compile(checkpointer=MemorySaver())

# Run with streaming instead of deep recursion
for chunk in app.stream(inputs, config, stream_mode="values"):
    print(chunk)

Using State to Control Loops

The state is your control center for loops. You should store control variables directly in the state.

class AgentState(TypedDict):
    messages: Annotated[list, add_messages]
    iterations: int
    last_tool_result: str | None
    confidence: float
    status: Literal["running", "complete", "failed"]

def router(state: AgentState):
    if state["iterations"] > 12:
        return "END"
    if state.get("confidence", 0) > 0.9:
        return "END"
    if state.get("status") == "failed":
        return "fallback"
    return "tools" if needs_tool(state) else "agent"
Tip: Treat state as the “brain” that decides when to stop.

Using Command for Dynamic Cycles

The modern and cleaner way to handle cycles is using the Command object.

from langgraph.types import Command

def agent_node(state: MessagesState):
    response = llm_with_tools.invoke(state["messages"])
    
    if response.tool_calls:
        return Command(
            update={"messages": [response]},
            goto="tools"
        )
    else:
        return Command(
            update={"messages": [response]},
            goto=END          # Exit cycle cleanly
        )


graph.add_node("agent", agent_node)
graph.add_edge("tools", "agent")   # Still need this for loop

Advantages of Command :

  • Combines state update + routing in one place
  • Cleaner and more readable
  • Better for complex routing logic

Nested Cycles and Subgraphs

You can have cycles inside subgraphs or use a subgraph as a node that itself contains cycles. This is very powerful for building modular agent teams.
# 1. Create a reusable research subgraph with its own cycle
def create_research_subgraph():
    subgraph = StateGraph(MessagesState)
    subgraph.add_node("planner", planner_node)
    subgraph.add_node("researcher", researcher_node)
    
    subgraph.add_edge(START, "planner")
    subgraph.add_conditional_edges("planner", research_router)
    subgraph.add_edge("researcher", "planner")   # Inner cycle
    subgraph.add_edge("planner", END)
    
    return subgraph.compile()

# 2. Use it in main graph
main_graph = StateGraph(MessagesState)
research_team = create_research_subgraph()

main_graph.add_node("research_team", research_team)   # Subgraph as node
main_graph.add_node("final_answer", final_answer_node)

main_graph.add_edge(START, "research_team")
main_graph.add_edge("research_team", "final_answer")
main_graph.add_edge("final_answer", END)

Benefit: Keeps complex looping logic encapsulated and reusable.

Cycles in Multi-Agent Systems

In multi-agent setups, cycles often occur between a supervisor and specialized agents.
def supervisor_router(state: MessagesState):
    last = state["messages"][-1].content.lower()
    
    if "research" in last:
        return "research_agent"
    elif "code" in last:
        return "coder_agent"
    elif "critique" in last:
        return "critic_agent"
    else:
        return "END"


graph.add_node("supervisor", supervisor_node)
graph.add_node("research_agent", research_agent)
graph.add_node("coder_agent", coder_agent)
graph.add_node("critic_agent", critic_agent)

graph.add_conditional_edges("supervisor", supervisor_router)

# Feedback cycles
graph.add_edge("research_agent", "supervisor")
graph.add_edge("coder_agent", "supervisor")
graph.add_edge("critic_agent", "supervisor")
This creates a collaborative cycle where agents hand off work through the supervisor.

Parallel Cycles

You can combine parallel execution with cycles using Send .
from langgraph.types import Send

def parallel_router(state):
    """Launch multiple agents in parallel and loop back"""
    return [
        Send("research_agent", state),
        Send("web_search_agent", state),
        Send("code_agent", state)
    ]

graph.add_conditional_edges("supervisor", parallel_router)

# All parallel agents return to supervisor (cycle)
graph.add_edge("research_agent", "supervisor")
graph.add_edge("web_search_agent", "supervisor")
graph.add_edge("code_agent", "supervisor")
This enables parallel thinking within a loop.

Debugging Cyclic Workflows

Useful techniques:
# 1. Enable detailed logging
def agent_node(state):
    print(f"Iteration {state.get('iterations', 0)} | Messages: {len(state['messages'])}")
    ...

# 2. Use streaming
for chunk in app.stream(inputs, config, stream_mode="values"):
    print("→", chunk)

# 3. Visualize the graph
graph.get_graph().draw_mermaid_png()   # or .draw_ascii()
Pro Tip: Add iterations and status to your state for easy debugging.

Visualizing Cycles in Graphs

Use LangGraph’s built-in visualization:
from langgraph.graph import StateGraph

# After building your graph
graph_builder = StateGraph(...)
# ... add nodes and edges

display_graph = graph_builder.compile()
display_graph.get_graph().draw_mermaid()        # Returns mermaid syntax
# or
display_graph.get_graph().draw_mermaid_png()    # Save as image
Cycles appear as arrows going backward in the diagram.

Performance Considerations

Cycles can significantly impact performance and cost. Here are the key things to watch out for:
  • Token Usage: Each iteration consumes tokens (especially with long message histories).
  • Latency: Multiple LLM calls in a loop increase response time.
  • Cost: Uncontrolled loops can become very expensive.
  • Memory Usage: Large message histories grow quickly.
Optimization Tips:
  • Use summarization or trim_messages every few iterations.
  • Set reasonable max_iterations (usually 8–15).
  • Use faster/cheaper models for routing or reflection.
  • Prefer stream() over invoke() for better UX in long-running loops.
  • Use Command and efficient routers to reduce overhead.

Common Mistakes with Cycles

1. Missing END Conditions

The graph keeps looping forever because there’s no clear exit path.
Bad:
def router(state):
    return "agent"   # Always continues

2. State Never Changes

The router condition depends on a value that never gets updated, causing a stuck loop.
Example: Checking confidence but never updating it in any node.

3. Infinite Retry Loops

A failing step keeps retrying without limit or backoff.
if failed:
    return "agent"   # No attempt counter

4. Circular Routing Bugs

Router returns a node name that doesn’t exist or creates unintended cycles.
Example: Typo in node name ("tool" instead of "tools").

5. Unbounded LLM Loops

Letting the LLM freely control routing without safety limits. The LLM may keep calling tools or reflecting indefinitely.

Best Practices for Cycles

1. Always Add Exit Conditions

Every cycle should have at least two ways to terminate:
  • Success condition
  • Safety / timeout condition

2. Track Iteration Count

Always maintain an iterations counter in your state.
return {
    "messages": [...],
    "iterations": state.get("iterations", 0) + 1
}

3. Log Routing Decisions

Helpful during development and debugging:
def router(state):
    next_node = "tools" if ... else "END"
    print(f"[ROUTING] Iteration {state.get('iterations')} → {next_node}")
    return next_node

4. Keep Loops Focused

Each loop should have a single clear purpose (e.g., tool calling, reflection, research). Avoid overly complex multi-purpose loops.
5. Use Safety Guards
Combine multiple protections:
def safe_router(state):
    if state.get("iterations", 0) >= 12:
        return "END"
    if confidence_high(state):
        return "END"
    if error_count_high(state):
        return "fallback"
    # ... normal routing
Final Tip:
Treat cycles like fire — extremely powerful, but you must always have strong safety controls around them.

AI agent LangChain LangGraph Python

← All training