# Chain to graph migration
## The mapping
- Each chain step -> one node function that takes state and returns a dict of state updates.
- The implicit dict passed between chains -> an explicit TypedDict state schema.
- Branching inside chain code -> conditional edges.
- `LLMChain` internals (prompt + model + parser) -> LCEL inside the node (`prompt | model | parser`), or a model call plus parsing code.
```python
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
topic: str
draft: str
approved: bool
def write(state: State):
return {"draft": model.invoke(f"Write about {state['topic']}").content}
def review(state: State):
...
builder = StateGraph(State)
builder.add_node("write", write)
builder.add_node("review", review)
builder.add_edge(START, "write")
builder.add_edge("write", "review")
builder.add_conditional_edges("review", lambda s: "write" if not s["approved"] else END)
graph = builder.compile(checkpointer=checkpointer)
```
## Rules
- Nodes return dicts on every path. See the InvalidUpdateError diagnostic.
- Add the checkpointer at compile time if any step needs retries, human approval, or time travel. Retrofitting persistence later means touching every invoke call site.
- Keep node functions small and single-purpose; a node that does three things is a chain hiding inside a graph.
- State keys written by parallel branches need reducers (`Annotated[list, operator.add]`). Design the schema before the edges.