Quick Reference

Contents

  1. Mycelium Quick Reference
  2. Thinking in Mycelium
    1. Design Process
    2. Key Mental Model
    3. Structuring a Solution
  3. Core API
    1. Compilation Options
  4. Accumulating Data Model
  5. Cell Registration
    1. defcell (recommended)
    2. defmethod (low-level)
    3. Cell Registry Helpers
  6. Workflow Definition
    1. Pipeline Shorthand
    2. Branching
    3. Default Transitions
    4. Graph-Level Timeouts
    5. Error Groups
  7. Join Nodes (Fork-Join)
    1. Key Concepts
    2. Join Options
    3. Output Key Conflicts
    4. Join Error Handling
    5. Join Trace
  8. Interceptors
  9. Edge Transforms
    1. Input Transform (reshape before cell runs)
    2. Output Transform (reshape after cell runs)
    3. Edge-Specific Output Transforms (branching cells)
    4. Combining Input and Edge-Specific Output Transforms
    5. When to Use Transforms vs. Cells
    6. Transform Spec
  10. Parameterized Cells
  11. Resilience Policies
  12. Manifest Loading
    1. Manifest Cell Fields
    2. Manifest Validation
  13. Fragment API
  14. Subworkflows (Nested Composition)
  15. Schema Syntax
  16. Output Schema Formats
  17. Constraints
  18. Compile-Time Validation
  19. Edge Targets
  20. Ring Middleware
  21. Dev Tools
  22. Agent Orchestration
    1. Regions
  23. Workflow Trace
    1. Asserting on Traces
  24. System Queries
  25. Halt & Resume (Human-in-the-Loop)
    1. Halting
    2. Resuming
    3. Key Behaviors
    4. Persistent Store
  26. Workflow Result Keys
    1. Unified Error Inspection

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

  1. 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.
  2. 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 :tax can be 5 steps downstream from the cell that produced :discounted-subtotal, and it will still receive that key.
  3. 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).
  4. 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
  • :default must not be the only edge (use an unconditional keyword edge instead)
  • You can provide an explicit :default predicate in :dispatches to override the auto-generated one
  • :default is always evaluated last, even if listed first in :dispatches
  • Works with join nodes — add :default alongside :done/:failure edges
  • Trace entries record :default as 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 :timeout edge target
  • A :timeout dispatch 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? true when a cell times out
  • Distinct from resilience :timeout policies — 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-error target
  • :on-error dispatch predicate is auto-injected and evaluated first
  • :mycelium/error contains {:cell :cell-name, :message "..."} — available to the error handler
  • Output schema validation is skipped for errored cells
  • Error handler cells can dissoc :mycelium/error to 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 :cells but have no entries in :edges — the join consumes them
  • The join name (:fetch-data) appears in :edges like 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 / :failure are provided based on whether any branch threw an exception

Join Options

OptionDefaultDescription
:cells(required)Vector of cell names to run
:strategy:parallel:parallel or :sequential
:merge-fnnil(fn [data results-vec]) — custom merge when output keys overlap
:timeout-ms30000Timeout 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-fn provided — 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:

ScopeMatches
:allEvery 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}]}
TypeMeaning
:must-followIf :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-togetherAll 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-reachable passes 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 :timeout edge
  • 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-fn provided)
  • Transform validation — referenced cells exist, edge labels match cell's edges, each spec has :fn (function) and valid :schema

Edge Targets

TargetMeaning
:cell-nameNext cell in workflow
:join-nameEnter a join node
:endWorkflow completes successfully
:errorWorkflow terminates with error
:haltWorkflow halts
:_exit/nameFragment 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-brief returns cell schemas, internal edges, entry/exit points, and a prompt
  • Exit point :transitions is 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/trace spans 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-compiled on 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-id returned (:mycelium/resume hidden 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

KeyDescription
:mycelium/traceVector of execution trace entries (see Workflow Trace)
:mycelium/input-errorInput schema validation failure (workflow didn't run)
:mycelium/schema-errorRuntime schema violation details (includes :message, :cell-name, :failed-keys, :cell-path, clean :data)
:mycelium/join-errorJoin node error details
:mycelium/timeouttrue when a cell exceeded its graph-level timeout
:mycelium/errorError group error details ({:cell :name, :message "..."})
:mycelium/resilience-errorResilience policy trigger details (:type, :cell, :message)
:mycelium/child-traceNested workflow trace (composed cells)
:mycelium/haltHalt context (true or map) — present when workflow is halted
:mycelium/resumeResume state token — present when workflow is halted
:mycelium/session-idStore 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.