A checkpointer persists graph state after each node, keyed by a thread_id. That single capability unlocks multi-turn memory, crash recovery, human-in-the-loop approval, and time-travel debugging.
save('customer-42', {'step': 'retrieve', 'attempts': 1, 'messages': ['refund?'], 'version': 2})
# later, after a crash or a new turn:
state = load('customer-42') # resume exactly where it stopped
Why it's not just logging. A log tells you what happened; a checkpoint lets you resume. Because state is snapshotted per node, an agent that fell over at node 7 of 10 restarts at node 7 — not from scratch — and a conversation can pause for a human approval and continue hours later on the same thread.
Version your state shape. The day you add a field or rename one, old checkpoints written by the previous graph version must still load. Stamp each checkpoint with a version and write a tiny migration, or resuming an in-flight thread after a deploy will crash. This is the single most overlooked production detail.
Thread identity is the contract. The thread_id is what ties a checkpoint to a user/session/run. Get it wrong and you either leak one user's state into another's conversation or lose continuity entirely — so derive it deterministically from a stable session key.