Quick Start
Letβs build a light bulb that toggles between Off and On. It is about the smallest machine that still shows every core idea: a configuration of states and transitions, an engine that runs it, and per-state behaviour that fires as the machine moves.
By the end you will have a running state machine in a dozen lines, and you will know where to go next for hierarchy, serialization, and the higher-level runner.
The machine, live
Here is the machine we are about to build. Click Toggle to dispatch the toggle event and watch the active state move between Off and On:
Step 1 β Describe the machine
A machine is a plain object. Give the root a key, then list the transitions as [from, event, to] triples. The empty string "" marks the initial transition (which state to enter first); "*" is a wildcard that matches any state or event.
const config = {
key: "LightBulb",
transitions: [
["", "*", "Off"], // start in Off
["Off", "toggle", "On"], // toggle turns it on
["On", "toggle", "Off"], // toggle turns it off
],
};
That is the entire model. There is no code in it yet β it is data you could serialize, diff, or generate. See States, Events & Transitions for the full semantics.
Step 2 β Run it with startProcess
The startProcess runner is the ergonomic way to attach behaviour. You give it a shared context object, the config, and a load callback. Each time the machine enters a state, load(stateKey) returns the handlers to run for that state. A handler receives the context and may return a cleanup function that runs on exit.
import { startProcess } from "@statewalker/fsm";
const handle = await startProcess(
{}, // shared context object
config,
(stateKey) => {
if (stateKey === "On") {
return [() => {
console.log("Light is ON");
return () => console.log("β¦leaving On");
}];
}
if (stateKey === "Off") {
return [() => console.log("Light is OFF")];
}
return [];
},
);
// The machine has entered "Off". Drive it from your own code:
const dispatch = /* your UI wiring */ null;
Inside a handler you dispatch events through the context. startProcess binds a dispatch function under a well-known key so any handler can move the machine forward:
import { KEY_DISPATCH } from "@statewalker/fsm";
(stateKey, context) => {
const dispatch = context[KEY_DISPATCH]; // (event) => Promise<void>
// e.g. wire a button: button.onclick = () => dispatch("toggle");
};
When you are done, handle.shutdown() unwinds every active state, running each stateβs cleanup in reverse order.
The startProcess Runner page covers the full handler contract, including generators that yield events to drive the machine reactively.
Step 3 β Or drive the engine directly
If you want full control, skip the runner and use FsmProcess. You register a single onStateCreate callback and attach onEnter / onExit hooks yourself:
import { FsmProcess } from "@statewalker/fsm";
const process = new FsmProcess(config);
process.onStateCreate((state) => {
state.onEnter(() => console.log("β", state.key));
state.onExit(() => console.log("β", state.key));
});
await process.dispatch(""); // enter the initial state (Off)
await process.dispatch("toggle"); // Off β On
await process.dispatch("toggle"); // On β Off
This is exactly what powers the live widget above. The Getting Started guide walks through the engine step by step.
What you built
A finite state machine whose behaviour is defined declaratively and whose transitions are explicit. The same config runs in the browser, Node.js, Bun, or Deno β it has no DOM or platform assumptions. Adding a state means adding a key and a transition, not threading a new flag through existing code.
Next steps
- States, Events & Transitions β the model in depth: wildcards, initial and final transitions.
- Hierarchy & Lifecycle β nest states and control enter/exit ordering.
- The startProcess Runner β the full handler contract and event-yielding generators.
- Examples β a traffic light and a hierarchical coffee machine.