FSM
Auditable in-process finite-state machine. Declare states + transitions at construction time; guards + on-enter / on-exit side-effects fire on every transition; every transition lands in the audit chain.
b.fsm is the lighter sibling of b.agent.saga. Saga handles distributed multi-step transactions with compensation across process / network boundaries (composes outbox + idempotency + persisted state). b.fsm handles in-process state lifecycles — order placed → paid → shipped → delivered, subscription trialing → active → past-due → canceled, refund requested → approved → processed → settled. They're complementary; reach for fsm when the lifecycle lives inside one process and saga when it spans multiple steps that each need their own compensation.
var orderFsm = b.fsm.define({
name: "order",
initial: "placed",
states: {
placed: {},
paid: { onEnter: function (ctx) { ctx.paidAt = Date.now(); } },
shipped: {},
delivered: {},
canceled: {},
},
transitions: [
{ from: "placed", to: "paid", on: "pay" },
{ from: "paid", to: "shipped", on: "ship",
guard: function (ctx) { return ctx.address != null; } },
{ from: "shipped", to: "delivered", on: "deliver" },
{ from: "placed", to: "canceled", on: "cancel" },
{ from: "paid", to: "canceled", on: "cancel" },
],
});
var order = orderFsm.create({ initialContext: { address: "..." } });
await order.transition("pay");
await order.transition("ship");
order.state; // → "shipped"
## Scope
v1 ships the flat statechart variant — every state lives at the same level. Hierarchical (nested) states, parallel regions, and history pseudo-states are deferred-with-condition: re-open when an operator surfaces a lifecycle that the flat-variant workaround (compose multiple FSMs) can't express. References: Harel statecharts (1987); UML State Machine (OMG UML 2.5.1 §14); ISO/IEC 19505 (UML).
## Transition discipline
* Guards are pure predicates — no side effects. A guard that returns false refuses the transition with fsm/guard-refused. * onExit on the current state runs before onEnter on the next state. Both may be sync or return a Promise; the primitive awaits the promise before returning from .transition(). * Concurrent .transition() calls serialize through an in-instance lock — transition() returns a Promise that other concurrent calls await before they start. * Every transition emits fsm. via audit.safeEmit (drop-silent — operator audit-sink failures don't crash the caller). The state commits before the destination's onEnter runs, so a throwing onEnter still records the transition (with outcome failure + the error) rather than silently losing the audit entry. * instance.target(event) resolves a transition's destination state side-effect-free — same edge + guard check as can() but returns the to-state (or null when the edge is illegal / guard-refused). Use it to compose an external compare-and-swap (the cross-instance claim on autocommit-only substrates) without calling transition() before the claim is known to land. * transition(event, { audit: false }) suppresses the built-in emit so that composition can emit its own enriched record once the external claim resolves.
## Serialization
.toJSON() returns { state, history, context }. The factory returned from define() exposes .restore(snapshot) which rebuilds an Instance with the captured state + history + context. The Machine definition is NOT in the snapshot — the operator pairs the snapshot with the same definition they used to create it. This avoids snapshot-rollover-on-definition-edit ambiguity.
b.fsm.define(definition) #
{
name: string, // required (identifier-shape)
initial: string, // required; must be a key of states
states: object, // { : { onEnter?, onExit? }, ... }
transitions: Array<{ from, to, on, guard? }>,
}
Compile a machine definition. Returns a frozen factory exposing create({ initialContext? }) to instantiate new machines and restore(snapshot) to rebuild from a .toJSON() output.
Throws FsmError on any malformed definition: missing name / initial / states / transitions, state name or transition name that isn't identifier-shape, transition referencing an unknown from / to state, duplicate (from, on) pair, or initial state not declared in states.
var fsm = b.fsm.define({
name: "door", initial: "closed",
states: { closed: {}, open: {} },
transitions: [
{ from: "closed", to: "open", on: "open" },
{ from: "open", to: "closed", on: "close" },
],
});
var door = fsm.create();
await door.transition("open");
Last updated 2026-08-08T16:39:49.652Z by seeder.