smithers-orchestrator/xstate runs XState v5 state
machines as an optional derived-state layer over the rows Smithers already
persists: not a backend, not a scheduler, and not a persisted actor.
Each frame, useSmithersMachine recomputes the machine’s state as a pure fold
over durable output and signal rows using XState’s pure
initialTransition / transition functions. Nothing about the machine is
persisted; resume, fork, and rewind are correct because the fold’s inputs are
exactly the rows those features restore.
Use it when a workflow’s control flow outgrows plain conditionals (revision
loops, approval ladders, multi-phase pipelines with re-entry) and you want
state.matches(...) instead of hand-rolled row checks. For linear pipelines,
ctx.outputs conditionals remain simpler.
bunx smithers-orchestrator signal RUN_ID REVISE --data '{...}'.
There is no send(), no onDone prop, and no actor.
The fold
Each frame,useSmithersMachine(machine, { id, input, events }):
- Collects events by evaluating each declared source against
ctx. - Orders them by (provenance seq, declaration index, mapped-event subindex): causal arrival order with deterministic tiebreaks, a total order that is a pure function of rows on every branch.
- Folds:
initialTransition(machine, input), thentransition(machine, snapshot, event)per event. Returned actions are discarded; builtinassignapplies insidetransition. Events not accepted in the current state are discarded, matching live-actor semantics. Events after the machine reaches a final state are discarded. - Returns the final
MachineSnapshot;state.matches(),state.can(),state.context,state.hasTag()work as@xstate/reactusers expect.
useSmithersMachine
call a distinct id.
Event sources
All sources are pure functions of
ctx; deriving a source’s nodeId from
the fold’s own output is circular and unsupported.
Constraints (enforced at mount)
A mount-time lint runs per machine identity (hot reload re-lints) and rejects machine features that cannot exist inside a durable fold, each with a typed error naming the Smithers-native alternative. There is no escape hatch.
Every callback in the fold (event mappers, context initializers, guards,
assigners, machine
output functions) must be pure, side-effect-free, free
of time/randomness, and must not mutate inputs. A throwing mapper, guard, or
assigner (or a throwing context initializer) fails the render with a typed
SmithersError naming the machine and event/phase, rather than an anonymous
crash deep inside the fold.
Listeners, re-entry, and final states
- The signal table is the event source; listeners are wake-and-park
plumbing. A parked
<WaitForEvent>matters for liveness: an idle machine state with nothing rendered makes the graph quiescent and ends the run. States awaiting external events should render a listener to keep the run parked and wake a frame on delivery. A missed wake delays the fold by one frame but never loses the event. - Machine re-entry never re-executes a completed Smithers task. Any task
rendered inside a machine state the machine can re-enter needs
context-versioned identity (
draft-r${rev}minted from anassigncounter). A bare-constant task id in a state on a machine cycle deadlocks the machine: no new row, no new event, and the quiescent graph ends the run. The lint catches this for any state that declaresmeta.smithersTaskId: a re-enterable state (reachable back to itself through the machine’s transition graph, whether a direct self-transition or a longer cycle) may only declare it as a function of context, never a bare string. - Final states are derived-only. Smithers can finish while the machine is
non-final (graph quiescent); a machine final state does not cancel
in-flight tasks (stop rendering them instead);
snapshot.outputis not the run output; a failure-flavored final state does not fail the run.
Durability and time travel
The fold’s inputs are exactly the rows Smithers restores, so crash/resume recomputes an identical machine state; rewinding to an earlier frame yields the machine state those earlier rows imply; forked runs fold their copied row history and diverge cleanly with their own signals. One carve-out: an in-place payload replacement at the same(nodeId, iteration) (manual retry-task of a completed node, HumanTask
reopen) keeps its seq but changes its payload, so folded history from that
position reinterprets. The in-process cache validates by content hash and
refolds automatically; workflows feeding machines from replaceable tasks
should prefer fork-and-migrate.
The prefix cache key is (runId, machine id, reducer hash), where the
reducer hash is a content hash
of the machine (config + setup() implementations) plus every declared
event source (kind, target, options, map/schema source), so one process
hosting many runs (or a fork, which inherits its parent’s row history and
machine id verbatim) never serves one run’s snapshot to another, and a
mid-run machine or event-source edit can’t reuse a fold computed under the
old reducer. The cache is bounded (LRU-evicted) so a long-running process
does not retain every historical snapshot. Mid-run machine edits ride the
acceptWorkflowChange gate: the reducer hash changing for an already-folded
(runId, id) logs a machine-history reinterpretation warning instead of
silently reinterpreting.
Performance
Recomputing per frame is cheap in practice: an in-process prefix cache validates folded history by content hash and folds only the new suffix each frame. The CI benchmark (packages/xstate/tests/fold-benchmark.test.js)
folds 10,000 events on a parallel + nested-compound machine in well under a
second full-refold (~20µs/event), and proves an incremental frame folds only
the new suffix by counting actual transitions executed (immune to machine
load), not by timing it; a wall-clock threshold is logged but kept
deliberately generous and non-strict, since it isn’t the real gate. Practical
limit: tens of thousands of events per machine per run; beyond that, prefer
summarizing history into machine context via coarser events.
Visualization
Machines are plain XState, so Stately Studio import andxstate/graph static
visualization work unmodified on the machine definition.