Debugging agents means asking 'what if it had gone differently here?' and trying it.
Because a checkpointer saves state after every step, a thread is a timeline you can move through.
History. graph.get_state_history(config) yields StateSnapshots, newest first. Each has .values (the state), .next (what would run next), .config (including a checkpoint_id that identifies this exact point) and .metadata such as the step number.
Replay. Invoke with a past snapshot's config and None as input: graph.invoke(None, snapshot.config). LangGraph resumes from that checkpoint. Steps before it are not re-run; steps after it run again, which with a model may give different results.
Fork. To ask "what if the state had been different?", call graph.update_state(snapshot.config, {"key": new_value}). This writes a new checkpoint branching from that point and returns its config; then graph.invoke(None, new_config) continues down the new branch. The original history stays intact.
update_state applies your values through the same reducers as a node would. Pass as_node="name" to make LangGraph treat the update as if that node produced it, which also decides what runs next.
Uses:
- debug a bad answer by editing the retrieved documents and re-running,
- let a user edit an earlier message and regenerate,
- fix state after a bug, then resume.
LangSmith Studio shows the same history visually and lets you fork with clicks.
Example
from typing import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
city: str
plan: str
def pick_city(state: State) -> dict:
return {"city": state["city"] or "Paris"}
def make_plan(state: State) -> dict:
return {"plan": f"3 days in {state['city']}"}
b = StateGraph(State)
b.add_node(pick_city)
b.add_node(make_plan)
b.add_edge(START, "pick_city")
b.add_edge("pick_city", "make_plan")
b.add_edge("make_plan", END)
graph = b.compile(checkpointer=InMemorySaver())
cfg = {"configurable": {"thread_id": "trip"}}
print(graph.invoke({"city": "", "plan": ""}, cfg)["plan"]) # 3 days in Paris
before_plan = next(s for s in graph.get_state_history(cfg) if s.next == ("make_plan",))
fork = graph.update_state(before_plan.config, {"city": "Lisbon"}) # new branch
print(graph.invoke(None, fork)["plan"]) # 3 days in LisbonRemember
- get_state_history lists every checkpoint of a thread, newest first
- invoke(None, snapshot.config) replays from that point
- update_state(past_config, values) forks a new branch
- Original history is never overwritten