Deterministic FSM workflow engine for hive-events.
Adapted from yogthos/maestro — state machine runner for expressing workflows as data. Integrated with hive-events fx/cofx system.
(require '[hive.events.fsm :as fsm])
;; Define FSM spec
(def my-workflow
{:fsm {::fsm/start {:handler (fn [resources data]
(assoc data :step :ready))
:dispatches [[::process (constantly true)]]}
::process {:handler (fn [resources data]
(assoc data :result ((:compute resources) data)))
:dispatches [[::fsm/end (fn [{:keys [result]}] (some? result))]
[::fsm/error (constantly true)]]}}
:opts {:max-trace 100}})
;; Compile and run
(-> (fsm/compile my-workflow)
(fsm/run {:compute my-fn} {:data {:input 42}}))
::start — initial state::end — terminal success, returns data::halt — pausable, returns FSM state for resume::error — terminal failure(fn [resources data] new-data)(fn [resources data] {:data new-data, :fx [[:effect-id params]]})
Effects are processed through the hive.events.fx pipeline after transition.run-sub-fsm and make-sub-fsm-handlerBased on yogthos/maestro v0.2.1 (MIT License).
Deterministic FSM workflow engine for hive-events.
Adapted from yogthos/maestro — state machine runner for expressing
workflows as data. Integrated with hive-events fx/cofx system.
## Usage
```clojure
(require '[hive.events.fsm :as fsm])
;; Define FSM spec
(def my-workflow
{:fsm {::fsm/start {:handler (fn [resources data]
(assoc data :step :ready))
:dispatches [[::process (constantly true)]]}
::process {:handler (fn [resources data]
(assoc data :result ((:compute resources) data)))
:dispatches [[::fsm/end (fn [{:keys [result]}] (some? result))]
[::fsm/error (constantly true)]]}}
:opts {:max-trace 100}})
;; Compile and run
(-> (fsm/compile my-workflow)
(fsm/run {:compute my-fn} {:data {:input 42}}))
```
## Privileged States
- `::start` — initial state
- `::end` — terminal success, returns data
- `::halt` — pausable, returns FSM state for resume
- `::error` — terminal failure
## Integration with hive-events
- Handlers are pure: `(fn [resources data] new-data)`
- Handlers may return fx-enhanced results:
`(fn [resources data] {:data new-data, :fx [[:effect-id params]]})`
Effects are processed through the hive.events.fx pipeline after transition.
- Subscriptions can dispatch events on state path changes
- Pre/post hooks for interceptor-style concerns
- EDN specs compiled with SCI
- Sub-FSM composition via `run-sub-fsm` and `make-sub-fsm-handler`
Based on yogthos/maestro v0.2.1 (MIT License).(compile spec)(compile spec handlers-map)Compile an FSM spec into a runnable state machine.
Two arities:
(compile spec) — handlers are inline functions(compile spec handlers-map) — handlers are keyword references resolved
from the map (for EDN specs)The compiled FSM should be passed to run.
Compile an FSM spec into a runnable state machine. Two arities: - `(compile spec)` — handlers are inline functions - `(compile spec handlers-map)` — handlers are keyword references resolved from the map (for EDN specs) The compiled FSM should be passed to `run`.
(make-sub-fsm-handler compiled-child)(make-sub-fsm-handler compiled-child
{:keys [data-fn merge-fn result-key resources-fn error-key
fx?]
:or {data-fn identity
merge-fn merge
resources-fn identity
error-key :error
fx? false}})Create a parent FSM handler that delegates to a compiled sub-FSM.
The returned handler:
data-fnrun-sub-fsmmerge-fnArgs: compiled-child — Compiled child FSM opts — Options map: :data-fn — (fn [parent-data] child-initial-data) Default: identity (pass full parent data) :merge-fn — (fn [parent-data child-result] merged-data) Default: merge (shallow merge child into parent) :result-key — If provided, assoc child result under this key instead of using merge-fn (convenience shorthand) :resources-fn — (fn [parent-resources] child-resources) Default: identity (share parent resources) :error-key — Key to store error message when sub-FSM fails Default: :error
Returns: (fn [resources data] data') — a standard FSM handler.
Example:
;; Delegate to context-gather sub-FSM, store under :context
(def my-spec
{:fsm {::fsm/start
{:handler (make-sub-fsm-handler
compiled-context-gather
{:data-fn #(select-keys % [:agent-id :directory])
:result-key :context})
:dispatches [[::next (constantly true)]]}}})
Create a parent FSM handler that delegates to a compiled sub-FSM.
The returned handler:
1. Extracts child initial data from parent data via `data-fn`
2. Runs the child FSM via `run-sub-fsm`
3. Merges child result into parent data via `merge-fn`
Args:
compiled-child — Compiled child FSM
opts — Options map:
:data-fn — (fn [parent-data] child-initial-data)
Default: identity (pass full parent data)
:merge-fn — (fn [parent-data child-result] merged-data)
Default: merge (shallow merge child into parent)
:result-key — If provided, assoc child result under this key
instead of using merge-fn (convenience shorthand)
:resources-fn — (fn [parent-resources] child-resources)
Default: identity (share parent resources)
:error-key — Key to store error message when sub-FSM fails
Default: :error
Returns:
(fn [resources data] data') — a standard FSM handler.
Example:
```clojure
;; Delegate to context-gather sub-FSM, store under :context
(def my-spec
{:fsm {::fsm/start
{:handler (make-sub-fsm-handler
compiled-context-gather
{:data-fn #(select-keys % [:agent-id :directory])
:result-key :context})
:dispatches [[::next (constantly true)]]}}})
```(run compiled-fsm)(run compiled-fsm resources)(run {fsm-graph :fsm
{:keys [max-trace subscriptions pre post]
opts-run-id :run-id
:or {max-trace 1000
pre (fn [fsm _resources] fsm)
post (fn [fsm _resources] fsm)}}
:opts}
resources
{init-trace :trace
init-data :data
init-state-id :current-state-id
init-run-id :run-id
:or {init-state-id :hive.events.fsm/start init-trace [] init-data {}}})Execute a compiled FSM.
Three arities:
(run fsm) — empty resources and state(run fsm resources) — empty initial state(run fsm resources initial-state) — full controlResources: map of external dependencies (DB conns, config, etc.) Initial state keys:
:data — initial data map:current-state-id — resume from specific state:trace — existing trace logReturns the final data map (on ::end) or halted FSM (on ::halt).
Execute a compiled FSM. Three arities: - `(run fsm)` — empty resources and state - `(run fsm resources)` — empty initial state - `(run fsm resources initial-state)` — full control Resources: map of external dependencies (DB conns, config, etc.) Initial state keys: - `:data` — initial data map - `:current-state-id` — resume from specific state - `:trace` — existing trace log Returns the final data map (on ::end) or halted FSM (on ::halt).
(run-async compiled-fsm)(run-async compiled-fsm resources)(run-async compiled-fsm resources initial-state)Execute FSM on a separate thread, return a promise.
Useful for long-running workflows that shouldn't block the caller.
Returns a future that yields the FSM result.
Execute FSM on a separate thread, return a promise. Useful for long-running workflows that shouldn't block the caller. Returns a future that yields the FSM result.
(run-sub-fsm compiled-child resources initial-data)Execute a compiled sub-FSM within a parent handler context.
Runs the child FSM to completion and returns its result data, which the parent handler can merge into its own data map.
Args:
compiled-child — Compiled FSM (from compile)
resources — Resources map (shared or child-specific)
initial-data — Initial data for the child FSM
Returns: The child FSM result (final data map on ::end).
Error handling:
Catches child FSM exceptions and returns an error map:
{:sub-fsm/error true :sub-fsm/message str :sub-fsm/cause ex}
This lets the parent handler decide how to handle child failures
without crashing the parent FSM.
Example:
(defn handle-with-context [resources data]
(let [ctx-result (run-sub-fsm compiled-context-fsm
resources
(select-keys data [:agent-id :directory]))]
(if (:sub-fsm/error ctx-result)
(assoc data :error (:sub-fsm/message ctx-result))
(merge data ctx-result))))
Execute a compiled sub-FSM within a parent handler context.
Runs the child FSM to completion and returns its result data,
which the parent handler can merge into its own data map.
Args:
compiled-child — Compiled FSM (from `compile`)
resources — Resources map (shared or child-specific)
initial-data — Initial data for the child FSM
Returns:
The child FSM result (final data map on ::end).
Error handling:
Catches child FSM exceptions and returns an error map:
`{:sub-fsm/error true :sub-fsm/message str :sub-fsm/cause ex}`
This lets the parent handler decide how to handle child failures
without crashing the parent FSM.
Example:
```clojure
(defn handle-with-context [resources data]
(let [ctx-result (run-sub-fsm compiled-context-fsm
resources
(select-keys data [:agent-id :directory]))]
(if (:sub-fsm/error ctx-result)
(assoc data :error (:sub-fsm/message ctx-result))
(merge data ctx-result))))
```(run-sub-fsm-fx compiled-child resources initial-data)Execute a compiled sub-FSM and return both data and accumulated effects.
Like run-sub-fsm but captures FX from child handlers instead of
executing them immediately. The parent handler can then include these
effects in its own :fx vector, surfacing them to the parent FSM's
FX pipeline.
This is essential for composability — child FSMs should not execute side effects independently when embedded in a parent context. The parent must control effect execution order.
Args:
compiled-child — Compiled FSM (from compile)
resources — Resources map (shared or child-specific)
initial-data — Initial data for the child FSM
Returns:
{:data result-data, :fx [...]} — child data + accumulated effects.
On error: {:sub-fsm/error true, :sub-fsm/message str, :sub-fsm/cause ex}
Example:
(defn handle-with-effects [resources data]
(let [{child-data :data child-fx :fx :as result}
(run-sub-fsm-fx compiled-child resources
(select-keys data [:input]))]
(if (sub-fsm-error? result)
(assoc data :error (:sub-fsm/message result))
{:data (merge data child-data)
:fx (vec child-fx)})))
Execute a compiled sub-FSM and return both data and accumulated effects.
Like `run-sub-fsm` but captures FX from child handlers instead of
executing them immediately. The parent handler can then include these
effects in its own `:fx` vector, surfacing them to the parent FSM's
FX pipeline.
This is essential for composability — child FSMs should not execute
side effects independently when embedded in a parent context. The
parent must control effect execution order.
Args:
compiled-child — Compiled FSM (from `compile`)
resources — Resources map (shared or child-specific)
initial-data — Initial data for the child FSM
Returns:
`{:data result-data, :fx [...]}` — child data + accumulated effects.
On error: `{:sub-fsm/error true, :sub-fsm/message str, :sub-fsm/cause ex}`
Example:
```clojure
(defn handle-with-effects [resources data]
(let [{child-data :data child-fx :fx :as result}
(run-sub-fsm-fx compiled-child resources
(select-keys data [:input]))]
(if (sub-fsm-error? result)
(assoc data :error (:sub-fsm/message result))
{:data (merge data child-data)
:fx (vec child-fx)})))
```(set-transition-tap! f)Register a per-transition observer (fn [run-token fsm]); nil clears it.
Register a per-transition observer (fn [run-token fsm]); nil clears it.
(step {:keys [fsm current-state-id data] :as halted-fsm} resources)Execute a single state transition (for debugging/testing).
Takes a running FSM state (as returned by ::halt handler) and executes one step, returning the new FSM state.
Useful for step-through debugging of workflows.
Execute a single state transition (for debugging/testing). Takes a running FSM state (as returned by ::halt handler) and executes one step, returning the new FSM state. Useful for step-through debugging of workflows.
(sub-fsm-error? result)Check if a sub-FSM result is an error map (from run-sub-fsm).
Check if a sub-FSM result is an error map (from run-sub-fsm).
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |