Skip to content

Machines glossary

re-frame2's optional state-machine capability — modelling a feature's lifecycle as an explicit statechart rather than a scatter of boolean flags. Every term below lives within a machine, and each entry links to the page that teaches it in depth.

Grouped by role: the core loop, state structure, transitions and timing, actors and composition, tags, and the runtime model.

The core loop

machine

A statechart-capable state machine, registered as an event handler with reg-machine. It models a feature's lifecycle as explicit states and transitions — driven by dispatched events, with guards, actions, timeouts, and child machines (see spawn) — instead of a scatter of boolean flags in app-db.

Its live value is a snapshot (the current state plus its :data), held in runtime-db and read like any other derived state.

(rf/reg-machine :auth.login/flow login-flow)   ;; login-flow is a defmachine value

Related: Machines; the reg-machine API.

transition table

The data a machine is: a map with :initial, the starting :data, the machine-local :guards / :actions maps, an optional :schemas, and the :states tree. The event keys under each state's :on map are what move it. reg-machine compiles this literal map into an ordinary event handler at registration, so the table is the whole specification — pretty-print it, diff it in review, render it as a diagram, or hand it to an AI in one piece.

{:initial :idle
 :data    {:attempts 0 :error nil}
 :guards  {:under-retry-limit (fn [{d :data}] (< (:attempts d) 3))}
 :actions {:clear-error (fn [_] {:data {:error nil}})}
 :states  {:idle       {:on {:submit {:target :submitting :action :clear-error}}}
           :submitting {...}}}

See The same flow as a transition table.

snapshot

A machine's live value at any moment — which state it's in, plus its :data. It lives in runtime-db, and you read it through a subscription addressed by the machine's id.

@(rf/subscribe [:rf/machine :auth.login/flow])   ;; {:state :authed :data {...}}

The snapshot is a plain, printable value (no functions or atoms), so undo, time-travel, persistence, and SSR hydration all work on machines for free. Its :state takes one of three forms: a single keyword (flat machine), a vector path ([:authenticated :cart :browsing], a compound machine), or a region → state map (a parallel machine).

Related: Machines; the [:rf/machine machine-id] sub.

:data

A machine's private working memory — the extended state that rides alongside the named state in every snapshot. Counters, the in-flight error string, captured credentials: anything that isn't itself a named mode. Guards and actions read it from their context map ({:data …}); an action updates it by returning {:data …} (see action effect map). It must be a printable value so the snapshot round-trips through persistence and time-travel, and a machine sees only its own :data — never app-db.

Taught in The same flow as a transition table.

state

One of a machine's fixed, named, mutually-exclusive modes — :idle, :submitting, :authed. A machine is always in exactly one (or, with parallel regions, one per region). A state with no nested :states is a leaf (or atomic) state; one that nests its own :states is a compound state.

transition

The move from one state to another in response to a dispatched event — optionally gated by a guard and running an action on the way. In the transition table it's an entry under a state's :on map. Transitions can also be eventless (:always, taken on entry when its guard passes) or timed (:after, taken after a delay that auto-cancels when the state exits).

guard

A pure yes/no predicate that decides whether a transition fires. Referenced by name from the table and defined once in the machine's :guards, so a visualiser can read the condition right off the arrow. It receives the context map {:data :event :state :meta} (note: no app-db) and returns a boolean. There's no {:and …} combinator DSL — compound logic goes in one named function, so the name is what tools and AI read off the arrow.

action

The side work a transition performs: it returns the same {:data … :fx …} shape an event handler returns — updating the machine's own :data or firing effects — and never writes app-db directly. Defined once in the machine's :actions, named from the arrow. The three action slots a transition can run are the source state's :exit, the transition's own :action, and the target's :entry.

action effect map

What an action returns: the same {:data … :fx …} map a reg-event handler returns. :data is merged into the snapshot key-by-key (last write wins; a key returned as explicit nil sets it to nil rather than removing it); :fx is a vector of [fx-id args] pairs the runtime performs for you. Returning nil does nothing. Because the action describes effects rather than performing them, it stays pure, testable, and replayable.

