TL;DR
LangGraph models an LLM application as a stateful graph: nodes (functions that read and update a shared State), edges (which node runs next), and a State object threaded through every step. Unlike an LCEL chain (linear, one-directional), a graph supports conditional edges (branch at runtime based on state) and loops (revisit a node until a condition is met). This one abstraction builds both deterministic workflows (fixed edges you wire yourself) and agents (edges chosen by the LLM). It's the framework you reach for when a chain isn't enough — you need branching, cycles, persistence, or human-in-the-loop. A trip app's group-suggestion feature and a fixed multi-step clinical review pipeline are both LangGraph.
Simple explanation + analogy
An LCEL chain is a train on a straight track — it goes from start to end through fixed stations. LangGraph is a subway map: there are junctions where the route depends on where you're headed, and lines can loop back on themselves.
The three pieces:
- State = a shared clipboard everyone writes on. It's a
TypedDict(or Pydantic model) carrying everything the run needs: inputs, intermediate results, the final answer. - Node = a worker who reads the clipboard, does a job (call an LLM, hit a retriever, run Python), and writes back a partial update.
- Edge = the arrow telling you which worker goes next. A plain edge is fixed ("after A, always B"). A conditional edge runs a small routing function that looks at the clipboard and decides ("if we still need research, loop back; else finalize").
Because edges can point backward, you get loops — the thing chains can't do.
Diagram
Group-Trip Graph:
START
│
▼
[classify_intent] ── trip-related? ──NO──▶ END
│ YES
▼
[classify_request] ──conditional edge──┐
│ │ │ │
followup new_sugg consensus booking
│ │ │ │
└───┬────┘ ▼ ▼
▼ [ask_task_ [create_task_node]
[retrieve] confirmation] │
│ │ ▼
┌──────┴───────┐ └──────────────▶ END
▼ ▼
[answer_followup] [generate_suggestion]
│ │
└──────┬───────┘
▼
END
State (TypedDict) is passed to every node and merged after each returns.
How it works (deep)
1. State — the shared, typed, mutable context
You declare a schema. Every node receives the whole state and returns a partial update, which LangGraph merges in:
class GroupTripState(TypedDict):
chat_snippet: str
should_suggest: bool
request_type: str # "new_suggestion" | "followup" | ...
destination_context: str
suggestion: Optional[str]
By default, an updated key overwrites the old value. For keys where you want to accumulate (e.g. a growing message list), you attach a reducer — Annotated[list, add_messages] — so returns are appended, not replaced. Reducers are the #1 "gotcha" interviewers test: without one, a node that returns {"messages": [new_msg]} replaces the entire history.
2. Nodes
A node is any callable state -> partial_state. It can be a plain function, an async function, an LCEL chain, or a compiled subgraph. Inside a node you do the real work — call an LLM, run a retriever, execute Python:
async def classify_intent(state: GroupTripState) -> GroupTripState:
chain = _INTENT_PROMPT | _llm() | StrOutputParser() # LCEL *inside* a node
result = await chain.ainvoke({"snippet": state["chat_snippet"]})
state["should_suggest"] = result.strip().upper().startswith("Y")
return state
Note: LCEL lives inside LangGraph nodes. The two aren't competitors — LangGraph orchestrates, LangChain does the per-node LLM work.
3. Edges — fixed vs conditional
- Fixed edge:
g.add_edge("suggest", END)— always go there next. - Conditional edge:
g.add_conditional_edges("classify_request", router_fn, {...}). Therouter_fnreads state and returns a key; the mapping says which node that key routes to.
def _route_after_request_type(state) -> str:
rt = state.get("request_type", "new_suggestion")
if rt == "consensus": return "ask_task_confirmation"
if rt == "task_confirmed": return "create_task_node"
return rt # "followup" or "new_suggestion" → both go to retrieve
4. Workflow vs agent — same graph, who picks the edge?
This is the crux. LangGraph builds both:
- Workflow — you write the routing functions (deterministic branching on state). A fixed multi-step clinical review pipeline is this: "LLM points, Python reads" — the LLM produces structured judgments, but Python code decides the control flow, giving auditability. Predictable, testable, no runaway loops.
- Agent — the LLM picks the next step. You give it tools and a loop (the prebuilt
create_react_agent/ a tool-calling node → conditional edge → back to the LLM). The graph is the same shape; the router just delegates the decision to the model.
5. Loops, persistence, human-in-the-loop
- Loops: a conditional edge that can point back to an earlier node ("need more research? → back to
web_research; else →finalize"). Always bound them — set arecursion_limitto prevent infinite cycles. - Checkpointing: compile with a checkpointer (
MemorySaver,SqliteSaver, Postgres). State is persisted per-thread after every step, giving durability, time-travel, and resumability. - Human-in-the-loop:
interrupt()pauses the graph mid-run; you inspect/edit state and resume — natural because state is an explicit, serializable object.
6. Compilation
graph = builder.compile() returns a Runnable. So a whole graph implements .invoke() / .ainvoke() / .stream() — meaning a graph can itself be a node in a bigger graph, or a step in an LCEL chain.
The math
LangGraph is a computation over a directed graph, so the useful formalism is graph/state-machine theory, not probability.
- A graph is
G = (V, E)— nodesV, edgesE. Chains are the special case whereGis a simple path (a DAG with one route). LangGraph allows cycles, soGis a general directed graph and execution is a state machine. - Execution is a transition function
δ(s, v) = (s', v_next): nodevtransforms states → s', then the (possibly conditional) edge picksv_next = route(s'). It's a Mealy-style machine whose transitions depend on the current state. - Termination: the run halts when it reaches
END. Because cycles exist, termination is not guaranteed by structure — you enforce it with an exit condition in a router plus arecursion_limit R. If no path reachesENDwithinRsuper-steps, LangGraph raises rather than looping forever. Always design a monotone progress measure (e.g. "confidence increases" or "attempts remaining decreases") so the loop provably exits. - State merge: after node
vreturns partial updateu, the new state iss' = merge(s, u), wheremergeper key is overwrite by default or a reducerr_k(s_k, u_k)(e.g. list concatenation) where declared.
Real code
From a group trip-recommendation app (group_trip_graph.py) — a conditional, multi-branch graph. First the state and a node:
from typing import Dict, List, Optional, TypedDict
from langgraph.graph import END, StateGraph
class GroupTripState(TypedDict):
chat_snippet: str
should_suggest: bool
request_type: str
destination_context: str
suggestion: Optional[str]
async def classify_request(state: GroupTripState) -> GroupTripState:
chain = _REQUEST_TYPE_PROMPT | _llm() | StrOutputParser()
result = (await chain.ainvoke({"snippet": state["chat_snippet"]})).strip().upper()
if "CONSENSUS" in result: state["request_type"] = "consensus"
elif "BOOKING" in result: state["request_type"] = "task_confirmed"
elif "FOLLOWUP" in result: state["request_type"] = "followup"
else: state["request_type"] = "new_suggestion"
return state
Now the routing functions and graph assembly — note add_conditional_edges and the edge that lets the graph short-circuit to END:
def _route_after_intent(state) -> str:
return "classify_request" if state["should_suggest"] else END
def _route_after_request_type(state) -> str:
rt = state.get("request_type", "new_suggestion")
if rt == "consensus": return "ask_task_confirmation"
if rt == "task_confirmed": return "create_task_node"
return rt
def _build_graph() -> StateGraph:
g = StateGraph(GroupTripState)
g.add_node("classify_intent", classify_intent)
g.add_node("classify_request", classify_request)
g.add_node("retrieve", retrieve_destinations)
g.add_node("suggest", generate_suggestion)
g.add_node("answer_followup", answer_followup)
g.add_node("ask_task_confirmation", ask_task_confirmation)
g.add_node("create_task_node", create_task_node)
g.set_entry_point("classify_intent")
g.add_conditional_edges("classify_intent", _route_after_intent,
{"classify_request": "classify_request", END: END})
g.add_conditional_edges("classify_request", _route_after_request_type, {
"followup": "retrieve", "new_suggestion": "retrieve",
"ask_task_confirmation": "ask_task_confirmation",
"create_task_node": "create_task_node",
})
g.add_conditional_edges("retrieve", _route_after_retrieve,
{"answer_followup": "answer_followup", "suggest": "suggest"})
g.add_edge("suggest", END)
g.add_edge("answer_followup", END)
g.add_edge("create_task_node", END)
return g.compile() # → a Runnable
_graph = _build_graph()
async def run_group_trip(...) -> Optional[str]:
initial: GroupTripState = { "chat_snippet": chat_snippet,
"should_suggest": False, "request_type": "new_suggestion",
"destination_context": "", "suggestion": None }
result = await _graph.ainvoke(initial)
return result.get("suggestion")
This is a workflow-style graph: the routers are hand-written Python, so behavior is deterministic and testable. Swap a router to "ask the LLM which node next" and the same machinery becomes an agent.
Real-world example (a trip app + a clinical review pipeline)
Group trip. In a group WhatsApp-style thread, the AI decides whether to chime in and what kind of response to give. A plain chain can't: the flow branches (is this trip-related? a new suggestion, a follow-up question, a consensus to book?) and some branches (booking) run database side-effects. The graph above (a) gates on intent so it stays silent off-topic, (b) classifies the request type via conditional edges, (c) only runs retrieval on the branches that need it, and (d) reaches distinct ENDs for chat replies vs. created tasks.
A fixed multi-step clinical review pipeline. A regulated clinical workflow where every decision must be auditable. The design principle is "LLM points, Python reads": LLM nodes emit structured judgments (scores, extracted entities, flags), and deterministic Python routers read those and decide control flow — evidence grading, evidence gathering loops, guideline synthesis. Using LangGraph means the entire decision path is inspectable and reproducible; using a free-form agent would sacrifice the auditability a clinical setting demands.
Interview questions companies actually ask
1. What is LangGraph and how does it differ from a LangChain (LCEL) chain? [easy] LangGraph orchestrates LLM apps as stateful graphs of nodes and edges with a shared State object. A chain is linear and one-directional; a graph adds explicit state, conditional branching, loops, checkpointing, and human-in-the-loop. You use LangChain components inside LangGraph nodes. (What is LangGraph, 2026)
2. What are nodes, edges, and State? [easy] A node is a function taking the current state and returning a partial update. An edge connects nodes — unconditional ("A→B") or conditional (a routing function picks the next node from state). State is a typed dict/Pydantic model threaded through and updated by every node. (LangGraph Basics: StateGraph)
3. How do conditional edges enable branching? [medium]
add_conditional_edges(source, router_fn, mapping) runs router_fn(state) after the source node; it returns a key, and the mapping routes to the corresponding node. This lets the graph pick different successors at runtime based on state. (Conditional Edges & Routing)
4. How do loops work, and how do you prevent infinite loops? [medium]
A conditional edge that can point back to an earlier node creates a cycle. You bound it with a recursion_limit and design an explicit exit condition (a monotone progress measure) in the router — e.g. "confidence ≥ threshold" or "retries remaining == 0" → route to finalize.
5. What is a reducer and when do you need one? [hard]
By default a returned key overwrites the state value. A reducer (e.g. Annotated[list, add_messages]) defines how updates merge — for message history you want append, not overwrite. Forgetting the reducer silently drops accumulated history, a classic bug.
6. Can LangGraph build both workflows and agents? Explain. [hard] Yes — same graph abstraction, difference is who chooses the edge. In a workflow you write deterministic routing functions (fixed control flow, auditable). In an agent the LLM decides the next node/tool via a tool-calling loop. A clinical review pipeline uses the workflow style ("LLM points, Python reads") for auditability; a research agent uses the agent style.
7. How does LangGraph support persistence and human-in-the-loop? [hard]
Compile with a checkpointer (MemorySaver, SqliteSaver, Postgres) to persist state per thread after every step — enabling resume, time-travel, and durability. interrupt() pauses mid-run so a human can inspect/edit the (serializable) state and resume. Both are natural because state is an explicit object.
8. When would you choose LangGraph over just LCEL? [medium] When you need cycles (iterate until good enough), dynamic multi-way branching, shared mutable state across steps, durable execution/checkpointing, or human approval mid-flow. If the flow is linear (RAG, extraction), a chain is simpler and you shouldn't reach for a graph. (From Basics to Advanced: LangGraph)
9. Why is a compiled graph itself a Runnable, and why does that matter? [medium]
.compile() returns an object implementing .invoke()/.stream()/.ainvoke(), so a graph composes into LCEL chains and can nest as a subgraph node. It makes graphs first-class, reusable building blocks. (250 LangGraph Q&A)
When to use / tradeoffs
Use LangGraph when:
- Control flow branches or loops (research-until-confident, multi-step agents, retry policies).
- You need shared, typed, mutable state across many steps.
- You need durability, checkpointing, time-travel, or human-in-the-loop.
- Auditability matters — the workflow style makes every decision a testable Python function.
Avoid when:
- The flow is a straight line (RAG, single extraction) — an LCEL chain is simpler and lighter.
- You want the absolute minimum abstraction — a hand-written state machine plus direct SDK calls can be clearer for a small, fixed agent (Anthropic's "start simple" advice).
Tradeoffs: more concepts to learn (State schemas, reducers, checkpointers), more boilerplate than a chain, and cycles require disciplined exit conditions. In return you get the control, durability, and inspectability that production agents need.
Summary + related articles
LangGraph = nodes + edges + shared State. Conditional edges give branching; back-edges give loops; checkpointers give durability. The same graph builds deterministic workflows (you route) and agents (the LLM routes). It's where you go when an LCEL chain runs out of road. A trip app's group feature and a clinical review pipeline's "LLM points, Python reads" workflow show both ends of the spectrum.
Related articles:
- LangChain — LCEL chains and the Runnable interface (used inside nodes).
- Framework Comparison — LangGraph vs Strands vs AutoGen vs CrewAI vs OpenAI Agents SDK.
- Agentic Design Patterns — routing, orchestrator–worker, evaluator–optimizer as graphs.
- Human-in-the-Loop & Checkpointing — durable, resumable agent execution.
Going deeper — module 6.6, LangGraph in Depth. This page is the overview; each of these takes one mechanism past it, with runnable code:
- Checkpointers, Threads, and Durable Execution — what a checkpoint stores, how
thread_idscopes it, and why storage is quadratic in thread length. - Human-in-the-Loop with interrupt() — pausing mid-node, and the replay rule that makes pre-
interrupt()side effects run twice. - Time Travel and State History — replaying and forking from any past checkpoint.
- Streaming Modes in LangGraph —
updatesvsvaluesvscustom, and their very different costs. - Dynamic Fan-Out with Send — running one node N times when N is only known at runtime.
- Command and Dynamic Routing — returning a state update and a destination together, instead of writing a router.
- Subgraphs and Graph Composition — a compiled graph as a node, and what sharing a state schema costs you.
- Long-Term Memory with Store — facts that outlive a thread, which checkpointed state cannot provide.