Skip to main content

Documentation Index

Fetch the complete documentation index at: https://nvd-54.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

LangGraph 的核心是将智能体工作流建模为图。你使用三个关键组件来定义智能体的行为:
  1. 状态:一个共享的数据结构,表示应用程序的当前快照。它可以是任何数据类型,但通常使用共享状态模式来定义。
  2. 节点:编码智能体逻辑的函数。它们接收当前状态作为输入,执行某些计算或副作用,并返回更新后的状态。
  3. :根据当前状态确定下一个要执行的节点的函数。它们可以是条件分支或固定转换。
通过组合节点,你可以创建复杂的、带循环的工作流,随着时间推移演化状态。然而,真正的力量来自于 LangGraph 管理状态的方式。 需要强调的是:节点只不过是函数——它们可以包含大语言模型(LLM)或仅仅是普通代码。 简而言之:节点执行工作,边告诉接下来做什么 LangGraph 的底层图算法使用消息传递来定义通用程序。当一个节点完成其操作时,它沿着一条或多条边向其他节点发送消息。这些接收节点随后执行它们的函数,将结果消息传递给下一组节点,如此持续。受 Google 的 Pregel 系统的启发,程序以离散的”超级步骤”进行。 超级步骤可以被视为对图节点的单次迭代。并行运行的节点属于同一超级步骤,而顺序运行的节点属于不同的超级步骤。在图执行开始时,所有节点处于 inactive 状态。当节点在其任何传入边(或”通道”)上接收到新消息(状态)时,它变为 active。活跃节点随后运行其函数并返回更新。在每个超级步骤结束时,没有传入消息的节点通过将自己标记为 inactive 来投票 halt。当所有节点都处于 inactive 状态且没有消息在传输中时,图执行终止。

StateGraph

StateGraph 类是要使用的主要图类。它由用户定义的 State 对象参数化。

编译你的图

要构建图,首先定义状态,然后添加节点,最后编译它。编译图到底是什么,为什么需要它? 编译是一个相当简单的步骤。它对图的结构提供一些基本检查(没有孤立节点等)。这也是你可以指定运行时参数(如检查点器和断点)的地方。你只需调用 .compile 方法来编译图:
const graph = new StateGraph(StateAnnotation)
  .addNode("nodeA", nodeA)
  .addEdge(START, "nodeA")
  .addEdge("nodeA", END)
  .compile();
必须在使用图之前编译它。

状态

定义图时的第一件事是定义图的状态状态图的模式以及归约器函数组成,归约器指定如何将更新应用到状态。状态的模式将是图中所有节点的输入模式。你使用 StateSchema 类定义状态,它接受任何标准模式(如 Zod)用于单个字段,以及特殊值类型如 ReducedValueMessagesValue。所有节点将发出对状态的更新,然后使用指定的归约器函数来应用这些更新。

模式

指定图模式的主要方式是使用 StateSchema 类。模式中的每个字段可以是:
  • 标准模式用于简单字段(成为更新时覆盖的”最后值”通道)
  • ReducedValue 用于需要自定义归约器函数的字段(当节点并行运行时)
  • MessagesValue 用于聊天消息列表(预构建了消息感知的归约器)
  • UntrackedValue 用于不应被检查点化的瞬态状态
import {
  StateSchema,
  ReducedValue,
  MessagesValue,
  UntrackedValue
} from "@langchain/langgraph";
import { z } from "zod/v4";

const AgentState = new StateSchema({
  // Prebuilt messages value with built-in reducer
  messages: MessagesValue,

  // Simple fields use Zod schemas directly
  currentStep: z.string(),

  // Fields with defaults
  retryCount: z.number().default(0),

  // Custom reducer for accumulating values
  allSteps: new ReducedValue(
    z.array(z.string()).default(() => []),
    {
      inputSchema: z.string(),
      reducer: (current, newStep) => [...current, newStep],
    }
  ),

  // Transient state not saved to checkpoints
  tempCache: new UntrackedValue(z.record(z.string(), z.unknown())),
});