:record-error
(fn [{data :data [_ {:keys [error]}] :event}]        ;; failure map rides under :error
  {:data (-> data (update :attempts inc)
                  (assoc :error (:message error)))})

See The action effect map.

State structure

compound state

A state that nests its own :states map plus a required :initial substate — a parent mode with child modes (:authenticated over :browsing / :checkout). Entering it cascades down the :initial chain to a leaf, and the snapshot's :state becomes a vector path like [:authenticated :cart :browsing]. The runtime resolves an event by walking from the active leaf up to the root — deepest-wins with parent fallthrough — so a parent can factor out a transition (:logout) that every descendant inherits, while a child can opt out or override.

See Hierarchical states.

parallel state

A machine declared :type :parallel at its root, holding a :regions map (not :states) — the regions are all active at once rather than one-at-a-time. Three orthogonal axes of three states each is three regions, not twenty-seven cross-product states. The snapshot's :state becomes a map of region-name → that region's state ({:data :loading :form :neutral :mode :active}); all regions share one :data and their tags union across regions. When the axes don't share data, prefer N separate machines instead.

See the nine_states example and Parallel states.

region

One orthogonal axis of a parallel state — its own independent sub-state-tree, running concurrently with its sibling regions and sharing the machine's single :data. A dispatched event broadcasts to every region, and each resolves it independently; a region's slice of the snapshot is keyed by its region-name. Cross-region coordination reads another region's tags — never reaching into its state directly.

final state

A leaf marked :final? true: entering it terminates the machine — the runtime auto-destroys it, even a top-level singleton. It's for "this run is over," not "this is the last screen" — for a state the machine rests in indefinitely (like :authed), use an ordinary leaf and omit :final?. A final leaf can name an :output-key to report a result, and can be flagged :error? true as a designated failure terminal. A final leaf nested inside a compound is different: it ends the sub-flow, not the machine.

See Actors → When a child finishes and Hierarchical states → nested final states.

history state

A :type :history pseudo-state you target to re-enter a compound state at whichever substate was active when you last left it — a paused player resuming mid-track. Shallow restores the immediate child; deep restores the full nested path; a :default-target covers the never-visited-yet case. The recording rides the snapshot, so it survives undo and SSR hydration for free.

See History states.

Transitions and timing

self-transition

A transition back into the state you're already in. re-frame2 follows internal-by-default, with three distinct shapes:

  • Targetless (omit :target) — a true internal no-op: the :action runs, but :exit / :entry don't, :after timers aren't restarted, :spawn children aren't torn down. The way to mutate :data without disturbing anything.
  • Explicit self-target, no :reenter? — your own :exit / :entry still don't fire, but a compound re-resolves its descendants to :initial.
  • External (:reenter? true) — genuinely exits and re-enters: :exit:action:entry, and on a compound the whole subtree restarts (:after timers reset, :spawn children respawn).

See Self-transitions.

wildcard transition

