Mycelium
Mycelium uses Maestro state machines and Malli contracts to define "The Law of the Graph," providing a high-integrity environment where humans architect and AI agents implement.
Install / Use
/learn @mycelium-clj/MyceliumREADME
Mycelium
Schema-enforced, composable workflow components for Clojure. Built on Maestro.
Mycelium structures applications as directed graphs of pure data transformations. Each node (cell) has explicit input/output schemas. Cells are developed and tested in complete isolation, then composed into workflows that are validated at compile time. Routing between cells is determined by dispatch predicates defined at the workflow level — handlers compute data, the graph decides where it goes.
Why
LLM coding agents fail at large codebases for the same reason humans do: unbounded context. Mycelium solves this by constraining each component to a fixed scope with an explicit contract. An agent implementing a cell never needs to see the rest of the application — just its schema and test harness.
See Managing Complexity with Mycelium for a longer rationale.
Quick Start
;; deps.edn
{:deps {io.github.yogthos/mycelium {:git/url "https://github.com/yogthos/mycelium"
:git/sha "..."}}}
Define Cells
A cell is a function with schema contracts, registered via defmethod:
(require '[mycelium.cell :as cell])
(defmethod cell/cell-spec :math/double [_]
{:id :math/double
:handler (fn [_resources data]
(assoc data :result (* 2 (:x data))))
:schema {:input [:map [:x :int]]
:output [:map [:result :int]]}})
(defmethod cell/cell-spec :math/add-ten [_]
{:id :math/add-ten
:handler (fn [_resources data]
(assoc data :result (+ 10 (:result data))))
:schema {:input [:map [:result :int]]
:output [:map [:result :int]]}})
Each cell is a miniature program akin to a microservice. The purpose of a cell is to encapsulate the implementation details for a particular step in the application workflow. For example, a cell handling authentication might check the database for the user account, compare it with the provided credentials, and then return an updated state with additional keys indicating the result of the operation.
Cells must:
- Return the data map (application state) with any computed values added
- Produce output satisfying their
:outputschema on every path - Only use resources passed via the first argument
Compose into Workflows
(require '[mycelium.core :as myc])
(let [result (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
(:result result))
;; => 20
Pipeline Shorthand
For linear (unbranched) workflows, use :pipeline instead of wiring :edges and :dispatches by hand:
(myc/run-workflow
{:cells {:start :math/double
:add :math/add-ten}
:pipeline [:start :add]}
{} {:x 5})
;; Equivalent to {:edges {:start :add, :add :end}, :dispatches {}}
:pipeline expands each pair into an unconditional edge and appends :end after the last cell. It is mutually exclusive with :edges, :dispatches, and :joins. Works in both programmatic workflow definitions and manifests.
Branching
Edges map transition labels to targets. Dispatch predicates examine the data to determine which edge to take:
(defmethod cell/cell-spec :check/threshold [_]
{:id :check/threshold
:handler (fn [_ data]
(assoc data :above-threshold (> (:value data) 10)))
:schema {:input [:map [:value :int]]
:output [:map]}})
(myc/run-workflow
{: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] (:above-threshold data))]
[:low (fn [data] (not (:above-threshold data)))]]
:big [[:done (constantly true)]]
:small [[:done (constantly true)]]}}
{} {:value 42})
Handlers compute data; dispatch predicates decide the route. This keeps business logic decoupled from graph navigation.
Default Transitions
Use :default as an edge label for a catch-all fallback. If no other dispatch predicate matches, the :default edge is taken automatically — no dispatch predicate needed:
(myc/run-workflow
{:cells {:start :check/threshold
:big :process/big-values
:err :process/error-handler}
:edges {:start {:high :big, :default :err}
:big {:done :end}
:err {:done :end}}
:dispatches {:start [[:high (fn [data] (> (:value data) 10))]]
:big [[:done (constantly true)]]
:err [[:done (constantly true)]]}}
{} {:value 3})
;; value <= 10, :high predicate fails → :default catches → routes to :err
This is especially useful as a safety net for agent-generated routing logic. :default must not be the only edge — if all paths lead to the same target, use an unconditional keyword edge instead.
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. Handlers stay pure — no timeout logic needed:
(myc/run-workflow
{:cells {:start :api/fetch-data
:render :ui/render-result
:fallback :ui/show-error}
:edges {:start {:done :render, :timeout :fallback}
:render :end
:fallback :end}
:dispatches {:start [[:done (fn [d] (not (:mycelium/timeout d)))]]}
:timeouts {:start 5000}} ;; 5 seconds
{} {:url "https://api.example.com/data"})
The :timeout dispatch is auto-injected and evaluated first (before user predicates and :default). This is distinct from resilience :timeout policies — graph timeouts route to an alternative cell, resilience timeouts produce :mycelium/resilience-error.
Error Groups
Declare shared error handling across 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:
(myc/run-workflow
{: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}}}
{} {})
This is syntactic sugar — each grouped cell gets an :on-error edge and dispatch predicate injected at compile time. The error handler receives :mycelium/error with {:cell :cell-name, :message "..."}.
Per-Transition Output Schemas
Cells with multiple outgoing edges can declare different output schemas for each transition:
(defmethod cell/cell-spec :user/fetch [_]
{:id :user/fetch
:handler (fn [{:keys [db]} data]
(if-let [profile (get-user db (:user-id data))]
(assoc data :profile profile)
(assoc data :error-message "Not found")))
:schema {:input [:map [:user-id :string]]
:output {:found [:map [:profile [:map [:name :string] [:email :string]]]]
:not-found [:map [:error-message :string]]}}
:requires [:db]})
In the workflow, per-transition schemas are validated based on which dispatch matched:
;; Single schema (all transitions must satisfy it)
:output [:map [:profile map?]]
;; Per-transition schemas (each transition has its own contract)
:output {:found [:map [:profile [:map [:name :string] [:email :string]]]]
:not-found [:map [:error-message :string]]}
The schema chain validator tracks which keys are available on each path independently, so a downstream cell on the :found path can require :profile without the :not-found path needing to produce it.
Resources
External dependencies are injected, never acquired by cells:
(defmethod cell/cell-spec :user/fetch [_]
{:id :user/fetch
:handler (fn [{:keys [db]} data]
(if-let [profile (get-user db (:user-id data))]
(assoc data :profile profile)
(assoc data :error-message "Not found")))
:schema {:input [:map [:user-id :string]]
:output {:found [:map [:profile [:map [:name :string] [:email :string]]]]
:not-found [:map [:error-message :string]]}}
:requires [:db]})
;; Resources are passed at run time
(myc/run-workflow workflow {:db my-db-conn} {:user-id "alice"})
Async Cells
(defmethod cell/cell-spec :api/fetch-data [_]
{:id :api/fetch-data
:handler (fn [_resources data callback error-callback]
(future
(try
(let [resp (http/get (:url data))]
(callback (assoc data :response resp)))
(catch Exception e
(error-callback e)))))
:schema {:input [:map [:url :string]]
:output [:map [:response map?]]}
:async? true})
Parameterized Cells
The same cell handler can be reused with different configuration by passing a map with :id and
Related Skills
node-connect
351.8kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
110.9kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
351.8kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
351.8kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
