Recipes
Recipes are the shortest path to “how do I do this in a real flow?”
Use Previous-Step Navigation By Default
Section titled “Use Previous-Step Navigation By Default”await machine.goToPreviousStep();Use the built-in previous-step helper when product behavior really means “move the history pointer backward”.
Override back When Product Rules Need It
Section titled “Override back When Product Rules Need It”const journey = { transitions: { review: { back: [{ to: "confirmExit", id: "review-confirm-exit" }] } }};Use this when “back” is not really “go to previous history item”, but a deliberate branch in the flow.
Skip A Step Without Hiding The Rule In The UI
Section titled “Skip A Step Without Hiding The Rule In The UI”const journey = { transitions: { details: { goToNextStep: [ { id: "details-skip-payment", to: "review", when: ({ context }) => context.isVip } ] } }};This keeps the skip rule with the transition instead of burying it inside button logic.
Update Context As Part Of A Transition
Section titled “Update Context As Part Of A Transition”const journey = { transitions: { payment: { applyCoupon: [ { id: "payment-review", to: "review", updateContext: ({ context, event }) => ({ ...context, couponCode: event.payload?.code ?? null }) } ] } }};Use transition updateContext when the context change belongs to the transition itself.
Add A Timeout To Async Work
Section titled “Add A Timeout To Async Work”const journey = { transitions: { verify: { goToNextStep: [ { id: "verify-approved", to: "approved", timeoutMs: 3_000, when: async ({ context }) => (context.score ?? 0) >= 80 } ] } }};Timeouts are useful when a stalled async guard should fail cleanly instead of leaving the caller waiting forever.
Jump Back More Than One Step
Section titled “Jump Back More Than One Step”await machine.goToPreviousStep(3);This moves the history pointer backward without rewriting visited state.
Return To The Current Tail
Section titled “Return To The Current Tail”await machine.goToLastVisitedStep();Use this when the user inspected an earlier point in history and should return to the most recently realized step.
Read Step Metadata
Section titled “Read Step Metadata”const meta = machine.getStepMeta("details");console.log(meta?.title);This is useful for reading UI-facing per-step definition data without mixing it into mutable runtime context.
Observe Lifecycle Events
Section titled “Observe Lifecycle Events”const unsubscribe = machine.subscribeEvent((event) => { if (event.type === "transition.error") { console.error(event.transitionId, event.error); }});Use lifecycle events when you care about how the flow changed, not just what the current snapshot is.
Handle Async Transition Errors
Section titled “Handle Async Transition Errors”When a guard or updateContext fails, Journey moves the source step into error phase and resolves send() with transitioned: false.
Retry by re-sending the same event
Section titled “Retry by re-sending the same event”Re-sending the event is all that is needed to retry. Journey clears the error phase and restarts the transition pipeline from the beginning.
const result = await machine.goToNextStep();
if (!result.transitioned && result.error) { // surface result.error to the UI — let the user retry // calling goToNextStep() again will start a fresh attempt}Dismiss without retrying
Section titled “Dismiss without retrying”Use clearStepError() when the user wants to cancel the failed operation and return the step to its normal interactive state without triggering a new transition.
// Resets the current step's async phase from "error" back to "idle"machine.clearStepError();
// Or target a specific stepmachine.clearStepError("payment");Read the error in UI
Section titled “Read the error in UI”snapshot.async.byStep[stepId].error holds the thrown value from the failed guard or updateContext. Use it to show a contextual message.
const asyncState = snapshot.async.byStep[snapshot.currentStepId];
if (asyncState.phase === "error") { return ( <ErrorPanel message={asyncState.error instanceof Error ? asyncState.error.message : "Something went wrong"} onRetry={() => machine.goToNextStep()} onDismiss={() => machine.clearStepError()} /> );}Branch on error via a fallback transition
Section titled “Branch on error via a fallback transition”If certain failures should navigate the user to a different step rather than retrying, use an additional transition with a when guard that inspects context set before the failure:
// On error: update context with failure info, then send a recovery eventconst result = await machine.goToNextStep();if (!result.transitioned && result.error) { machine.updateContext((ctx) => ({ ...ctx, submitError: result.error })); await machine.send({ type: "handleError" });}
// In the journey definition:transitions: { payment: { handleError: [ { to: "errorFallback", when: ({ context }) => context.submitError != null }, { to: "review" } ]; }}Build Ordered Branches
Section titled “Build Ordered Branches”const journey = { transitions: { details: { goToNextStep: [ { id: "details-extra", to: "extra", when: ({ context }) => context.includeExtra }, { id: "details-review", to: "review" } ] } }};Use ordered event arrays when a branch wants to read like a decision tree but the runtime still needs a simple first-match-wins model.