TECHNICAL REPORT

LangGraph

Stateful Agent Orchestration as Directed Graphs
Graph Execution State Management Persistence Human-in-the-Loop

LangGraph is an MIT-licensed Python (and TypeScript) library for building long-running, stateful agent workflows as directed, potentially cyclic graphs — addressing the fundamental limitations of linear chain-based LLM orchestration.

Inspired by Google Pregel  ·  Apache Beam  ·  NetworkX API
01

Motivation — Beyond Linear Chains

The Chain Limitation

Early LLM orchestration frameworks modelled pipelines as directed acyclic sequences of operations. This works for simple prompt-response tasks but breaks down as soon as an agent must loop, retry, or branch based on intermediate results.

  • No native support for cycles or loops
  • State must be passed manually between steps
  • No built-in pause / resume semantics
  • Difficult to insert human feedback mid-run
  • Debugging long runs requires custom instrumentation

What Real Agents Need

Cycles

ReAct loops: Reason → Act → Observe → Reason, repeating until a goal is met or a budget exceeded.

Persistent State

Shared memory visible to every node — updated incrementally, not reconstructed each step.

Human-in-the-Loop

Ability to pause at an arbitrary node, await human input, then resume with full history intact.

02

Architecture Overview

A LangGraph application is a compiled StateGraph — a finite state machine whose transitions are driven by LLM outputs and tool results rather than hardcoded logic.

Three primitives compose every graph:

State

A typed shared dictionary (TypedDict or Pydantic) that flows through every node. Nodes read the full state and return only the keys they modify.

Nodes

Pure Python callables — plain functions, async functions, LangChain Runnables, or nested compiled sub-graphs — that transform state.

Edges

Direct edges unconditionally connect two nodes. Conditional edges call a router function on the current state to select the next node at runtime.

State TypedDict · Reducers Node A LLM / tool call Node B tool / custom router? read state read state direct edge cond. edge
03

State — The Shared Memory Model

Schema Definition

Every graph is parameterised by a State schema. All nodes and edges receive an instance of this schema as their sole argument.

from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages

class AgentState(TypedDict):
    # add_messages is a reducer — it appends
    # rather than overwriting the list
    messages: Annotated[list, add_messages]
    tool_output: str
    iteration:  int

Reducers

When a node returns a partial dict, LangGraph merges each key using its reducer function. The default reducer is last-write-wins. Custom reducers enable append semantics, set union, or conflict resolution.

Update Semantics

A node only needs to return the keys it modifies — not the entire state object. This avoids race conditions in parallel branches and keeps node logic cohesive.

def call_llm(state: AgentState) -> dict:
    response = llm.invoke(state["messages"])
    # return only the keys this node touches
    return {
        "messages": [response],  # reducer appends
        "iteration": state["iteration"] + 1,
    }

Private State Channels

Nodes can declare additional state keys not in the root schema — useful for subgraph-internal bookkeeping that should not propagate to the parent graph.

04

Nodes — Units of Computation

A node is any callable that accepts a state dict and returns a (partial) state dict. This uniformity means nodes are trivially composable and independently testable.

Built-in Node Types

ToolNode

Executes tool calls embedded in the last AI message and writes results back as ToolMessage objects — handles serialisation, error propagation, and parallel fan-out automatically.

LLM Node (custom)

Calls any LangChain ChatModel with bound tools. Returns updated message list. The standard pattern for ReAct-style reasoning.

Subgraph Node

A compiled StateGraph can be registered as a node in a parent graph, enabling hierarchical, modular multi-agent architectures.

Async & Parallel Nodes

Nodes support async def natively. When multiple nodes have no data dependency in the same super-step they execute in parallel within that super-step.

from langgraph.graph import StateGraph, START, END

graph = StateGraph(AgentState)

# register functions as nodes
graph.add_node("reason", call_llm)
graph.add_node("act",    ToolNode(tools))

# direct edge: reason always proceeds to act
graph.add_edge(START,    "reason")
graph.add_edge("act",    "reason")   # ← cycle!

# conditional edge: route after reasoning
graph.add_conditional_edges(
    "reason",
    should_continue,   # router fn → str
    {"continue": "act", "end": END}
)

app = graph.compile()
05

Edges & the Pregel Execution Model

Edge Types

TypeAPIBehaviour
Directadd_edge(a, b)Always transitions A→B
Conditionaladd_conditional_edges(a, fn, map)Calls fn(state) → key → next node
Fan-outrouter returns a listSpawns N parallel branches
SendSend(node, state)Dynamically dispatches sub-tasks mid-graph