// Type extraction
type State = typeof AgentState.State;   // Full state type
type Update = typeof AgentState.Update; // Partial update type

// Use in graph
const graph = new StateGraph(AgentState)
  .addNode("myNode", ...)
  .compile();
默认情况下,图的输入和输出模式相同。如果你想更改此设置,也可以直接指定显式的输入和输出模式。当你有很多键,其中一些是专门用于输入、另一些用于输出时,这很有用。

多模式

通常,所有图节点使用单一模式通信。这意味着它们将读取和写入相同的状态通道。但在某些情况下,我们需要更多控制:
  • 内部节点可以传递不需要在图的输入/输出中的信息。
  • 我们可能还想为图使用不同的输入/输出模式。例如,输出可能只包含一个相关的输出键。
可以让节点写入图内部的私有状态通道以供内部节点通信。我们只需定义一个私有模式 PrivateState 也可以为图定义显式的输入和输出模式。在这些情况下,我们定义一个包含_所有_与图操作相关的键的”内部”模式。但是,我们还定义了inputoutput模式,它们是”内部”模式的子集,用于约束图的输入和输出。有关更多详细信息,请参阅定义输入和输出模式 让我们看一个例子:
import { StateSchema, GraphNode } from "@langchain/langgraph";
import * as z from "zod";

const InputState = new StateSchema({
  userInput: z.string(),
});

const OutputState = new StateSchema({
  graphOutput: z.string(),
});

const OverallState = new StateSchema({
  foo: z.string(),
  userInput: z.string(),
  graphOutput: z.string(),
});

const PrivateState = new StateSchema({
  bar: z.string(),
});

const graph = new StateGraph({
  state: OverallState,
  input: InputState,
  output: OutputState,
})
  .addNode("node1", (state) => {
    // Write to OverallState
    return { foo: state.userInput + " name" };
  })
  .addNode("node2", (state) => {
    // Read from OverallState, write to PrivateState
    return { bar: state.foo + " is" };
  })
  .addNode(
    "node3",
    (state) => {
      // Read from PrivateState, write to OutputState
      return { graphOutput: state.bar + " Lance" };
    },
    { input: PrivateState }
  )
  .addEdge(START, "node1")
  .addEdge("node1", "node2")
  .addEdge("node2", "node3")
  .addEdge("node3", END)
  .compile();

await graph.invoke({ userInput: "My" });
// { graphOutput: 'My name is Lance' }
There are two subtle and important points to note here:
  1. We pass state as the input schema to node1. But, we write out to foo, a channel in OverallState. How can we write out to a state channel that is not included in the input schema? This is because a node can write to any state channel in the graph state. The graph state is the union of the state channels defined at initialization, which includes OverallState and the filters InputState and OutputState.
  2. We initialize the graph with StateGraph({ state: OverallState, input: InputState, output: OutputState }). How can we write to PrivateState in node2? How does the graph gain access to this schema if it was not passed in the StateGraph initialization? We can do this because nodes can also declare additional state channels as long as the state schema definition exists. In this case, the PrivateState schema is defined, so we can add bar as a new state channel in the graph and write to it.

归约器

归约器是理解节点更新如何应用到状态的关键。状态中的每个键都有自己独立的归约器函数。如果没有显式指定归约器函数,则假定对该键的所有更新都应覆盖它。有几种不同类型的归约器,从默认类型的归约器开始:

默认归约器

以下两个示例展示如何使用默认归约器:
Example A
import { StateSchema } from "@langchain/langgraph";
import * as z from "zod";

const State = new StateSchema({
  foo: z.number(),
  bar: z.array(z.string()),
});
In this example, no reducer functions are specified for any key. Let’s assume the input to the graph is: { foo: 1, bar: ["hi"] }. Let’s then assume the first Node returns { foo: 2 }. This is treated as an update to the state. Notice that the Node does not need to return the whole State schema - just an update. After applying this update, the State would then be { foo: 2, bar: ["hi"] }. If the second node returns { bar: ["bye"] } then the State would then be { foo: 2, bar: ["bye"] }
Example B
import { StateSchema, ReducedValue } from "@langchain/langgraph";
import { z } from "zod/v4";