An :on entry that matches a class of events instead of one id. :on resolves three tiers, most-specific first: the exact id (:mouse/down), then the namespace wildcard :ns/* (:mouse/* catches every mouse-namespaced event and only those), then the total wildcard :* (anything). A guard-blocked exact match falls through to the coarser tiers.

:tracking {:on {:mouse/down {:action :begin-drag}    ;; exact wins for :mouse/down
                :mouse/*    {:action :note-move}      ;; any other :mouse/… event
                :*          {:action :log-unknown}}}  ;; anything else

See Wildcard transitions.

forbidden transition

A present :on key whose value is the empty map ({:on {:E {}}}) or nil — a handler that consumes an event and stops the deepest-wins search without changing state. It's how a child compound state opts out of a transition its parent would otherwise inherit to it. Distinct from an unhandled event (which falls through to coarser tiers and up to ancestors): a forbidden block is itself a match, so the search stops there.

Covered with wildcards.

eventless transition (:always)

A state-node key holding a vector of guarded transitions that fire with no event — checked on entry (and after any transition landing in the state), first-passing-guard wins. The way to say "the snapshot just changed; if X is now true, move immediately." An :always may not target its own state (rejected at registration); the fixed-point pattern is a targetless guarded :always whose :action flips the guard.

:checking-form {:always [{:guard :form-valid?   :target :submitting}
                         {:guard :form-invalid? :target :show-errors}]}

Settled inside the microstep loop. See Automatic transitions.

delayed transition (:after)

The declarative timer: a state-node key mapping a delay to a transition. Enter the state and the timer arms; leave it and the timer cancels — no setTimeout-plus-cancel-flag to wire. A late-firing timer from an earlier visit is ignored (each visit is epoch-tagged), so you never get a ghost transition. The delay is a positive-integer millisecond count, a subscription vector, or a (fn [{:keys [snapshot]}] ms).

:reconnecting {:after {5000 {:target :connecting}}   ;; retry in 5s
               :on    {:net/give-up :failed}}

(ISO-8601 durations and the "5s"-shorthand rejection belong to :timeout, not :after — an :after key is an ms int, a sub vector, or a fn.) See Automatic transitions.

timeout

The named-intent spelling of "this state — or its spawned child — must finish before this deadline": a :timeout duration paired with an :on-timeout transition. It lowers onto the same :after timer machinery; it just reads as a deadline rather than a general delay. A duration is integer-ms (5000) or an ISO-8601 string ("PT5S", "PT1H30M") — "5s" is rejected, failing loud at registration.

See Automatic transitions.

choice state

A :type :choice transient routing node: enter it and it resolves immediately — same step, no event — to the first candidate whose :guard passes. It's :always's decision pattern with a name, so a diagram renders an explicit fork. The candidate list is a declarative array that must include an unguarded default and must not declare ordinary state keys.

See Automatic transitions.

run-to-completion

The guarantee that a machine processes one event to a settled, stable configuration before the next event is seen — every :always microstep and every :raised event drains, then the snapshot commits once. External observers (subs, other machines, tools) only ever see the settled state, never a mid-cascade intermediate. A non-converging :always / :raise loop can't hang the runtime: it's bounded (default depth 16) and aborts atomically with the snapshot unchanged.

See microstep and macrostep.

Actors and composition

spawn

A declarative key that starts a child machine on entering a state and tears it down on leaving; the child reports a result back through :on-done. (:spawn-all starts several in parallel and joins on completion.) Under the hood it's the reserved [:rf.machine/spawn …] effect; emit that directly from any handler when you need a dynamic count rather than one-child-per-state. The running instance it creates is an actor.

See Actors.

actor

A live machine instance — a snapshot sitting at [:rf.runtime/machines :snapshots <id>] in runtime-db. Two kinds: a long-lived singleton (one per reg-machine id) and a dynamically spawned child (a per-request protocol machine, a wizard's per-step subprocess). An actor's liveness IS its snapshot's presence — there's no parallel registry; destroying it removes the snapshot. Address a spawned actor by its gensym'd id, or by role via a system-id.

The live instance is just a value in the frame, so time-travel and SSR extend to it for free.

spawn-all

The fan-out sibling of :spawn: on entering a state it starts N children in parallel and joins on their completion, resolving when they've all finished (or on the first failure, with a cooperative cancellation cascade). Used for "spawn a worker per chunk and continue when all return."

See the long_running_work example and Actors → Fan-out and join.

:on-done and :output-key

How a finishing child reports back. A final state names an :output-key — the slot of its :data to surface as its result. The parent's :spawn declares :on-done, a (fn [{:keys [data result]}] new-data) that fires the instant the child enters its final state, receiving that value as result; the runtime then tears the child down. Completion is event-shaped — it happens and flows, rather than sitting in a readable output slot.

:done {:final? true :output-key :token}             ;; child reports :data's :token
;; parent:
:authenticating {:spawn {:machine-id :auth-flow
                         :on-done (fn [{:keys [data result]}]
                                    (assoc data :token result))}}

See Actors → When a child finishes.

system-id

A stable role name (:logger, :websocket, :retry-coordinator) bound to a spawned actor, so a parent can address its child by role instead of by gensym'd id. The action-side surface is the reserved [:rf.machine/dispatch-to-system [system-id event]] effect — a machine action can't read app-db, so the fx tuple is how it messages a named child.

{:fx [[:rf.machine/dispatch-to-system [:logger [:logger/flush]]]]}

The fx is a parked named-addressing escape — zero in-repo consumers as of 2026-07-10, retained for XState v6 actor-system parity (systemId addressing); the everyday send is plain dispatch to the id you hold (a machine is an event handler). See the machine-by-system-id lookup helper.

:raise

A reserved, machine-only fx-id. Inside an action's :fx, [:raise [:some-event …]] loops an event back into this same machineatomically, pre-commit, drained FIFO through a local queue inside the one handler invocation (it never touches the router queue). Use it when one transition's outcome should immediately drive another, all inside one macrostep. Contrast :fx [[:dispatch [self-id …]]], which is a separate, post-commit event (its own epoch).

{:actions {:kick (fn [_] {:fx [[:raise [:tick]]]})}}

See [:raise event-vec] and When the machine grows.

:internal-events

A top-level set of event ids that are machine plumbing — events the machine :raises at itself, not for a view or test to dispatch. An internal :raise of one is handled normally; an external dispatch is refused at the machine's boundary (no transition; a :rf.error/machine-internal-event-external-dispatch trace fires).

{:internal-events #{:check-complete}
 :states {...}}

See When the machine grows.

Tags

state tag

A label like :auth/busy attached to several machine states; the active state's tags ride on the snapshot, so a view can ask "is it busy?" ([:rf.machine/has-tag? …]) instead of enumerating exact state names — ask, don't tell. Add a sixth busy state and no view changes. Across parallel regions, every active state's tags union onto the one snapshot.

(when @(rf/subscribe [:rf.machine/has-tag? :auth.login/flow :auth/busy])
  [spinner])

See Tags.

The runtime model

These terms describe how a transition actually runs. You can build machines without them, but they're the vocabulary for reasoning about ordering and atomicity — and what the trace stream shows you.

drain

The deterministic ordering rules by which a machine's effects compose at four nested levels: within one action's :fx (vector order; :data lands before :fx); across the :exit:action:entry slots of one transition; within one machine event (the microstep loop over :always and :raise); and across the runtime's per-frame queue (FIFO, except machine-originated continuation events leap-frog to the front so a machine settles its macrostep before the next external event). The order in the source is the order at runtime — no reordering for "optimisation."

microstep

One iteration of the settle loop inside a single machine event: after the resolving transition, the runtime prefers an enabled :always transition; only when none is enabled does it dequeue one :raised event (FIFO). It loops until no :always is enabled and the raise-queue is empty — the fixed point. Microsteps are not separately observable; they're the internal steps that compose one macrostep. Bounded at depth 16 (:always-depth-limit / :raise-depth-limit).

macrostep

The whole machine event — the resolving transition plus every :always microstep and every :raised sub-event — seen as one logical step from outside: one commit, one trace row, one epoch. External observers only ever see the post-commit settled snapshot, never an intermediate. This is what makes a machine run-to-completion.

commit

The single, deferred runtime-db write that lands a macrostep's settled snapshot at [:rf.runtime/machines :snapshots <id>]once per transition, no matter how many actions or microsteps fired. It's the boundary at which the :schemas :data check runs (a violation rolls the whole transition back) and the point a reading subscription re-fires. (See the general framework commit.)

LCCA (least common compound ancestor)

Also written LCA. For a transition from source path A to target path B inside a compound state, the LCCA is the deepest state that stays active across the move — it neither exits nor enters. The exit cascade runs :exit from A's leaf up to (not including) the LCCA (deepest-first); the entry cascade runs :entry from just below the LCCA down to B's leaf (shallowest-first); the transition :action fires once at the boundary. For the common case it's just the longest common prefix of A and B — except when B is on A's active path or a descendant the declaring compound names, where it pulls up to a proper ancestor so the targeted subtree actually re-resolves. For a flat machine the LCCA is the root, and the rule collapses to plain :exit:action:entry.