createFsm
createFsm is the standard choice for most use cases. One config object produces one FSM instance with one internal context object (there’s no separate client to pass around). Handler signatures receive ({ ctx, inputName, defer, emit }, ...args) as a plain destructurable object, so arrow functions work without caveats and there’s no this to think about. Return a state name to transition, or return nothing to stay put.
If you need one FSM definition to manage many independent client objects, see createBehavioralFsm.
Config
Section titled “Config”| Property | Type | Description |
|---|---|---|
id |
string |
Identifier for this FSM. Appears in lifecycle event payloads. |
initialState |
string |
The state the FSM boots into. Must match a key in states. |
context |
object |
Mutable data scoped to this FSM. Passed to every handler as ctx. Omit for context-free FSMs. |
onDispose |
function |
Optional teardown hook. dispose() calls it once with { ctx, emit }. See dispose. |
states |
object |
Map of state names to handler definitions. Keys become the state name union. |
State handler keys
Section titled “State handler keys”Within each state, the following keys have special meaning:
| Key | Type | Description |
|---|---|---|
_onEnter |
HandlerFn |
Lifecycle hook. Called when entering this state. |
_onExit |
HandlerFn |
Lifecycle hook. Called when exiting this state. |
_child |
Fsm | BehavioralFsm |
Child FSM. Inputs are delegated here first, and unhandled inputs bubble up. |
"*" |
HandlerFn |
Catch-all. Handles any input without its own handler. |
| anything else | string | HandlerFn |
Named input handler. String shorthand always transitions, and a function returns a state name or void. |
Handler signatures
Section titled “Handler signatures”Every handler (a named input handler, _onEnter, _onExit, or the catch-all) receives the same args object as its first parameter:
| Parameter | Description |
|---|---|
ctx |
The context object from the config. Mutate it freely. |
inputName |
The name of the input currently being handled. Useful in catch-all handlers. |
defer |
Queues the current input for replay after the next transition. See Deferred Input. |
emit |
Emits a custom event through the FSM’s event emitter. |
...args |
Additional arguments passed through handle("inputName", arg1, arg2). |
Handlers return a state name to transition or void/undefined to stay in the current state. A returned state name and string shorthand express the same transition in two forms.
Public API
Section titled “Public API”| Method | Description |
|---|---|
handle(inputName, ...args) |
Dispatch an input to the current state’s handler. |
canHandle(inputName) |
true if the current state has a handler (or "*") for this input, or if the current state’s _child chain can handle it. |
transition(toState) |
Directly transition; fires _onExit, _onEnter, and lifecycle events. Same-state transitions are silently ignored. |
reset() |
Transition back to initialState. Fires _onEnter as if entering fresh. |
currentState() |
Returns the current state name. |
compositeState() |
Dot-delimited path including active child FSM states (e.g. "active.uploading.retrying"). Returns just the current state when no child is active. |
on(eventName, callback) |
Subscribe to a lifecycle event. Returns { off() }. |
emit(eventName, data?) |
Emit a custom event through the FSM to all on() subscribers. |
dispose(options?) |
Permanently shut down. All subsequent calls are silent no-ops. Cascades to child FSMs unless { preserveChildren: true } is passed. |
handle
Section titled “handle”Extra args are untyped by default. To type them per input, and have handle() reject a missing or malformed payload at compile time, declare an input payload map.
canHandle
Section titled “canHandle”This method is used internally by machina when dealing with hierarchical FSMs. It’s useful for conditionally triggering inputs without swallowing nohandler events (since the parent will ask the child FSM “can you handle this input?”). When the current state mounts a child FSM, canHandle() answers for the whole _child chain, the same depth that handle()’s delegation covers:
Returns false after dispose().
transition
Section titled “transition”For transitioning from outside a handler (a timer, an event listener, anything external to the FSM):
Prefer returning a state name from inside handlers over calling transition() there. The transition logic then stays declarative and co-located with the state.
on and emit
Section titled “on and emit”Subscribe to built-in lifecycle events or custom ones:
Built-in events emitted by lifecycle:
| Event | Payload | Fired when |
|---|---|---|
transitioning |
{ fromState, toState } |
A transition is about to occur. |
transitioned |
{ fromState, toState } |
A transition completed. |
handling |
{ inputName } |
An input is about to be dispatched. |
handled |
{ inputName } |
An input was successfully handled. |
nohandler |
{ inputName, args } |
No handler found in the current state. |
invalidstate |
{ stateName } |
Transition targeted a nonexistent state. |
deferred |
{ inputName } |
An input was deferred. |
See Events for the full event reference.
dispose
Section titled “dispose”After dispose(), every method call is a silent no-op. on() returns a no-op Subscription. This is intentional: you don’t need to null-guard callsites just because something may have been torn down.
When the config declares an onDispose hook, dispose() calls it once with { ctx, emit } before it clears listeners and disposes children. Put the release of any resource that lasts as long as the FSM there. A resource that lasts as long as one state belongs in that state’s _onExit, which dispose() does not run.
A throwing hook does not stop teardown. dispose() finishes its cleanup and then throws one AggregateError whose errors array lists every failure, including failures from child FSMs. The Disposal section of the concepts guide has the full contract.
TypeScript
Section titled “TypeScript”You don’t pass manual type parameters to createFsm. TypeScript infers the context type from config.context, the state name union from the keys of config.states, and the input name union from the handler keys across all states.
String shorthand targets are validated against actual state keys, so timeout: "yellw" is a compile-time type error.
When you need the inferred types explicitly, the StateNamesOf and InputNamesOf utility types extract them from your states config. A function that takes input names is the typical case:
You won’t need these often, because the FSM’s own handle() and transition() methods are already narrowed to the correct literal unions.
Full example
Section titled “Full example”A traffic light with tick-based timing, event subscription, and lifecycle hooks: