machine
@statorjs/stator/machine is the engine itself — browser-safe (no server imports), running identically on the server and in a client island.
defineMachine
Section titled “defineMachine”function defineMachine(config: DefineMachineConfig): MachineDef
// config fields{ name: string lifecycle: 'app' | 'session' context: C // initial context initial: S // initial state name states: Record<S, { on?: OnMap; entry?: EntryEffect; after?: AfterEntry[] }> on?: OnMap // machine-level transitions: apply in ANY state, consulted only // when the current state doesn't declare the event (a state- // scoped handler always wins, even if its guard drops) events?: E // typed event surface — pass `{} as MyEvents` selectors?: Record<string, (ctx: C, helpers?) => unknown> // helpers.reads for cross-machine views reads?: MachineDef[] // machines this one reads (typed helpers.reads) subscribes?: SubscribeEntry[] // cross-machine subscriptions emits?: string[] | Record<string, { payload?: (ctx, ev) => object }> persist?: boolean // app machines only: survive restarts (while the machine's // code is unchanged) via the AppStore}Defines a machine: flat states, typed events, and inline transitions. events is a phantom carrier — the engine reads only its type, and each transition’s action/guard then sees the event narrowed to exactly its on key. A machine that declares reads gets a typed helpers.reads map (keyed by machine name, selectors preserved) in its actions, guards, and selectors — reads-aware selectors project cross-machine verdicts as display state, and bindings on the reading machine re-diff when a read machine changes. Declaring reads server-pins the machine, since cross-machine reads can’t resolve in the browser. persist: true on a session machine is an error; sessions always persist through the session Store.
Snapshots are bound to the machine’s code. Every persisted snapshot is stamped with a hash of the machine’s code (the file plus every module it reaches, tree-shaken), and a snapshot whose hash no longer matches the running machine is discarded at hydration — the machine starts fresh. Machine state is working state, not persistence; see What survives a deploy for the rule, what moves the hash, and the reload-on-entry idiom for durable facts.
Each entry in an on map is a transition (or an ordered array of guarded candidates — first passing when wins):
{ to?: S // target state; omit for a self-transition when?: (ctx, ev, helpers) => boolean do?: (ctx, ev, helpers) => void // mutates a draft; the engine owns clone + commit emit?: string | string[] // declared emits fired after the action commits effect?: (ctx, ev, meta) => Promise<Events | null>}A bare function is sugar for { do: fn }.
createActor
Section titled “createActor”function createActor(def: MachineDef, opts?: CreateActorOptions): Actor
interface CreateActorOptions { snapshot?: Snapshot // restore from persisted state or a client seed resolveHelpers?: () => ActionHelpers // host-provided `reads` resolver onEffect?: (invocation: EffectInvocation) => void // host effect scheduler}
interface Actor<C, E> { start(): Actor<C, E> stop(): void send(event: E): void seed(partial: Partial<C>): void // merge into context before start(); no-op after getSnapshot(): Snapshot<C> getPersistedSnapshot(): Snapshot<C> subscribe(listener: (snapshot: Snapshot<C>) => void): { unsubscribe(): void } on(emitName: string, listener: (event) => void): () => void}Instantiates a running machine. The two injection points are what keep the engine isomorphic: the server wires resolveHelpers to the active dispatch context and onEffect to its post-commit queue; the client omits both, so effects run locally on a microtask and a reads dereference throws with a clear error. You call this directly in unit tests; the framework calls it everywhere else.
Effects
Section titled “Effects”type Effect = (ctx: C, ev: E, meta: EffectMeta) => Promise<Events | null>
interface EffectMeta { effectId: string signal?: AbortSignal // aborts on state exit — entry effects only}
interface EffectInvocation { machineName: string effectId: string kind: 'entry' | 'transition' stateKey?: string // owning state — set for entry effects run: (signal?: AbortSignal) => Promise<EventObject | null>}An effect is async I/O declared on a transition and run by the host after the transition commits — the engine itself never performs I/O. It receives structuredClone snapshots of context and event taken at commit time (never live state), plus meta.effectId, unique per invocation — thread it to external calls as an idempotency key and use it for log correlation. Return the completion event to dispatch, or null for fire-and-forget.
Two rules to know:
- Annotate the return type:
effect: async (ctx, ev, meta): Promise<Events | null> => …. TypeScript defers context-sensitive arrows duringdefineMachine’s inference, so an unannotated effect fails to typecheck; the annotation restores full checking of the completion event against your event union. - Effects are infallible by construction: catch inside and return your declared failure event. A throw is the runtime backstop — logged and dropped, never a crash.
meta.session
Section titled “meta.session”interface EffectMeta { effectId: string signal?: AbortSignal session?: { id: string; claims<T = unknown>(): T | undefined }}For session machines the server host sets meta.session on every effect — the session id and its app-defined claims as of the moment the effect started (the same claims middleware reads with stator(c).claims()). It is what lets an entry effect reload a durable fact by identity on a fresh start or after a snapshot reset, with no client round trip. App machines have no session and client islands run no host, so it is undefined there.
Entry effects
Section titled “Entry effects”type EntryEffect = (ctx: C, meta: EffectMeta) => Promise<Events | null>
states: { loading: { entry: async (ctx, meta): Promise<Events | null> => { const data = await fetchForecast(ctx.location, { signal: meta.signal }) return { type: 'LOADED', data } }, on: { LOADED: { to: 'ready', do: (ctx, ev) => { ctx.data = ev.data } } }, },}A state’s entry is async I/O the host schedules when the state is entered — a fresh start at the initial state, or a value-changing transition (never on a bare snapshot restore). Same host-scheduled, off-lock pipeline as a transition effect, minus the event argument (a state entry has no triggering event).
The role split matters: entry effects are the load role and transition effects are the command role. The host may re-invoke an entry effect whose completion never settled (a process died mid-flight) and aborts meta.signal when the state is exited — so write entry effects as re-runnable reads, and keep non-idempotent external writes (charges, sends) in transition effects, which are at-most-once and never aborted. See the effects guide.
State timeouts: after
Section titled “State timeouts: after”interface AfterEntry { delay: number | ((ctx: C) => number) send: Events}
states: { shown: { after: [{ delay: 5_000, send: { type: 'DISMISS' } }] },}Each after entry dispatches send after delay ms in the state — armed on entry, cancelled on exit. Host-scheduled and in-memory: a restart drops armed timers (a host restoring a snapshot re-arms with elapsed credit via enteredAt). Durable schedules are deferred work.
describeMachine
Section titled “describeMachine”function describeMachine(def: AnyMachineDef): MachineDescriptionSerializes a machine def to plain JSON-able data — the def is the statechart, and this walks it: states, per-event transition candidates (to, guard/action/effect presence, emit names), entry effects and after timers (delay reported as its number, or 'dynamic' for a context-dependent one), machine-level fallback handlers, the handled-event list, serverOnly, emit and selector names, reads/subscribes (as machine names), and the initial context. Closures stay opaque — a guard or effect reports presence, never its body. Pure and side-effect free; it never invokes app code.
This is the introspection substrate: the dev inspector’s Machines tab is built on it, and it’s the walk to reach for when building your own tooling over a machine’s shape. MachineDescription, StateDescription, and TransitionDescription are exported alongside it.
Lower-level exports
Section titled “Lower-level exports”Type-level plumbing, exported for tooling and advanced typing:
EventObject— the base event shape ({ type: string }).EventOf<Def>— a machine’s event union; whatdispatch(Machine, event)checks against.InstanceOf<Def>— a machine’s instance shape (each selector as a typed property).ReadsMap<Reads>— the typedhelpers.readsmap built from areadstuple.Snapshot<C>—{ value: string[]; context: C; enteredAt?: number; pendingEntry?: { effectId: string } }; serializes to the Store and seeds a client island on upgrade.enteredAtlets a restoring host re-armaftertimers with elapsed credit;pendingEntrymarks an entry effect whose completion never settled, so the host re-invokes it.Action,Guard,ActionHelpers— the function shapes transitions are built from.EntryEffect,AfterEntry— the state-entry effect and timeout-entry shapes.Transition,TransitionConfig,StateNode— the transition-graph node types.EmitDeclaration,EmitsConfig— normalized emit declarations.SubscribeEntry— a cross-machine subscription entry.Capabilities—{ serverPinned, reasons }; why a machine can’t run client-side.Lifecycle—'app' | 'session'.MachineDef,AnyMachineDef,AnyActor— the def/actor types and their heterogeneous-collection “top types”.isStatorMachine— brand guard for a machine def.