Pregel Super-Steps

Execution proceeds in discrete super-steps, inspired by Google's Pregel BSP model. A super-step ends when all currently active nodes have completed. Nodes activated in the same super-step run concurrently.

t=0 START → reason → act → END
t=1 START → reason → act → END
t=2 START → reason → act → END
START reason LLM call should_ continue? act ToolNode END loop done
06

Graph Compilation

Before a graph can be executed it must be compiled via graph.compile(). Compilation is lightweight but serves several critical roles.

Validation Checks

  • All declared nodes are reachable from START
  • No orphaned nodes (nodes with no outgoing or incoming edges)
  • All conditional edge targets exist as registered nodes
  • State schema is a valid TypedDict or Pydantic model

Runtime Configuration

Compile-time parameters attach persistent infrastructure to the immutable graph:

app = graph.compile(
    checkpointer = PostgresSaver(conn),
    interrupt_before = ["human_review"],
    interrupt_after  = ["tool_call"],
)

Output: CompiledGraph

The compiled object is a LangChain Runnable, giving it a standard interface regardless of internal complexity:

invoke(input, config)

Synchronous, blocking execution. Returns the final state dict.

stream(input, config)

Yields chunks as each node completes. Supports multiple stream modes: values, updates, debug, messages.

astream_events(input, config)

Async generator emitting fine-grained lifecycle events — token streaming from LLM nodes, tool start/end, etc.

The compiled graph is immutable — runtime modifications that could destabilise execution are prevented by design.

07

Persistence & Checkpointing

LangGraph's checkpointer saves a complete snapshot of graph state after every super-step. Snapshots are keyed by (thread_id, checkpoint_id).

Checkpointer Backends

BackendClassUse Case
In-memoryInMemorySaverDevelopment, tests
SQLiteSqliteSaverLocal / single-process
PostgreSQLPostgresSaverProduction
Azure Cosmos DBCosmosDBSaverAzure-hosted production

All backends implement a common BaseCheckpointSaver interface with .put, .get_tuple, and .list methods, making backends interchangeable with a single line change.

Capabilities Unlocked by Checkpointing

Fault Tolerance & Resumption

A crashed or timed-out run can be restarted from its last checkpoint by re-invoking with the same thread_id.

Time Travel & Forking

Pass a historic checkpoint_id to re-execute the graph from that point, creating a new branch. Invaluable for debugging agent decisions.

Cross-Thread Long-Term Memory

The Store interface complements checkpoints by persisting facts across different thread IDs — e.g. user preferences across sessions.

Human-in-the-Loop (prerequisite)

Interrupts require checkpointing: the graph serialises state on pause, allowing resumption after an arbitrarily long wait for human input.

08

Human-in-the-Loop

LangGraph provides two complementary mechanisms for injecting human oversight at arbitrary points in a running graph.

Interrupt / Breakpoints

Setting interrupt_before=["node_name"] or interrupt_after=["node_name"] at compile time causes execution to pause and serialise state before or after that node fires. The __interrupt__ key in the returned state carries a structured payload describing why execution paused.

# Resume after human provides approval
app.invoke(
    Command(resume={"approved": True}),
    config={"configurable": {"thread_id": tid}}
)

Dynamic interrupt() Call

Inside any node, interrupt(payload) pauses execution programmatically — no compile-time configuration required. The payload is surfaced to the caller.

State Editing

While paused, the caller can patch graph state via app.update_state() before resuming — enabling human correction of LLM-generated content mid-run.

plan_task ⏸ interrupt awaiting human 👤 approve / edit Command( resume=…) execute_task END checkpoint persisted
09

Multi-Agent Patterns

Supervisor / Orchestrator

A central LLM node decides which specialised sub-agent to invoke next. Each sub-agent is a compiled subgraph registered as a node in the parent graph. Common in tool-heavy research or coding pipelines.

supervisor → agent_A
supervisor → agent_B
agent_A   → supervisor  # report back
agent_B   → supervisor

Peer / Swarm

Agents hand off to each other directly via Command(goto="agent_B") without a central coordinator. LangGraph's Send API enables dynamic dispatch of sub-tasks to whichever peer is best placed to handle them.

agent_A → agent_B
agent_B → agent_C
agent_C → agent_A  # or END

Scatter-Gather