const State = new StateSchema({
  foo: z.number(),
  bar: new ReducedValue(
    z.array(z.string()).default(() => []),
    { reducer: (x, y) => x.concat(y) }
  ),
});
In this example, we’ve used ReducedValue to specify a reducer function for the second key (bar). Note that the first key remains unchanged. Let’s assume the input to the graph is { foo: 1, bar: ["hi"] }. Let’s then assume the first Node returns { foo: 2 }. This is treated as an update to the state. Notice that the Node does not need to return the whole State schema - just an update. After applying this update, the State would then be { foo: 2, bar: ["hi"] }. If the second node returns { bar: ["bye"] } then the State would then be { foo: 2, bar: ["hi", "bye"] }. Notice here that the bar key is updated by concatenating the two arrays together.

Untracked values

UntrackedValue is used for state fields that should exist during graph execution but should never be checkpointed. When a graph resumes from a checkpoint, untracked values will be reset to their initial state (or be unavailable). This is useful for:
  • Database connections that can’t be serialized
  • Temporary caches that should be rebuilt on resume
  • Large objects you don’t want to persist
  • Runtime-only configuration that should be passed fresh each time
import { StateSchema, UntrackedValue, MessagesValue } from "@langchain/langgraph";
import { z } from "zod/v4";

const State = new StateSchema({
  messages: MessagesValue,

  // Untracked: throws if multiple nodes write in same step (guard: true is default)
  dbConnection: new UntrackedValue<DatabaseConnection>(),

  // Untracked with guard: false allows multiple writes, keeps last value
  tempCache: new UntrackedValue(
    z.record(z.string(), z.unknown()),
    { guard: false }
  ),

  // Untracked without a schema (for maximum flexibility)
  runtimeConfig: new UntrackedValue(),
});
Behavior:
  • During execution: Values are stored and accessible like normal state
  • On checkpoint: Untracked values are excluded from the checkpoint data
  • On resume: Untracked values start fresh (empty or with their default value)
  • With guard: true (default): Throws error if multiple nodes write in the same step
  • With guard: false: Multiple writes allowed, last value wins
Don’t use UntrackedValue for data you need to persist across interrupts or time travel. Use regular state fields or ReducedValue for persistent data.

Type utilities

LangGraph provides several type utilities for better TypeScript type safety when defining nodes and conditional edges.

GraphNode

Use GraphNode to type node functions defined outside the graph builder:
import { GraphNode, StateSchema, Command } from "@langchain/langgraph";
import { z } from "zod/v4";

const State = new StateSchema({
  count: z.number().default(0),
  result: z.string(),
});

// Basic node - receives state, returns partial update
const incrementNode: GraphNode<typeof State> = (state) => {
  return { count: state.count + 1 };
};

// Async node
const fetchNode: GraphNode<typeof State> = async (state, config) => {
  const response = await fetch(`/api/data/${state.count}`);
  return { result: await response.text() };
};

// Node with Command routing - specify valid destinations
const routerNode: GraphNode<typeof State, "process" | "done"> = (state) => {
  if (state.count >= 10) {
    return new Command({ goto: "done" });
  }
  return new Command({
    update: { count: state.count + 1 },
    goto: "process"
  });
};

State.Node shorthand

Each StateSchema instance has a Node property that provides a shorthand for typing nodes:
const State = new StateSchema({
  messages: MessagesValue,
  step: z.string(),
});

// These are equivalent:
const myNode1: GraphNode<typeof State> = (state) => ({ step: "done" });
const myNode2: typeof State.Node = (state) => ({ step: "done" });

ConditionalEdgeRouter

Use ConditionalEdgeRouter for routing functions in conditional edges (no state updates, just routing):
import { ConditionalEdgeRouter, END } from "@langchain/langgraph";

const State = new StateSchema({
  shouldContinue: z.boolean(),
  step: z.string(),
});

