In LangGraph you define a typed state (a TypedDict or Pydantic model), nodes (functions that take state and return updates) and edges. Conditional edges route based on state: 'if the last message has tool calls, go to the tool node; otherwise end'. Cycles make loops natural.
State updates can use reducers: for example messages appends rather than overwrites.
Compile the graph with a checkpointer and every step is persisted per thread. The graph is also a diagram you can show in your README.
from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.checkpoint.memory import MemorySaver
def agent(state: MessagesState):
return {"messages": [llm_with_tools.invoke(state["messages"])]}
g = StateGraph(MessagesState)
g.add_node("agent", agent)
g.add_node("tools", ToolNode(tools))
g.add_edge(START, "agent")
g.add_conditional_edges("agent", tools_condition) # tools or END
g.add_edge("tools", "agent")
app = g.compile(checkpointer=MemorySaver())
app.invoke({"messages": [("user", "Refund order ORD-123456")]}, {"configurable": {"thread_id": "t1"}})Going deeper
LangGraph 1.0 is the runtime underneath LangChain's create_agent. Use the graph API directly when you need explicit control flow: parallel branches (fan-out/fan-in), sub-graphs per agent, or deterministic steps mixed with agentic ones.
The Send API dynamically fans out work (one branch per item, map-reduce style), useful for processing many documents in parallel within one graph.