1.0.0-rc.2
Your wizard became a graph
One import to a running machine
No provider to mount, no store to configure, no scheduler to reason about. Define the steps, define how they connect, and start it.
Install
npm i @rxova/journey-coreUse
import { createLinearJourney } from "@rxova/journey-core";
const machine = createLinearJourney({ context: { name: "" }, steps: ["account", "review"]});
await machine.navigate.goToNextStep();See for yourself
This is not a diagram. Drive it.
A real machine from @rxova/journey-core, running in this page with no framework around it. Change the plan, walk the flow, then step back through the timeline and watch the guard re-decide.
- plan
- account
- payment
- review
Snapshot
- currentStepId
- plan
- status
- running
- context.plan
- free
Timelinehistory.timeline
- plan
Pick Pro and the flow routes through the payment step. Pick Free and it skips straight to review. Step back, change your mind, and the guard is re-evaluated on the way forward.
Three modes, one field
Start as a list. Become a graph.
The transitions field is the only thing that changes as a flow grows. There is no rewrite between these three, and no new runtime concepts to learn on the way.
Linear
A fixed sequence. Use the array shorthand when every step just goes to the next one.
steps: ["account", "details", "payment", "review"];Graph
Branching, retries and conditional routing. Keyed by step, then by event, matched in order.
steps: {login: {on: {submit: [{ to: "admin", when: ({ context }) => context.role === "admin" },{ to: "dashboard" }];}}}
Why this one
Explicit enough to model real flows, small enough to stay practical
Journey sits between step arrays that collapse the moment a flow branches and general statechart engines whose modelling surface is wider than product journeys need.
Typed step IDs
Invalid step names, events and transition targets are compile errors, not runtime bugs. The types are checked where the flow is authored.
Timeline history
The runtime keeps the realized path, so
goToPreviousStep()andgoToLastVisitedStep()are deterministic instead of guesswork.Async guards
Loading, failure, timeout and retry are part of flow behaviour. Check with async
when, and read the phase off the snapshot.Plugins, not baggage
Persistence, analytics, replay and execution paths extend the machine without adding to the base cost.
Zero dependencies
Nothing to inherit and nothing to audit. Tree-shakeable, so you ship the parts of it you actually reach for.
Framework-agnostic core
The runtime is vanilla TypeScript. React bindings are a thin separate package; Vue, Svelte and plain TS need no shim at all.
Proof
The numbers, and where to check them
Every figure below is read out of the thing that enforces it, at the moment this page is built. The bundle sizes are measured by running size-limit against each package's built output — brotli-compressed, dependencies included — not restated from a changelog.
0
Runtime dependencies
Across core, the React bindings and the DevTools bridge
≤ ?
Core, brotlied
The whole framework-agnostic runtime, measured by size-limit
≤ ?
React bindings
Provider, step renderer and typed hooks
3.02 kB
DevTools bridge
Opt-in — nothing reaches your bundle unless you attach it
95%
Coverage floor, per file
Statements, branches, functions and lines — enforced by vitest in CI
3
Published packages
Take only what you need — each one is independently installable
MIT
Licensed, every package
Open source, developed in the open at github.com/rxova/journey
DevTools
Watch the runtime tell the truth
A native Chrome extension that reads the machine while your app runs: the realized timeline, the context behind the current step, the transition each event matched, and a command panel to send events back in.

Timeline and state
The realized timeline, the current step, and the context behind it.

Transition log
Every event, the transition it matched, and the diff it committed.

Live commands
Send events into the running machine and watch the snapshot answer.
React
Thin bindings, not a second state system
The React package exposes the runtime and gets out of the way. Flow rules stay in the definition; components render the active step.
import { createLinearJourney } from "@rxova/journey-react";
const signup = createLinearJourney({ name: "signup", context: { email: "" }, steps: ["start", "review"]});
const App = () => ( <signup.Provider views={{ start: <Start />, review: <Review /> }}> <signup.StepRenderer /> </signup.Provider>);Need one runtime per request, route boundary or owned component boundary? Use useJourney instead of creating the machine at module scope.