August 5, 2026
Time-Travel and Fork Logic in LangGraph
How Fork Creates a New Execution Branch

By Nachiket Mehendale
6 min read
Introduction
LangGraph supports time-travel, which lets you revisit any saved checkpoint and resume execution from that point.
Time-travel provides 2 operations:
Replay resumes execution from an existing checkpoint without changing its state.
Fork resumes execution from a checkpoint after modifying its state.
This article focuses on Fork. For Replay, see Time-Travel and Replay Logic in LangGraph.
Time-Travel and Replay Logic in LangGraph medium.com
To understand fork, consider an order-processing workflow with 3 steps: calculate the subtotal, apply a discount, and add shipping. Suppose the workflow originally applies a 10% discount, but we want to see what the final order total would be with a 20% discount.
Instead of running the workflow again from the beginning, fork lets us start from the checkpoint after the subtotal has been calculated, change the discount information, and continue execution with the modified state.
The original execution remains unchanged.
Fork Concept
Fork works by creating a new checkpoint from an existing checkpoint after modifying its state.
The selected checkpoint becomes the starting point for a new branch. The original checkpoint history is not changed.
For example — assume the original workflow produces:
subtotal = 500
discount_rate = 0.90 (10% off)
discounted_total = 450
final_total = 490subtotal = 500
discount_rate = 0.90 (10% off)
discounted_total = 450
final_total = 490If we fork from the checkpoint before apply_discount and change discount_rate from 0.90 to 0.80 (20% off), the new branch calculates:
subtotal = 500
discount_rate = 0.80
discounted_total = 400
final_total = 440subtotal = 500
discount_rate = 0.80
discounted_total = 400
final_total = 440The important point is that calculate_subtotal does not run again. Its result already exists in the selected checkpoint. The forked execution starts from that saved state and continues with apply_discount and add_shipping.
LangGraph creates the fork using update_state(). It then uses the returned configuration to continue execution with graph.invoke(None, …).
The Coding Part
— — — -Step 1 : Import required libraries — — — — — —
StateGraph and START are used to build the workflow. InMemorySaver stores checkpoints during execution. TypedDict and NotRequired define the structure of the shared state. uuid7 generates a unique ID for each workflow run.
— — — — Step 2: Define the shared state — — — — —
The shared state contains the subtotal, the discount rate, the discounted total, and the final total.
discount_rate is included because we will change this value when creating the fork. It's stored as a multiplier — 0.90 means "10% off," 0.80 means "20% off", so the node logic can use it directly in a calculation.
Each field is marked as NotRequired because it is added to the shared state only after the node that produces it has executed.
— — — — — — — — -Step 3: Define the nodes — — — — — — —
The first node, calculate_subtotal, calculates the subtotal. In this example it also sets the starting discount_rate (0.90, i.e. 10% off) — this is the value we will later change when we fork.
The second node, apply_discount, reads subtotal and discount_rate from the state and multiplies them together to get discounted_total.
The third node, add_shipping, adds a flat shipping cost of 40.0 to the discounted total to produce final_total.
Each node reads the required values from the shared state and returns a dictionary containing only the new values it produces.
— — — Step 4: Set Up the Checkpointer and Build the Graph — —
InMemorySaver() enables checkpointing. .add_node(…) registers each node in the graph. .add_edge(…) defines the execution order.
And finally, .compile(checkpointer=checkpointer) compiles the graph and attaches the checkpointer.
— — — — — — — — — — -Step 5: Run the Workflow — — — — — — —
thread_id identifies this run so its checkpoints can be retrieved later.
The graph is invoked with an empty input ({}) because none of the state fields need to be provided up front. calculate_subtotal creates both subtotal and the starting discount_rate.
This call runs all three nodes in order and saves a checkpoint after each one.
The original execution produces:
{'subtotal': 500.0, 'discount_rate': 0.9, 'discounted_total': 450.0, 'final_total': 490.0}{'subtotal': 500.0, 'discount_rate': 0.9, 'discounted_total': 450.0, 'final_total': 490.0}— — — — — — — Step 6: Look at the saved checkpoints — — — —
get_state_history() returns all saved checkpoints for the selected thread. The checkpoints are returned with the most recent one first.
The .next property of a checkpoint shows which node would run next if the workflow resumed from that checkpoint.
This workflow has 5 saved checkpoints. In the order they were created:
1]Before the graph starts → ('start',) 2]After START, before calculate_subtotal runs → ('calculate_subtotal',) 3]After calculate_subtotal completes → ('apply_discount',) 4]After apply_discount completes → ('add_shipping',) 5]After add_shipping completes → ()
The checkpoint where .next is ('apply_discount',) is used because calculate_subtotal has already finished, and apply_discount has not run yet. The saved state at this checkpoint includes the values produced before apply_discount executes.
— — — — — — Step 7: Select the Checkpoint to Fork From — — — —
This expression:
(s for s in history if s.next == ("apply_discount",))(s for s in history if s.next == ("apply_discount",))searches the checkpoint history for the checkpoint whose .next value is ("apply_discount",).
next(…) returns the first checkpoint that matches.
This selects the checkpoint where apply_discount is the next node to run.
The selected checkpoint is not resumed immediately. The state is modified before the workflow continues.
— — — — — Step 8: Modify the State and Create the Fork — — —
This is the fork step.
update_state() is called with:
1]the selected checkpoint's configuration 2]the new value (discount_rate: 0.80) 3]as_node="calculate_subtotal"
The selected checkpoint contains the state from the original execution.
update_state() changes discount_rate from 0.90 to 0.80 in the new state.
as_node="calculate_subtotal" records the update as if it came from the calculate_subtotal node. This identifies which node the update is associated with in the new checkpoint.
update_state() does not modify the original execution history. It creates a new checkpoint that branches from the selected checkpoint.
— — — — — Step 9: Resume Execution from the Fork — — — —
fork_config identifies the new checkpoint created by update_state().
Passing None as the first argument tells LangGraph to continue from the checkpoint identified by fork_config.
Execution continues with apply_discount.
calculate_subtotal does not run again because the checkpoint already contains its saved state.
apply_discount uses the updated discount_rate.
After apply_discount finishes, add_shipping runs next.
The forked execution produces:
{'subtotal': 500.0, 'discount_rate': 0.8, 'discounted_total': 400.0, 'final_total': 440.0}{'subtotal': 500.0, 'discount_rate': 0.8, 'discounted_total': 400.0, 'final_total': 440.0}The original execution is unchanged. Its original result is still available.
{'subtotal': 500.0, 'discount_rate': 0.9, 'discounted_total': 450.0, 'final_total': 490.0}{'subtotal': 500.0, 'discount_rate': 0.9, 'discounted_total': 450.0, 'final_total': 490.0}Demo Output
The first execution runs the workflow and produces this final state:
{
'subtotal': 500.0,
'discount_rate': 0.9,
'discounted_total': 450.0,
'final_total': 490.0
}{
'subtotal': 500.0,
'discount_rate': 0.9,
'discounted_total': 450.0,
'final_total': 490.0
}get_state_history() then returns the checkpoints from that execution. The checkpoints are listed with the most recent one first.
next=(), checkpoint_id=...
next=('add_shipping',), checkpoint_id=...
next=('apply_discount',), checkpoint_id=...
next=('calculate_subtotal',), checkpoint_id=...
next=('__start__',), checkpoint_id=...next=(), checkpoint_id=...
next=('add_shipping',), checkpoint_id=...
next=('apply_discount',), checkpoint_id=...
next=('calculate_subtotal',), checkpoint_id=...
next=('__start__',), checkpoint_id=...The checkpoint where .next is ('apply_discount',) is selected.
The state is updated using:
graph.update_state(
before_discount.config,
values={"discount_rate": 0.80},
as_node="calculate_subtotal"
)graph.update_state(
before_discount.config,
values={"discount_rate": 0.80},
as_node="calculate_subtotal"
)This creates a new checkpoint based on the selected checkpoint. The original execution is unchanged.
The workflow is then resumed:
graph.invoke(None, fork_config)graph.invoke(None, fork_config)Execution continues from apply_discount.
calculate_subtotal does not run again because its state is already present in the selected checkpoint.
apply_discount uses the updated discount_rate, and add_shipping runs after it.
The forked execution produces:
{
'subtotal': 500.0,
'discount_rate': 0.8,
'discounted_total': 400.0,
'final_total': 440.0
}{
'subtotal': 500.0,
'discount_rate': 0.8,
'discounted_total': 400.0,
'final_total': 440.0
}Summary
Checkpointing saves the workflow state after each node.
Forking starts from a saved checkpoint. It uses update_state() to create a new checkpoint with modified state. The as_node argument records which node the update is associated with.
The original execution history is not changed. The fork creates a new branch from the selected checkpoint.
In this example - the original execution calculates a subtotal of 500 and applies a 10% discount (discount_rate = 0.90).
The fork starts from the checkpoint after calculate_subtotal. It changes discount_rate to 0.80 and continues execution from that point.
The 2 executions produce different results:
Original execution:
500 ----> 450 -----> 490500 ----> 450 -----> 490Forked execution :
500 ----> 400 -----> 440500 ----> 400 -----> 440Forking is useful when you want to continue execution from a previous checkpoint with modified state while keeping the original execution unchanged.
Thanks for reading.
If this article helped, please give it a clap. If you noticed anything that could be improved, please leave a comment.