Mycelium Quick Reference
Thinking in Mycelium
A mycelium application is a directed graph where each node (cell) is a pure data transformation with an explicit contract. You are building a state machine: the manifest declares the structure, cells implement the logic.
Design Process
- Decompose the problem into steps. Each distinct operation becomes a cell. A cell does ONE thing: validate input, look up a price, compute tax, render HTML. If you're writing a cell that does two things, split it into two cells.
- Define the data flow. Each cell declares what keys it needs (input schema) and what keys it produces (output schema). Data accumulates through the graph – every cell receives all keys produced by every upstream cell. A cell that computes
:taxcan be 5 steps downstream from the cell that produced:discounted-subtotal, and it will still receive that key. - Draw the graph. Connect cells with edges. Most flows are linear pipelines. Add branching where decisions are needed (fraud check: approve or reject). Add join nodes where steps are independent and can run in parallel (compute tax, shipping, and gift wrap concurrently).
- Write the manifest first, then implement cells. The manifest is the architecture. It declares cells, edges, dispatches, and schemas. Once the manifest compiles, each cell can be implemented independently using only its schema as context – you don't need to read any other cell's code.
Key Mental Model
The manifest is a contract, not configuration. It's not a passive description of the system – it's an executable specification that the framework validates at compile time. If a cell's input schema requires :shipping-groups but no upstream cell produces that key, compilation fails. This catches the class of bugs that traditional codebases accumulate silently.
Cells are stateless data transformations. A cell handler receives a resources map (database connections, external services) and the accumulated data map. It returns the data map with new keys added via assoc. Side effects (database writes, API calls) go through the resources map, keeping cells testable in isolation.
;; Cell handler signature:
(fn [resources data]
(assoc data :tax (compute-tax (:discounted-subtotal data))))
Branching is separate from computation. Cell handlers compute data. Dispatch predicates examine the result and choose the edge. This separates "what happened" from "what to do next":
;; The cell computes a fraud status:
(fn [_ data] (assoc data :fraud-status (if (> (:total data) 5000) :reject :ok)))
;; Dispatch predicates choose the route:
:dispatches {:fraud [[:approved (fn [d] (not= :reject (:fraud-status d)))]
[:reject (fn [d] (= :reject (:fraud-status d)))]]}
Errors are data, not exceptions. Expected error conditions (validation failures, business rule violations, declined payments) should be represented by setting a key on the data map, not by throwing exceptions. The workflow then routes these states via dispatch predicates to the appropriate handler cell:
;; The payment cell sets an error key instead of throwing:
(fn [resources data]
(let [result (charge-card resources (:card data) (:total data))]
(assoc data :payment-status (if (:success result) :approved :declined))))
;; The workflow routes on that key:
:dispatches {:payment [[:approved (fn [d] (= :approved (:payment-status d)))]
[:declined (fn [d] (= :declined (:payment-status d)))]]}
This keeps the graph's control flow explicit and visible in the manifest. Every possible outcome has a named edge you can see and test. Reserve exceptions for truly unexpected failures (IO errors, connection drops, out of memory) – things that represent broken infrastructure, not business outcomes.
Joins declare parallelism. When cells don't depend on each other's output, declare them as a join. Each member receives the same input snapshot and produces non-overlapping output keys. The framework runs them concurrently and merges the results:
:joins {:fees {:cells [:calc-tax :calc-shipping :calc-gift-wrap] :strategy :parallel}}
Structuring a Solution
Given a problem like "process an order", think of it as a state machine:
[expand items] -> [apply discounts] -> [compute subtotal]
-> [calc tax | calc shipping | calc gift-wrap] (parallel join)
-> [compute total] -> [fraud check]
-> approved: [reserve inventory] -> [process payment]
-> approved: [finalize] -> END
-> declined: [rollback] -> END
-> rejected: END
Each box is a cell. Each arrow is an edge. Each fork is a dispatch. Each parallel group is a join. The manifest makes this structure explicit and validates it.
The manifest is also what you hand to another developer (or agent) to understand the system. Reading a 100-line manifest gives you the complete architecture. Reading 900 lines of imperative code gives you implementation details you have to mentally reconstruct the architecture from.
Core API
(require '[mycelium.core :as myc])
;; Pre-compile once at startup (recommended for production)
(def compiled (myc/pre-compile workflow-def opts))
;; Run a pre-compiled workflow (zero compilation overhead)
(myc/run-compiled compiled resources initial-data)
(myc/run-compiled-async compiled resources initial-data) ;; returns a future
;; Convenience: compile + run in one step (re-compiles every call)
(myc/run-workflow workflow-def resources initial-data)
(myc/run-workflow workflow-def resources initial-data opts)
;; Async convenience (returns a future)
(myc/run-workflow-async workflow-def resources initial-data)
;; Resume a halted workflow (human-in-the-loop)
(myc/resume-compiled compiled resources halted-result)
(myc/resume-compiled compiled resources halted-result {:human-input "value"})
;; Compile a system (all workflows)
(myc/compile-system {"/route" manifest, ...})
Compilation Options
pre-compile and run-workflow accept an opts map:
{:pre (fn [fsm-state resources] fsm-state) ;; pre-interceptor for every state
:post (fn [fsm-state resources] fsm-state) ;; post-interceptor for every state
:on-error (fn [resources fsm-state] data) ;; runs when FSM enters error state
:on-end (fn [resources fsm-state] data) ;; runs when FSM enters end state
:coerce? true ;; auto-coerce numeric types (int↔double)
:propagate-keys? false ;; disable auto key propagation (on by default)
:validate :warn ;; :strict (default), :warn, or :off
:on-trace (fn [entry] ...)} ;; callback after each cell completes
Accumulating Data Model
Cells communicate through an accumulating data map. Every cell receives the full map of all keys produced by every prior cell in the path. Cells assoc their outputs and the enriched map flows forward.
start → validate → fetch-profile → fetch-orders → render
adds :user-id adds :profile adds :orders needs :profile AND :orders
A cell can depend on data produced several steps earlier without special wiring — keys persist through intermediate cells. The schema chain validator walks each path and confirms required keys are available from upstream outputs.
Key propagation is on by default — cells can return only new/changed keys and input keys are automatically merged into the output. This eliminates the (assoc data ...) boilerplate:
;; Without propagation: (fn [_ data] (assoc data :tax (* (:subtotal data) 0.1)))
;; With propagation: (fn [_ data] {:tax (* (:subtotal data) 0.1)})
Cell Registration
defcell (recommended)
(require '[mycelium.cell :as cell])
;; Minimal — no schema, :doc is required
(cell/defcell :namespace/cell-id
{:doc "Describe what this cell does and how it should be used"}
(fn [resources data] {:result "value"}))
;; With schema (lite syntax — recommended)
(cell/defcell :namespace/cell-id
{:doc "Transforms input key into a result string"
:input {:key :type}
:output {:result :string}}
(fn [resources data] {:result "value"}))
;; With schema (full Malli vector syntax)
(cell/defcell :namespace/cell-id
{:doc "Transforms input key into a result string"
:input [:map [:key :type]]
:output [:map [:result :string]]}
(fn [resources data] {:result "value"}))
;; With schema + options
(cell/defcell :namespace/cell-id
{:doc "Describe what this cell does and how it should be used"
:input {:key :type}
:output {:result :string}
:requires [:db]
:async? true}
(fn [resources data callback error-callback] ...))
defcell eliminates ID duplication — the cell-id is specified once. The opts map must contain a :doc string describing the cell's purpose and semantics for LLM consumption. Schema, :requires, and :async? are also passed in the opts map. The handler function is always the last argument.
defmethod (low-level)
(defmethod cell/cell-spec :namespace/cell-id [_]
{:id :namespace/cell-id
:doc "Describe what this cell does and how it should be used"
:handler (fn [resources data] {:result "value"})
:schema {:input [:map [:key :type]]
:output [:map [:result :string]]}
:requires [:db]
:async? true})
Cell Registry Helpers
(cell/list-cells) ;; => (:ns/a :ns/b ...) — all registered IDs
(cell/get-cell :ns/id) ;; => spec map or nil
(cell/get-cell! :ns/id) ;; => spec map or throws
(cell/set-cell-schema! :ns/id schema) ;; overwrite schema on registered cell
(cell/clear-registry!) ;; remove all cells (testing only)
Workflow Definition
{:cells {:start :cell/id, :step2 :cell/id2} ;; or {:id :cell/id :params {...}}
:edges {:start {:label :step2}, :step2 {:done :end}}
:dispatches {:start [[:label (fn [data] (:key data))]]
:step2 [[:done (constantly true)]]}
:joins {:join-name {:cells [:a :b] :strategy :parallel}} ;; optional
:input-schema [:map [:key :type]] ;; optional
:interceptors [{:id :x :scope :all :pre (fn [d] d)}] ;; optional
:resilience {:start {:timeout {:timeout-ms 5000}}} ;; optional
:transforms {:cell-name {:input {:fn f :schema {:input [...] :output [...]}} ;; optional
:output {:fn f :schema {:input [...] :output [...]}}}}
}
Pipeline Shorthand
;; Instead of :edges + :dispatches for linear flows:
{:pipeline [:start :process :render]
:cells {:start :cell/a, :process :cell/b, :render :cell/c}}
;; Expands to :edges {:start :process, :process :render, :render :end}
;; Mutually exclusive with :edges, :dispatches, :fragments, :joins
Branching
Edges map transition labels to targets. Dispatch predicates examine data to pick the edge:
{:cells {:start :check/threshold
:big :process/big-values
:small :process/small-values}
:edges {:start {:high :big, :low :small}
:big {:done :end}
:small {:done :end}}
:dispatches {:start [[:high (fn [data] (> (:value data) 10))]
[:low (fn [data] (<= (:value data) 10))]]
:big [[:done (constantly true)]]
:small [[:done (constantly true)]]}}
Handlers compute data; dispatch predicates decide the route.
Default Transitions
Use :default as an edge label for a catch-all fallback when no other dispatch predicate matches:
{:cells {:start :check/validate, :ok :process/run, :err :process/error}
:edges {:start {:success :ok, :default :err}
:ok :end, :err :end}
:dispatches {:start [[:success (fn [d] (:valid d))]]}}
;; :default auto-generates (constantly true) as the last predicate
;; No need to add [:default ...] to :dispatches
:defaultmust not be the only edge (use an unconditional keyword edge instead)- You can provide an explicit
:defaultpredicate in:dispatchesto override the auto-generated one :defaultis always evaluated last, even if listed first in:dispatches- Works with join nodes — add
:defaultalongside:done/:failureedges - Trace entries record
:defaultas the transition label
Graph-Level Timeouts
Move timeout logic from handlers to the workflow definition. When a cell exceeds its timeout, the framework injects :mycelium/timeout true into data and routes to the :timeout edge target:
{:cells {:fetch-tags :mp3/parse-id3
:render :ui/render-tags
:fallback :ui/show-error}
:edges {:fetch-tags {:done :render, :timeout :fallback}
:render :end
:fallback :end}
:dispatches {:fetch-tags [[:done (fn [d] (not (:mycelium/timeout d)))]]}
:timeouts {:fetch-tags 5000}} ;; ms
- Timeout values must be positive integers (milliseconds)
- Cells with timeouts must have a
:timeoutedge target - A
:timeoutdispatch predicate is auto-injected and evaluated first (before user predicates) - Works with both sync and async cells (async cells become blocking with timeout)
- Output schema validation is skipped for timed-out cells (handler didn't produce normal output)
- Trace entries include
:timeout? truewhen a cell times out - Distinct from resilience
:timeoutpolicies — graph timeouts route, resilience timeouts error
Error Groups
Declare shared error handling for sets of cells. If any cell in the group throws, the framework catches the exception, injects :mycelium/error into data, and routes to the group's error handler:
{:cells {:fetch :data/fetch
:transform :data/transform
:err :data/handle-error}
:edges {:fetch :transform
:transform :end
:err :end}
:error-groups {:pipeline {:cells [:fetch :transform]
:on-error :err}}}
- At compile time, unconditional edges are expanded to map edges with
:on-errortarget :on-errordispatch predicate is auto-injected and evaluated first:mycelium/errorcontains{:cell :cell-name, :message "..."}— available to the error handler- Output schema validation is skipped for errored cells
- Error handler cells can
dissoc :mycelium/errorto clean up - Grouped cells must exist, error handler must exist, no cell in multiple groups
Join Nodes (Fork-Join)
When multiple independent cells can run concurrently, declare a join node:
{:cells {:start :auth/validate-session
:fetch-profile :user/fetch-profile ;; join member
:fetch-orders :user/fetch-orders ;; join member
:render-summary :ui/render-summary
:render-error :ui/render-error}
:joins {:fetch-data {:cells [:fetch-profile :fetch-orders]
:strategy :parallel}}
:edges {:start {:authorized :fetch-data, :unauthorized :render-error}
:fetch-data {:done :render-summary, :failure :render-error}
:render-summary {:done :end}
:render-error {:done :end}}
:dispatches {:start [[:authorized (fn [d] (:session-valid d))]
[:unauthorized (fn [d] (not (:session-valid d)))]]
:render-summary [[:done (constantly true)]]
:render-error [[:done (constantly true)]]}}
Key Concepts
- Join members (
:fetch-profile,:fetch-orders) exist in:cellsbut have no entries in:edges— the join consumes them - The join name (
:fetch-data) appears in:edgeslike a regular cell - Each member receives the same input snapshot — branches cannot see each other's outputs
- After all branches complete, results are merged into the data map
- Default dispatches
:done/:failureare provided based on whether any branch threw an exception
Join Options
| Option | Default | Description |
|---|---|---|
:cells | (required) | Vector of cell names to run |
:strategy | :parallel | :parallel or :sequential |
:merge-fn | nil | (fn [data results-vec]) — custom merge when output keys overlap |
:timeout-ms | 30000 | Timeout for async cells within the join |
Output Key Conflicts
At compile time, output keys from all join members are checked for overlap:
- No
:merge-fn— compile-time error listing conflicting keys :merge-fnprovided — user handles conflict resolution;; Both cells produce :items — requires :merge-fn :joins {:gather {:cells [:source-a :source-b] :merge-fn (fn [data results] (assoc data :items (vec (mapcat :items results))))}}
Join Error Handling
All branches run to completion (no early cancellation). Errors are collected in :mycelium/join-error. The join's default dispatches route to :failure when errors are present:
:edges {:fetch-data {:done :render-summary, :failure :render-error}}
Join Trace
Each join produces a trace entry with per-member timing:
{:cell :fetch-data
:cell-id :mycelium.join/fetch-data
:transition :done
:join-traces [{:cell :fetch-profile, :cell-id :user/fetch-profile,
:duration-ms 12.3, :status :ok}
{:cell :fetch-orders, :cell-id :user/fetch-orders,
:duration-ms 8.7, :status :ok}]}
Interceptors
Workflow-level interceptors wrap cell handlers at compile time:
:interceptors [{:id :log-timing
:scope :all ;; every cell
:pre (fn [data] (assoc data ::t0 (System/nanoTime)))
:post (fn [data] (dissoc data ::t0))}
{:id :ui-only
:scope {:id-match "ui/*"} ;; glob on cell :id
:pre (fn [data] data)}
{:id :targeted
:scope {:cells [:render :fetch]} ;; explicit cell names
:post (fn [data] data)}]
Scope forms:
| Scope | Matches |
|---|---|
:all | Every cell |
{:id-match "ui/*"} | Cell registry :id matching glob (e.g. :ui/render-dashboard) |
{:cells [:x :y]} | Specific workflow cell names |
Interceptor :pre/:post receive and return the data map (not fsm-state).
Edge Transforms
Declarative data reshaping at cell boundaries. Transforms run outside the cell handler — the cell stays pure while data is adapted between cells.
Input Transform (reshape before cell runs)
:transforms {:greet {:input {:fn (fn [data] (assoc data :name (:user-name data)))
:schema {:input [:map [:user-name :string]]
:output [:map [:name :string]]}}}}
The :fn runs before the cell's input schema validation. The :schema documents what the transform expects and produces, and is validated at compile time against the schema chain.
Output Transform (reshape after cell runs)
:transforms {:start {:output {:fn (fn [data] (assoc data :name (:user-name data)))
:schema {:input [:map [:user-name :string]]
:output [:map [:name :string]]}}}}
The :fn runs after the cell's output schema validation, before the next cell's input validation.
Edge-Specific Output Transforms (branching cells)
When a cell dispatches to multiple edges, apply different transforms per edge:
:transforms {:classify {:premium {:output {:fn (fn [data] (assoc data :level (:tier data)))
:schema {:input [:map [:tier :keyword]]
:output [:map [:level :keyword]]}}}
:basic {:output {:fn (fn [data] (assoc data :category (name (:tier data))))
:schema {:input [:map [:tier :keyword]]
:output [:map [:category :string]]}}}}}
Only the transform for the taken edge is applied.
Combining Input and Edge-Specific Output Transforms
:transforms {:classify {:input {:fn (fn [data] (assoc data :score (:raw-value data)))
:schema {:input [:map [:raw-value :int]]
:output [:map [:score :int]]}}
:premium {:output {:fn (fn [data] (assoc data :level (:tier data)))
:schema {:input [:map [:tier :keyword]]
:output [:map [:level :keyword]]}}}
:basic {:output {:fn (fn [data] (assoc data :category (name (:tier data))))
:schema {:input [:map [:tier :keyword]]
:output [:map [:category :string]]}}}}}
When to Use Transforms vs. Cells
- Transforms — mechanical key renaming, type adaptation, or structural reshaping between cells that don't share a schema contract. Zero runtime overhead beyond the function call.
- New cell — when the reshaping involves domain logic, side effects, or is complex enough to warrant its own schema and tests.
Transform Spec
Each transform is {:fn f, :schema {:input [...] :output [...]}}:
:fn—(fn [data] -> data), a pure function that reshapes the data map:schema :input— Malli schema documenting what the transform expects:schema :output— Malli schema documenting what the transform produces
Both schemas are validated at compile time. The schema chain validator uses transform schemas to verify key availability across cells.
Parameterized Cells
Reuse the same handler with different config by passing a map instead of a bare keyword:
{:cells {:triple {:id :math/multiply :params {:factor 3}}
:double {:id :math/multiply :params {:factor 2}}}
:pipeline [:triple :double]}
Params are injected as :mycelium/params in the data map and cleaned up after each step. Access via (get-in data [:mycelium/params :factor]).
Resilience Policies
Wrap cells with resilience4j policies via :resilience:
{:cells {:start :api/call, :fallback :ui/error}
:edges {:start {:done :end, :failed :fallback}, :fallback :end}
:dispatches {:start [[:failed (fn [d] (some? (:mycelium/resilience-error d)))]
[:done (fn [d] (nil? (:mycelium/resilience-error d)))]]}
:resilience {:start {:timeout {:timeout-ms 5000}
:retry {:max-attempts 3 :wait-ms 200}
:circuit-breaker {:failure-rate 50 :minimum-calls 10
:sliding-window-size 100 :wait-in-open-ms 60000}
:bulkhead {:max-concurrent 25 :max-wait-ms 0}
:rate-limiter {:limit-for-period 50
:limit-refresh-period-ms 500 :timeout-ms 5000}
:async-timeout-ms 30000}}}
When triggered, handler returns data with :mycelium/resilience-error (map with :type, :cell, :message). Error types: :timeout, :circuit-open, :bulkhead-full, :rate-limited, :unknown.
Stateful policies (circuit breaker, rate limiter) require pre-compile + run-compiled to share state across calls.
:async-timeout-ms controls how long the resilience wrapper waits for an async handler's promise (default 30s). Independent of the resilience4j :timeout policy.
Manifest Loading
(require '[mycelium.manifest :as manifest])
(def m (manifest/load-manifest "path/to/file.edn"))
(def wf (manifest/manifest->workflow m))
(manifest/cell-brief m :cell-name) ;; LLM-friendly prompt
Manifest Cell Fields
{:id :namespace/name ;; cell registry ID
:doc "..." ;; required — describes purpose and semantics for LLMs
:schema {:input [...] ;; Malli schema
:output [...]} ;; single or per-transition
:requires [:db] ;; optional resource dependencies
:on-error :cell-name} ;; or nil — required in strict mode (default)
Use :schema :inherit to resolve schema from the cell registry (avoids duplicating schemas in manifest):
{:id :user/fetch-profile
:schema :inherit ;; pulls :input/:output from cell/cell-spec
:on-error :error-handler}
Manifest Validation
(manifest/validate-manifest manifest) ;; strict mode (default)
(manifest/validate-manifest manifest {:strict? false}) ;; skip :on-error requirement
Fragment API
(require '[mycelium.fragment :as fragment])
(fragment/load-fragment "fragments/auth.edn") ;; load from classpath
(fragment/validate-fragment fragment-data) ;; validate structure
(fragment/expand-fragment frag mapping host-cells) ;; expand one
(fragment/expand-all-fragments manifest) ;; expand all
Manifest fragment references support classpath refs or inline data:
:fragments
{:auth {:ref "fragments/auth.edn" ;; loaded from classpath
:as :start
:exits {:success :dashboard, :failure :login-error}}
:log {:fragment {...inline fragment data...} ;; inline definition
:as :start
:exits {:success :next-step}}}
Subworkflows (Nested Composition)
Wrap a workflow as a single opaque cell. See subworkflows.md.
(require '[mycelium.compose :as compose])
;; Register a workflow as a reusable cell
(compose/register-workflow-cell!
:payment/flow
{:cells {...} :edges {...} :dispatches {...}}
{:input [:map ...] :output [:map ...]})
;; Or create spec without registering
(compose/workflow->cell :payment/flow workflow-def schema)
Default dispatches :success / :failure are provided automatically based on :mycelium/error.
Output schema is inferred automatically by walking child workflow edges to :end and collecting output keys. Pass an explicit :output schema to override.
Schema Syntax
Two equivalent ways to write schemas:
;; Lite syntax (simpler, recommended for most cases)
{:input {:subtotal :double, :state :string}
:output {:tax :double}}
;; Malli vector syntax (full power)
{:input [:map [:subtotal :double] [:state :string]]
:output [:map [:tax :double]]}
Lite syntax auto-converts {:key :type} to [:map [:key :type]]. It works in defcell, set-cell-schema!, and manifest schemas. Nested maps are supported:
{:input {:address {:street :string, :city :string}}}
;; becomes [:map [:address [:map [:street :string] [:city :string]]]]
Use full Malli syntax when you need: enums ([:enum :a :b]), unions ([:or :string :int]), optional fields, or other advanced features. Both syntaxes can be mixed freely.
Output Schema Formats
;; Single (all transitions)
:output [:map [:result :int]]
;; Per-transition
:output {:success [:map [:data :string]]
:failure [:map [:error :string]]}
Per-transition schemas are validated based on which dispatch matched. The schema chain validator tracks available keys independently per path.
Constraints
Declare compile-time path invariants that are checked against all enumerated paths:
{:constraints [{:type :must-follow, :if :flag-missing, :then :apply-tags}
{:type :must-precede, :cell :validate, :before :process}
{:type :never-together, :cells [:manual-review :auto-approve]}
{:type :always-reachable, :cell :audit-log}]}
| Type | Meaning |
|---|---|
:must-follow | If :if cell appears on a path, :then cell must appear later on that path |
:must-precede | :cell must appear before :before on every path containing :before |
:never-together | All listed :cells must never appear on the same path |
:always-reachable | :cell must appear on every path that reaches :end (ignores :error/:halt paths) |
- Constraints reference workflow cell names (including join names), not join member cells or cell registry IDs
:always-reachablepasses vacuously if no paths reach:end(all paths go to:error/:halt)- Violations throw at compile time with the specific path that violates the constraint
Compile-Time Validation
compile-workflow validates before any code runs:
- Cell existence — all referenced cells must be registered
- Edge targets — must point to valid cells, join names, or
:end/:error/:halt - Reachability — every cell and join must be reachable from
:start - Dispatch coverage — every edge label must have a dispatch predicate, and vice versa
- Schema chain — each cell's input keys must be available from upstream outputs (join-aware)
- Constraints — path invariants checked against all enumerated paths (see Constraints)
- Graph timeouts — timeout cells exist, values are positive integers, cells have
:timeoutedge - Error groups — grouped cells exist, error handler exists, no cell in multiple groups
- Resilience validation — policy keys valid, referenced cells exist, timeout-ms positive
- Join validation — member cells exist, no name collisions, members have no edges, output keys disjoint (or
:merge-fnprovided) - Transform validation — referenced cells exist, edge labels match cell's edges, each spec has
:fn(function) and valid:schema
Edge Targets
| Target | Meaning |
|---|---|
:cell-name | Next cell in workflow |
:join-name | Enter a join node |
:end | Workflow completes successfully |
:error | Workflow terminates with error |
:halt | Workflow halts |
:_exit/name | Fragment exit reference (resolved during expansion) |
Ring Middleware
(require '[mycelium.middleware :as mw])
;; Create a Ring handler from a pre-compiled workflow
(mw/workflow-handler compiled {:resources {:db db}})
;; With custom input/output transforms
(mw/workflow-handler compiled
{:resources {:db db}
:input-fn (fn [req] {:http-request req}) ;; default
:output-fn mw/html-response}) ;; default
;; :resources can be a function for per-request construction
(mw/workflow-handler compiled
{:resources (fn [req] {:db db :request req})})
;; Standard HTML response helper
(mw/html-response result) ;; => {:status 200 :body (:html result) ...}
Dev Tools
(require '[mycelium.dev :as dev])
;; Test a cell in isolation
(dev/test-cell :cell/id {:input {:key "val"} :resources {:db db}})
;; => {:pass? true, :output {...}, :errors [], :duration-ms 0.42}
;; Test with dispatch verification
(dev/test-cell :cell/id
{:input {:key "val"}
:dispatches [[:success (fn [d] (:result d))]
[:failure (fn [d] (:error d))]]
:expected-dispatch :success})
;; => {:pass? true, :matched-dispatch :success, :output {...}, ...}
;; Test multiple transitions
(dev/test-transitions :cell/id
{:found {:input {:id "alice"} :resources {:db db}
:dispatches [[:found (fn [d] (:profile d))]
[:not-found (fn [d] (:error d))]]}
:not-found {:input {:id "nobody"} :resources {:db db}
:dispatches [[:found (fn [d] (:profile d))]
[:not-found (fn [d] (:error d))]]}})
;; Enumerate all paths from :start to terminal states
(dev/enumerate-paths workflow-def)
;; Generate DOT graph for visualization
(dev/workflow->dot workflow-def)
;; Check cell implementation status
(dev/workflow-status manifest)
;; => {:total 4, :passing 2, :failing 1, :pending 1, :cells [...]}
;; Static analysis — reachability, unreachable states, cycles
(dev/analyze-workflow workflow-def)
;; => {:reachable #{:start :step2} :unreachable #{} :no-path-to-end #{} :cycles []}
;; Infer accumulated schema at each cell
(dev/infer-workflow-schema workflow-def)
;; => {:start {:available-before #{:x}, :adds #{:result}, :available-after #{:x :result}}
;; :step2 {:available-before #{:x :result}, :adds #{:total}, ...}}
;; Generate cell stubs from a workflow definition
(dev/generate-stubs workflow-def)
;; => prints defcell forms with schemas and TODO handlers
;; Useful for scaffolding — fill in handler logic after generating
;; Infer schemas from test data (run workflow, observe actual shapes)
(dev/infer-schemas workflow-def {} [test-input-1 test-input-2])
;; => {:start {:input [:map [:x :int]], :output [:map [:x :int] [:result :int]]}
;; :step2 {:input [:map [:x :int] [:result :int]], :output [:map ...]}}
;; Apply inferred schemas to cell registry
(dev/apply-inferred-schemas! inferred workflow-def)
analyze-workflow, infer-workflow-schema, generate-stubs, infer-schemas, and apply-inferred-schemas! are also available via myc/.
Agent Orchestration
(require '[mycelium.orchestrate :as orch])
;; Generate briefs for all cells (for parallel agent assignment)
(orch/cell-briefs manifest)
;; => {:start {:id :auth/parse, :prompt "## Cell: ...", ...}, ...}
;; Generate a targeted brief after a cell implementation fails
(orch/reassignment-brief manifest :validate
{:error "Output missing key :session-valid"
:input {:user-id "alice" :auth-token "tok_abc"}
:output {:session-valid nil}})
;; Build plan — which cells can be implemented in parallel
(orch/plan manifest)
;; => {:scaffold [:start :validate ...], :parallel [[...]], :sequential []}
;; Progress report
(println (orch/progress manifest))
;; Region brief — scoped context for a subgraph cluster
(orch/region-brief manifest :auth)
;; => {:cells [{:name :start, :id :auth/parse, :schema {...}}, ...]
;; :internal-edges {:start {:ok :validate-session}}
;; :entry-points [:start]
;; :exit-points [{:cell :validate-session, :transitions {:authorized :fetch-profile}}]
;; :prompt "## Region: auth\n..."}
Regions
Group cells into named regions in the manifest for LLM context scoping:
{:cells {:start :auth/parse, :validate :auth/validate, :fetch :user/fetch, :render :ui/render}
:regions {:auth [:start :validate]
:data-fetch [:fetch]}}
- Region cells must exist in
:cells - No cell may appear in multiple regions
region-briefreturns cell schemas, internal edges, entry/exit points, and a prompt- Exit point
:transitionsis always a map — unconditional edges use{:unconditional :target} - Regions are purely informational — no runtime behavior change
Workflow Trace
Every run produces :mycelium/trace — a vector of step-by-step execution records:
;; Each trace entry:
{:cell :fetch-user ;; workflow cell name
:cell-id :user/fetch ;; registry cell ID
:transition :success ;; dispatch label taken (nil for unconditional)
:data {...} ;; data snapshot after handler ran
:duration-ms 12.4 ;; execution time
:error {...} ;; schema error details (only on validation failure)
:join-traces [{...}]} ;; per-member timing (only for join nodes)
Asserting on Traces
(let [result (myc/run-workflow wf {} {:x 5})
trace (:mycelium/trace result)]
(is (= [:start :add] (mapv :cell trace)))
(is (= [:done :done] (mapv :transition trace)))
(is (= 20 (get-in (last trace) [:data :result]))))
System Queries
(require '[mycelium.system :as sys])
(sys/cell-usage system :cell/id) ;; => ["/route1" "/route2"]
(sys/route-cells system "/route") ;; => #{:cell/a :cell/b}
(sys/route-resources system "/route") ;; => #{:db}
(sys/schema-conflicts system) ;; => [{:cell-id ... :routes ...}]
(sys/system->dot system) ;; => DOT graph string
Halt & Resume (Human-in-the-Loop)
A cell can pause the workflow by returning :mycelium/halt in its data. The workflow halts after that cell, preserving all accumulated data and trace. A human (or external process) can later resume the workflow from where it stopped.
Halting
;; Cell signals halt by assoc'ing :mycelium/halt into data
(defmethod cell/cell-spec :review/check [_]
{:id :review/check
:handler (fn [_ data]
(assoc data :mycelium/halt {:reason :needs-approval
:item (:item-id data)}))
:schema {:input [:map [:item-id :string]] :output [:map]}})
:mycelium/halt can be true or a map with context for the human reviewer.
Resuming
(let [compiled (myc/pre-compile workflow-def)
halted (myc/run-compiled compiled resources {:item-id "X"})
;; halted contains :mycelium/halt and :mycelium/resume
;; Inspect halted, get human input, then resume:
result (myc/resume-compiled compiled resources halted {:approved true})]
;; result has data from before + after halt, :mycelium/halt cleared
(:approved result)) ;; => true
resume-compiled takes an optional merge-data map (4th arg) that is merged into the data before resuming — useful for injecting human-provided input.
Key Behaviors
- Data accumulates — all keys from before the halt are available after resume
- Trace is continuous —
:mycelium/tracespans the full execution (before + after halt) - Multiple halts — a workflow can halt and resume multiple times
- Branching — if a halting cell dispatches to a branch, resume continues on the correct branch
- Halt trace entry — the trace entry for the halting cell has
:halted true - Resume validation — calling
resume-compiledon a non-halted result throws an exception
Persistent Store
For workflows that halt across process restarts or sessions, use the WorkflowStore protocol to persist and retrieve halted state:
(require '[mycelium.store :as store])
;; In-memory store (dev/testing)
(def s (store/memory-store))
;; Run — auto-persists on halt, returns {:mycelium/session-id id, :mycelium/halt context}
(def halted (store/run-with-store compiled resources initial-data s))
;; Resume by session ID — loads from store, resumes, cleans up on completion
(def result (store/resume-with-store compiled resources (:mycelium/session-id halted) s))
;; Resume with human input
(store/resume-with-store compiled resources session-id s {:approved true})
Implement the protocol for your persistence backend (DB, Redis, etc.):
(defrecord MyStore [conn]
store/WorkflowStore
(save-workflow! [_ session-id data] ...)
(load-workflow [_ session-id] ...)
(delete-workflow! [_ session-id] ...)
(list-workflows [_] ...))
- On halt: state persisted,
:mycelium/session-idreturned (:mycelium/resumehidden from caller) - On resume completion: state deleted from store automatically
- On re-halt: store updated with new state, same session ID reused
- Custom session IDs:
(store/run-with-store compiled res data s {:session-id "my-id"})
Workflow Result Keys
| Key | Description |
|---|---|
:mycelium/trace | Vector of execution trace entries (see Workflow Trace) |
:mycelium/input-error | Input schema validation failure (workflow didn't run) |
:mycelium/schema-error | Runtime schema violation details (includes :message, :cell-name, :failed-keys, :cell-path, clean :data) |
:mycelium/join-error | Join node error details |
:mycelium/timeout | true when a cell exceeded its graph-level timeout |
:mycelium/error | Error group error details ({:cell :name, :message "..."}) |
:mycelium/resilience-error | Resilience policy trigger details (:type, :cell, :message) |
:mycelium/child-trace | Nested workflow trace (composed cells) |
:mycelium/halt | Halt context (true or map) — present when workflow is halted |
:mycelium/resume | Resume state token — present when workflow is halted |
:mycelium/session-id | Store session ID — present when using run-with-store/resume-with-store on halt |
Unified Error Inspection
Instead of checking individual keys, use workflow-error and error?:
(myc/error? result) ;; => true/false
(myc/workflow-error result) ;; => {:error-type :schema/output, :cell-id :app/step,
;; :cell-name :step, :failed-keys {:x {...}},
;; :key-diff {:missing #{:expected-key}, :extra #{:typo-key}},
;; :message "Schema output validation failed at step (:app/step)
;; Missing key(s): #{:expected-key}
;; Extra key(s): #{:typo-key}",
;; :details {...}} or nil
Error types: :schema/input, :schema/output, :handler, :resilience/timeout, :resilience/circuit-open, :resilience/bulkhead-full, :resilience/rate-limited, :join, :timeout, :input.
Mycelium