# 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.