A fan-out edge spawns N parallel worker agents (via Send); a downstream aggregator node merges their outputs using a list-append reducer. Suits parallel web search or document analysis.

plan → Send("worker", s0)
plan → Send("worker", s1)
plan → Send("worker", s2)
worker → aggregate

Subgraph Composition Rules

A subgraph shares state keys that appear in both the parent and child schemas; additional keys in the child schema are private. The parent's checkpointer automatically propagates to all subgraphs — a single compile(checkpointer=…) at the root is all that is required.

10

Streaming

LangGraph is designed from the ground up for token-by-token streaming — essential for responsive UIs in agentic applications.

Stream Modes

ModeYields
valuesFull state after each super-step
updatesPartial state dict from each node
debugFull execution trace events
messagesToken-level chunks from LLM nodes
customUser-defined payloads via StreamWriter

Multiple modes can be combined in a single call. Subgraph events are exposed by passing subgraphs=True.

astream_events

The highest-resolution API, emitting structured lifecycle events for every LLM call, tool invocation, and node transition. Powers real-time "thinking" indicators and step-by-step progress UIs.

async for event in app.astream_events(
    {"messages": [HumanMessage(query)]},
    config={"configurable": {"thread_id": "1"}},
    version="v2"
):
    kind = event["event"]
    if kind == "on_chat_model_stream":
        token = event["data"]["chunk"].content
        print(token, end="", flush=True)
    elif kind == "on_tool_start":
        print(f"\n[tool] {event['name']}")

Zero Overhead

LangGraph adds no latency to streaming paths — the framework is explicitly designed so that streaming workflows carry no orchestration overhead.

11

Deployment — LangGraph Platform

While LangGraph can be deployed as a plain Python library, LangChain provides a managed LangGraph Platform (LangSmith Deployment) that wraps any compiled graph behind a production API server.

Platform Capabilities

  • Automatic checkpointing — no manual PostgresSaver wiring
  • Autoscaling background task workers for long-running runs
  • Built-in execution queue for fan-out workloads
  • REST & WebSocket endpoints auto-generated from the graph
  • LangGraph Studio — visual graph debugger & execution explorer
  • LangSmith integration — traces, evals, and production monitoring

LangGraph Studio

A local desktop GUI that renders the compiled graph topology, lets developers step through execution node-by-node, inspect state at each checkpoint, and replay from any prior step — dramatically reducing iteration time for complex agents.

Self-Hosted Deployment

LangGraph Server can be self-hosted as a Docker container using the langgraph-cli tool. It provides the same REST API and handles persistence, streaming, and task queuing independently.

# build and serve
langgraph build -t my-agent:latest
langgraph up --port 8000

Configuration at Runtime

Graphs accept a typed Configuration schema in addition to state. These are runtime-tunable parameters — which LLM to call, search depth limits, etc. — that callers inject per-request via configurable without recompiling the graph.

LangGraph in Chrome / Claude in Excel

The same graph-compilation primitives underpin several Anthropic and LangChain browser-extension agents, illustrating the framework's range from local scripts to production SaaS agents.

12

Framework Comparison

Feature LangGraph CrewAI AutoGen / AG2 OpenAI Agents SDK
Execution model Directed graph (cyclic) Role-based sequential crew Conversation-driven Tool-loop with handoffs
State management Typed, reducer-driven Implicit via task context Message history only SDK-managed context
Cycles / loops First-class Limited Via conversation turns Via tool retries
Persistence Pluggable checkpointers None built-in Partial (memory store) Limited
Human-in-the-loop interrupt() / breakpoints Human agent role human_input_mode Hooks (limited)
Multi-agent topology Supervisor, peer, subgraph Hierarchical crew GroupChat, Nested Handoffs
LLM provider agnosticism Any LangChain ChatModel Any (via LangChain) OpenAI-centric OpenAI-only
Streaming Token-level, zero overhead Limited Partial SSE stream
Observability LangSmith (deep) CrewAI Plus OpenTelemetry OpenAI dashboard
Licence MIT MIT CC-BY-4 / MIT MIT
13

Strengths & Trade-Offs

Strengths

Maximum Expressiveness

Arbitrary graph topologies — including cycles, fan-out, fan-in, and nested subgraphs — model real-world agent behaviour without contorting the code.

Production Durability

Checkpointing, time-travel, and fault-tolerant resumption make long-running multi-step agents viable in production at scale.

Controllability

