LangChain and LangGraph 1.0
Both reached 1.0 in October 2025; LangGraph is at 1.3.x as of mid-2026. The 1.0 line committed to API stability with no breaking changes until 2.0. If your knowledge is the chains-and-LCEL era, it’s a generation out of date.
The relationship: LangGraph is the runtime; LangChain 1.0 is an opinionated, middleware-driven high-level API on top of it. That’s the sentence to have ready.
What changed at 1.0
| Before | 1.0 |
|---|---|
Chains, LLMChain, sprawling abstractions |
focused on the agent loop |
| Subclass to customise | middleware hooks |
| Ad-hoc state, lost on restart | durable execution with checkpointing |
| Many overlapping entry points | create_agent as the front door |
LangChain 1.0 narrowed its scope deliberately: the core agent loop plus integrations, rather than an abstraction for everything.
create_agent and middleware
from langchain.agents import create_agent
agent = create_agent(
model="...",
tools=[search, calculator],
middleware=[RateLimitMiddleware(), PIIRedactionMiddleware()],
)
Middleware is the 1.0-era customisation mechanism. The hooks wrap the loop:
| Hook | Fires |
|---|---|
before_agent / after_agent |
around the whole run |
before_model / after_model |
around each model call |
wrap_model_call |
around the model call, can modify or short-circuit |
wrap_tool_call |
around each tool execution |
This is the same idea as HTTP middleware, and it’s what you use instead of subclassing. Practical uses: redact PII before the model sees it, enforce per-user rate limits, inject dynamic context, log and trace, retry on a schema violation, or block a tool call pending approval.
If asked “how do you customise agent behaviour in LangChain 1.0”, the answer is middleware, not inheritance.
LangGraph: state machines, not chains
You define a graph of nodes (functions) and edges (transitions). State flows through and is merged by reducers.
from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
class State(TypedDict):
messages: Annotated[list, add_messages] # reducer: append, don't replace
retries: int
def call_model(state: State) -> dict:
return {"messages": [llm.invoke(state["messages"])]}
def should_continue(state: State) -> str:
return "tools" if state["messages"][-1].tool_calls else END
builder = StateGraph(State)
builder.add_node("model", call_model)
builder.add_node("tools", ToolNode(TOOLS))
builder.set_entry_point("model")
builder.add_conditional_edges("model", should_continue)
builder.add_edge("tools", "model") # the loop
graph = builder.compile(checkpointer=checkpointer)
Three things to understand:
- Reducers decide how a node’s output merges into state.
add_messagesappends; the default replaces. Getting this wrong silently drops history — a classic first bug. - Conditional edges are where control flow lives. That
should_continuefunction is the agent loop’s termination check. - Cycles are explicit.
tools -> modelis the loop, drawn rather than implied.
Durable execution — the real 1.0 feature
LangGraph treats agent execution as durable graph execution rather than a Python function call. Every node transition is checkpointed through a pluggable persistence layer.
from langgraph.checkpoint.postgres import PostgresSaver
graph = builder.compile(checkpointer=PostgresSaver.from_conn_string(DSN))
config = {"configurable": {"thread_id": "conversation-42"}}
graph.invoke({"messages": [msg]}, config) # resumes from wherever it left off
What that buys, and why it matters to a backend engineer:
- Survives process restart. A deploy mid-conversation doesn’t lose state.
- Pause and resume, including across days — which is what makes approval workflows practical.
- Time-travel debugging. Rewind to any checkpoint, alter state, re-run.
- Thread-scoped memory for free — conversation state is the checkpoint.
This is the thing that makes LangGraph more than a nicer loop. An agent as a durable, resumable process is a fundamentally different operational object from an agent as a function call that dies with its worker. See 05_durable_execution_hitl.md.
Human-in-the-loop
from langgraph.types import interrupt, Command
def approve_refund(state: State):
decision = interrupt({"order": state["order_id"], "amount": state["amount"]})
if decision != "approve":
return {"messages": [{"role": "tool", "content": "Refund denied by reviewer."}]}
return {"messages": [execute_refund(state)]}
# Later, possibly a different process, hours later:
graph.invoke(Command(resume="approve"), config)
interrupt suspends execution and persists state. Resumption can happen from anywhere with the thread_id. Without durable checkpointing this would require you to build a state machine and a job queue yourself.
The ecosystem
| Piece | Does |
|---|---|
| LangGraph | the runtime — graphs, state, checkpointing |
| LangChain | high-level agent API, middleware, 100+ integrations |
| LangSmith | tracing, evaluation, prompt management |
| LangGraph Platform | hosted deployment, scheduling, long-running runs |
LangSmith is the observability answer within this stack; OpenTelemetry GenAI conventions are the vendor-neutral alternative. Either is fine — having none is not.
Alternatives
| Framework | Character |
|---|---|
| LangGraph | most control, durable execution, steepest learning curve |
| Pydantic AI | type-safe, Pythonic, lighter — see 12_pydantic_ai.md |
| OpenAI Agents SDK | minimal, provider-aligned |
| CrewAI | role-based multi-agent, opinionated |
| AutoGen | conversational multi-agent, research-leaning |
| no framework | a loop and a while statement |
Choose LangGraph when you need durable state, human-in-the-loop, or non-trivial topology. Choose Pydantic AI when you want type safety and a smaller surface. Choose nothing when you have a loop with three tools — and be willing to say so.
Interview angle
- “LangChain or LangGraph?” — LangGraph is the runtime; LangChain 1.0 is a middleware-driven high-level API on top of it. You’re not choosing between them so much as choosing a level of abstraction.
- “What changed at 1.0?” — scope narrowed to the agent loop, middleware replaced subclassing as the customisation mechanism, and durable execution with checkpointing became the foundation. API stability committed until 2.0.
- “What is durable execution and why does it matter?” — every node transition is checkpointed to a persistence layer, so a run survives restarts, can pause for hours awaiting approval, and can be rewound for debugging. It turns an agent from a function call into a resumable process.
- “How do you customise agent behaviour in LangChain 1.0?” — middleware hooks around the agent, model call and tool call. Not subclassing.
- “What’s a reducer in LangGraph state?” — it decides how a node’s returned value merges into state.
add_messagesappends to history; the default replaces it. Using the default on a message list silently discards the conversation. - “When would you not use a framework?” — a bounded loop with a few tools. Frameworks earn their cost when you need durability, approval gates, streaming to a UI, or complex topology.