// Router returns node name(s) or END
const router: ConditionalEdgeRouter<typeof State, "process" | "summarize"> = (state) => {
  if (!state.shouldContinue) {
    return END;
  }
  return state.step === "initial" ? "process" : "summarize";
};

// Use in graph
graph.addConditionalEdges("check", router);

StateSchema.State and StateSchema.Update

Extract the state and update types from a schema for use in custom type definitions:
import { StateSchema } from "@langchain/langgraph";

const MyStateSchema = new StateSchema({
  messages: MessagesValue,
  count: z.number().default(0),
});

// Extract the full state type
type MyState = typeof MyStateSchema.State;
// { messages: BaseMessage[], count: number }

// Extract the update type (partial, with reducer input types)
type MyUpdate = typeof MyStateSchema.Update;
// { messages?: Messages, count?: number }

Working with messages in graph state

Why use messages?

Most modern LLM providers have a chat model interface that accepts a list of messages as input. LangChain’s chat model interface in particular accepts a list of message objects as inputs. These messages come in a variety of forms such as HumanMessage (user input) or AIMessage (LLM response). To read more about what message objects are, please refer to the Messages conceptual guide.

Using messages in your graph

In many cases, it is helpful to store prior conversation history as a list of messages in your graph state. To do so, you can use the prebuilt MessagesValue which provides a message-aware reducer that handles message IDs, updates, and deletions automatically. The MessagesValue reducer is vital to telling the graph how to update the list of Message objects in the state with each state update. If you don’t specify a reducer, every state update will overwrite the list of messages with the most recently provided value. MessagesValue handles this correctly: for brand new messages, it appends to the existing list, and for existing messages (matched by ID), it updates them in place.
MessagesValue is actually a special case of ReducedValue, preconfigured with an internal messagesStateReducer that handles message lists and updates. This provides convenient, message-aware state management for chat message history in LangGraph graphs.

Serialization

In addition to keeping track of message IDs, MessagesValue will also try to deserialize messages into LangChain Message objects whenever a state update is received on the messages channel. This allows sending graph inputs / state updates in the following format:
// this is supported
{
  messages: [new HumanMessage("message")];
}

// and this is also supported
{
  messages: [{ role: "human", content: "message" }];
}
Since the state updates are always deserialized into LangChain Messages when using MessagesValue, you should use dot notation to access message attributes, like state.messages.at(-1).content. Below is an example of a graph that uses MessagesValue:
import { StateGraph, StateSchema, MessagesValue } from "@langchain/langgraph";

const State = new StateSchema({
  messages: MessagesValue,
});

const graph = new StateGraph(State)
  ...
The messages field is defined as a MessagesValue which is a list of BaseMessage objects with a built-in reducer. Typically, there is more state to track than just messages, so we see people extend this state and add more fields, like:
import { StateSchema, MessagesValue } from "@langchain/langgraph";
import * as z from "zod";

const State = new StateSchema({
  messages: MessagesValue,
  documents: z.array(z.string()),
});

节点

在 LangGraph 中,节点通常是接受以下参数的函数(同步或异步):
  1. state——图的状态
  2. config——一个 RunnableConfig 对象,包含 thread_id 等配置信息和 tags 等追踪信息
You can add nodes to a graph using the addNode method. For better type safety, use the GraphNode type utility or State.Node to type your node functions:
import { StateGraph, StateSchema, GraphNode } from "@langchain/langgraph";
import * as z from "zod";

const State = new StateSchema({
  input: z.string(),
  results: z.string(),
});

// Option 1: Use GraphNode type utility
const myNode: GraphNode<typeof State> = (state, config) => {
  console.log("In node: ", config?.configurable?.user_id);
  return { results: `Hello, ${state.input}!` };
};

// Option 2: Use State.Node shorthand
const otherNode: typeof State.Node = (state) => {
  return state;
};

const builder = new StateGraph(State)
  .addNode("myNode", myNode)
  .addNode("otherNode", otherNode)
  ...
Behind the scenes, functions are converted to RunnableLambda, which add batch and async support to your function, along with native tracing and debugging. If you add a node to a graph without specifying a name, it will be given a default name equivalent to the function name.
builder.addNode(myNode);
// You can then create edges to/from this node by referencing it as `"myNode"`

