PythonTypeScriptMedium — primitives with opinions

LangGraph

Durable, resumable agent workflows expressed as an explicit state graph with checkpoints, so long-running and human-interrupted runs can pause and continue.

Architecture

  • State: a typed dict/TypedDict (or Pydantic model) shared by all nodes; reducers such as add_messages define how partial updates merge.
  • Nodes are plain functions (state) -> partial state; an LLM call, a tool executor, a validator, or pure code — the graph does not care.
  • Edges are fixed or conditional (add_conditional_edges routes on a function of state); cycles are allowed, which is what makes an agent loop expressible.
  • Checkpointer: every super-step persists state keyed by thread_id (memory, SQLite, Postgres), enabling interrupt() for human approval, retries from a step, and time-travel inspection.
  • Prebuilt create_react_agent and ToolNode cover the common tool-calling loop; you drop to raw graphs when control flow gets specific.

Best use cases

  • Workflows with branches, loops, and approval gates that must survive process restarts.
  • Human-in-the-loop flows: pause on interrupt, resume days later with the same state.
  • Supervisor / multi-agent topologies where each agent is a subgraph.
  • Teams that want the control flow visible as a diagram rather than buried in prompts.

Weaknesses

  • Ceremony: a trivial tool loop becomes state schema + nodes + edges + compile; the boilerplate is only worth it once you need checkpoints or branching.
  • Reducer semantics (add_messages, list concatenation) surprise newcomers; state bugs are silent until a node reads stale or duplicated data.
  • Debugging conditional edges with many branches requires LangSmith or LangGraph Studio to be pleasant; raw logs are noisy.
  • Platform pull: the persistence and deployment story steers you toward LangGraph Platform / LangSmith; self-hosting durable execution is on you.
  • API surface still moves (functional API, Command, Send for map-reduce) — expect to revisit code between minor versions.

When NOT to use it

  • A single-turn or fixed-sequence pipeline with no loops — a function calling the SDK is clearer.
  • You do not need persistence: an in-memory loop with a step budget is 20 lines.
  • The whole team is TypeScript-first and already uses the Vercel AI SDK; adding a second runtime fragments the stack.

Code example

Illustrative — APIs change between versions.

1from typing import Annotated, TypedDict
2from langgraph.graph import StateGraph, START, END
3from langgraph.graph.message import add_messages
4from langgraph.prebuilt import ToolNode
5from langgraph.checkpoint.memory import MemorySaver
6
7class State(TypedDict):
8 messages: Annotated[list, add_messages] # reducer: append, dedupe by id
9
10tools = [search_docs, create_ticket]
11model = llm.bind_tools(tools)
12
13def agent(state: State):
14 return {"messages": [model.invoke(state["messages"])]}
15
16def route(state: State) -> str:
17 last = state["messages"][-1]
18 return "tools" if last.tool_calls else END
19
20g = StateGraph(State)
21g.add_node("agent", agent)
22g.add_node("tools", ToolNode(tools))
23g.add_edge(START, "agent")
24g.add_conditional_edges("agent", route) # agent -> tools | END
25g.add_edge("tools", "agent") # the loop
26app = g.compile(checkpointer=MemorySaver(), interrupt_before=["tools"]) # HITL gate
27cfg = {"configurable": {"thread_id": "user-42"}}
28app.invoke({"messages": [("user", "Open a ticket for the login bug")]}, cfg)
29# ... human approves ...
30app.invoke(None, cfg) # resume from checkpoint

Alternatives

Related lessons