npx skills add ...
npx skills add langchain-ai/skills-benchmarks --skill langgraph-fundamentals
npx skills add langchain-ai/skills-benchmarks --skill langgraph-fundamentals
INVOKE THIS SKILL when writing ANY LangGraph code. Covers StateGraph, state schemas, nodes, edges, Command, Send, invoke, streaming, and error handling.
Graphs must be compile()d before execution.
Follow these 5 steps when building a new graph:
| Use LangGraph When | Use Alternatives When |
|---|---|
| Need fine-grained control over agent orchestration | Quick prototyping → LangChain agents |
| Building complex workflows with branching/loops | Simple stateless workflows → LangChain direct |
| Require human-in-the-loop, persistence | Batteries-included features → Deep Agents |
| Need | Solution | Example |
|---|---|---|
| Overwrite value | No reducer (default) | Simple fields like counters |
| Append to list | Reducer (operator.add / concat) | Message history, logs |
| Custom logic | Custom reducer function | Complex merging |
class State(TypedDict): name: str # Default: overwrites on update messages: Annotated[list, operator.add] # Appends to list total: Annotated[int, operator.add] # Sums integers
from typing import Annotated import operator
class State(TypedDict): messages: Annotated[list, operator.add]
def my_node(state: State) -> dict: return {"field": "updated"}
Node functions accept these arguments:
| Signature | When to Use |
|---|---|
def node(state: State) | Simple nodes that only need state |
def node(state: State, config: RunnableConfig) | Need thread_id, tags, or configurable values |
def node(state: State, runtime: Runtime[Context]) | Need runtime context, store, or stream_writer |
| Signature | When to Use |
|---|---|
(state) => {...} | Simple nodes that only need state |
(state, config) => {...} | Need thread_id, tags, or configurable values |
| Need | Edge Type | When to Use |
|---|---|---|
| Always go to same node | add_edge() | Fixed, deterministic flow |
| Route based on state | add_conditional_edges() | Dynamic branching |
| Update state AND route | Command | Combine logic in single node |
| Fan-out to multiple nodes | Send | Parallel processing with dynamic inputs |
class State(TypedDict): input: str output: str
def process_input(state: State) -> dict: return {"output": f"Processed: {state['input']}"}
def finalize(state: State) -> dict: return {"output": state["output"].upper()}
graph = ( StateGraph(State) .add_node("process", process_input) .add_node("finalize", finalize) .add_edge(START, "process") .add_edge("process", "finalize") .add_edge("finalize", END) .compile() )
result = graph.invoke({"input": "hello"}) print(result["output"]) # "PROCESSED: HELLO"
class State(TypedDict): query: str route: str result: str
def classify(state: State) -> dict: if "weather" in state["query"].lower(): return {"route": "weather"} return {"route": "general"}
def route_query(state: State) -> Literal["weather", "general"]: return state["route"]
graph = ( StateGraph(State) .add_node("classify", classify) .add_node("weather", lambda s: {"result": "Sunny, 72F"}) .add_node("general", lambda s: {"result": "General response"}) .add_edge(START, "classify") .add_conditional_edges("classify", route_query, ["weather", "general"]) .add_edge("weather", END) .add_edge("general", END) .compile() )
Command combines state updates and routing in a single return value. Fields:
update: State updates to apply (like returning a dict from a node)goto: Node name(s) to navigate to nextresume: Value to resume after interrupt() — see human-in-the-loop skillclass State(TypedDict): count: int result: str
def node_a(state: State) -> Command[Literal["node_b", "node_c"]]: """Update state AND decide next node in one return.""" new_count = state["count"] + 1 if new_count > 5: return Command(update={"count": new_count}, goto="node_c") return Command(update={"count": new_count}, goto="node_b")
graph = ( StateGraph(State) .add_node("node_a", node_a) .add_node("node_b", lambda s: {"result": "B"}) .add_node("node_c", lambda s: {"result": "C"}) .add_edge(START, "node_a") .add_edge("node_b", END) .add_edge("node_c", END) .compile() )
Python: Use Command[Literal["node_a", "node_b"]] as the return type annotation to declare valid goto destinations.
TypeScript: Pass { ends: ["node_a", "node_b"] } as the third argument to addNode to declare valid goto destinations.
Warning: Command only adds dynamic edges — static edges defined with add_edge / addEdge still execute. If node_a returns Command(goto="node_c") and you also have graph.add_edge("node_a", "node_b"), both node_b and node_c will run.
Fan-out with Send: return [Send("worker", {...})] from a conditional edge to spawn parallel workers. Requires a reducer on the results field.
class OrchestratorState(TypedDict): tasks: list[str] results: Annotated[list, operator.add] summary: str
def orchestrator(state: OrchestratorState): """Fan out tasks to workers.""" return [Send("worker", {"task": task}) for task in state["tasks"]]
def worker(state: dict) -> dict: return {"results": [f"Completed: {state['task']}"]}
def synthesize(state: OrchestratorState) -> dict: return {"summary": f"Processed {len(state['results'])} tasks"}
graph = ( StateGraph(OrchestratorState) .add_node("worker", worker) .add_node("synthesize", synthesize) .add_conditional_edges(START, orchestrator, ["worker"]) .add_edge("worker", "synthesize") .add_edge("synthesize", END) .compile() )
result = graph.invoke({"tasks": ["Task A", "Task B", "Task C"]})
class State(TypedDict): results: Annotated[list, operator.add] # Accumulates
Call graph.invoke(input, config) to run a graph to completion and return the final state.
| Mode | What it Streams | Use Case |
|---|---|---|
values | Full state after each step | Monitor complete state |
updates | State deltas | Track incremental updates |
messages | LLM tokens + metadata | Chat UIs |
custom | User-defined data | Progress indicators |
def my_node(state): writer = get_stream_writer() writer("Processing step 1...") # Do work writer("Complete!") return {"result": "done"}
for chunk in graph.stream({"data": "test"}, stream_mode="custom"): print(chunk)
Match the error type to the right handler:
| Error Type | Who Fixes | Strategy | Example |
|---|---|---|---|
| Transient (network, rate limits) | System | RetryPolicy(max_attempts=3) | add_node(..., retry_policy=...) |
| LLM-recoverable (tool failures) | LLM | ToolNode(tools, handle_tool_errors=True) | Error returned as ToolMessage |
| User-fixable (missing info) | Human | interrupt({"message": ...}) | Collect missing data (see HITL skill) |
| Unexpected | Developer | Let bubble up | raise |
workflow.add_node( "search_documentation", search_documentation, retry_policy=RetryPolicy(max_attempts=3, initial_interval=1.0) )
tool_node = ToolNode(tools, handle_tool_errors=True)
workflow.add_node("tools", tool_node)
graph = builder.compile() graph.invoke({"input": "test"})
def should_continue(state): return END if state["count"] > 10 else "node_b" builder.add_conditional_edges("node_a", should_continue)
def node_a(state) -> Command[Literal["node_b", "node_c"]]: return Command(goto="node_b")
builder.add_edge("node_a", START) # WRONG! builder.add_edge("node_a", "entry") # Use a named entry node instead
return {"items": ["item"]} # List for list reducer, not a string
from langchain_core.runnables import RunnableConfig
from langgraph.runtime import Runtime
def plain_node(state: State):
return {"results": "done"}
def node_with_config(state: State, config: RunnableConfig):
thread_id = config["configurable"]["thread_id"]
return {"results": f"Thread: {thread_id}"}
def node_with_runtime(state: State, runtime: Runtime[Context]):
user_id = runtime.context.user_id
return {"results": f"User: {user_id}"}import { GraphNode, StateSchema } from "@langchain/langgraph";
const plainNode: GraphNode<typeof State> = (state) => {
return { results: "done" };
};
const nodeWithConfig: GraphNode<typeof State> = (state, config) => {
const threadId = config?.configurable?.thread_id;
return { results: `Thread: ${threadId}` };
};