Transition Syntax
Journey supports two current ways to define transitions:
- linear step arrays for simple sequential flows
- event-keyed graph objects for branching or cross-cutting flow logic
Both resolve to the same runtime model. Transition matching stays deterministic: first valid transition wins.
Reserved step ids: *, global, COMPLETE, and TERMINATED. They are part of the runtime contract and cannot be reused as actual step names.
Option A: Linear Shorthand
Use a linear array when the flow is just a fixed goToNextStep sequence.
const journey = {
initial: "start",
context: {},
steps: {
start: {},
details: {},
review: {}
},
transitions: ["start", "details", "review"]
};
This is shorthand for:
start --goToNextStep--> detailsdetails --goToNextStep--> review
You can also annotate the next step without switching to graph syntax:
const journey = {
initial: "start",
context: { draftId: null },
steps: {
start: {},
details: {},
review: {}
},
transitions: [
"start",
{
step: "details",
id: "start-next",
timeoutMs: 5_000,
updateContext: ({ context }) => ({
...context,
completedSteps: context.completedSteps + 1
})
},
"review"
]
};
In linear mode:
- strings are shorthand for the next step id
- object entries use
step, notto - object entries support
id,updateContext,onEnter,onLeave, andtimeoutMs whenis not supported
It is best when the flow really is linear and you do not need branching, custom events, guards, or explicit terminal transitions.
Option B: Transition Graph Object
Use the graph object when you need branching, custom events, async logic, or wildcard behavior.
const journey = {
initial: "start",
context: { canContinue: false },
steps: {
start: {},
details: {},
review: {}
},
transitions: {
start: {
goToNextStep: [{ id: "start-next", to: "details" }]
},
details: {
goToNextStep: [
{
id: "details-next-guarded",
to: "review",
when: ({ context }) => context.canContinue
},
{
id: "details-save",
to: "review",
updateContext: ({ context }) => ({
...context,
draftSaved: true
})
}
]
},
review: {
completeJourney: true
},
global: {
terminateJourney: [{ id: "cancel-anywhere" }]
}
}
};
How The Graph Object Works
- Top-level keys are source step ids.
- Event names under each step map to ordered arrays of candidate edges.
- Each edge is an object containing
toplus optionalid,when,updateContext,onEnter,onLeave, andtimeoutMs. globalis the reserved wildcard bucket for cross-cutting transitions.COMPLETEandTERMINATEDare reserved terminal outcomes, not regular step ids.
Order matters inside each event array. Journey evaluates the candidates in order and picks the first valid match.
Option C: Graph Builder
createJourneyBuilder is an alternative to the inline graph object. Instead of one central transition object, each step declares its own transitions and can be co-located with its component. The builder compiles to the same JourneyDefinition — no new runtime concepts.
// builder.ts — typed singleton, no local deps
const { createStep, to, build } = createJourneyBuilder<Context, StepId, EventMap>();
// steps/login.step.ts — co-located with Login.tsx
export const loginStep = createStep("login", {
on: {
submit: [to("admin").when(({ context }) => context.role === "admin"), to("dashboard")]
}
});
// journey.ts — one-screen assembly
const definition = build({
initial: "login",
context: { role: "user" },
steps: [loginStep, adminStep, dashboardStep],
global: { completeJourney: true, terminateJourney: true }
});
Use the factory form when you need event.payload narrowed to the specific event type:
submit: ({ to }) => [to("admin").when(({ context, event }) => event.payload?.username !== "")];
See the Graph Builder API reference for the full API including .id(), .timeoutMs(), .updateContext(), typed event payloads, and file organization patterns.
Which Style Should You Use?
- Choose the linear array when the doc or feature is teaching a simple fixed sequence.
- Choose the graph object when the flow has branching, skips, retries, custom events, guards, lifecycle callbacks, or global behavior.
- Choose the graph builder when the flow is large, multiple people own different steps, or you want transitions co-located with the components that drive them.
- Move from linear to graph as soon as the next step itself becomes conditional.
Most real product flows start with the linear shorthand and then move to the graph object (or builder) as soon as the sequence stops being fixed.
Transition Fields
Core fields you can use in transition records and graph edges:
to: target step or terminal outcomewhen: optional guard (sync or async)updateContext: optional synchronous context updateronEnter: optional observational callback after commit on the target sideonLeave: optional observational callback after commit on the source sidetimeoutMs: optional finite millisecond limit for asyncwhenid: optional stable identifier for observability/debugging
For runtime semantics of guards and context updates, see Async Behavior.