← Back to Learning Hub

LangGraph

LangChainLangGraphIntermediate12 min

By: Anacodic Team

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 reducerAnnotated[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, {...}). The router_fn reads 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:

  • Workflowyou 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 a recursion_limit to 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) — nodes V, edges E. Chains are the special case where G is a simple path (a DAG with one route). LangGraph allows cycles, so G is a general directed graph and execution is a state machine.
  • Execution is a transition function δ(s, v) = (s', v_next): node v transforms state s → s', then the (possibly conditional) edge picks v_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 a recursion_limit R. If no path reaches END within R super-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 v returns partial update u, the new state is s' = merge(s, u), where merge per key is overwrite by default or a reducer r_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.


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:

Runnable notebook

Run it end to end — the mock model needs no API key; add your own key for the real Claude section.

Open In Colab