Composing Workflows
Workflows are directed graphs of cells connected by edges and dispatch predicates.
Workflow Definition Structure
{:cells {cell-name cell-id-or-spec, ...}
:edges {cell-name edge-def, ...}
:dispatches {cell-name [[label predicate], ...], ...}
:joins {join-name join-spec, ...} ;; optional
:input-schema malli-schema ;; optional
:interceptors [interceptor-spec, ...] ;; optional
}
Minimal Example
(require '[mycelium.core :as myc])
(myc/run-workflow
{:cells {:start :math/double
:add :math/add-ten}
:edges {:start {:done :add}
:add {:done :end}}
:dispatches {:start [[:done (constantly true)]]
:add [[:done (constantly true)]]}}
{} ;; resources
{:x 5}) ;; initial data
;; => {:x 5, :result 20, :mycelium/trace [...]}
Edges
Edges define transitions from one cell to another. Two forms:
Unconditional — single keyword target (no dispatches needed):
:edges {:start :next-cell}
Conditional — map of label→target (requires dispatches):
:edges {:start {:success :dashboard, :failure :error-page}}
Special targets:
:end— workflow completes successfully:error— workflow terminates with error (Maestro terminal state):halt— workflow halts (Maestro terminal state)
Dispatches
Dispatch predicates examine the data map and determine which edge to take. They are checked in order; the first truthy result wins:
:dispatches {:start [[:success (fn [data] (:auth-token data))]
[:failure (fn [data] (:error-type data))]]}
- Predicate receives the
datamap (not the full FSM state) - Labels must match the edge map keys for that cell
- First truthy match wins — order matters
Accumulating Data Model
Cells communicate through an accumulating data map. Every cell receives ALL keys produced by every prior cell in the path:
start → validate → fetch-profile → render
adds :user-id adds :profile needs :user-id AND :profile
:render can access :user-id even though :fetch-profile is between them — keys persist through the chain.
Running Workflows
Pre-compiled (recommended for production)
Pre-compile once at startup, run with zero compilation overhead per request:
;; At startup
(def compiled (myc/pre-compile workflow-def opts))
;; Per request — zero compilation overhead
(myc/run-compiled compiled resources initial-data)
;; Async variant — returns a future
(myc/run-compiled-async compiled resources initial-data)
pre-compile performs all validation, FSM compilation, and Malli schema compilation at call time. run-compiled does only input-schema validation and FSM execution.
Convenience (compiles every call)
;; Sync — compiles and runs in one step
(myc/run-workflow workflow-def resources initial-data)
(myc/run-workflow workflow-def resources initial-data opts)
;; Async — returns a future
(myc/run-workflow-async workflow-def resources initial-data)
(myc/run-workflow-async workflow-def resources initial-data opts)
These are convenient for tests and one-off runs. For repeated execution of the same workflow (e.g. HTTP request handling), use pre-compile + run-compiled.
opts map (optional, passed to pre-compile or run-workflow):
{:pre (fn [fsm-state resources] -> fsm-state) ;; FSM-level pre interceptor
:post (fn [fsm-state resources] -> fsm-state) ;; FSM-level post interceptor
:on-error (fn [resources fsm-state] -> fsm-state)} ;; global error handler
Join Nodes (Parallel Execution)
When multiple cells can run concurrently:
{:cells {:start :auth/validate
:fetch-profile :user/fetch-profile ;; join member
:fetch-orders :user/fetch-orders ;; join member
:render-summary :ui/render-summary}
:joins {:fetch-data {:cells [:fetch-profile :fetch-orders]
:strategy :parallel}}
:edges {:start {:authorized :fetch-data}
:fetch-data {:done :render-summary, :failure :render-error}
:render-summary {:done :end}}}
Key rules:
- Join members exist in
:cellsbut have NO entries in:edges— the join consumes them - The join name appears in
:edgeslike a regular cell - Each member receives the same input snapshot (branches can't see each other's output)
- Default dispatches
:done/:failureare injected automatically - Output keys from members must be disjoint unless
:merge-fnis provided
Input Schema Validation
Validate data entering the workflow before any cell runs:
{:input-schema [:map [:http-request [:map [:cookies map?]]]]
:cells {...}
:edges {...}}
If validation fails, run-workflow returns immediately with:
{:mycelium/input-error {:schema [...], :errors [...], :data {...}}}
Workflow-Level Interceptors
Transform data before/after cell handlers, scoped to matching cells:
{:interceptors
[{:id :nav-context
:scope {:id-match "ui/*"} ;; applies to cells with :id matching glob
:pre (fn [data]
(assoc data :logged-in true))}
{:id :request-logging
:scope :all ;; applies to every cell
:post (fn [data]
(println "Cell done" (keys data))
data)}]
:cells {...}}
Scope options:
:all— every cell{:id-match "ui/*"}— cells whose:idmatches the glob pattern{:cells [:render-dashboard :render-error]}— explicit cell name list
Interceptors are (fn [data] -> data), distinct from Maestro's FSM-level interceptors.
Compile-Time Validation
compile-workflow validates:
- Cell existence — all cell IDs must be registered
- Edge targets — all targets must be valid cells, joins, or terminal states
- Reachability — every cell/join must be reachable from
:start - Dispatch coverage — every edge label must have a dispatch predicate
- Schema chain — each cell's input keys must be available from upstream outputs
- Join validation — member cells exist, no name collisions, disjoint outputs
Workflow Trace
Every run produces :mycelium/trace in the result:
(:mycelium/trace result)
;; => [{:cell :start, :cell-id :auth/parse, :transition :success, :data {...}}
;; {:cell :validate, :cell-id :auth/check, :transition :authorized, :data {...}}]
Mycelium