# TypeScript implementation reference Use this reference only for LangGraph projects written in TypeScript. The shared concepts, decisions, and invariants remain in `../SKILL.md`. ## Contents - [State Management](#state-management) - [Nodes](#nodes) - [Edges](#edges) - [Command](#command) - [Send API](#send-api) - [Running Graphs: Invoke and Stream](#running-graphs-invoke-and-stream) - [Error Handling](#error-handling) - [Common Fixes](#common-fixes) ## State Management ### State with reducer Use StateSchema with ReducedValue for accumulating arrays. ```typescript import { StateSchema, ReducedValue, MessagesValue } from "@langchain/langgraph"; import { z } from "zod"; const State = new StateSchema({ name: z.string(), // Default: overwrites messages: MessagesValue, // Built-in for messages items: new ReducedValue( z.array(z.string()).default(() => []), { reducer: (current, update) => current.concat(update) } ), }); ``` ### Forgot reducer for list Without ReducedValue, arrays are overwritten not appended. ```typescript // WRONG: Array will be overwritten const State = new StateSchema({ items: z.array(z.string()), // No reducer! }); // Node 1: { items: ["A"] }, Node 2: { items: ["B"] } // Final: { items: ["B"] } // A is lost! // CORRECT: Use ReducedValue const State = new StateSchema({ items: new ReducedValue( z.array(z.string()).default(() => []), { reducer: (current, update) => current.concat(update) } ), }); // Final: { items: ["A", "B"] } ``` ### Return partial state updates Return partial updates only, not the full state object. ```typescript // WRONG: Returning entire state const myNode = async (state: typeof State.State) => { state.field = "updated"; return state; // Don't do this! }; // CORRECT: Return partial updates const myNode = async (state: typeof State.State) => { return { field: "updated" }; }; ``` ## Nodes ### Node function signatures | Signature | When to Use | |-----------|-------------| | `(state) => {...}` | Simple nodes that only need state | | `(state, config) => {...}` | Need thread_id, tags, or configurable values | ```typescript import { GraphNode, StateSchema } from "@langchain/langgraph"; const plainNode: GraphNode = (state) => { return { results: "done" }; }; const nodeWithConfig: GraphNode = (state, config) => { const threadId = config?.configurable?.thread_id; return { results: `Thread: ${threadId}` }; }; ``` ## Edges ### Basic graph Chain nodes with addEdge and compile before invoking. ```typescript import { StateGraph, StateSchema, START, END } from "@langchain/langgraph"; import { z } from "zod"; const State = new StateSchema({ input: z.string(), output: z.string().default(""), }); const processInput = async (state: typeof State.State) => { return { output: `Processed: ${state.input}` }; }; const finalize = async (state: typeof State.State) => { return { output: state.output.toUpperCase() }; }; const graph = new StateGraph(State) .addNode("process", processInput) .addNode("finalize", finalize) .addEdge(START, "process") .addEdge("process", "finalize") .addEdge("finalize", END) .compile(); const result = await graph.invoke({ input: "hello" }); console.log(result.output); // "PROCESSED: HELLO" ``` ### Conditional edges addConditionalEdges routes based on function return value. ```typescript import { StateGraph, StateSchema, START, END } from "@langchain/langgraph"; import { z } from "zod"; const State = new StateSchema({ query: z.string(), route: z.string().default(""), result: z.string().default(""), }); const classify = async (state: typeof State.State) => { if (state.query.toLowerCase().includes("weather")) { return { route: "weather" }; } return { route: "general" }; }; const routeQuery = (state: typeof State.State) => state.route; const graph = new StateGraph(State) .addNode("classify", classify) .addNode("weather", async () => ({ result: "Sunny, 72F" })) .addNode("general", async () => ({ result: "General response" })) .addEdge(START, "classify") .addConditionalEdges("classify", routeQuery, ["weather", "general"]) .addEdge("weather", END) .addEdge("general", END) .compile(); ``` ## Command ### Command state and routing Return Command with update and goto to combine state change with routing. ```typescript import { StateGraph, StateSchema, START, END, Command } from "@langchain/langgraph"; import { z } from "zod"; const State = new StateSchema({ count: z.number().default(0), result: z.string().default(""), }); const nodeA = async (state: typeof State.State) => { const newCount = state.count + 1; if (newCount > 5) { return new Command({ update: { count: newCount }, goto: "node_c" }); } return new Command({ update: { count: newCount }, goto: "node_b" }); }; const graph = new StateGraph(State) .addNode("node_a", nodeA, { ends: ["node_b", "node_c"] }) .addNode("node_b", async () => ({ result: "B" })) .addNode("node_c", async () => ({ result: "C" })) .addEdge(START, "node_a") .addEdge("node_b", END) .addEdge("node_c", END) .compile(); ``` ## Send API ### Orchestrator worker Fan out tasks to parallel workers using the Send API and aggregate results. ```typescript import { Send, StateGraph, StateSchema, ReducedValue, START, END } from "@langchain/langgraph"; import { z } from "zod"; const State = new StateSchema({ tasks: z.array(z.string()), results: new ReducedValue( z.array(z.string()).default(() => []), { reducer: (curr, upd) => curr.concat(upd) } ), summary: z.string().default(""), }); const orchestrator = (state: typeof State.State) => { return state.tasks.map((task) => new Send("worker", { task })); }; const worker = async (state: { task: string }) => { return { results: [`Completed: ${state.task}`] }; }; const synthesize = async (state: typeof State.State) => { return { summary: `Processed ${state.results.length} tasks` }; }; const graph = new StateGraph(State) .addNode("worker", worker) .addNode("synthesize", synthesize) .addConditionalEdges(START, orchestrator, ["worker"]) .addEdge("worker", "synthesize") .addEdge("synthesize", END) .compile(); ``` ### Send accumulator Use ReducedValue to accumulate parallel worker results. ```typescript // WRONG: No reducer const State = new StateSchema({ results: z.array(z.string()) }); // CORRECT const State = new StateSchema({ results: new ReducedValue(z.array(z.string()).default(() => []), { reducer: (curr, upd) => curr.concat(upd) }), }); ``` ## Running Graphs: Invoke and Stream ### Invoke basics ```typescript const result = await graph.invoke({ input: "hello" }); // With config const result = await graph.invoke({ input: "hello" }, { configurable: { thread_id: "1" } }); ``` ### Stream llm tokens Stream LLM tokens in real-time for chat UI display. ```typescript for await (const chunk of graph.stream( { messages: [new HumanMessage("Hello")] }, { streamMode: "messages" } )) { const [token, metadata] = chunk; if (token.content) { process.stdout.write(token.content); } } ``` ### Stream custom data Emit custom progress updates from within nodes using the stream writer. ```typescript import { getWriter } from "@langchain/langgraph"; const myNode = async (state: typeof State.State) => { const writer = getWriter(); writer("Processing step 1..."); // Do work writer("Complete!"); return { result: "done" }; }; for await (const chunk of graph.stream({ data: "test" }, { streamMode: "custom" })) { console.log(chunk); } ``` ## Error Handling ### Retry policy Use retryPolicy for transient errors. ```typescript workflow.addNode( "searchDocumentation", searchDocumentation, { retryPolicy: { maxAttempts: 3, initialInterval: 1.0 }, }, ); ``` ### Tool node error handling Use ToolNode from @langchain/langgraph/prebuilt to handle tool execution and errors. When handleToolErrors is true, errors are returned as ToolMessages so the LLM can recover. ```typescript import { ToolNode } from "@langchain/langgraph/prebuilt"; const toolNode = new ToolNode(tools, { handleToolErrors: true }); workflow.addNode("tools", toolNode); ``` ## Common Fixes ### Compile before execution Must compile() to get executable graph. ```typescript // WRONG await builder.invoke({ input: "test" }); // CORRECT const graph = builder.compile(); await graph.invoke({ input: "test" }); ``` ### Infinite loop needs exit Use conditional edges with END return to break loops. ```typescript // WRONG: Loops forever builder.addEdge("node_a", "node_b").addEdge("node_b", "node_a"); // CORRECT builder.addConditionalEdges("node_a", (state) => state.count > 10 ? END : "node_b"); ``` ### Additional common mistakes ```typescript // Always await graph.invoke() - it returns a Promise const result = await graph.invoke({ input: "test" }); // TS Command nodes need { ends } to declare routing destinations builder.addNode("router", routerFn, { ends: ["node_b", "node_c"] }); ```