Mycelium Implementation Guide
A concise reference for implementing workflows with Mycelium. Designed for use as LLM context — include this file when prompting an agent to build Mycelium applications.
Workflow in 60 Seconds
A Mycelium workflow is a directed graph of cells (pure data transformations) connected by edges (routing). Data accumulates through the graph — each cell receives all keys from every upstream cell.
(require '[mycelium.cell :as cell]
'[mycelium.core :as myc])
;; 1. Define cells (lite schema syntax — {key type})
(cell/defcell :app/validate
{:input {:name :string}
:output {:valid :boolean}}
(fn [_resources data]
{:valid (not (empty? (:name data)))}))
(cell/defcell :app/greet
{:input {:name :string}
:output {:greeting :string}}
(fn [_resources data]
{:greeting (str "Hello, " (:name data) "!")}))
;; 2. Define workflow
(def workflow
{:cells {:start :app/validate
:greet :app/greet}
:edges {:start :greet
:greet :end}})
;; 3. Run it
(myc/run-workflow workflow {} {:name "Alice"})
;; => {:name "Alice", :valid true, :greeting "Hello, Alice!", :mycelium/trace [...]}
Cell Definition
Use cell/defcell to register cells. The ID is specified once (no duplication).
;; Minimal — no schema
(cell/defcell :ns/cell-id
(fn [resources data]
{:result-key "value"}))
;; With schema (recommended — lite syntax)
(cell/defcell :ns/cell-id
{:input {:x :int}
:output {:y :int}}
(fn [resources data]
{:y (* 2 (:x data))}))
;; With schema + options
(cell/defcell :ns/cell-id
{:input {:x :int}
:output {:y :int}
:doc "Doubles the input"
:requires [:db]}
(fn [{:keys [db]} data]
{:y (* 2 (:x data))}))
Cell Rules
- Handler signature:
(fn [resources data] -> data-map) - Return only new keys. Key propagation is on by default — input keys are merged automatically. Don't
(assoc data ...), just return{:new-key value}. - Resources for infrastructure. Database connections, HTTP clients, config go in
resources. Data map is for workflow state only. - Errors are data. Set a key (
:status :failed) and let dispatch predicates route it. Don't throw for expected failures. - Schemas use Malli syntax or lite syntax (see below).
Schema Syntax
Two equivalent ways to write schemas:
;; Malli vector syntax (full power)
{:input [:map [:subtotal :double] [:state :string]]
:output [:map [:tax :double]]}
;; Lite syntax (simpler, recommended for most cases)
{:input {:subtotal :double, :state :string}
:output {: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.
Available types: :string, :int, :double, :boolean, :keyword, :uuid, [:enum :a :b], [:vector :type], [:map-of :key-type :val-type], etc.
Workflow Patterns
Linear Pipeline
{:cells {:start :app/a, :step2 :app/b, :step3 :app/c}
:pipeline [:start :step2 :step3]}
;; Expands to: :edges {:start :step2, :step2 :step3, :step3 :end}
Branching
{:cells {:start :app/check
:ok :app/process
:err :app/error}
:edges {:start {:pass :ok, :fail :err}
:ok :end, :err :end}
:dispatches {:start [[:pass (fn [d] (:valid d))]
[:fail (fn [d] (not (:valid d)))]]}}
Handlers compute data. Dispatch predicates choose the route. Separate concerns.
Per-Transition Output Schema
When a cell branches, declare output per edge:
(cell/defcell :app/check
{:input [:map [:value :int]]
:output [:per-transition
{:pass [:map [:status [:= :ok]]]
:fail [:map [:status [:= :error]] [:reason :string]]}]}
(fn [_ data]
(if (pos? (:value data))
{:status :ok}
{:status :error :reason "must be positive"})))
Parallel Join
{:cells {:start :app/validate
:tax :app/calc-tax ;; join member — no edges entry
:ship :app/calc-shipping ;; join member — no edges entry
:total :app/compute-total}
:joins {:fees {:cells [:tax :ship] :strategy :parallel}}
:edges {:start :fees
:fees {:done :total, :failure :end}
:total :end}}
Join members have no entries in :edges. Each gets the same input snapshot. Output keys must not overlap.
Default Fallback
:edges {:start {:success :ok, :default :fallback}}
:dispatches {:start [[:success (fn [d] (:valid d))]]}
;; :default auto-generates (constantly true) as last predicate
Development Workflow (3 Phases)
Mycelium supports an iterative development workflow. You don't need to get everything right on the first try.
When working from an EDN manifest, the same loop is available from the shell through the Agent CLI: myc status to find failing cells, myc brief <cell> for the contract, myc test <cell> to verify in isolation, and myc patch for checked structural edits.
Phase 1: Structure (compile-time feedback)
Write the manifest with cells, edges, and schemas. Pre-compilation catches structural errors immediately:
(myc/pre-compile workflow-def) ;; Throws on: missing edges, unreachable cells, bad schemas
Generate cell stubs from the workflow structure:
(println (myc/generate-stubs workflow-def))
;; Prints defcell forms with schemas and TODO handlers
Phase 2: Logic (run with warnings, iterate)
Write handler implementations. Run with :validate :warn to see all problems at once without halting:
(let [result (myc/run-workflow workflow {} test-data {:validate :warn})]
;; Workflow runs to completion even if schemas don't match
(println (:mycelium/warnings result)))
;; => [{:cell-id :app/tax, :phase :output,
;; :message "Schema output validation failed at :app/tax
;; Missing key(s): #{:tax}
;; Extra key(s): #{:tax-amount}",
;; :key-diff {:missing #{:tax}, :extra #{:tax-amount}}}]
Schema errors now include key-diff suggestions — if you misname a key, the error tells you exactly what to rename.
You can also skip validation entirely during early development:
(myc/run-workflow workflow {} test-data {:validate :off})
Or infer schemas from test runs — write handlers first, let Mycelium figure out the schemas:
(require '[mycelium.dev :as dev])
;; Run with test data, observe actual shapes
(def inferred (dev/infer-schemas workflow-def {} test-inputs))
;; => {:start {:input [:map [:x :int]], :output [:map [:x :int] [:result :int]]}
;; :fmt {:input [:map [:x :int] [:result :int]], :output [:map ...]}}
;; Apply inferred schemas to cell registry
(dev/apply-inferred-schemas! inferred workflow-def)
Phase 3: Contract (strict mode)
Once logic is correct, lock down schemas. Run with :validate :strict (the default):
(let [result (myc/run-workflow workflow {} data)]
;; Schema violations halt execution with precise error
(when-let [err (myc/workflow-error result)]
(println (:message err)) ;; human-readable with key-diff suggestions
(println (:key-diff err)) ;; {:missing #{...} :extra #{...}}
(println (:failed-keys err)) ;; per-key details with types
))
Testing Cells in Isolation
(dev/test-cell :app/validate {:input {:name "Alice"}})
;; => {:pass? true, :output {:valid true}, :errors [], :duration-ms 0.1}
From the shell, against a manifest: myc test workflow.edn validate --input '{:name "Alice"}' --require app.cells (see Agent CLI).
Common Mistakes
1. Returning the full data map
;; WRONG — don't thread the full map
(fn [_ data] (assoc data :y (* 2 (:x data))))
;; RIGHT — return only new keys (key propagation merges input)
(fn [_ data] {:y (* 2 (:x data))})
Both work, but the second is idiomatic. Key propagation is on by default.
2. Wrong items format
If the spec says items are [{"laptop" 1} {"shirt" 2}], keep that format. Mycelium doesn't impose any format on your data — it flows whatever you put in. Match the domain spec exactly.
3. Returning nil from a cell
Cell handlers must return a map! Returning nil causes downstream failures. If a cell has nothing to add, return {}.
Quick Reference
| Concept | Syntax |
|---|---|
| Register cell | (cell/defcell :ns/id {:input {:x :int} :output {:y :int}} handler-fn) |
| Lite schema | {:input {:x :int}} → {:input [:map [:x :int]]} |
| Linear pipeline | {:pipeline [:a :b :c]} |
| Branching edges | {:edges {:start {:label :target}}} |
| Dispatch | {:dispatches {:start [[:label (fn [d] pred)]]}} |
| Parallel join | {:joins {:name {:cells [:a :b] :strategy :parallel}}} |
| Run workflow | (myc/run-workflow wf resources data) |
| Run with warnings | (myc/run-workflow wf res data {:validate :warn}) |
| Run without validation | (myc/run-workflow wf res data {:validate :off}) |
| Pre-compile | (def c (myc/pre-compile wf)) then (myc/run-compiled c res data) |
| Check error | (myc/workflow-error result) — includes :key-diff suggestions |
| Infer schemas | (dev/infer-schemas wf res [test-data ...]) |
| Apply inferred | (dev/apply-inferred-schemas! inferred wf) |
| Test cell | (dev/test-cell :ns/id {:input {...}}) |
| Generate stubs | (println (myc/generate-stubs wf)) |
| Coerce types | (myc/pre-compile wf {:coerce? true}) |
Mycelium