Nodes and edges
A node is a function of the state. An edge is a rule about which node runs next. Everything else in a graph is built from those two things.
The entry point is not implicit
A graph needs to know where to start. In LangGraph that is START, an explicit edge into the first node. Without it, the graph has nodes and no beginning, and compiling fails rather than guessing.
The same is true of the end. A node with no outgoing edge finishes the run, which is convenient and is also how a missing edge hides: the workflow stops early, the state looks half-done, and nothing reports an error.
The compiler cannot infer the first node because edges only say what follows what. A graph with a cycle has no natural start at all — every node has an incoming edge — so the entry point has to be declared. That is a design decision, not a limitation: the same set of nodes can be entered at different points for different tasks.
Edges are static, until they are not
A normal edge says “after this node, always run that one”. That covers linear sections and is the majority of most graphs. A conditional edge says “call this function with the state, and run whatever node it names” — that is the next chapter.
Mixing them is normal. A graph is usually a few linear stretches joined by one or two decisions.
Counting the decisions is a quick sanity check: a graph with ten nodes and one decision is mostly a pipeline, and a graph with ten decisions is a router that may need splitting. A missing edge is the most common wiring bug. The graph compiles, the run finishes, and the state is missing the key the next stage would have written. If a node seems never to run, check the edge into it before checking the node.
Wiring a graph
from langgraph.graph import StateGraph, START, END
builder = StateGraph(State)
builder.add_node("draft", draft)
builder.add_node("review", review)
builder.add_node("publish", publish)
builder.add_edge(START, "draft")
builder.add_edge("draft", "review")
builder.add_edge("review", "publish")
builder.add_edge("publish", END)
graph = builder.compile()A graph that stops early
Run it, then remove the edge from `review` to `publish`. The run ends after `review` with no error — find the missing key in the state that proves it stopped early.
One responsibility per node
A node that drafts, reviews and publishes in one function is a node that cannot be retried, branched or paused. The graph exists to make those boundaries visible, so a node that does three things defeats it.
The same rule as functions applies: if the honest name needs “and”, it is two nodes. The cost of splitting is one extra edge; the benefit is being able to put a decision between them.
A worked split: prepare does two things — trims the question and builds the prompt. Split it into clean and prompt, and the trace names both. More importantly, a retry can re-run prompt without repeating clean, and a branch can send the cleaned question to a different prompt. The extra edge is the price of those options.
The rule is about boundaries, not about line count. Two nodes of five lines each are fine if they can be retried or branched separately; one node of twenty lines is fine if it is genuinely one step.
Name nodes after what they produce, not what they call: draft, critique, revise reads better in a trace than llm_call_1 and llm_call_2.
Name nodes for the trace
Node names are the vocabulary of every log, trace and error message the graph produces. draft, critique, revise reads as a workflow; llm_call_1, llm_call_2 reads as a stack trace. A name that says what the node produces is also a name that survives a refactor of how it produces it.
The same rule applies to edge labels in a conditional graph. “revise” and “publish” are decisions a reader can follow; “route_a” and “route_b” are not.
What a trace shows
Every node transition is a line in the trace: node name, state in, state out. That is the debugging surface of a graph, and it is why node names matter more here than in a chain.
When an answer is wrong, the trace tells you which node produced the bad value and what it received. In a chain of nested calls, that information exists only if someone remembered to log it.
A trace line for one step might read: node draft, state keys question and draft, duration 1.2 seconds. The next line says review. When the answer is wrong, the trace shows whether review saw the draft it was supposed to see, and whether publish ran at all. That is the difference between debugging a graph and guessing.
Three nodes, one job each
Run the graph, then split `prepare` into two nodes that each do one thing. The output should be identical and the trace should name both steps.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
Why does a graph need an explicit entry point?
