re-frame.machines¶
Use a state machine when a feature moves through named states (idle, submitting, locked out) and the events it accepts depend on the state it is in. You write the transition table as data and register it with rf/reg-machine. The machine is then an ordinary event handler: dispatching [machine-id event] runs the table, which picks a transition, updates the machine's snapshot and returns effects through the normal event pipeline. Tracing, time-travel and overrides work for machines as they do for any other handler.
Machines ship in the optional day8/re-frame2-machines artefact. Add it alongside core at the same version. Require re-frame.machines once, from a boot or feature namespace, to load it; without it, rf/reg-machine throws :rf.error/machines-artefact-missing.
(:require [re-frame.core :as rf] ;; rf/reg-machine, rf/defmachine
[re-frame.machines :as rf.machines]) ;; everything else on this page
(rf/reg-machine :auth.login/flow
{:initial :idle
:states {:idle {:on {:auth.login/submit :submitting}}
:submitting {:on {:auth.login/success :authed
:auth.login/failure :idle}}
:authed {}}})
(rf/reg-view login-button []
;; The snapshot is nil until the machine handles its first event.
(let [{:keys [state]} @(subscribe [:rf/machine :auth.login/flow])]
(case state
:submitting [:p "Signing in…"]
:authed [:p "Signed in"]
[:button {:on-click #(dispatch [:auth.login/flow [:auth.login/submit]])}
"Sign in"])))
A :status keyword in app-db, checked by ordinary event handlers, is enough while there are two or three states and each check is one line. A machine pays off when those checks spread: several events are legal only in some states, a state needs a timeout or must cancel work when it is left, or you want to test (state, event) → next state + effects as a pure function with machine-transition. For fetching and caching server data, use resources instead. When not to use a machine has the fuller table.
A registered machine runs as one instance per frame, addressed by its machine id. To run several copies of one machine at once (one per upload, one per open socket), or a child machine that lives only while its parent is in one state, spawn actors: each actor has its own id and snapshot, and you dispatch to its id the same way. See :rf.machine/spawn.
reg-machine and defmachine are called on the re-frame.core facade. Everything else is called on this namespace as rf.machines/…, or addressed by keyword. Use the rf.machines alias and keep the bare machines alias for your application's own namespaces.
The machines guide teaches the model, starting from The table.
Registration¶
reg-machine¶
- Kind: macro
- Signature:
- Description: Registers
machine-specas the event handler formachine-id. Dispatching[machine-id event]runs the transition table. Called asrf/reg-machine.opts, the optional middle slot, is a registration-metadata map. Its:schemavalidates the dispatched outer event vector at the:where :eventboundary; any other keys are stored on the registration metadata.- Returns
machine-id. Registering it again replaces the definition, while a live snapshot is retained and reconciled on the next event. - The macro walks the literal spec at expansion time. It attaches source (
{:fn .. :source-coords .. :source-code ..}) to each:guardsand:actionsentry, and a reference-site:source-coordsto each state node and transition map under:states. Xray uses these to go from a snapshot to the guard, action or state definition. The call site's own coordinates are onhandler-meta. - Pass either a literal spec or a value defined with
defmachine. A value bound with plaindefcarries no source. - The snapshot lives in the frame's
runtime-db(notapp-db) at[:rf.runtime/machines :snapshots machine-id]. Its shape is{:state … :data …}plus framework-managed slots for:aftertimer epochs and tags. Read it with the[:rf/machine machine-id]subscription, or once withsubscribe-once.
- Errors: every refusal throws at registration, not on first dispatch; Registration errors below lists them.
- Example:
(rf/reg-machine :session {:initial :anonymous :data {:credentials nil} :actions {:capture-credentials ;; Remember who's signing in so the snapshot carries it through the flow. (fn [{[_ creds] :event}] {:data {:credentials creds}}) :issue-auth ;; Fire the login request; the reply loops back as :auth-ok / :auth-fail. (fn [{[_ creds] :event}] {:fx [[:rf.http/managed {:request {:method :post :url "/api/login" :body creds :request-content-type :json :sensitive? true} :request-id :session/login :decode :json :on-success [:session [:auth-ok]] :on-failure [:session [:auth-fail]]}]]})} :states {:anonymous {:on {:login {:target :authenticating :action :capture-credentials}}} :authenticating {:entry :issue-auth :after {500 {:target :timeout}} ;; ms — auth taking too long :on {:auth-ok {:target :authenticated} :auth-fail {:target :anonymous}}} :authenticated {:on {:logout {:target :anonymous}}} :timeout {:on {:retry {:target :anonymous}}}}}) ;; The machine is an event handler: dispatch a wrapped event at its id. (rf/dispatch [:session [:login {:user "alice" :pass "correct-horse"}]])
Registration errors¶
reg-machine and reg-machine* check the whole spec before anything is registered, most of it through validate-machine!. Each refusal is an ex-info whose ex-data carries the id under :rf.error/id, and whose message says what to write instead. The machines guide explains each one on the page that teaches the feature, in that page's Troubleshooting table.
| Error id | Thrown when |
|---|---|
:rf.error/machines-artefact-missing |
rf/reg-machine runs without re-frame.machines loaded. |
:rf.error/invalid-machine-opts |
opts is not a map. |
:rf.error/machine-reserved-meta-in-opts |
opts carries :rf/machine? or :rf/machine, which the registration sets itself. |
:rf.error/machine-bad-structure |
:states is not a map, or a state node is not a map. |
:rf.error/machine-unknown-node-key |
A state or transition map carries an unknown bare key, such as the XState spellings :invoke and :cond; a root-only key sits below the root; or a leaf declares :on-done. |
:rf.error/machine-root-slot-not-supported |
The root carries a key only a state takes (:always, :choice, :final?, :spawn-all …), or :regions without :type :parallel. |
:rf.error/machine-compound-state-missing-initial |
A state with :states has no :initial. |
:rf.error/machine-bad-on-clause |
:on is not a map, or a transition in it is not a keyword, path, map, candidate vector or nil. |
:rf.error/machine-bad-always |
:always is not a transition or a candidate vector. |
:rf.error/machine-bad-target |
A :target is not a keyword or a path vector. |
:rf.error/machine-unresolved-target |
A target names no declared state. |
:rf.error/machine-unresolved-guard |
A guard id has no entry in :guards. |
:rf.error/machine-unresolved-action |
An action id has no entry in :actions. |
:rf.error/machine-bad-guard-form |
A :guard is neither an id nor a fn. |
:rf.error/machine-bad-action-form |
An :entry, :exit or :action is not one id or one fn, a vector of them say. |
:rf.error/machine-always-self-loop |
An :always candidate targets its own declaring state. |
:rf.error/machine-always-unguarded-targetless |
An :always candidate has neither :guard nor :target, so it could never settle. |
:rf.error/machine-bad-after-spec |
:after is not a map of delay to transition. |
:rf.error/machine-bad-after-delay |
A literal :after delay is not a positive integer or an ISO-8601 duration string ("5s", 0 and -1 are refused). |
:rf.error/machine-bad-timeout-duration |
A :timeout is not a positive integer or an ISO-8601 duration string. |
:rf.error/machine-timeout-without-on-timeout |
A state or spawn spec has :timeout but no :on-timeout. |
:rf.error/machine-on-timeout-without-timeout |
A state or spawn spec has :on-timeout but no :timeout. |
:rf.error/machine-timeout-after-collision |
A :timeout resolves to the same delay as an :after key on the same state. |
:rf.error/machine-non-parallel-root-after-not-supported |
:after or :timeout sits on a flat or compound root, or on a parallel region body. |
:rf.error/machine-bad-choice |
:choice is a fn, empty, or otherwise not a candidate vector. |
:rf.error/machine-choice-missing-choice |
A state has :type :choice but no :choice. |
:rf.error/machine-choice-without-type |
A state has :choice but not :type :choice. |
:rf.error/machine-choice-no-default |
Every :choice candidate is guarded. |
:rf.error/machine-choice-self-loop |
A :choice candidate targets the choice state itself. |
:rf.error/machine-choice-extra-keys |
A choice state also declares waiting-state keys such as :on, :entry or :after. |
:rf.error/machine-final-state-has-transitions |
A :final? state declares :on, :always, :after, :spawn or :spawn-all. |
:rf.error/machine-final-state-compound |
A :final? state has child :states. |
:rf.error/machine-output-key-without-final |
A state without :final? declares :output-key. |
:rf.error/machine-error-flag-without-final |
A state without :final? declares :error?. |
:rf.error/machine-history-misplaced |
A history node has no enclosing compound state. |
:rf.error/machine-history-extra-keys |
A history node carries a key other than :type, :deep? and :default-target. |
:rf.error/machine-history-duplicate |
One compound declares two history nodes. |
:rf.error/machine-history-bad-default-target |
A history node's :default-target does not resolve. |
:rf.error/machine-parallel-bad-shape |
A parallel root also has :initial or :states, a region has no :initial, or two regions declare a spawn at the same in-region path. |
:rf.error/machine-parallel-nested-not-supported |
A region declares :type :parallel. |
:rf.error/machine-parallel-root-on-bad-target |
A parallel root's :on uses a bare keyword target instead of a region-qualified path. |
:rf.error/machine-parallel-on-done-target |
A parallel root's :on-done has a :target. |
:rf.error/machine-parallel-region-order-required |
:regions has more than eight entries and there is no :region-order. |
:rf.error/machine-parallel-region-order-mismatch |
:region-order does not name every region exactly once. |
:rf.error/machine-spawn-bad-shape |
A spawn spec does not have exactly one of :machine-id and :definition, or an inline :definition has neither :id-prefix nor :fixed-actor-id. |
:rf.error/machine-unknown-spawn-key |
A spawn spec carries an unknown bare key, or a :spawn-all child declares :on-error. |
:rf.error/machine-bad-on-done-clause |
An :on-done has the wrong form: a spawn's must be a fn or a transition, and a :spawn-all child's must be a fn. |
:rf.error/machine-bad-on-error-clause |
A spawn's :on-error is not a transition. |
:rf.error/machine-spawn-all-bad-shape |
A :spawn-all block has an unknown key, a :join other than :all or :any, no :on-all-complete for :all or :on-some-complete for :any, or :children that is not a vector of specs. |
:rf.error/machine-spawn-all-duplicate-id |
Two :spawn-all children share an :id. |
:rf.error/machine-spawn-all-with-spawn |
One state declares both :spawn and :spawn-all. |
:rf.error/spawn-timeout-ms-removed |
A :spawn or :spawn-all carries :timeout-ms. |
:rf.error/machine-bad-tags |
:tags is not a set of keywords, or a tag is in the reserved :rf/* or :rf.*/* namespaces. |
:rf.error/machine-bad-schemas |
:schemas is not a map. |
:rf.error/machine-bad-schemas-key |
:schemas carries a key other than :data, :output, :events, :tags and :meta, :input included. |
:rf.error/machine-bad-internal-events |
:internal-events is not a set of keywords, or has a wildcard member. |
:rf.error/machine-internal-event-reserved |
An :internal-events member is a reserved :rf/* id. |
:rf.error/machine-cofx-requires-inline |
:rf.cofx/requires appears anywhere but a named :guards or :actions entry map. |
:rf.error/cofx-request-invalid |
An entry in a :rf.cofx/requires vector is malformed. |
:rf.error/cofx-name-collision |
Two :rf.cofx/requires entries on one callback use the same :as name. |
:rf.error/invalid-machine-classification |
:sensitive or :large is not a vector of paths into the snapshot. |
Development-only registration warnings do not prevent registration:
| Warning | Meaning and remedy |
|---|---|
:rf.warning/machine-source-unstamped |
The definition has no captured source. Use defmachine or a literal reg-machine map for source navigation. |
:rf.warning/machine-cofx-consume-undeclared |
A named callback reads a :rf.cofx key absent from its requirements. Declare the fact it needs. |
:rf.warning/machine-cofx-ambient-durable |
A named action requests an ambient coeffect. Prefer a recorded fact for decisions or data that must replay; see coeffect grades. |
defmachine¶
- Kind: macro
- Signature:
- Description: Defines
nameas a machine-spec value and captures its per-element source, so a laterreg-machineof that value keeps it. Use it in place ofdeffor a named spec. Called asrf/defmachine.- It walks the literal spec the same way
reg-machinedoes and stores the source on the value.(rf/handler-meta {:source :store :kind :machine-guard :id [machine-id guard-id]})and Xray's machine source view then work for the registered value as they do for an inline spec. - With
(def m {…})and(reg-machine :id m),reg-machinesees only the symbol and captures nothing. In development the registration warns:rf.warning/machine-source-unstamped, once per machine id. - Production builds remove the development-only
:source-*slots.
- It walks the literal spec the same way
- Example:
Machine spec¶
A machine spec is one map: the root state node plus the machine's own blocks. Every map in it takes a closed set of bare keys. An unknown bare key throws :rf.error/machine-unknown-node-key, and the message names the valid keys; namespaced keys pass, so put your own annotations under a namespaced key or under :meta. Spawn specs are described under :rf.machine/spawn, a :spawn-all block in Fan-out and join, and the snapshot a machine produces under [:rf/machine machine-id]. The machines guide teaches the grammar, starting from The table.
Machine-root keys¶
Beside :initial and :states, a machine spec's root takes the keys below. The runtime reads them only at the root: on a nested state they throw :rf.error/machine-unknown-node-key. The root is a state node too, so it also takes the state node keys, apart from those it refuses with :rf.error/machine-root-slot-not-supported.
| Root key | What it takes and does |
|---|---|
:doc |
Optional description retained with the definition for tools. Registration metadata can also carry :doc in the middle opts map. |
:schema |
Accepted on the root, but does not install event validation there. Put the outer-event schema in the middle opts map of reg-machine; put private-data validation under :schemas :data. |
:data |
The machine's initial private data, a map. It must survive pr-str and read-string, so it holds no functions, atoms or host objects. |
:guards, :actions |
Maps from an id to a callback: a fn, or an entry map {:fn f :rf.cofx/requires […]} that declares coeffects. A parallel machine's regions use the root's maps. See Callbacks. |
:regions |
With :type :parallel, a map from region name to region body. It makes a parallel machine, whose root takes no :initial or :states. See Parallel regions. |
:schemas |
A map whose keys are among :data, :output, :events, :tags and :meta, each a schema. :data validates the snapshot's :data; see validate-machine-data!. :output is the next row. :events, :tags and :meta are accepted and not checked. Any other key, :input included, throws :rf.error/machine-bad-schemas-key, and a non-map throws :rf.error/machine-bad-schemas. |
[:schemas :output] |
A schema for the value a root-level :final? leaf reports through :output-key, which is nil when the leaf has none. It is checked once, as the machine finishes, in development builds only. A failing value emits :rf.error/schema-validation-failure with :where :machine-output, :phase :completion and :rollback? false, and nothing is rolled back: the machine has already finished, so it is still destroyed and a parent's :on-done still receives the value. A schema the validator throws on emits :rf.error/malformed-schema with :where :machine-output, and completion proceeds the same way. See Schemas in the machines guide. |
:sensitive, :large |
A vector of paths into the snapshot, such as [[:data :payment :token]]. They classify those slots of every instance: each actor's paths are registered when it is spawned, first boots or is restored, and removed when it is destroyed. Trace and tool projections redact sensitive slots and size-mark large values. The SSR hydration payload redacts sensitive :data slots but preserves large values in full, because the client needs them to resume. In-process snapshots remain unchanged. A malformed declaration throws :rf.error/invalid-machine-classification at registration. A :sensitive? prop inside [:schemas :data] does not classify the snapshot; it only redacts a failed validation's trace. See Classify subsystem data on the subsystem. |
:internal-events |
A set of keywords, such as #{:tick}, naming events the machine raises for itself. [:raise event-vec] says what an external dispatch of one does. A vector, a non-keyword member or a wildcard member such as :tick/* throws :rf.error/machine-bad-internal-events, and a reserved :rf/* id throws :rf.error/machine-internal-event-reserved. See Raise and internal events in the machines guide. |
:region-order |
A vector naming a :type :parallel machine's regions in the order their actions run. It is required when :regions has more than eight entries, which a map does not keep in written order; without it registration throws :rf.error/machine-parallel-region-order-required, and an order that does not name every region exactly once throws :rf.error/machine-parallel-region-order-mismatch. See Parallel regions in the machines guide. |
:always-depth-limit |
An integer, 16 by default. It bounds the :always transitions the machine takes while it settles after an event. Exceeding it aborts the whole macrostep with :rf.error/machine-always-depth-exceeded, and no snapshot or effects commit. |
:raise-depth-limit |
An integer, 16 by default. It bounds the raised events one macrostep handles; [:raise event-vec] has the rule. See Run to completion in the machines guide. |
State node keys¶
These are the bare keys a state takes:
| Key | Meaning | Taught in |
|---|---|---|
:on |
Transitions taken on an event | Transition forms |
:entry, :exit |
Action run on entering or leaving the state | Entry, exit, and transition actions |
:initial, :states |
Child states, and the one entered first | Hierarchical states |
:on-done |
Transition taken when a child :final? state is reached |
Nested final states |
:always |
Eventless transitions | Automatic transitions |
:after |
Delayed transitions | Delayed :after |
:timeout, :on-timeout |
A deadline and the transition it takes | :timeout and :on-timeout |
:type |
:choice or :history on a state; :parallel on the root |
Choice states, History, Parallel regions |
:choice |
A choice state's candidate vector | Choice states |
:deep?, :default-target |
A history pseudo-state's options | History |
:spawn, :spawn-all |
Child actors that live while the state is active | Actors, Fan-out and join |
:tags |
A set of labels projected onto the snapshot | Tags |
:final?, :output-key, :error? |
A finishing leaf and what it reports | Final states |
:meta |
Your own static metadata, such as {:terminal? true} |
Final states |
A history pseudo-state takes only :type, :deep? and :default-target. A choice state only routes, so it refuses the keys of a state that waits (:on, :entry, :after and the rest). Registration errors lists both refusals.
Key placement¶
The root and parallel region bodies have narrower lifecycle rules than ordinary states:
| Location | Accepted lifecycle and transition keys | Put these on a contained state instead |
|---|---|---|
| Flat or compound machine root | :entry, :exit, :tags, :on, :spawn |
:always, :choice, :after, :timeout, :on-timeout, :on-done, :spawn-all, final/history keys |
| Parallel machine root | The same keys, plus :after, :timeout / :on-timeout, and targetless :on-done |
:always, :choice, :spawn-all, final/history keys |
| Parallel region body | :entry, :exit, :tags, :on, :on-done, alongside its :initial and :states |
:always, timers, :spawn, :spawn-all, final/history keys |
Unsupported timer placement raises
:rf.error/machine-non-parallel-root-after-not-supported. Other unread root
or region slots raise :rf.error/machine-root-slot-not-supported. Wrap a
region's states in one compound when a child must live across those states.
Transitions¶
Wherever the grammar takes a transition (an :on value, an :after value, :always, :choice, :on-timeout, an :on-done, a spawn's :on-error), it takes one of these forms:
| Form | Meaning |
|---|---|
:same-state |
The declaring state itself. A leaf stays active; a compound resets its descendants to :initial. At a flat or compound root it resets the active configuration. |
:settings |
A target keyword, sugar for {:target :settings}. It names a sibling of the declaring state. |
[:authenticated :settings] |
A path target, absolute from the root. At a parallel root it is region-qualified, and a vector of such paths targets several regions. |
{:target … :guard … :action …} |
A transition map, with the keys below. |
[{…} {…}] |
A candidate vector: the first candidate whose guard passes is taken. :choice always takes this form. |
{} or nil |
As an :on value, a forbidden transition: it consumes the event, so no ancestor's transition for it runs. |
| Transition key | Meaning |
|---|---|
:target |
Where to go: a keyword or a path. Without it the transition is targetless and runs only its action, with no exit or entry. |
:guard |
A guard id from :guards, or an inline fn. The transition is taken only when it returns truthy. |
:action |
One action id from :actions, or one inline fn. |
:reenter? |
Defaults to false. true exits and re-enters the declaring state on a self-target or a target inside that state, restarting its timers and children. A targetless transition does not exit or enter. |
:meta |
Your own static metadata. |
A path target inside a parallel region is relative to that region's root;
it cannot target a sibling region. Only the parallel root can use
region-qualified targets. :always accepts the transition forms above
(a candidate vector is usual), but cannot be an unguarded targetless step or
target its own declaring state. :choice specifically requires a non-empty
candidate vector with an unguarded default.
Declarative :spawn and :spawn-all¶
A state's :spawn map accepts these application keys:
| Key | Value and default |
|---|---|
:machine-id / :definition |
Exactly one: a registered machine id, or an inline machine map. |
:data |
A replacement initial-data map, or (fn [{:keys [snapshot event]}] data-map) evaluated on state entry after the transition action. Omit it to use the definition's :data. |
:id-prefix |
Keyword prefix for generated actor ids. Defaults to :machine-id; inline definitions need this or :fixed-actor-id. |
:fixed-actor-id |
Explicit keyword address. Replaces an existing actor at that address, running its exits first. |
:start |
Optional first trigger vector; defaults to [:rf.machine.spawn/spawned]. Initial entry runs before this trigger. |
:on-done |
Optional transition, or (fn [{:keys [data result]}] new-data) that returns the parent's whole next data map. See completion. |
:on-error |
Optional transition for child failure. Its event payload is the failure value, rather than the success {:result …} wrapper. |
:timeout, :on-timeout |
Optional positive integer milliseconds or ISO-8601 duration and its transition; both required together. These arm a deadline on the spawning state. |
The :spawn-all block has a separate closed grammar:
| Key | Value and default |
|---|---|
:children |
Required non-empty vector of child spawn maps. Every child adds a unique keyword :id. Its :on-done, if supplied, must be a data-fold fn. Child :on-error is rejected. Put a join deadline on the parent state. |
:join |
:all (default) or :any. There is no quorum or predicate form. |
:on-all-complete |
Event vector required for :all, dispatched when every child succeeds. |
:on-some-complete |
Event vector required for :any, dispatched on the first success. |
:on-any-failed |
Optional event vector that resolves either join immediately on the first failure. Omit it to keep waiting for successes; add a parent deadline if the join could become unsatisfiable. |
Each resolution event appends the logical child :id and its result to the
configured trigger vector. Resolution always cancels remaining children.
Without :on-any-failed, an impossible success condition reports
:rf.warning/spawn-all-join-unsatisfiable and the parent keeps waiting.
Fan-out and join teaches both join policies.
Callbacks¶
Every guard and action, :entry and :exit included, receives one context map:
| Key | Value |
|---|---|
:data |
The machine's :data. A guard sees it before the transition's actions run; each action sees the writes of the action slots that ran before it. |
:event |
The trigger vector. It is nil in an :always step, and [:rf.machine/start] for the initial :entry. |
:state |
The state the machine was in before this transition, in every slot. |
:meta |
The snapshot's :meta, which starts as the root's :meta. |
:tags, :all-state |
Inside a parallel region only: the machine-wide tag union and the region-to-active-state map, frozen for each selection round. |
:rf.cofx |
The coeffects a named callback declares in :rf.cofx/requires. |
An action returns a map, or nil for no effects:
| Key | Meaning |
|---|---|
:data |
Merged into the snapshot's :data, top-level keys only. |
:fx |
An ordinary effects vector, plus the machine-only :raise. |
A returned :db is dropped with :rf.error/machine-action-wrote-db. An :after delay fn is the one callback with a different context: it receives {:snapshot …}. Guards and actions in the guide teaches callbacks.
Keyword surfaces¶
These are the subscriptions and effects the machines artefact registers. They are included in every image whatever its :select-ns selection, so a frame loaded from an image resolves them the same way the default frame does.
[:rf/machine machine-id]¶
- Kind: subscription
- Payload:
machine-id, a registered machine id or a spawned actor's id. - Description: Returns the machine's snapshot
{:state :data}, plus the framework-managed:tags.:stateis a keyword for a flat machine; for a compound machine it is the vector path from the root to the active leaf, so a root-level leaf reads[:idle]; for a:type :parallelmachine it is a map from each region to that region's keyword or path. Returnsnilfor an unknown machine, and for a registered machine that has not yet handled its first event. To give views narrower values, register subscriptions that take this one as an input, as shown in The table.- A view that renders before the first event must handle
nil. To create the snapshot at startup instead, dispatch the reserved trigger[machine-id [:rf.machine/start]]: it runs the initial state's:entryactions and arms its:aftertimers, and matches no:ontransition.
- A view that renders before the first event must handle
- Example:
[:rf.machine/has-tag? machine-id tag]¶
- Kind: subscription
- Payload:
machine-idandtag. - Description: Returns
truewhen the:tagsset in the machine's current snapshot containstag, andfalseotherwise, including for an unknown or not-yet-started machine. It reads the tag directly rather than through:rf/machine, so a view that reads only this re-renders only when the answer changes. - Example:
[:rf.machine/spawn spawn-spec]¶
- Kind: effect (reserved fx-id)
- Payload: a
spawn-specmap with exactly one of:machine-id(a registered machine to instantiate) or:definition(an inline spec map), plus the optional keys below. - Description: Starts a new instance of a machine, called an actor. Emit it from any event handler's
:fx, including a machine action's. A declarative:spawnstate node emits it for you.- Choose by lifetime. When a child should live exactly as long as one state of a parent machine, put
:spawnon that state: leaving the state, or destroying the parent, destroys the child. Emit this effect yourself when the actor's lifetime is not one state, such as a logger started with the session. Nothing tracks an actor you spawn this way; it lives until a:rf.machine/destroynames it. - A supplied
:datamap replaces the machine's initial:data; omitting it uses the definition's data. The runtime adds the actor's own id to it as:rf/self-id. :id-prefixsets the prefix of the actor's id, which is the deterministic<prefix>#<n>from a per-type counter. The prefix defaults to:machine-id.:fixed-actor-idgives the actor an explicit id instead. Use it when the spawner needs to hold the child's address: choose a fresh keyword, store it in ordinary:data, and pass it here. Spawning at a:fixed-actor-idthat a live actor already holds destroys that actor first (its:exitactions run) and then installs the new one.:startis an event vector dispatched to the new actor as[<spawned-id> <start>]. Without it, the runtime dispatches[<spawned-id> [:rf.machine.spawn/spawned]]. Either way the actor's initial:entryactions run first.- A declarative
:spawnstate node accepts the same keys plus:on-done,:on-error,:timeoutand:on-timeout, and stores the child's id in the parent's:dataat[:rf/spawned <invoke-id>]. See Actors.
- Choose by lifetime. When a child should live exactly as long as one state of a parent machine, put
- Errors:
:rf.error/machine-spawn-unregistered-type::machine-idnames no registered machine and there is no:definition. Nothing is spawned.:rf.error/machine-spawn-bad-shape: an inline:definitionnames neither:id-prefixnor:fixed-actor-id, so the actor would have no id. The effect handler throws, the effect runner reports it as:rf.error/fx-handler-exception, and nothing is spawned.:rf.error/machine-spawn-all-duplicate-id: the generated<prefix>#<n>id is already held by a live actor, as when two parent machines spawn one type without distinct:id-prefixvalues. Nothing is spawned.
- Example:
(rf/reg-event :session/start-logger (fn [_ _] {:fx [[:rf.machine/spawn {:machine-id :machines/log-shipper :fixed-actor-id :logger ;; a well-known address the app holds :data {:buffer []} :start [:logger/connect]}]]})) ;; Address the actor by the id you chose. (rf/reg-event :session/flush-logs (fn [_ _] {:fx [[:dispatch [:logger [:logger/flush]]]]}))
[:rf.machine/destroy actor-id]¶
- Kind: effect (reserved fx-id)
- Payload:
actor-id. - Description: Stops an actor. It runs the
:exitactions of the actor's active states, cancels its pending:aftertimers and removes its snapshot from[:rf.runtime/machines :snapshots actor-id]inruntime-db.- It also aborts the actor's in-flight
:rf.http/managedrequests and releases any resources the actor owns. Hold anything else the actor uses, such as a websocket or an interval, in a custom effect handler keyed by the actor's id, and have the actor's:exitaction return the close effect::exitruns, and its effects execute, on every destroy path. - It ends the instance, not the machine's registration. A singleton's
reg-machineregistration survives, soregistrationsandhandler-metastill report it, and its next event starts it again from:initial. To remove the registration as well, callclear:(rf/clear :event <machine-id>). A spawned actor has no registration of its own: it exists for as long as its snapshot does. - Destroying an actor that is already gone does nothing. A payload that is not an actor-id keyword emits
:rf.error/machine-destroy-bad-argand destroys nothing. - A declarative child rarely needs it: the runtime destroys it when its parent leaves the spawning state or is destroyed, and when it enters a root-level
:final?state (see Final states). An actor you started with:rf.machine/spawnhas no parent state to end it, so emit this effect when you are done with it, unless it finishes by entering a root-level:final?state.
- It also aborts the actor's in-flight
- Example:
[:rf.machine/update-snapshot patch]¶
- Kind: effect (reserved fx-id)
- Payload:
{:rf/machine-id <id> :rf/patch {:state … :meta … :data {…}}}, each:rf/patchkey optional. - Description: Writes a machine's snapshot directly, without taking a transition. A machine action can return only
:dataand:fx; emit this from the action's:fx, or from any event handler, when you must also set:stateor:metain the same atomic write. Prefer a transition where one will do: this effect does not check that a patched:stateexists in the machine's definition.:stateand:metareplace the snapshot's values.:datais merged into the existing:data, as an action's:datareturn is, so the runtime's own:rf/*keys in it survive.- This is a direct write: it does not run exits, entries,
:always, spawn reconciliation or tag recomputation. Use transitions for normal lifecycle changes and frame-state installation for restore. - Other
:rf/patchkeys are ignored. A:dbkey emits:rf.error/machine-action-wrote-dband is dropped; the rest of the patch is still written. - Does nothing when the machine has no snapshot (not started, or destroyed).
- The
:datapatch is validated against the machine's[:schemas :data]schema before it is written, byvalidate-update-snapshot-data!. A patch that fails is not written, so this effect is subject to the:where :machine-databoundary like a transition.
- Example:
[:raise event-vec]¶
- Kind: effect (reserved fx-id, machine actions only)
- Payload:
event-vec, an event for the same machine. - Description: Sends
event-vecback into the same machine within the current macrostep (the machine's handling of one dispatched event, including every raised event and:alwaysstep it leads to), before the snapshot commits. Only a machine action's:fxcan use it; there is no:raiseeffect handler outside machines. See Raise and internal events.- Use it when an action decides the machine's next step.
[:dispatch [machine-id event]]handles the event later, as a separate event, after this one's snapshot has committed. A raised event is handled within this macrostep, before anything else, and the snapshot commits once. - An event listed in the machine's
:internal-eventscan only be raised. Dispatching one from outside emits:rf.error/machine-internal-event-external-dispatchand changes nothing. - Raises are depth-bounded: 16 by default, or the machine spec's
:raise-depth-limit. Exceeding the bound aborts the whole macrostep with:rf.error/machine-raise-depth-exceeded.
- Use it when an action decides the machine's next step.
- Example:
Final states and :on-done¶
A machine finishes by entering a :final? leaf that is a direct child of its root. The runtime then destroys it, so you do not emit :rf.machine/destroy. A singleton's registration survives: only its snapshot is removed, and its next event starts it again from :initial. Use :final? for work that ends, such as a request actor. A resting state that views still read, such as :authed, stays a plain leaf.
A child started by a :spawn or :spawn-all state reports to its parent by finishing; it dispatches nothing itself. (An actor started with a hand-emitted :rf.machine/spawn has no parent to report to.) The runtime sends the parent a :rf.machine.spawn/done event, handled in the parent's ordinary macrostep: the parent receives the result through :on-done, and can also transition on it with a transition-shaped :on-done, an :always, or :on {:rf.machine.spawn/done …}. A child that fails arrives as :rf.machine.spawn/error instead.
A :final? leaf nested inside a compound state finishes only that compound. The machine keeps running, and the compound's own :on-done names the state to move to; see nested final states. A :type :parallel machine finishes when every region's active state is :final?, unless the root declares :on-done: that action runs and the all-final snapshot is retained.
| State-node key | What it does |
|---|---|
:final? |
Marks a leaf state as terminal. Directly under the root, entering it finishes and destroys the machine; nested in a compound state, it finishes that compound. |
:error? |
Requires :final?, or registration throws :rf.error/machine-error-flag-without-final. Marks the terminal state as a failure: the parent's :spawn :on-error transition fires instead of :on-done, and under a :spawn-all join the child counts as failed. |
:output-key |
Requires :final?, or registration throws :rf.error/machine-output-key-without-final. Names the child's :data slot that is reported to the parent's :on-done. |
On a parent's :spawn map, :on-done fires when the child enters a non-error :final? state, and is applied on the parent's next macrostep rather than inside the child's teardown. result is the child's :data slot named by the final state's :output-key, or nil. It takes one of two forms:
- An
:on-shaped transition that moves the parent:{:target :loaded :action …}, a keyword or path target, or guarded candidates (XState'sinvoke onDone). It resolves at the spawning state's level, like:on-error, and the result is at(:result (nth ev 2)). - A function
(fn [{:keys [data result]}] new-data)that folds the result into the parent's:data. On a:spawn-allchild spec, this is the only form accepted.
Any other value throws :rf.error/machine-bad-on-done-clause at registration. See Final states in the guide.
:on-error takes the transition forms only. On its event, (nth ev 2) is the failure payload itself rather than a {:result …} map: the error leaf's :output-key slot, or the exception details when a child action threw.
Cross-machine messaging¶
To send an event to another machine, dispatch it at that machine's id: [:dispatch [<actor-id> <event>]]. There is no separate name registry.
A child spawned declaratively has its id stored in the parent's :data at [:rf/spawned <invoke-id>], so the parent reads the address from its own snapshot. A hand-emitted :rf.machine/spawn has no invoke-id, so the spawner chooses a fresh keyword, passes it as :fixed-actor-id and stores it in ordinary :data.
Querying registered machines¶
There is no machines or machine-meta function. A machine is an :event registration carrying :rf/machine? true, so you list machines and read their specs with the generic registrar queries, registrations and handler-meta.
- To list every registered machine id, filter the
:eventregistrations on:rf/machine?:This lists registered machines. A spawned actor has no registration of its own, so read live actors from the snapshots map at(keys (into {} (filter (fn [[_ m]] (:rf/machine? m))) (rf/registrations {:source :store :kind :event}))) ;; → (:session :auth.login/flow …)[:rf.runtime/machines :snapshots]in the frame'sruntime-db. - To read one machine's spec, take the
:rf/machinekey of its registration. It holds the transition table,:doc,:schemasand per-element source coordinates, and isnilunless the:eventregistration is a machine:
Plain-function registration and testing¶
re-frame.machines/reg-machine*¶
- Kind: function
- Signature:
- Description: Registers a machine like
reg-machine, as a plain function that captures no source. Use it when the spec is built at runtime: generated code, the REPL, test harnesses.- The 3-arity takes the same
optsmap asreg-machine, with the same:schemabehaviour and the same errors. - Because the spec carries no source, development builds warn
:rf.warning/machine-source-unstampedonce per machine id.
- The 3-arity takes the same
- Example:
re-frame.machines/make-machine-handler¶
- Kind: function
- Signature:
- Description: Compiles a transition table into the event-handler function that
reg-machinewould register, and returns it without registering it.- A spec with a
[:schemas :data]schema throws:rf.error/machine-schema-requires-reg-machine. This path does not record the:rf/machineregistration metadata the schema check reads, so the schema would validate nothing. Register such a machine withreg-machineorreg-machine*.
- A spec with a
- Example:
re-frame.machines/machine-transition¶
- Kind: function
- Signature:
- Description: Runs one transition as a pure function: given a machine definition, a current snapshot and an event, it returns a plain map. Use it to unit-test a transition table. It runs on the JVM and needs no frame;
re-frame.machinesis the only namespace to require.:status :okcarries the new:snapshotand the ordered effects vector:fx. The effects are described, never run.:fxalso holds the runtime's own effects, such as one[:rf.machine/after-schedule …]for each:aftertimer the new state arms, so assert on the entries you care about rather than on the whole vector. An event that no transition matches returns:okwith the snapshot unchanged and:fx [].:handled?istruewhen the event selected a transition, even a targetless one that changed nothing, andfalsewhen nothing took it, so a test can tell a declined event from an accepted no-op.:status :errorreports a failed macrostep. Either a guard, action or:datafunction threw (:kind :rf.error/machine-action-exception, with:exceptionand the throwing ref), or a depth limit tripped (:kind :rf.error/machine-always-depth-exceededor:rf.error/machine-raise-depth-exceeded). A failure carries no snapshot, because the macrostep is atomic.- The supplied snapshot is the starting point. This function does not create a singleton, run host effects, or apply registered schema validation; use a test frame for those boundaries.
- Mistakes in the input, such as a malformed
:stateor a guard or action ref with no entry, throw the same:rf.error/*ex-infothe registration checks throw rather than returning a result.
- Example:
login-flowis the definition built in the guide's first machine.(require '[clojure.test :refer [is]] '[re-frame.machines :as rf.machines]) (let [{:keys [status snapshot fx]} (rf.machines/machine-transition login-flow {:state :idle :data {}} [:auth.login/submit {:email "a@b.com" :password "secret"}])] (is (= :ok status)) (is (= :submitting (:state snapshot))) (is (= :rf.http/managed (ffirst fx)))) ;; the :submitting :entry fired the request
Runtime diagnostics¶
Registration failures are listed above. During a run, distinguish a rejected definition from a failed callback or an ignored event:
| Diagnostic | Behavior and remedy |
|---|---|
:rf.error/machine-action-exception |
A guard, action or spawn-data fn threw. The macrostep rolls back; fix that callback. A child's failure can drive its parent's :on-error. |
:rf.error/machine-always-depth-exceeded, :rf.error/machine-raise-depth-exceeded |
The whole macrostep rolls back. Make the cycle terminate, or raise the corresponding limit for intentionally longer work. |
:rf.error/machine-action-wrote-db |
The illegal :db write is dropped; other action or patch values proceed. Return machine :data, or dispatch an application event. |
:rf.error/machine-state-not-in-definition, :rf.error/machine-snapshot-version-mismatch |
The next event restarts an incompatible snapshot from :initial. Migrate persisted state when continuity matters. |
:rf.error/machine-bad-state-form |
A supplied snapshot has a malformed state. Supply a keyword, path vector or region map matching the definition. |
:rf.error/machine-after-sub-threw, :rf.error/machine-after-fn-threw |
Dynamic delay evaluation failed; no timer is armed. Fix the subscription or delay fn. |
:rf.error/machine-bad-after-delay |
A dynamic delay is invalid and the timer is skipped. Return positive milliseconds. |
:rf.error/machine-after-watch-failed |
A delay subscription could not be watched. Its current timer is armed, but delay changes will not reschedule it; inspect the subscription adapter. |
:rf.error/machine-spawn-all-bad-child-id |
A completion names a child outside the join; it is ignored. Let a child's final state report completion instead of constructing runtime completion events. |
:rf.error/machine-parallel-output-key-conflict |
Final regions name different output keys; the first region's key wins. Use one output key across the machine. |
Spawn/destroy failures are documented with their effects,
and schema failure behavior with machine-root keys.
Shared missing or invalid coeffects use the ordinary
coeffect diagnostics. An unhandled event is a benign
:rf.machine.event/unhandled-no-op trace, not an error.
Framework integration¶
Not for application code — used by adapters, tools and the test harness.
Tooling (JVM)¶
These two functions are JVM-only aliases on re-frame.machines for functions in re-frame.machines.tooling. ClojureScript tools require re-frame.machines.tooling and call them there. re-frame.machines does not require the tooling namespace on ClojureScript, so an app that loads no tools drops that code from its build.
The static and live machine views that Xray draws have no public accessor: tools require re-frame.machines.tooling directly. The framework has no machine->xstate-json, machine->mermaid or Stately exporter; those are in the separate day8/re-frame2-machines-viz library.
re-frame.machines/machine-selector?¶
- Kind: function
- Signature:
- Description: Returns
truewhen the subscription registered undersub-idreads a machine: an ordinaryreg-subwhose literal:inputsinclude a[:rf/machine …]or[:rf.machine/has-tag? …]query vector. Such subscriptions are ordinary:derivationsubscription nodes; this lets a graph tool mark them. JVM-only. - Example:
re-frame.machines/machine-selector-targets¶
- Kind: function
- Signature:
- Description: Returns the set of machine ids that the subscription registered under
sub-idreads, taken from the second element of each[:rf/machine machine-id …]or[:rf.machine/has-tag? machine-id …]literal:inputsentry. A graph tool uses it to draw the edgesmachine-selector?only detects. JVM-only. - Example:
Effect handlers¶
These are the handlers this namespace registers for the reserved :rf.machine/* effect ids. Application code emits the effect (see Keyword surfaces) and does not call these. Each takes (fx-ctx args), and the frame is the fx context's :frame. Because they are registered here, an app that does not use the artefact carries none of their code or trace strings.
re-frame.machines/spawn-fx¶
- Kind: function
- Signature:
- Description: The handler for
:rf.machine/spawn. Installs the new actor's snapshot at[:rf.runtime/machines :snapshots <spawned-id>]in the spawning frame'sruntime-db.- It stores the machine type in the snapshot under
:rf/machine-type, so the runtime and epoch restore can rebuild the actor fromruntime-dbalone. The actor has no event-handler registration of its own; it exists while that snapshot does. - A
:machine-idthat names no registered machine, with no inline:definition, emits:rf.error/machine-spawn-unregistered-typeand installs nothing.
- It stores the machine type in the snapshot under
re-frame.machines/spawn-all-init-fx¶
- Kind: function
- Signature:
- Description: The handler for
:rf.machine/spawn-all-init, which the runtime emits beside the per-child:rf.machine/spawneffects when a:spawn-allstate is entered. It seeds the join state at[:rf.runtime/machines :spawned <parent> <invoke-id>]as{:children {…} :done #{} :failed #{} :resolved? false :spec …}. When a child reaches a final state,:error? trueor not, the result is folded into the join state and resolves the join; children dispatch nothing to the parent themselves.
re-frame.machines/destroy-machine-fx¶
- Kind: function
- Signature:
- Description: The handler for
:rf.machine/destroy. It chooses the teardown from the shape ofargs: a single actor (the keyword form, or a single:spawn), or every child of a:spawn-all. It runs the actor's:exitactions and clears its[:rf.runtime/machines :snapshots <actor-id>]slot. A singleton'sreg-machineregistration survives.
re-frame.machines/after-schedule-fx¶
- Kind: function
- Signature:
- Description: The handler for
:rf.machine/after-schedule, which the runtime emits once per:afterentry when a state with:afteris entered. It resolves the delay and schedules a wall-clock timer through the clock abstraction.- The delay can be a literal
pos-int?, an ISO-8601 duration string such as"PT5S", a subscription vector, or(fn [{:keys [snapshot]}] ms). For a subscription delay it also adds a watch that cancels and reschedules the timer when the subscription's value changes. - On expiry it dispatches
[<parent-id> [:rf.machine.timer/after-elapsed <delay-key> <epoch> <decl-path>]], which takes effect only if the scheduling state is still active and the epoch matches.
- The delay can be a literal
re-frame.machines/after-cancel-fx¶
- Kind: function
- Signature:
- Description: The handler for
:rf.machine/after-cancel. Cancels a previously scheduled:aftertimer for a machine state.
Validators¶
These run the registration-time checks and the :data schema checks. The three :data validators run only in development builds; in production builds they skip validation and return true.
re-frame.machines/validate-machine!¶
- Kind: function
- Signature:
- Description: Runs every registration-time check on a machine definition and throws on a violation.
make-machine-handlercalls it first, so every registration runs it.- It checks the whole machine spec: the closed key sets, transition shapes and targets, guard and action refs, timers, choice, final, history, parallel and spawn rules.
:regionsis accepted only on a:type :parallelnode, because regions only run on a:type :parallelroot. On a flat or compound root it throws:rf.error/machine-root-slot-not-supportedbefore any other check reads the root; the error's:offending-keyslists every root key the runtime does not read there. On a state it throws:rf.error/machine-unknown-node-key.- It throws the grammar ids listed under Registration errors. The
opts, artefact,:region-order,:rf.cofx/requiresand:sensitive/:largechecks run inreg-machinearound it, so this function alone does not throw those. - The conformance corpus tests its registration errors against this function.
re-frame.machines/validate-machine-data!¶
- Kind: function
- Signature:
- Description: Validates the
:dataof every snapshot under[:rf.runtime/machines :snapshots]inruntime-dbagainst its machine's[:schemas :data]schema. This is the:where :machine-databoundary: the router runs it withvalidate-app-schema!against the candidateruntime-db, before commit.- Returns
truewhen every snapshot conforms or has no schema or validator, andfalseotherwise. It validates every snapshot without stopping at the first failure, so each failing machine emits its own trace. Onfalsethe router rejects the whole candidate, as it does for a:where :app-dbfailure. - It finds the schema for a registered machine through the
:rf/machineregistration, and for a spawned actor through the snapshot's:rf/machine-type. - The 4-arity takes
continue?, a predicate that is true while the owning frame is still current; the 3-arity builds it from the frame. If the owning frame is destroyed or replaced during validation, the function returns:rf/stale-incarnationinstead of a boolean.
- Returns
re-frame.machines/validate-spawn-data!¶
- Kind: function
- Signature:
- Description: Validates a new actor's initial snapshot
:dataagainst its machine's[:schemas :data]schema before:rf.machine/spawninstalls it. Returnstrueon conform, no schema or no validator. Returnsfalseon failure, and the spawn installs nothing. Nothing was committed, so the failure trace has:phase :spawnand:rollback? false. The 4-aritycontinue?and the:rf/stale-incarnationreturn work as forvalidate-machine-data!.
re-frame.machines/validate-update-snapshot-data!¶
- Kind: function
- Signature:
- Description: Validates the
:dataof the snapshot that:rf.machine/update-snapshotwould produce, against the machine's[:schemas :data]schema, before the effect writes it. Returnstrueon conform, no schema or no validator, and the effect writes the patch. Returnsfalseon failure, and the effect skips the write.
Runtime and lifecycle¶
re-frame.machines/install-machine-runtime!¶
- Kind: function
- Signature:
- Description: Registers the machine effects and subscriptions again, in both the registrar and the framework-standard registry, so an image-loaded frame can resolve
[:rf.machine/spawn …]and[:rf/machine …]. Idempotent.- It works from descriptors captured when the namespace loaded, so it restores them even after a test fixture has cleared the registrar. It does for machines what the standard re-seed does for
:rf/set-db. - It runs when the namespace loads and from the shared reset fixture. Tests that clear the registrar call it directly.
- It works from descriptors captured when the namespace loaded, so it restores them even after a test fixture has cleared the registrar. It does for machines what the standard re-seed does for
re-frame.machines/reset-timers!¶
- Kind: function
- Signature:
- Description: Cancels in-flight
:aftertimers.- The 0-arity clears every frame's timers. Test teardown uses this form: the fixture built by
make-reset-runtime-fixture, and the per-artefact test fixtures. - The 1-arity clears one frame's timers. Destroying a frame calls this form, releasing that frame's host clock handles and subscription watches without touching other frames.
- Spawn-id counters reset with the registrar and frame reset, so this function only clears the per-frame timer table.
- The 0-arity clears every frame's timers. Test teardown uses this form: the fixture built by
re-frame.machines/owning-actor-id¶
- Kind: function
- Signature:
- Description: Returns
event-idwhen it is the address of a spawned actor whose snapshot is installed at[:rf.runtime/machines :snapshots <event-id>]inframe-id, andnilotherwise, meaning the event belongs to an ordinary handler or a registered machine.- Membership is decided by
:rf/machine-typeat the snapshot root, so it covers declarative:spawn/:spawn-allactors and actors started with[:rf.machine/spawn …]. - Managed HTTP uses it to find which actor owns a request's originating event, so it can abort the request when that actor is destroyed, without requiring this artefact. Without the machines artefact, HTTP treats every request as unowned.
- Membership is decided by
See also¶
- re-frame.core:
dispatch,subscribeandreg-event, which drive and read a machine. - re-frame.schemas: machines declare a
:dataschema the same way handlers declare theirs. - Glossary: the machines vocabulary in one place.
- Coming from XState: what differs from XState v6.