Time travel, and forking a run
Replaying from a past checkpoint reads history up to that point and then genuinely re-runs everything after it — including model calls and interrupts, which can return different results; editing state at that checkpoint and running forward creates a branch instead. A UI that hides which of the two just happened will lose the user’s work.
A broker looks at the summary the parser produced and says the completion date is wrong. Not wrong now — wrong three steps ago, in the extraction, and everything since has been built on it.
The thread has every checkpoint. The correct value is one call away. So you sketch a rewind control, because that is what the situation obviously wants, and it is at this exact point that the two things you could actually build diverge, cost different amounts of money, and produce different results.
Getting this distinction into the interface is the lesson. It is not an API detail. A control that says undo and quietly re-runs four model calls is a different product from one that shows a saved state.
Three operations, not two
Start by separating what a thread actually lets you do, because “time travel” is one phrase covering three things with very different consequences.
- Read the history.
getStateHistoryhands back the thread’s checkpoints, most recent first. Nothing runs. Nothing changes. This is the only one of the three that is genuinely free. - Replay from a checkpoint. Invoke the graph with a past snapshot’s config. Work already done before that point is not repeated. Everything at and after it runs again, for real.
- Fork from a checkpoint. Write different values at that point, which creates a new branch, then run forward along it. The original history stays exactly where it was.
Most descriptions of this feature collapse the first two, and it is worth being precise about why that is wrong.
Replay is not a read
The replay call is small enough to be misleading:
// Replay: nodes before the checkpoint are not re-run; nodes at or after it are.
await graph.invoke(null, pastSnapshot.config);Passing null as the input means “no new input”; the config carries the checkpoint_id that says where to start. The documentation describes the split plainly: “Nodes before the checkpoint are not re-executed (results are already saved). Nodes after the checkpoint re-execute, including any LLM calls, API requests, and interrupts.”
And then, in case that was not emphatic enough:
“Replay re-executes nodes—it doesn’t just read from cache. LLM calls, API requests, and interrupts fire again and may return different results.” (LangChain, Use time travel)
So only the history before your target checkpoint is a pure read. From the checkpoint forward, replay is a live execution: it bills, it calls other people’s APIs, it takes as long as the original did, and because models are not deterministic it can produce a different answer than the one the user is trying to get back to.
Read that last clause again with a rewind button in mind. A user presses it expecting to see what was there before. They may instead get a fresh result they have never seen, arrived at from the same starting point. That is not a bug in the framework. It is what replay means, and a button labelled undo is a promise the mechanism cannot keep.
Where people get burned
Side effects re-fire. If a node after your target checkpoint sends an email, charges a card, or writes to somebody else’s system, a replay does it again. The node-placement discipline the interrupting-a-tool-call lesson teaches is what protects you here, and a rewind control is the feature most likely to expose that you did not apply it. Before you ship replay to users, walk the nodes after every checkpoint they can reach and name every side effect in them.
Forking, which appends rather than rewrites
The other operation writes new values at a past point and runs forward from there. The correction the broker asked for is this one.
// Fork: creates a NEW checkpoint, does not touch the original.
const forkConfig = await graph.updateState(
pastSnapshot.config,
{ topic: "chickens" },
{ asNode: "generateTopic" } // optional; needed to disambiguate
);
await graph.invoke(null, forkConfig);Three things to take from the shape. The first argument is a past snapshot’s config, which is where the branch attaches. The second is the values to write. The third, asNode, is optional and attributes the update to a particular node when there is ambiguity about which node the write should be credited to — it matters because the node you claim to be determines what runs next.
The documentation is emphatic about what this does not do:
updateState “does not roll back a thread. It creates a new checkpoint that branches from the specified point. The original execution history remains intact.” (LangChain, Use time travel)The return value is the fork’s config, and it is the handle to the new branch. Hold onto it. It is the only thing that addresses what you just created, and a UI that discards it has made a branch nobody can navigate back to.
Note the consequence for your mental model of a thread: it is not a line, it is a tree. Everything the checkpointers-and-threads lesson said about thread_id being your persistent cursor still holds, but as soon as anyone forks, “the state of this thread” needs a checkpoint to be unambiguous.
Finding the checkpoint to act on
Neither operation is useful until you can name a point. The documented approach searches history by what still needed to run, then reuses that snapshot’s own config rather than assembling one by hand:
const history = await graph.getStateHistory(config); // most-recent-first
const target = history.find((s) => s.next.includes("writeJoke"));
await graph.invoke(null, target.config); // replay from there
// or fork from there:
const forkConfig = await graph.updateState(target.config, { topic: "chickens" });Searching on next is worth noticing as a design hint. You are not locating “the third checkpoint” or “16:40 yesterday.” You are locating the moment just before a particular node was going to run, which is nearly always what a person means when they point at a step and say it went wrong there. That also gives you the label: a history list reads far better as “before extraction,” “before review,” “before summary” than as a column of checkpoint ids.
One more documented behaviour that lands directly on the review flow: interrupts are re-triggered during time travel. The node containing the interrupt re-executes and interrupt() pauses again for a new new Command({ resume }). If a broker replays past her own approval, she will be asked to approve again, and the answer she gave the first time is not reused.
What this means for the control you were about to draw
This section is this course’s own design argument, drawn from the mechanics above rather than from any vendor guidance. LangChain documents the calls and has nothing to say about what your buttons are called.
Three operations means three controls, with three honest labels and three different costs:
| Operation | Honest label | What the user must be told |
|---|---|---|
| Read history | View earlier step | Nothing. It is free and reversible. Make it the default gesture. |
| Replay | Run again from here | That it re-runs the work, costs what the original cost, may produce a different answer, and will ask any approvals again. |
| Fork | Change this and continue | That the original is kept, and where they can get back to it. |
The words undo, revert and restore are wrong for all three. Undo implies the later work is discarded, and forking keeps it. Restore implies the earlier answer comes back unchanged, and replay does not guarantee that. Both words are load-bearing lies in a product where the underlying operation appends.
The harder version of the same problem is that a fork makes the run a tree, and most agent UIs render a list. If a broker corrects a field and continues, there are now two versions of everything downstream. Say so, or she will believe the earlier one is gone. Then decide whether the old branch is visible, hidden, or garbage-collected — and note that the framework does not delete it for you, so “hidden” and “gone” are not the same thing when somebody later asks what the system originally extracted.
Check your recall
Answer from memory — no scrolling back.
Retrieval check
Product asks for a single “go back to step 2” button on the parser. What do you say, and what do you propose instead?
Check your answer
Say that “go back” is two operations wearing one label, and that the cheap-sounding one is the expensive one. Viewing step 2 is free and instant. Running from step 2 re-executes every node at and after it, bills for the model calls again, may return a different extraction than the one on screen, and will re-ask any approval the broker already gave.
Propose three affordances instead. A history list labelled by what was about to run — before extraction, before review, before summary — that opens a read-only view of that point. Inside that view, run again from here, with the cost and the re-approval stated before it fires. And, where the user actually wants to change something, change this and continue, which is updateState plus an invoke on the returned config.
Then raise the branching question early, because it is the one that gets discovered late: after a fork there are two versions of everything downstream, the framework keeps both, and somebody has to decide whether the old one is shown, hidden or referenced in an audit trail. For a document that ends up in a mortgage file, “what did the system originally extract” is a question with consequences.
Hands on
Replay it, fork it, and write down what each one cost
Done when: ARTIFACT.md has both a replay and a fork run against the parser thread, with the checkpoint you targeted, the node names that re-executed, whether the replayed result matched the original, and the three control labels you would actually ship.
- Add a print of node entry inside every node, or watch the
updatesstream. You need to see which nodes run, not infer it. This is the whole experiment. - Run the parser to completion. Then pull
getStateHistoryand find the checkpoint before the extraction node using itsnextarray. Paste the list of checkpoints with thenextvalue of each, and label them in the words you would show a broker. - Replay from that checkpoint with
invoke(null, target.config). Record which nodes ran and whether the extraction produced the same values as the first time. If any node calls a model, run this twice and compare. - Fork from the same checkpoint:
updateStatewith a corrected field, then invoke on the returned config. Keep the fork config. Then read the history again and write down what you can now see about both branches, and what you cannot. - Walk every node after that checkpoint and list its side effects. For each one, write whether a replay repeating it is harmless, and what you would move if it is not. This list is the precondition for shipping any rewind control at all.
- Write the three control labels you would ship, and beside each the sentence the user sees before it fires. Bring them into the chat. I will look for the word undo, and for a fork whose original is not reachable from the UI.
What this does not cover
Everything here assumed a checkpoint is yours to show. It walked through history, rendered earlier values and proposed a list of past states as a product feature, and never once asked what is inside them. On a document parser the answer includes the document. What belongs in a browser, what belongs in an audit trail, and what should never have been a checkpointed channel in the first place is the state-you-can-show lesson, which closes this module and is the one to read before a history view ships.
The re-approval behaviour got named here and not designed. What an interrupt asks, what the resume payload carries, and how to place a side effect so that re-execution is survivable are the interrupt module’s subject — the decision-surface and interrupting-a-tool-call lessons in particular.
Nothing here covered reconstructing what happened inside a node during a run you did not watch. Timings, inputs, outputs and errors per node come from a tracing backend rather than from checkpoints, and that is the traces-as-the-observability-substrate lesson in the last module.
Read this next — primary source
Use time travelLangChain — docs.langchain.com, JavaScript docs, fetched 2026-09-05. Vendor documenting its own product, and the docs lead to the hosted platform it sells
Every call shape in this lesson comes from this page, including the sentence that corrects the way most people describe replay. Read it for the explicit warning that replay is not a cache read, and for the exact wording on what updateState does to a thread — the page is unusually careful about saying that nothing is rolled back or overwritten, and that carefulness is the thing your undo button has to inherit.
Stuck, curious, or think this lesson is wrong? Ask your teaching agent. The lessons are the scaffold; the conversation is where the learning gets unstuck.