START node

The START Node is a special node that represents the node that sends user input to the graph. The main purpose for referencing this node is to determine which nodes should be called first.
import { START } from "@langchain/langgraph";

graph.addEdge(START, "nodeA");

END node

The END Node is a special node that represents a terminal node. This node is referenced when you want to denote which edges have no actions after they are done.
import { END } from "@langchain/langgraph";

graph.addEdge("nodeA", END);

Node caching

LangGraph supports caching of tasks/nodes based on the input to the node. To use caching:
  • Specify a cache when compiling a graph (or specifying an entrypoint)
  • Specify a cache policy for nodes. Each cache policy supports:
    • keyFunc, which is used to generate a cache key based on the input to a node.
    • ttl, the time to live for the cache in seconds. If not specified, the cache will never expire.
import { StateGraph, StateSchema, GraphNode, START } from "@langchain/langgraph";
import { InMemoryCache } from "@langchain/langgraph-checkpoint";
import { z } from "zod/v4";

const State = new StateSchema({
  x: z.number(),
  result: z.number(),
});

const expensiveNode: GraphNode<typeof State> = async (state) => {
  // Simulate an expensive operation
  await new Promise((resolve) => setTimeout(resolve, 3000));
  return { result: state.x * 2 };
};

const graph = new StateGraph(State)
  .addNode("expensive_node", expensiveNode, { cachePolicy: { ttl: 3 } })
  .addEdge(START, "expensive_node")
  .compile({ cache: new InMemoryCache() });

await graph.invoke({ x: 5 }, { streamMode: "updates" });
// [{"expensive_node": {"result": 10}}]
await graph.invoke({ x: 5 }, { streamMode: "updates" });
// [{"expensive_node": {"result": 10}, "__metadata__": {"cached": true}}]

边定义了逻辑如何路由以及图如何决定停止。这是智能体工作方式和不同节点之间通信方式的重要组成部分。有几种关键类型的边:
  • 普通边:直接从一个节点到下一个节点。
  • 条件边:调用函数来确定接下来去哪个节点。
  • 入口点:当用户输入到达时,首先调用哪个节点。
  • 条件入口点:调用函数来确定用户输入到达时首先调用哪个节点。
一个节点可以有多条出边。如果一个节点有多条出边,所有这些目标节点将作为下一个超级步骤的一部分并行执行。
For each node, choose one routing mechanism: use normal edges for static routing, or use conditional edges / Command for dynamic routing. Do not mix normal edges and dynamic routing from the same node, because both paths can execute and make graph behavior harder to reason about.

Normal edges

If you always want to go from node A to node B, you can use the addEdge method directly.
graph.addEdge("nodeA", "nodeB");

Conditional edges

If you want to optionally route to one or more edges (or optionally terminate), you can use the addConditionalEdges method. This method accepts the name of a node and a “routing function” to call after that node is executed:
graph.addConditionalEdges("nodeA", routingFunction);
Similar to nodes, the routingFunction accepts the current state of the graph and returns a value. By default, the return value routingFunction is used as the name of the node (or list of nodes) to send the state to next. All those nodes will be run in parallel as a part of the next superstep. You can optionally provide an object that maps the routingFunction’s output to the name of the next node.
graph.addConditionalEdges("nodeA", routingFunction, {
  true: "nodeB",
  false: "nodeC",
});
Use Command instead of conditional edges if you want to combine state updates and routing in a single function.

Entry point

The entry point is the first node(s) that are run when the graph starts. You can use the addEdge method from the virtual START node to the first node to execute to specify where to enter the graph.
import { START } from "@langchain/langgraph";

graph.addEdge(START, "nodeA");

Conditional entry point

A conditional entry point lets you start at different nodes depending on custom logic. You can use addConditionalEdges from the virtual START node to accomplish this.
import { START } from "@langchain/langgraph";

