Timeline Navigation
Journey navigation is built on a timeline + pointer model.
This is the reason back/forward behavior is deterministic instead of guesswork.
Mental Model
Section titled “Mental Model”timeline: [start] -> [details] -> [payment] -> [review]index: ^currentStepId: "payment"The timeline records realized history. The index chooses which point in that realized history is considered “now”.
The Model
Section titled “The Model”history.timeline: the path of reached steps.history.index: the current pointer into that path.currentStepId: alwayshistory.timeline[history.index].
Why This Matters
Section titled “Why This Matters”You get two benefits at once:
- true history (what happened)
- current position (where you are now)
That makes debugging and replay much easier.
Navigation Modes
Section titled “Navigation Modes”| Operation | What changes | What does not change |
|---|---|---|
| step transition | appends or rewrites realized tail | earlier realized history before current pointer |
goToPreviousStep(steps?) | moves pointer backward | timeline entries, visited |
goToLastVisitedStep() | moves pointer to the realized tail | timeline entries, visited |
headless goToStepById(stepId) | appends target as a new realized step when needed | earlier realized history before current pointer |
Built-in Navigation APIs
Section titled “Built-in Navigation APIs”goToPreviousStep(steps?): move pointer backwardgoToLastVisitedStep(): move pointer to timeline tailgoToStepById(stepId): direct jump in headless mode, or a declared transition in graph/linear mode
You can also model a custom back event:
await machine.send({ type: "back" });Journey does not treat back as a built-in event. If you want back behavior, declare it as a custom event and
add explicit back transitions.
Direct helper and send fallback are related but not identical:
machine.goToPreviousStep()is pure pointer navigationmachine.send({ type: "goToPreviousStep" })goes through the send pipeline firstmachine.send({ type: "back" })is just a custom event, so it only does something when your journey declares a matchingbacktransition
Branching After Going Back
Section titled “Branching After Going Back”When you are not at the tail and a forward transition happens, Journey:
- truncates timeline to
history.index + 1 - appends the new target step
- moves pointer to the new end
Example:
- before:
timeline = ["start", "details", "payment", "review"],index = 1 - forward to
upsell - after:
timeline = ["start", "details", "upsell"],index = 2
before[start] -> [details] -> [payment] -> [review] ^
after forward transition to "upsell"[start] -> [details] -> [upsell] ^This is the same mental model users expect from history systems.
visited vs Pointer
Section titled “visited vs Pointer”visited tracks whether a step was ever entered.
Pointer navigation does not rewrite visited, because revisiting old timeline positions should not erase historical facts.
That distinction is useful in UI:
- use
history.indexandcurrentStepIdfor current position - use
visited[stepId]for “has this step ever happened?”
No-Op Cases
Section titled “No-Op Cases”goToPreviousStep(...)is a no-op at the start of historygoToLastVisitedStep()is a no-op when you are already at the realized tail- pointer helpers are no-ops when machine status is terminal
- explicit
goToPreviousSteptransitions still win over built-in previous-step navigation fallback
Recommended Reading
Section titled “Recommended Reading”- Read Snapshot for the full shape of
historyandvisited. - Read Lifecycle for
navigation.previousandnavigation.lastVisited. - Read Navigation Commits for the implementation side.
- Read Send Pipeline if you want to understand fallback navigation from
send(...).