Human-in-the-loop interrupts, state editing, and typed schemas give engineers precise control over agent behaviour — essential for safety-sensitive applications.

LangChain Ecosystem

Drop-in use of hundreds of LangChain integrations — LLMs, vector stores, tools, retrievers — with no adaptor layer.

Trade-Offs

Steeper Learning Curve

Graph primitives, reducers, super-steps, and checkpointer semantics require meaningful upfront investment compared to simple chain-based frameworks.

Debugging Complexity

Distributed agent failures, non-deterministic LLM routing, and cycle detection are harder to trace than linear pipelines. LangSmith partially mitigates this.

Infrastructure for Production

Realising fault tolerance requires a PostgreSQL-backed checkpointer and careful thread management — non-trivial to configure and operate.

LangChain Coupling

While standalone use is possible, the richest tooling (LangSmith, LangGraph Platform) ties the stack to the LangChain ecosystem.

14

Worked Example — ReAct Research Agent

A complete, minimal ReAct agent with persistent memory and human approval before each tool call.

from typing import Annotated, TypedDict
from langchain_core.messages import BaseMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.memory import InMemorySaver

# ── State ──────────────────────────────────────
class State(TypedDict):
    messages: Annotated[list[BaseMessage],
                        add_messages]

# ── Nodes ──────────────────────────────────────
llm_with_tools = llm.bind_tools(tools)

def agent(state: State) -> dict:
    return {"messages": [
        llm_with_tools.invoke(state["messages"])
    ]}

def should_continue(state: State) -> str:
    last = state["messages"][-1]
    if last.tool_calls:
        return "tools"
    return END
# ── Graph construction ─────────────────────────
builder = StateGraph(State)
builder.add_node("agent", agent)
builder.add_node("tools", ToolNode(tools))

builder.add_edge(START, "agent")
builder.add_conditional_edges(
    "agent", should_continue,
    {"tools": "tools", END: END}
)
builder.add_edge("tools", "agent")

# ── Compile with persistence + HITL ────────────
memory = InMemorySaver()
app = builder.compile(
    checkpointer      = memory,
    interrupt_before  = ["tools"],   # pause for approval
)

# ── Invoke ─────────────────────────────────────
cfg = {"configurable": {"thread_id": "t1"}}
state = app.invoke(
    {"messages": [HumanMessage("Research LangGraph")]},
    config=cfg
)
# state["__interrupt__"] signals pending approval
# after human approval:
app.invoke(Command(resume=True), config=cfg)

This pattern — under 40 lines — captures the full LangGraph capability surface: typed state, reducers, conditional edges, cyclic execution, checkpointing, and human-in-the-loop interrupts.

15

LangGraph.js — TypeScript Support

LangGraph ships a first-class TypeScript / JavaScript SDK (@langchain/langgraph) with API parity to the Python version. The same graph primitives — state, nodes, edges, checkpointers — work identically.

Installation

npm install @langchain/langgraph @langchain/core
# or
yarn add @langchain/langgraph @langchain/core

State Definition (Annotation API)

TypeScript uses an Annotation builder instead of Python's TypedDict. Reducers are declared inline.

import { Annotation } from "@langchain/langgraph";
import { BaseMessage } from "@langchain/core/messages";

const AgentState = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: (curr, update) => [...curr, ...update],
    default: () => [],
  }),
  iteration: Annotation<number>({
    reducer: (_curr, update) => update,
    default: () => 0,
  }),
});

Graph Construction

import { StateGraph, START, END } from "@langchain/langgraph";
import { ToolNode } from "@langchain/langgraph/prebuilt";

const graph = new StateGraph(AgentState)
  .addNode("reason", callLlm)
  .addNode("act", new ToolNode(tools))
  .addEdge(START, "reason")
  .addEdge("act", "reason")
  .addConditionalEdges("reason", shouldContinue, {
    continue: "act",
    end: END,
  });

const app = graph.compile();

Node Implementation

async function callLlm(
  state: typeof AgentState.State
) {
  const response = await llmWithTools.invoke(
    state.messages
  );
  return {
    messages: [response],
    iteration: state.iteration + 1,
  };
}

function shouldContinue(
  state: typeof AgentState.State
): string {
  const last = state.messages[state.messages.length - 1];
  return last.tool_calls?.length ? "continue" : "end";
}
16

TypeScript — Streaming, Persistence & Deployment

Streaming (Token-Level)

