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 asadd_messagesdefine 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_edgesroutes 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), enablinginterrupt()for human approval, retries from a step, and time-travel inspection. - Prebuilt
create_react_agentandToolNodecover 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,Sendfor 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, TypedDict2from langgraph.graph import StateGraph, START, END3from langgraph.graph.message import add_messages4from langgraph.prebuilt import ToolNode5from langgraph.checkpoint.memory import MemorySaver6 7class State(TypedDict):8 messages: Annotated[list, add_messages] # reducer: append, dedupe by id9 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 END19 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 | END25g.add_edge("tools", "agent") # the loop26app = g.compile(checkpointer=MemorySaver(), interrupt_before=["tools"]) # HITL gate27cfg = {"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