graph.addConditionalEdges(START, routingFunction);
You can optionally provide an object that maps the routingFunction’s output to the name of the next node.
graph.addConditionalEdges(START, routingFunction, {
  true: "nodeB",
  false: "nodeC",
});

Send

By default, Nodes and Edges are defined ahead of time and operate on the same shared state. However, there can be cases where the exact edges are not known ahead of time and/or you may want different versions of State to exist at the same time. A common example of this is with map-reduce design patterns. In this design pattern, a first node may generate a list of objects, and you may want to apply some other node to all those objects. The number of objects may be unknown ahead of time (meaning the number of edges may not be known) and the input State to the downstream Node should be different (one for each generated object). To support this design pattern, LangGraph supports returning Send objects from conditional edges. Send takes two arguments: first is the name of the node, and second is the state to pass to that node.
import { Send } from "@langchain/langgraph";

graph.addConditionalEdges("nodeA", (state) => {
  return state.subjects.map(
    (subject) => new Send("generateJoke", { subject })
  );
});

Command

Command is a versatile primitive for controlling graph execution. It accepts four parameters:
  • update: Apply state updates (similar to returning updates from a node).
  • goto: Navigate to specific nodes (similar to conditional edges).
  • graph: Target a parent graph when navigating from subgraphs.
  • resume: Provide a value to resume execution after an interrupt.
Command is used in three contexts:

Return from nodes

update and goto

Return Command from node functions to update state and route to the next node in a single step:
import { Command } from "@langchain/langgraph";

graph.addNode("myNode", (state) => {
  return new Command({
    update: { foo: "bar" },
    goto: "myOtherNode",
  });
});
With Command you can also achieve dynamic control flow behavior (identical to conditional edges):
import { Command } from "@langchain/langgraph";

graph.addNode("myNode", (state) => {
  if (state.foo === "bar") {
    return new Command({
      update: { foo: "baz" },
      goto: "myOtherNode",
    });
  }
});
Use Command when you need to both update state and route to a different node. If you only need to route without updating state, use conditional edges instead. When using Command in your node functions, you must add the ends parameter when adding the node to specify which nodes it can route to:
builder.addNode("myNode", myNode, {
  ends: ["myOtherNode", END],
});
Command only adds dynamic edges—static edges defined with add_edge / addEdge still execute. For example, if node_a returns Command(goto="my_other_node") and you also have graph.add_edge("node_a", "node_b"), both node_b and my_other_node will run. For each node, use either Command or static edges to route to the next nodes, not both.
Check out this how-to guide for an end-to-end example of how to use Command.

graph

If you are using subgraphs, you can navigate from a node within a subgraph to a different node in the parent graph by specifying graph: Command.PARENT in Command:
import { Command } from "@langchain/langgraph";

graph.addNode("myNode", (state) => {
  return new Command({
    update: { foo: "bar" },
    goto: "otherSubgraph", // where `otherSubgraph` is a node in the parent graph
    graph: Command.PARENT,
  });
});
Setting graph to Command.PARENT will navigate to the closest parent graph.When you send updates from a subgraph node to a parent graph node for a key that’s shared by both parent and subgraph state schemas, you must define a reducer for the key you’re updating in the parent graph state.
This is particularly useful when implementing multi-agent handoffs. Check out Navigate to a node in a parent graph for detail.

Input to invoke or stream

new Command({ resume: ... }) is the only Command pattern intended as input to invoke()/stream(). Do not use new Command({ update: ... }) as input to continue multi-turn conversations—because passing any Command as input resumes from the latest checkpoint (i.e. the last step that ran, not __start__), the graph will appear stuck if it already finished. To continue a conversation on an existing thread, pass a plain input object:
// WRONG - graph resumes from the latest checkpoint
// (last step that ran), appears stuck
await graph.invoke(new Command({ update: { messages: [{ role: "user", content: "follow up" }] } }), config);

// CORRECT - plain object restarts from __start__
await graph.invoke({ messages: [{ role: "user", content: "follow up" }] }, config);

resume

