LangGraph looks like it has a big API surface, but almost everything in it is a combination of four ideas. Get comfortable with these four and the rest of the framework - checkpointers, interrupts, multi-agent supervisors - all read as "clever things built on top of state, nodes, and edges," rather than new concepts to memorize.
State: the one shared object everything reads and writes
State is a single structure - usually a Python TypedDict or a small dataclass - that represents everything your graph currently knows: the conversation so far, a running list of search results, a counter, whatever your logic needs. Every node receives the current state as its input and returns a dictionary of the fields it wants to update. LangGraph merges those updates back into the shared state before handing it to whichever node runs next.
The one wrinkle worth knowing up front: by default, a field gets overwritten by whatever a node returns for it. If you want a field to accumulate instead - the classic example is a running list of chat messages - you attach a reducer function to that field so new values get appended rather than replacing what was there. This one design choice is what makes it possible for a graph to build up context over many steps instead of losing it between nodes.
Nodes: plain functions that do the work
A node is just a Python function. It takes the current state in, does something - call a model, call a tool, run some plain logic - and returns the piece of state it wants to change. That's the entire contract. Nothing about a node needs to know what ran before it or what runs after it; it just reads state in and writes state out.
This is part of why LangGraph code tends to stay readable as agents grow: each node is small and testable on its own, and the graph structure - not deeply nested code - is what decides the order things happen in.
Edges: fixed paths, and edges that decide for themselves
Edges connect nodes. A normal edge is fixed: after Node A finishes, always run Node B next. A conditional edge is where LangGraph earns its keep - instead of a fixed next step, you attach a small routing function that looks at the current state and returns the name of whichever node should run next. That's how a graph branches: "if the input passed validation, go to the handler; if not, go back and ask for clarification."
The Router node doesn't hard-code what happens next - it inspects state and picks the edge to follow.
Graphs: wiring it all together with StateGraph
The graph itself - a StateGraph object - is where you register everything: you add each node with a name, connect them with add_edge for fixed transitions or add_conditional_edges for branching ones, and mark where execution should start and where it's allowed to finish (LangGraph gives you START and END markers for this). Once it's wired up, you call .compile(), which turns your definition into an actual runnable object you can invoke with an initial state.
graph = StateGraph(AgentState)
graph.add_node("router", router_node)
graph.add_node("handle_valid", handle_valid_node)
graph.add_node("ask_for_clarification", clarify_node)
graph.add_edge(START, "router")
graph.add_conditional_edges(
"router",
decide_next_step, # returns "handle_valid" or "ask_for_clarification"
)
graph.add_edge("handle_valid", END)
graph.add_edge("ask_for_clarification", END)
app = graph.compile()
result = app.invoke({"input": user_message})
That's genuinely most of it. Everything else in LangGraph - looping an agent back through a tool node, pausing for human approval, coordinating several agents - is built by combining these same four pieces in different shapes, which is exactly what the rest of this series covers.