When creating a graph with a loop, we require a mechanism for terminating execution. This is most commonly done by adding a conditional edge that routes to the END node once we reach some termination condition.
You can also set the graph recursion limit when invoking or streaming the graph. The recursion limit sets the number of supersteps that the graph is allowed to execute before it raises an error. Read more about the concept of recursion limits here.
Let's consider a simple graph with a loop to better understand how these mechanisms work.
Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started here.
Let's define a graph with a simple loop. Note that we use a conditional edge to implement a termination condition.
import{StateGraph,Annotation}from"@langchain/langgraph";// Define the state with a reducerconstStateAnnotation=Annotation.Root({aggregate:Annotation<string[]>({reducer:(a,b)=>a.concat(b),default:()=>[],}),});// Define nodesconsta=asyncfunction(state:typeofStateAnnotation.State){console.log(`Node A sees ${state.aggregate}`);return{aggregate:["A"]};}constb=asyncfunction(state:typeofStateAnnotation.State){console.log(`Node B sees ${state.aggregate}`);return{aggregate:["B"]};}// Define edgesconstroute=asyncfunction(state:typeofStateAnnotation.State){if(state.aggregate.length<7){return"b";}else{return"__end__";}}// Define the graphconstgraph=newStateGraph(StateAnnotation).addNode("a",a).addNode("b",b).addEdge("__start__","a").addConditionalEdges("a",route).addEdge("b","a").compile();
This architecture is similar to a ReAct agent in which node "a" is a tool-calling model, and node "b" represents the tools.
In our route conditional edge, we specify that we should end after the "aggregate" list in the state passes a threshold length.
Invoking the graph, we see that we alternate between nodes "a" and "b" before terminating once we reach the termination condition.
awaitgraph.invoke({aggregate:[]});
Node A sees Node B sees ANode A sees A,BNode B sees A,B,ANode A sees A,B,A,BNode B sees A,B,A,B,ANode A sees A,B,A,B,A,B{ aggregate: [ 'A', 'B', 'A', 'B', 'A', 'B', 'A' ]}
In some applications, we may not have a guarantee that we will reach a given termination condition. In these cases, we can set the graph's recursion limit. This will raise a GraphRecursionError after a given number of supersteps. We can then catch and handle this exception:
To better understand how the recursion limit works, let's consider a more complex example. Below we implement a loop, but one step fans out into two nodes:
import{StateGraph,Annotation}from"@langchain/langgraph";// Define the state with a reducerconstStateAnnotationWithLoops=Annotation.Root({aggregate:Annotation<string[]>({reducer:(a,b)=>a.concat(b),default:()=>[],}),});// Define nodesconstnodeA=asyncfunction(state:typeofStateAnnotationWithLoops.State){console.log(`Node A sees ${state.aggregate}`);return{aggregate:["A"]};}constnodeB=asyncfunction(state:typeofStateAnnotationWithLoops.State){console.log(`Node B sees ${state.aggregate}`);return{aggregate:["B"]};}constnodeC=asyncfunction(state:typeofStateAnnotationWithLoops.State){console.log(`Node C sees ${state.aggregate}`);return{aggregate:["C"]};}constnodeD=asyncfunction(state:typeofStateAnnotationWithLoops.State){console.log(`Node D sees ${state.aggregate}`);return{aggregate:["D"]};}// Define edgesconstloopRouter=asyncfunction(state:typeofStateAnnotationWithLoops.State){if(state.aggregate.length<7){return"b";}else{return"__end__";}}// Define the graphconstgraphWithLoops=newStateGraph(StateAnnotationWithLoops).addNode("a",nodeA).addNode("b",nodeB).addNode("c",nodeC).addNode("d",nodeD).addEdge("__start__","a").addConditionalEdges("a",loopRouter).addEdge("b","c").addEdge("b","d").addEdge(["c","d"],"a").compile();
This graph looks complex, but can be conceptualized as loop of supersteps:
Node A
Node B
Nodes C and D
Node A
...
We have a loop of four supersteps, where nodes C and D are executed concurrently.
Invoking the graph as before, we see that we complete two full "laps" before hitting the termination condition:
awaitgraphWithLoops.invoke({aggregate:[]})
Node A sees Node B sees ANode C sees A,BNode D sees A,BNode A sees A,B,C,DNode B sees A,B,C,D,ANode C sees A,B,C,D,A,BNode D sees A,B,C,D,A,BNode A sees A,B,C,D,A,B,C,D{ aggregate: [ 'A', 'B', 'C', 'D', 'A', 'B', 'C', 'D', 'A' ]}
However, if we set the recursion limit to four, we only complete one lap because each lap is four supersteps: