Skip to main content
Core 1.0.0-rc.1

Recipes

Recipes are the shortest path to “how do I do this in a real flow?”

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

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

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

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

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

await machine.goToPreviousStep(3);

This moves the history pointer backward without rewriting visited state.

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

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

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

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

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

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 step
machine.clearStepError("payment");

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

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 event
const 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

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.