Use new Command({ resume: ... }) to provide a value and resume graph execution after an interrupt. The value passed to resume becomes the return value of the interrupt() call inside the paused node:
import { Command, interrupt } from "@langchain/langgraph";

const humanReview = async (state: typeof StateAnnotation.State) => {
  // Pauses the graph and waits for a value
  const answer = interrupt("Do you approve?");
  return { messages: [{ role: "user", content: answer }] };
};

// First invocation - hits the interrupt and pauses
const result = await graph.invoke({ messages: [...] }, config);

// Resume with a value - the interrupt() call returns "yes"
const resumed = await graph.invoke(new Command({ resume: "yes" }), config);
Check out the interrupts conceptual guide for full details on interrupt patterns, including multiple interrupts and validation loops.

Return from tools

You can return Command from tools to update graph state and control flow. Use update to modify state (e.g., saving customer information looked up during a conversation) and goto to route to a specific node after the tool completes.
When used inside tools, goto adds a dynamic edge—any static edges already defined on the node that called the tool will still execute. For each node, use either tool-driven dynamic routing or static edges to route to the next nodes, not both.
Refer to Use inside tools for detail.

Graph migrations

LangGraph can easily handle migrations of graph definitions (nodes, edges, and state) even when using a checkpointer to track state.
  • For threads at the end of the graph (i.e. not interrupted) you can change the entire topology of the graph (i.e. all nodes and edges, remove, add, rename, etc)
  • For threads currently interrupted, we support all topology changes other than renaming / removing nodes (as that thread could now be about to enter a node that no longer exists) — if this is a blocker please reach out and we can prioritize a solution.
  • For modifying state, we have full backwards and forwards compatibility for adding and removing keys
  • State keys that are renamed lose their saved state in existing threads
  • State keys whose types change in incompatible ways could currently cause issues in threads with state from before the change — if this is a blocker please reach out and we can prioritize a solution.

Runtime context

When creating a graph, you can specify a contextSchema for runtime context passed to nodes. This is useful for passing information to nodes that is not part of the graph state. For example, you might want to pass dependencies such as model name or a database connection.
import { StateGraph, StateSchema } from "@langchain/langgraph";
import * as z from "zod";

const State = new StateSchema({
  input: z.string(),
  output: z.string(),
});

const ContextSchema = z.object({
  llm: z.union([z.literal("openai"), z.literal("anthropic")]),
});

const graph = new StateGraph(State, ContextSchema);
You can then pass this configuration into the graph using the context property.
const config = { context: { llm: "anthropic" } };

await graph.invoke(inputs, config);
You can then access and use this context inside a node or conditional edge:
import { Runtime, GraphNode } from "@langchain/langgraph";
import * as z from "zod";

const nodeA: GraphNode<typeof State> = (state, config) => {
  const llm = getLLM(runtime.context?.llm);
  // ...
  return {};
};
See Add runtime configuration for a full breakdown on configuration.
graph.addNode("myNode", (state, config) => {
  const llmType = config.context?.llm || "openai";
  const llm = getLLM(llmType);
  return { results: `Hello, ${state.input}!` };
});

Recursion limit

The recursion limit sets the maximum number of super-steps the graph can execute during a single execution. Once the limit is reached, LangGraph will raise GraphRecursionError. By default this value is set to 25 steps. The recursion limit can be set on any graph at runtime, and is passed to invoke/stream via the config object. Importantly, recursionLimit is a standalone config key and should not be passed inside the configurable key as all other user-defined configuration. See the example below:
await graph.invoke(inputs, {
  recursionLimit: 5,
  context: { llm: "anthropic" },
});

Accessing and handling the recursion counter

The current step counter is accessible in config.metadata.langgraph_step within any node, allowing for proactive recursion handling before hitting the recursion limit. This enables you to implement graceful degradation strategies within your graph logic.

How it works

The step counter is stored in config.metadata.langgraph_step. The recursion limit check follows the logic: step > stop where stop = step + recursionLimit + 1. When the limit is exceeded, LangGraph raises a GraphRecursionError.

Accessing the current step counter

