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..transition 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) #

stable0.11.25
{
  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.