Skip to main content
Core 1.0.0-rc.1

Snapshot

The snapshot is the single read model for a live journey machine.

If you only inspect one value to understand what is true right now, inspect the snapshot.

Mental Model

snapshot
├─ currentStepId -> where the machine is now
├─ history -> realized path + current pointer
├─ context -> shared runtime data
├─ visited -> whether each step was ever entered
├─ status -> idled / running / completed / terminated
└─ async -> loading phases and last async error by step

Snapshot Shape

type JourneySnapshot<TContext, TStepId extends string> = {
currentStepId: TStepId;
history: {
timeline: readonly TStepId[];
index: number;
};
context: TContext;
visited: Record<TStepId, boolean>;
status: "idled" | "running" | "completed" | "terminated";
async: JourneyAsyncState<TStepId>;
};

Field Guide

FieldAnswersRelated docs
currentStepIdWhich step is active right now?History
history.timelineWhich realized path did the user actually take?History
history.indexWhich timeline entry is considered "now"?History
contextWhat shared data do guards, transition updates, and UI read from?Async Behavior
visitedWhich steps have ever been entered at least once?History
statusIs the machine idled, active, complete, or terminated?Lifecycle
asyncIs async work in flight, and which step owns the last async error?Async Behavior

Treat the snapshot as a read model. Rendering, debugging, persistence, and selector subscriptions should all be able to explain themselves from this one object.

The value returned from getSnapshot() is immutable runtime output. Read it, derive from it, and discard it. Do not try to mutate it in place.

Example Snapshot

const snapshot = machine.getSnapshot();

const exampleSnapshot = {
currentStepId: "payment",
history: {
timeline: ["start", "details", "payment"],
index: 2
},
context: {
isVip: false
},
visited: {
start: true,
details: true,
payment: true,
review: false
},
status: "running",
async: {
isLoading: false,
byStep: {
start: { phase: "idle", eventType: null, transitionId: null, error: null },
details: { phase: "idle", eventType: null, transitionId: null, error: null },
payment: { phase: "idle", eventType: null, transitionId: null, error: null },
review: { phase: "idle", eventType: null, transitionId: null, error: null }
}
}
};

Invariants You Can Trust

These stay true while a machine exists:

  • history.timeline.length >= 1
  • 0 <= history.index < history.timeline.length
  • currentStepId === history.timeline[history.index]

Those invariants are what make history navigation, snapshot selectors, and persistence safer to reason about.

How Snapshot Writes Happen

Different runtime actions update the snapshot for different reasons:

ReasonTypical sourceSee implementation
transitionstep-to-step sends, terminal sends, headless goToStepById, declared goToStepById transitionsSend Pipeline, Navigation Commits
navigationgoToPreviousStep(...), goToLastVisitedStep()Navigation Commits
asyncguard loading, idle, or error updatesAsync State
contextupdateContext(...)Controls
startstart()Controls
resetresetJourney()Controls

This is mainly visible to plugins and advanced instrumentation, but it is also a useful debugging frame: not every snapshot write means "a transition happened".

Practical Notes

  • history.timeline is realized history, not authored step order.
  • A fresh machine snapshot is idled until start() is called.
  • Pointer moves such as goToPreviousStep(...) and goToLastVisitedStep() do not rewrite visited.
  • context and async are immutable read branches in the returned snapshot. Change runtime state through updateContext(...), transition updates, or reset/start APIs instead of mutating the snapshot object.
  • async.isLoading is machine-wide, while async.byStep[stepId] gives you the step-level detail for UI.
  • Step definition metadata lives outside the snapshot. Read it through machine.getStepMeta(stepId) when needed.

Useful Reads

const snapshot = machine.getSnapshot();

const currentStep = snapshot.currentStepId;
const currentAsync = snapshot.async.byStep[currentStep];
const atHistoryTail = snapshot.history.index === snapshot.history.timeline.length - 1;
const canRenderNormally = snapshot.status === "running" && currentAsync.phase === "idle";