You can access the current step counter within any node to monitor execution progress.
import { RunnableConfig } from "@langchain/core/runnables";
import { StateGraph } from "@langchain/langgraph";

const myNode: GraphNode<typeof State> = async (state, config) => {
  const currentStep = config.metadata?.langgraph_step;
  console.log(`Currently on step: ${currentStep}`);
  return state;
}
Design your graph with explicit termination conditions, and catch GraphRecursionError as a safety net:
import {
  StateGraph,
  StateSchema,
  ReducedValue,
  GraphNode,
  ConditionalEdgeRouter,
  END,
  GraphRecursionError
} from "@langchain/langgraph";
import { z } from "zod/v4";

const State = new StateSchema({
  messages: new ReducedValue(
    z.array(z.string()).default(() => []),
    { reducer: (x, y) => x.concat(y) }
  ),
});

// Build graph with explicit termination logic
const graph = new StateGraph(State)
  .addNode("reasoning", async (state) => {
    // Normal processing - design your graph with explicit termination conditions
    return {
      messages: ["thinking..."]
    };
  })
  .addConditionalEdges("reasoning", (state) => {
    // Add your termination condition here
    if (state.messages.length >= 5) {
      return END;
    }
    return "reasoning";
  });

const app = graph.compile();

// Catch GraphRecursionError as a safety net
try {
  const result = await app.invoke(
    { messages: [] },
    { recursionLimit: 10 }
  );
} catch (error) {
  if (error instanceof GraphRecursionError) {
    console.log("Recursion limit reached, handling gracefully");
    // Handle the error - return partial results, notify user, etc.
  }
}

Proactive vs reactive approaches

There are two main approaches to handling recursion limits: proactive (monitoring within the graph) and reactive (catching errors externally).
import {
  StateGraph,
  StateSchema,
  ReducedValue,
  GraphNode,
  ConditionalEdgeRouter,
  END,
  GraphRecursionError
} from "@langchain/langgraph";
import { z } from "zod/v4";

const State = new StateSchema({
  messages: new ReducedValue(
    z.array(z.string()).default(() => []),
    { reducer: (x, y) => x.concat(y) }
  ),
});


// Build graph with explicit termination logic
const builder = new StateGraph(State)
  .addNode("agent", async (state) => {
    return {
      messages: ["Processing..."]
    };
  })
  .addConditionalEdges("agent", (state) => {
    // Design termination conditions into your graph
    if (state.messages.length >= 5) {
      return END;
    }
    return "agent";
  });

const graph = builder.compile();

// Reactive Approach - catch GraphRecursionError as safety net
try {
  const result = await graph.invoke(
    { messages: [] },
    { recursionLimit: 10 }
  );
} catch (error) {
  if (error instanceof GraphRecursionError) {
    // Handle externally after graph execution fails
    console.log("Recursion limit exceeded, handling gracefully");
  }
}
The reactive approach catches GraphRecursionError after the limit is exceeded. Design your graph with explicit termination conditions to avoid hitting the limit in the first place.
ApproachDetectionHandlingControl Flow
Reactive (catching GraphRecursionError)After limit exceededOutside graph in try/catchGraph execution terminated
Reactive advantages:
  • Simple implementation
  • No need to modify graph logic
  • Centralized error handling

Other available metadata

Along with langgraph_step, the following metadata is also available in config.metadata:
const inspectMetadata: GraphNode<typeof State> = async (state, config) => {
  const metadata = config.metadata;

  console.log(`Step: ${metadata?.langgraph_step}`);
  console.log(`Node: ${metadata?.langgraph_node}`);
  console.log(`Triggers: ${metadata?.langgraph_triggers}`);
  console.log(`Path: ${metadata?.langgraph_path}`);
  console.log(`Checkpoint NS: ${metadata?.langgraph_checkpoint_ns}`);

  return state;
}

可视化

能够可视化图通常非常有用,尤其是当图变得更复杂时。LangGraph 附带了几种内置的图可视化方式。有关更多信息,请参阅可视化你的图

可观测性和追踪

要追踪、调试和评估你的智能体,请使用 LangSmith

了解更多