const stream = await app.stream(
  { messages: [new HumanMessage("Research AI")] },
  { configurable: { thread_id: "t1" },
    streamMode: "messages" }
);

for await (const [message, metadata] of stream) {
  if (message.content) {
    process.stdout.write(String(message.content));
  }
}

Checkpointing

import { MemorySaver } from "@langchain/langgraph";

const memory = new MemorySaver();
const app = graph.compile({
  checkpointer: memory,
  interruptBefore: ["act"],
});

// Resume after human approval
await app.invoke(
  { messages: [] },   // empty — resumes from checkpoint
  { configurable: { thread_id: "t1" } }
);

Full ReAct Agent (TypeScript)

import { ChatOpenAI } from "@langchain/openai";
import { createReactAgent } from
  "@langchain/langgraph/prebuilt";
import { TavilySearchResults } from
  "@langchain/community/tools/tavily_search";

const llm = new ChatOpenAI({ model: "gpt-4o" });
const tools = [new TavilySearchResults()];

const agent = createReactAgent({
  llm,
  tools,
});

const result = await agent.invoke({
  messages: [
    { role: "user", content: "What is LangGraph?" }
  ],
});

Deployment

LangGraph Platform

TypeScript graphs deploy to LangGraph Platform identically to Python. Use langgraph.json to point at your TS entrypoint.

Self-Hosted (Express / Next.js)

The compiled graph is a plain async function — wrap it in any Node.js server framework. Stream via SSE or WebSockets.

17

Code Examples — Advanced Patterns

Custom Reducer (Python)

from typing import Annotated

def merge_dicts(current: dict, update: dict) -> dict:
    """Deep merge — update wins on conflict."""
    return {**current, **update}

class ResearchState(TypedDict):
    messages: Annotated[list, add_messages]
    findings: Annotated[dict, merge_dicts]
    sources:  Annotated[list, lambda a, b: a + b]

Scatter-Gather (Python)

from langgraph.types import Send

def fan_out(state: State) -> list[Send]:
    """Dispatch one worker per query."""
    return [
        Send("search_worker", {"query": q})
        for q in state["queries"]
    ]

graph.add_conditional_edges(
    "planner", fan_out
)
graph.add_edge("search_worker", "aggregator")

Supervisor Multi-Agent (Python)

def supervisor(state: State) -> dict:
    """Central LLM decides which agent to call."""
    response = llm.invoke([
        SystemMessage("Route to: researcher, "
                      "coder, or FINISH"),
        *state["messages"]
    ])
    return {"next_agent": response.content}

graph = StateGraph(State)
graph.add_node("supervisor", supervisor)
graph.add_node("researcher", research_agent)
graph.add_node("coder", coding_agent)

graph.add_conditional_edges("supervisor",
    lambda s: s["next_agent"],
    {"researcher": "researcher",
     "coder": "coder",
     "FINISH": END})
graph.add_edge("researcher", "supervisor")
graph.add_edge("coder", "supervisor")

Dynamic interrupt() (Python)

from langgraph.types import interrupt

def risky_action(state: State) -> dict:
    approval = interrupt({
        "action": "delete_file",
        "path": state["target_path"],
    })
    if not approval.get("approved"):
        return {"messages": [AIMessage("Aborted.")]}
    # proceed with action...
    return {"messages": [AIMessage("Done.")]}
18

Summary

Graph Execution

Directed, potentially cyclic graphs with Pregel-style super-step execution. Supports fan-out, fan-in, conditional routing, and nested subgraphs.

State & Reducers

Typed shared state with per-key reducer functions. Nodes return partial updates; LangGraph merges deterministically — no global state mutation.

Persistence

Pluggable checkpointer backends. Enables fault tolerance, time travel, multi-session memory, and — as a prerequisite — human-in-the-loop interrupts.

Key Design Decisions

  • Low-level primitives — no opinionated cognitive architecture imposed
  • Immutable compiled graph — behaviour is consistent across all executions
  • Runnable interface — composable with LangChain LCEL pipelines
  • Pregel super-steps — clean parallelism model without explicit thread management
  • Single checkpointer at root — automatically propagates to all subgraphs

References

LangGraph Documentation — docs.langchain.com
LangGraph GitHub — github.com/langchain-ai/langgraph
Malewicz et al., "Pregel: A System for Large-Scale Graph Processing," SIGMOD 2010
LangChain Academy — academy.langchain.com
LangGraph Platform — langchain.com/langgraph
LangSmith Deployment Guide — docs.smith.langchain.com