Building graphs with StateGraph, nodes, edges, START/END nodes, and the Command API for combining control flow with state updates
Install
npx skillscat add christian-bromann/langchain-skills/langgraph-graph-api Install via the SkillsCat registry.
langgraph-graph-api (JavaScript/TypeScript)
name: langgraph-graph-api
description: Building graphs with StateGraph, nodes, edges, START/END nodes, and the Command API for combining control flow with state updates
Overview
The LangGraph Graph API allows you to define agent workflows as directed graphs composed of nodes (functions) and edges (control flow). This provides fine-grained control over agent orchestration.
Core Components:
- StateGraph: Main class for building stateful graphs
- Nodes: Functions that perform work and update state
- Edges: Define execution order (static or conditional)
- START/END: Special nodes marking graph entry and exit points
- Command: Combine state updates with dynamic routing
Decision Table: Edge Types
| Need | Edge Type | When to Use |
|---|---|---|
| Always go to same node | addEdge() |
Fixed, deterministic flow |
| Route based on state | addConditionalEdges() |
Dynamic branching logic |
| Fan-out to multiple nodes | Send API |
Map-reduce, parallel execution |
| Update state AND route | Command |
Combine logic in single node |
Key Concepts
1. Graph Execution Model
LangGraph uses a message-passing model inspired by Google's Pregel:
- Execution proceeds in super-steps (discrete iterations)
- Nodes in parallel are part of the same super-step
- Sequential nodes belong to separate super-steps
- Graph ends when all nodes are inactive and no messages in transit
2. Nodes
Nodes are async functions that:
- Receive the current state as input
- Perform computation or side effects
- Return state updates (partial or full)
const myNode = async (state: State): Promise<Partial<State>> => {
// Nodes are just async functions!
return { key: "updated_value" };
};3. Edges
| Edge Type | Description | Example |
|---|---|---|
| Static | Always routes to same node | addEdge("A", "B") |
| Conditional | Routes based on state/logic | addConditionalEdges("A", router) |
| Dynamic (Send) | Fan-out to multiple nodes | new Send("worker", {...}) |
| Command | State update + routing | new Command({ goto: "B" }) |
4. Special Nodes
- START: Entry point of the graph (virtual node)
- END: Terminal node (graph halts)
Code Examples
Basic Graph with Static Edges
import { StateGraph, StateSchema, START, END } from "@langchain/langgraph";
import { z } from "zod";
// 1. Define state
const State = new StateSchema({
input: z.string(),
output: z.string(),
});
// 2. Define nodes
const processInput = async (state: typeof State.State) => {
return { output: `Processed: ${state.input}` };
};
const finalize = async (state: typeof State.State) => {
return { output: state.output.toUpperCase() };
};
// 3. Build graph
const graph = new StateGraph(State)
.addNode("process", processInput)
.addNode("finalize", finalize)
.addEdge(START, "process") // Entry point
.addEdge("process", "finalize") // Static edge
.addEdge("finalize", END) // Exit point
.compile();
// 4. Execute
const result = await graph.invoke({ input: "hello" });
console.log(result.output); // "PROCESSED: HELLO"Conditional Edges (Branching)
import { StateGraph, StateSchema, ConditionalEdgeRouter, START, END } from "@langchain/langgraph";
import { z } from "zod";
const State = new StateSchema({
query: z.string(),
route: z.string(),
result: z.string().optional(),
});
const classify = async (state: typeof State.State) => {
if (state.query.toLowerCase().includes("weather")) {
return { route: "weather" };
}
return { route: "general" };
};
const weatherNode = async (state: typeof State.State) => {
return { result: "Sunny, 72°F" };
};
const generalNode = async (state: typeof State.State) => {
return { result: "General response" };
};
// Router function
const routeQuery: ConditionalEdgeRouter<typeof State, "weather" | "general"> = (state) => {
return state.route as "weather" | "general";
};
const graph = new StateGraph(State)
.addNode("classify", classify)
.addNode("weather", weatherNode)
.addNode("general", generalNode)
.addEdge(START, "classify")
// Conditional edge based on state
.addConditionalEdges(
"classify",
routeQuery,
["weather", "general"] // Possible destinations
)
.addEdge("weather", END)
.addEdge("general", END)
.compile();
const result = await graph.invoke({ query: "What's the weather?" });Using Command for State + Routing
import { StateGraph, StateSchema, Command, START, END } from "@langchain/langgraph";
import { z } from "zod";
const State = new StateSchema({
count: z.number(),
result: z.string(),
});
const nodeA = async (state: typeof State.State) => {
const newCount = state.count + 1;
if (newCount > 5) {
// Go to nodeC
return new Command({
update: { count: newCount, result: "Going to C" },
goto: "nodeC"
});
} else {
// Go to nodeB
return new Command({
update: { count: newCount, result: "Going to B" },
goto: "nodeB"
});
}
};
const nodeB = async (state: typeof State.State) => {
return { result: `B executed, count=${state.count}` };
};
const nodeC = async (state: typeof State.State) => {
return { result: `C executed, count=${state.count}` };
};
const graph = new StateGraph(State)
.addNode("nodeA", nodeA, { ends: ["nodeB", "nodeC"] }) // Specify possible routes
.addNode("nodeB", nodeB)
.addNode("nodeC", nodeC)
.addEdge(START, "nodeA")
.addEdge("nodeB", END)
.addEdge("nodeC", END)
.compile();
const result1 = await graph.invoke({ count: 0 });
console.log(result1.result); // "B executed, count=1"
const result2 = await graph.invoke({ count: 5 });
console.log(result2.result); // "C executed, count=6"Map-Reduce with Send API
import { StateGraph, StateSchema, Send, ReducedValue, START, END } from "@langchain/langgraph";
import { z } from "zod";
const State = new StateSchema({
items: z.array(z.string()),
results: new ReducedValue(
z.array(z.string()).default(() => []),
{ reducer: (current, update) => current.concat(update) }
),
final: z.string().optional(),
});
const fanOut = (state: typeof State.State) => {
// Send each item to a worker node
return state.items.map(item =>
new Send("worker", { item })
);
};
const worker = async (state: { item: string }) => {
// Process a single item
return { results: [`Processed: ${state.item}`] };
};
const aggregate = async (state: typeof State.State) => {
// Combine results
return { final: state.results.join(", ") };
};
const graph = new StateGraph(State)
.addNode("worker", worker)
.addNode("aggregate", aggregate)
.addConditionalEdges(START, fanOut, ["worker"])
.addEdge("worker", "aggregate")
.addEdge("aggregate", END)
.compile();
const result = await graph.invoke({ items: ["A", "B", "C"] });
console.log(result.final); // "Processed: A, Processed: B, Processed: C"Graph with Loops
import { StateGraph, StateSchema, ConditionalEdgeRouter, START, END } from "@langchain/langgraph";
import { z } from "zod";
const State = new StateSchema({
count: z.number(),
maxIterations: z.number(),
});
const increment = async (state: typeof State.State) => {
return { count: state.count + 1 };
};
const shouldContinue: ConditionalEdgeRouter<typeof State, "increment"> = (state) => {
if (state.count >= state.maxIterations) {
return END;
}
return "increment";
};
const graph = new StateGraph(State)
.addNode("increment", increment)
.addEdge(START, "increment")
.addConditionalEdges("increment", shouldContinue, ["increment", END])
.compile();
const result = await graph.invoke({ count: 0, maxIterations: 5 });
console.log(result.count); // 5Compiling with Options
import { MemorySaver } from "@langchain/langgraph";
const checkpointer = new MemorySaver();
const graph = new StateGraph(State)
.addNode("nodeA", nodeA)
.addEdge(START, "nodeA")
.addEdge("nodeA", END)
.compile({
checkpointer, // Enable persistence
interruptBefore: ["nodeA"], // Breakpoint before node
interruptAfter: ["nodeA"], // Breakpoint after node
});Boundaries
What Agents CAN Configure
✅ Define custom nodes (any async function)
✅ Add static edges between nodes
✅ Add conditional edges with custom logic
✅ Use Command for combined state/routing
✅ Create loops with conditional termination
✅ Fan-out with Send API (map-reduce)
✅ Set breakpoints (interruptBefore/After)
✅ Customize state schema
✅ Specify checkpointer for persistence
What Agents CANNOT Configure
❌ Modify START/END node behavior
❌ Change super-step execution model
❌ Alter message-passing protocol
❌ Override graph compilation logic
❌ Bypass state update mechanism
Gotchas
1. Must Compile Before Execution
// ❌ WRONG
const builder = new StateGraph(State).addNode("node", func);
await builder.invoke({ input: "test" }); // Error!
// ✅ CORRECT
const graph = builder.compile();
await graph.invoke({ input: "test" });2. Conditional Edge Destinations Must Exist
// ❌ WRONG - "missingNode" not added to graph
const router = (state) => "missingNode";
builder.addConditionalEdges("nodeA", router, ["missingNode"]);
// ✅ CORRECT - Add all possible destinations
builder.addNode("missingNode", func);
builder.addConditionalEdges("nodeA", router, ["missingNode"]);3. Command Requires ends Parameter
// ❌ WRONG - No ends specified
const nodeA = async (state) => {
return new Command({ goto: "nodeB" });
};
builder.addNode("nodeA", nodeA); // Error when using Command!
// ✅ CORRECT - Specify possible destinations
builder.addNode("nodeA", nodeA, { ends: ["nodeB", "nodeC"] });4. Loops Need Exit Condition
// ❌ WRONG - Infinite loop
builder
.addEdge("nodeA", "nodeB")
.addEdge("nodeB", "nodeA"); // No way out!
// ✅ CORRECT - Conditional edge to END
const shouldContinue = (state) => {
if (state.count > 10) return END;
return "nodeB";
};
builder.addConditionalEdges("nodeA", shouldContinue, ["nodeB", END]);5. Send API Requires Reducer
// ❌ WRONG - Results will be overwritten
const State = new StateSchema({
results: z.array(z.string()), // No reducer!
});
// ✅ CORRECT - Use ReducedValue
import { ReducedValue } from "@langchain/langgraph";
const State = new StateSchema({
results: new ReducedValue(
z.array(z.string()).default(() => []),
{ reducer: (current, update) => current.concat(update) }
),
});6. START is Virtual, Cannot Be a Destination
// ❌ WRONG - Cannot route back to START
builder.addEdge("nodeA", START); // Error!
// ✅ CORRECT - Use named entry node instead
builder.addNode("entry", entryFunc);
builder.addEdge(START, "entry");
builder.addEdge("nodeA", "entry"); // OK7. Always Use Await
// ❌ WRONG - Forgetting await
const result = graph.invoke({ input: "test" });
console.log(result.output); // undefined (Promise!)
// ✅ CORRECT
const result = await graph.invoke({ input: "test" });
console.log(result.output); // Works!