Commando is a flexible Clojure/ClojureScript library for building data-driven DSLs.
;; deps.edn with git
{org.clojars.funkcjonariusze/commando {:mvn/version "1.2.0"}}
;; leiningen
[org.clojars.funkcjonariusze/commando "1.2.0"]
(require '[commando.core :as commando])
(require '[commando.commands.builtin :as commands-builtin])
(commando/execute
[commands-builtin/command-from-spec]
{"1" 1
"2" {:commando/from ["1"]}
"3" {:commando/from ["2"]}})
;; RETURN =>
;; {:instruction {"1" 1, "2" 1, "3" 1}
;; :status :ok
;; :errors []
;; :warnings []
;; :successes [{:message "All commands executed successfully"}]}
Hands-on intro — open
examples/walkthrough.cljin your REPL and evaluate forms one by one to get a feel for the library.
Commando turns a plain Clojure map into a small program:
① Instruction (your data + commands)
{:price 100
:tax 10.2
:total {:commando/fn + ◄── "call (+ price tax-amount)"
:args [{:commando/from [:price]}
{:commando/from [:tax]}]}}
② Dependency graph (built automatically)
:price ──┐
├──► :total
:tax ──┘
③ Result (commands replaced with values)
{:price 100
:tax 10.2
:total 110.2}
The commands themselves are pluggable. Commando ships with a handful of built-in ones (:commando/from, :commando/fn, :commando/apply, ...), but the real power is that you define your own — any predicate that recognizes a pattern in data can become a command.
This makes Commando a toolkit for building domain-specific languages out of ordinary maps: migration scripts, integration pipelines, configuration layers, form builders — anything where you want a declarative data structure that does something.
The examples below use custom commands (see Adding New Commands). Nothing here is built into Commando — it shows what a domain DSL can look like once you define your own command specs.
Database migration — roles are created before users, because Commando resolves the dependency graph automatically:
(commando/execute
my-db-registry
{:roles
{:admin {:sql/insert :permissions :name "admin"}
:viewer {:sql/insert :permissions :name "viewer"}}
:users
[{:sql/insert :users
:record {:name "Alice"
:role {:commando/from [:roles :admin] :=> [:get :id]}}}
{:sql/insert :users
:record {:name "Bob"
:role {:commando/from [:roles :viewer] :=> [:get :id]}}}]})
Chained queries — a tree of database queries where each depends on a previous result:
(commando/execute
[db-query-spec builtin/command-from-spec]
{:user {:db/query :users :where {:id 42}}
:posts {:db/query :posts
:where {:author-id {:commando/from [:user] :=> [:get :id]}}}
:comments {:db/query :comments
:where {:post-ids {:commando/from [:posts] :=> [:fn #(mapv :id %)]}}}})
;; => {:user {:id 42 :name "Alice" ...}
;; :posts [{:id 1 ...} {:id 2 ...}]
;; :comments [{:post-id 1 ...} {:post-id 2 ...} ...]}
Design tokens / style map — build a Reagent-friendly style system where component styles derive from shared tokens:
(commando/execute
[builtin/command-from-spec
builtin/command-fn-spec]
{:tokens {:primary "#3b82f6" :danger "#ef4444"
:radius "8px" :space-sm "8px"}
:button {:commando/fn merge
:args [{:background {:commando/from [:tokens :primary]}}
{:border-radius {:commando/from [:tokens :radius]}}
{:padding {:commando/from [:tokens :space-sm]}}]}
:alert {:commando/fn merge
:args [{:background {:commando/from [:tokens :danger]}}
{:border-radius {:commando/from [:tokens :radius]}}
{:padding {:commando/from [:tokens :space-sm]}}]}})
;; => {:tokens {...}
;; :button {:background "#3b82f6" :border-radius "8px" :padding "8px"}
;; :alert {:background "#ef4444" :border-radius "8px" :padding "8px"}}
Commando is a graph-based resolver with easy configuration — it is not limited by any architectural constraints or specific framework.
(require '[commando.core :as commando])
(require '[commando.commands.builtin :as commands-builtin])
(commando/execute
[commands-builtin/command-from-spec] ;; <--- CommandRegistry
;;
;; .---- Instruction
;; V
{"1" 1
;; Command -----.
;; |
"2" {:commando/from ["1"] :=> [:fn inc]} ;; <---'
"3" {:commando/from ["2"] :=> [:fn inc]}})
;; => {:instruction {"1" 1, "2" 2, "3" 3}}
The above function composes "Instructions", "Commands", and a "CommandRegistry".
:commando/from returns a value by absolute or relative path, with optional post-processing via the :=> driver key.registry-create and pass it directly. Use registry-add / registry-remove to modify a built registry.The basic commands are found in namespace commando.commands.builtin. It describes core commands and their behaviors. Behavior of those commands declared with configuration map called CommandMapSpecs.
Retrieves a value from the instruction by path. Use the :=> driver key to post-process the result (see Drivers).
Absolute path — list of keys from the instruction root:
(commando/execute
[commands-builtin/command-from-spec]
{:catalog {:price 99}
:ref {:commando/from [:catalog :price]}})
;; => {:catalog {:price 99}, :ref 99}
Relative path — "../" goes up one level from the command's position, "./" stays at the current level:
(commando/execute
[commands-builtin/command-from-spec]
{"section-a" {"price" 10 "ref" {"commando-from" ["../" "price"]}}
"section-b" {"price" 20 "ref" {"commando-from" ["../" "price"]}}})
;; => {"section-a" {"price" 10, "ref" 10}
;; "section-b" {"price" 20, "ref" 20}}
Named anchors — mark any map with "__anchor" or :__anchor, then jump to it with "@name" regardless of nesting depth. The resolver finds the nearest ancestor with that name, so duplicate anchor names are safe — each command resolves to its own closest one:
(commando/execute
[commands-builtin/command-from-spec]
{:items [{:__anchor "item" :price 10 :total {:commando/from ["@item" :price]}}
{:__anchor "item" :price 20 :total {:commando/from ["@item" :price]}}]})
;; => {:items [{:__anchor "item", :price 10, :total 10}
;; {:__anchor "item", :price 20, :total 20}]}
Anchors and "../" can be combined in one path — after jumping to the anchor, navigation continues from there:
{:commando/from ["@section" "../" :base-price]}
A convenient wrapper over apply.
(commando/execute
[commands-builtin/command-fn-spec]
{:commando/fn +
:args [1, 2, 3]})
;; => 6
(commando/execute
[commands-builtin/command-fn-spec]
{"v1" 1
"v2" 2
"sum="
{:commando/fn +
:args [{:commando/from ["v1"]}
{:commando/from ["v2"]}
3]}})
;; => {"v1" 1 "v2" 2 "sum=" 6}
Returns the value of :commando/apply as-is. Use :=> driver to post-process the result.
(commando/execute
[commands-builtin/command-apply-spec]
{"0" {:commando/apply
{"1" {:commando/apply
{"2" {:commando/apply
{"3" {:commando/apply {"4" {:final "5"}}
:=> [:get "4"]}}
:=> [:get "3"]}}
:=> [:get "2"]}}
:=> [:get "1"]}})
;; => {"0" {:final "5"}}
Imagine the following instruction is your initial database migration, adding users to the DB:
(commando/execute
[commands-builtin/command-from-spec
commands-builtin/command-mutation-spec]
{"add-new-user-01" {:commando/mutation :add-user :name "Bob Anderson"
:permissions [{:commando/from ["perm_send_mail"] :=> [:get :id]}
{:commando/from ["perm_recv_mail"] :=> [:get :id]}]}
"add-new-user-02" {:commando/mutation :add-user :name "Damian Nowak"
:permissions [{:commando/from ["perm_recv_mail"] :=> [:get :id]}]}
"perm_recv_mail" {:commando/mutation :add-permission
:name "receive-email-notification"}
"perm_send_mail" {:commando/mutation :add-permission
:name "send-email-notification"}})
You can see that you need both :add-permission and :add-user commands. In most cases, such patterns can be abstracted and reused, simplifying your migrations and business logic.
commando-mutation-spec uses defmethod commando.commands.builtin/command-mutation underneath, making it easy to wrap business logic into commands and integrate them into your instructions/migrations:
(defmethod commands-builtin/command-mutation :add-user [_ {:keys [name permissions]}]
;; => INSERT INTO user VALUES (name, permissions)
;; => SELECT * FROM user WHERE name = name
;; =RETURN> {:id 1 :name "Bob Anderson"}
)
(defmethod commands-builtin/command-mutation :add-permission [_ {:keys [name]}]
;; => INSERT INTO permission VALUES (name)
;; => SELECT * FROM permission WHERE name = name
;; =RETURN> {:id 1 :name name}
)
This approach enables you to quickly encapsulate business logic into reusable commands, which can then be easily composed in your instructions or migrations.
Allows describing reusable command templates that are expanded into regular Commando commands at runtime. This is useful when you want to describe a pattern for building a complex command or a set of related commands without duplicating the same structure throughout an instruction
Assume we have an instruction that calculates the mean.
(commando/execute
[commands-builtin/command-from-spec
commands-builtin/command-apply-spec
commands-builtin/command-fn-spec]
{:=> [:get :result]
:commando/apply
{:vector-of-numbers [1, 2, 3, 4, 5]
:result
{:fn (fn [& [vector-of-numbers]]
(/ (reduce + 0 vector-of-numbers)
(count vector-of-numbers)))
:args [{:commando/from [:commando/apply :vector-of-numbers]}]}}})
;; => 3
This works, but the structure is not very easy to read when repeated. When you need the same mean calculation many times, the instruction quickly grows and becomes hard to follow. A macro can help by encapsulating the pattern into a readable reusable shortcut.
Define a macro
(defmethod commands-builtin/command-macro :mean-calc [{vector-of-numbers :vector-of-numbers}]
{:=> [:get :result]
:commando/apply
{:vector-of-numbers vector-of-numbers
:result
{:fn (fn [& [vector-of-numbers]]
(/ (reduce + 0 vector-of-numbers)
(count vector-of-numbers)))
:args [{:commando/from [:commando/apply :vector-of-numbers]}]}}})
(commando/execute
[commands-builtin/command-macro-spec
commands-builtin/command-from-spec
commands-builtin/command-apply-spec
commands-builtin/command-fn-spec]
{:v1 {:commando/macro :mean-calc :vector-of-numbers [1, 2, 3, 4, 5]}
:v2 {:commando/macro :mean-calc :vector-of-numbers [10, 22, 33]}
:v3 {:commando/macro :mean-calc :vector-of-numbers [7, 8, 1000, 1]}})
;; =>
;; {:v1 3
;; :v2 21.666
;; :v3 254}
command-macro-spec detects entries with :commando/macro and calls the multimethod (defmethod) commands-builtin/command-macro using the macro identifier (e.g. :mean-calc) and the parameter map from the instruction.
The defmethod should return a Instruction. Commando will then treat that returned map as a fully separate instruction: dependencies (like :commando/from) are discovered inside the macro hierarchy.
Use these macro handlers to hide repeated command structure and keep your instructions shorter and easier to read.
Injects external reference data (dictionaries, config, feature flags) into instructions without duplicating it. Unlike other commands, command-context-spec is a function — call it with your context map to get a CommandMapSpec. The data is captured via closure and resolves before other commands ({:mode :none}), so :commando/from and :commando/fn can reference context results through the standard dependency mechanism.
(def game-config
{:heroes {"warrior" {:hp 120 :damage 15}
"mage" {:hp 80 :damage 25}}
:buffs {:fire-sword 1.5 :shield 2.0}
:settings {:difficulty "hard" :max-level 60}})
(commando/execute
[(commands-builtin/command-context-spec game-config)
commands-builtin/command-from-spec
commands-builtin/command-fn-spec]
{:warrior {:commando/context [:heroes "warrior"]}
:fire-bonus {:commando/context [:buffs :fire-sword]}
:hit-damage {:commando/fn * :args [{:commando/from [:warrior] :=> [:get :damage]}
{:commando/from [:fire-bonus]}]}
:arena-name {:commando/context [:arenas :default] :default "Colosseum"}})
;; => {:warrior {:hp 120 :damage 15}
;; :fire-bonus 1.5
;; :hit-damage 22.5
;; :arena-name "Colosseum"}
Missing path returns nil; use :default for an explicit fallback. Use :=> driver for post-processing the resolved value, same as in :commando/from. String-key form ("commando-context", "default", "=>") is available for JSON compatibility.
Drivers provide a data-driven way to post-process command results. After a command's :apply function produces a raw result, the driver transforms it before the value is placed back into the instruction.
A driver is declared via the :=> key (or "=>" for string-key/JSON maps) on any command, using a vector DSL:
;; Vector form — [driver-name & params]
{:commando/.. :=> [:get :name]}
{:commando/.. :=> [:get-in [:address :city]]}
{:commando/.. :=> [:select-keys [:name :email]]}
{:commando/.. :=> [:fn inc]}
;; Keyword form — driver with no params
{:commando/.. :=> :my-custom-driver}
;; Pipeline — first element is a vector, steps are chained left-to-right
{:commando/.. :=> [[:get :address] [:get-in [:location :city]] :uppercase]}
If no :=> is specified, the default driver is :identity (pass-through).
(commando/execute
[commands-builtin/command-from-spec]
{:data {:name "John" :age 30 :email "john@example.com" :internal-id 999}
:subset {:commando/from [:data]
:=> [:select-keys [:name :email]]}})
;; => {:data {...}, :subset {:name "John", :email "john@example.com"}}
:identity — pass-through behavior (enabled by default):
{:commando/from [:data] :=> :identity}
;; {:name "John" :age 30} => {:name "John" :age 30}
:get — extract a single key from the result:
{:commando/from [:data] :=> [:get :name]}
;; {:name "John" :age 30} => "John"
:get-in — extract a value at a deep path (also the default driver):
{:commando/from [:data] :=> [:get-in [:address :location :city]]}
;; {:address {:location {:city "Kyiv"}}} => "Kyiv"
:select-keys — select a subset of keys:
{:commando/from [:data] :=> [:select-keys [:name :email]]}
;; {:name "John" :age 30 :email "j@e.com"} => {:name "John" :email "j@e.com"}
:default — if nil return default value
{:commando/from [:age] :=> [:default 30]}
;; {:name "John" :age nil} => 30
:fn — apply an arbitrary function (for cases when you need runtime transforms):
{:commando/from [:data] :=> [:fn inc]}
{:commando/from [:data] :=> [:fn #(reduce + %)]}
:projection — rename and reshape fields (pure data, no function transforms):
{:commando/from [:data]
:=> [:projection [[:user-id :id]
[:user-name [:profile :full-name]]
[:city [:address :location :city]]]]}
;; Each field spec: [output-key]
;; [output-key source-key-or-path]
Full projection example:
(commando/execute
[commands-builtin/command-from-spec]
{:data {:id "u-101"
:profile {:full-name "John Doe"}
:address {:location {:city "kyiv"} :zip "01001"}
:metadata {:labels ["important" "urgent"]}}
:result {:commando/from [:data]
:=> [:projection [[:user-id :id]
[:user-name [:profile :full-name]]
[:city [:address :location :city]]
[:tags [:metadata :labels]]]]}})
;; => {:data {...}
;; :result {:user-id "u-101"
;; :user-name "John Doe"
;; :city "kyiv"
;; :tags ["important" "urgent"]}}
When the first element of :=> is itself a vector, the entire value is treated as a pipeline — a sequence of driver steps chained left-to-right. Each step's output becomes the next step's input:
(commando/execute
[commands-builtin/command-from-spec]
{:data {:profile {:name "john doe"} :age 30 :secret "x"}
;; Step 1: get :profile → {:name "john doe"}
;; Step 2: get :name → "john doe"
;; Step 3: uppercase → "JOHN DOE"
:result {:commando/from [:data]
:=> [[:get :profile] [:get :name] :uppercase]}})
;; => {:data {...}, :result "JOHN DOE"}
Each step in the pipeline can be:
[:driver-name & params] — e.g. [:get :name], [:get-in [:a :b]], [:fn inc]:driver-name — shorthand for a parameterless driver, e.g. :uppercase"driver-name" — for JSON compatibility, keywordized at runtimePipeline works with JSON string keys too:
{"data" {"city" "kyiv"}
"result" {"commando-from" ["data"] "=>" [["get" "city"] "uppercase"]}}
Drivers use Clojure's multimethod system. Define a new driver by extending commando.impl.executing/command-driver:
(require '[commando.impl.executing :as commando-executing])
(defmethod commando-executing/command-driver :uppercase
[_driver-name _driver-params applied-result _command-data _instruction _command-path-obj]
(if (string? applied-result)
(clojure.string/upper-case applied-result)
applied-result))
;; Usage:
{:commando/from [:data] :=> :uppercase}
The driver function receives six arguments:
driver-name — keyword (dispatch value)driver-params — seq of parameters from the :=> vector (nil for keyword-only drivers)applied-result — the value returned by :applycommand-data — the original command map before :apply raninstruction — the full instruction mapcommand-path-obj — CommandMapPath objectAll built-in driver declarations (except :fn) are plain data — keywords, strings, and vectors — making them fully serializable to JSON, EDN, or Transit:
;; JSON-compatible instruction with drivers
{"data" {"name" "John" "age" 30}
"name" {"commando-from" ["data"]
"=>" ["get" "name"]}}
;; Pipeline in JSON
{"data" {"address" {"city" "kyiv"}}
"result" {"commando-from" ["data"]
"=>" [["get-in" ["address" "city"]] "uppercase"]}}
String driver names are automatically keywordized at runtime.
As you start using commando, you will start writing your own command specs to match your data needs.
Here's an example of another instruction, utilizing step-by-step extraction of keys A and B from a structure:
(commando/execute
[commands-builtin/command-from-spec]
{"1" {:values {:a 1 :b -1}}
"a-value" {:commando/from ["1" :values :a] :=> [:fn (partial * 100)]}
"b-value" {:commando/from ["1" :values :b] :=> [:fn (partial * -100)]}
"args" {:a {:commando/from ["a-value"]}
:b {:commando/from ["b-value"]}}
"summ=" {:commando/from ["args"] :=> [:fn (fn [{:keys [a b]}] (+ a b))]}})
;; =>
;; {"1" {:values {:a 1, :b -1}},
;; "a-value" 100,
;; "b-value" 100,
;; "args" {:a 100, :b 100},
;; "summ=" 200}
The main challenge with the above instruction is that everything is processed via the :=> [:fn ...] driver. This may be improved by introducing custom command types.
Let's create a new command using a CommandMapSpec configuration map:
{:type :CALC=
:recognize-fn #(and (map? %) (contains? % :CALC=))
:validate-params-fn (fn [m]
(and
(fn? (:CALC= m))
(not-empty (:ARGS m))))
:apply (fn [_instruction _command m]
(apply (:CALC= m) (:ARGS m)))
:dependencies {:mode :all-inside}}
:type - a unique identifier for this command.:recognize-fn - a predicate that recognizes that a structure {:CALC= ...} is a command, not just a generic map.:validate-params-fn (optional) - validates the structure after recognition.:apply - the function that directly executes the command as params it receives whole instruction, command spec and as a last argument what was recognized by :cm/recognize:dependencies - describes the type of dependency this command has. Commando supports three modes:
{:mode :all-inside} - the command scans itself for dependencies on other commands within its body.{:mode :none} - the command has no dependencies and can be evaluated whenever.{:mode :point :point-key [:commando/from]} - allowing to be dependent anywhere in the instructions. Expects point-key(-s) which tells where is the dependency (commando/from as an example uses this)Now you can use it for more expressive operations like "summ=" and "multiply=" as shown below:
(commando/execute
[commands-builtin/command-from-spec
{:type :CALC=
:recognize-fn #(and (map? %) (contains? % :CALC=))
:validate-params-fn (fn [m]
(and
(fn? (:CALC= m))
(not-empty (:ARGS m))))
:apply (fn [_instruction _command m]
(apply (:CALC= m) (:ARGS m)))
:dependencies {:mode :all-inside}}]
{"1" {:values {:a 1 :b -1}}
"a-value" {:commando/from ["1" :values :a] :=> [:fn (partial * 100)]}
"b-value" {:commando/from ["1" :values :b] :=> [:fn (partial * -100)]}
"summ=" {:CALC= +
:ARGS [{:commando/from ["a-value"]}
{:commando/from ["b-value"]}
1
11]}
"multiply=" {:CALC= *
:ARGS [{:commando/from ["a-value"]}
{:commando/from ["b-value"]}
2
22]}})
;; =>
;; {"1" {:values {:a 1, :b -1}},
;; "a-value" 100,
;; "b-value" 100,
;; "summ=" 212,
;; "multiply=" 440000}
The concept of a command is not limited to map structures it is basically anything that you can express with recognize predicate. For example, you can define a command that recognizes and parses JSON strings:
(commando/execute
[{:type :custom/json
:recognize-fn #(and (string? %) (clojure.string/starts-with? % "json"))
:apply (fn [_instruction _command-map string-value]
(clojure.data.json/read-str (apply str (drop 4 string-value))
:key-fn keyword))
:dependencies {:mode :none}}]
{:json-command-1 "json{\"some-json-value-1\": 123}"
:json-command-2 "json{\"some-json-value-2\": [1, 2, 3]}"})
;; =>
;; {:json-command-1 {:some-json-value-1 123},
;; :json-command-2 {:some-json-value-2 [1 2 3]}}
You can pre-build a registry with registry-create and later modify it with registry-add / registry-remove:
;; Build a registry — vector order defines scan priority
(def command-registry
(commando/registry-create
[commands-builtin/command-from-spec
{:type :CALC=
:recognize-fn #(and (map? %) (contains? % :CALC=))
:apply (fn [_ _ m] (apply (:CALC= m) (:ARGS m)))
:dependencies {:mode :all-inside}}]))
;; Add a new command spec to an existing registry
(def extended-registry
(commando/registry-add command-registry
{:type :NEW-CMD
:recognize-fn #(and (map? %) (contains? % :NEW-CMD))
:apply (fn [_ _ m] (:NEW-CMD m))
:dependencies {:mode :none}}))
;; Remove a command spec by type
(def shrunk-registry
(commando/registry-remove extended-registry :NEW-CMD))
The commando.debug namespace provides two main functions for inspecting instruction execution:
execute-debug
Executes an instruction with debug enabled and prints a visual representation. Accepts a display mode (default :table):
(require '[commando.debug :as debug])
;; Default :table mode — tabular execution map with order, type, deps, values
(debug/execute-debug registry instruction)
;; Other modes: :table, :tree, :graph, :stats, :instr-before, :instr-after
(debug/execute-debug registry instruction :tree)
;; Combine multiple modes in one printing
(debug/execute-debug registry instruction [:instr-before :table :instr-after :stats])
execute-trace
Traces all nested commando/execute calls (including recursive calls from macros/mutations) with timing:
(debug/execute-trace registry instruction)
;; With opts
(debug/execute-trace registry instruction {:error-data-string false})
Add :__title (or "__title") to an instruction to label it in the trace output.
commando.core/execute returns a Status-Map — a data structure that contains the full outcome of instruction execution. All keys are always present in the result, regardless of whether execution succeeded or failed.
:status — :ok or :failed. Outcome of the execution.:instruction — on :ok, the fully evaluated instruction with all commands resolved to their values. On :failed, the partially or completely unexecuted original instruction.:errors — vector of error objects accumulated during execution. Each entry is a map with at least :message; may also contain :error (serialized exception), :command-path, :command-type. Empty [] on success.:warnings — vector of non-critical issues, e.g. skipped pipeline steps after a failure. Empty [] when there are no warnings.:successes — vector of informational messages about completed pipeline steps. Each entry is a map with :message.:stats — vector of timing measurements for each pipeline step. Each entry is a tuple [step-name duration-ns formatted-string], e.g. ["execute-commands!" 95838 "95.838µs"].:uuid — unique identifier for this execution invocation.:registry — the built command registry used for this execution.:internal/cm-list — set of all discovered Command objects (CommandMapPath) found during the find-commands step.:internal/cm-dependency — forward dependency graph {CommandMapPath → #{deps}}, which commands each command depends on.:internal/cm-running-order — vector of commands in topologically sorted execution order (Kahn's algorithm).:internal/cm-results — map {CommandMapPath → resolved-value}, the result of each command's :apply function.:internal/path-trie — nested trie structure for O(depth) command lookup by path.:internal/original-instruction — the original instruction as passed by the user, before any command evaluation.(require '[commando.core :as commando])
(require '[commando.commands.builtin :as commands-builtin])
(commando/execute
[commands-builtin/command-from-spec]
{"1" 1
"2" {:commando/from ["1"]}
"3" {:commando/from ["2"]}})
;; =>
{:status :ok
:instruction {"1" 1, "2" 1, "3" 1}
:errors []
:warnings []
:successes
[{:message "Commands were successfully collected"}
{:message "Dependency map was successfully built"}
{:message "..."}]
:stats
[["use-registry" 12500 "12.5µs"]
["find-commands" 35000 "35µs"]
["build-deps-tree" 18000 "18µs"]
["sort-commands-by-deps" 9000 "9µs"]
["execute-commands!" 95838 "95.838µs"]
["execute" 1085471 "1.085471ms"]]
:uuid "a1b2c3..."
:registry ...
:internal/cm-list #{...}
:internal/cm-dependency {...}
:internal/cm-running-order [...]
:internal/cm-results {...}
:internal/path-trie {...}
:internal/original-instruction {"1" 1, "2" {:commando/from ["1"]}, "3" {:commando/from ["2"]}}}
(commando/execute
[commands-builtin/command-from-spec]
{"1" 1
"2" {:commando/from ["1"]}
"3" {:commando/from ["WRONG" "PATH"]}})
;; =>
{:status :failed
:instruction
{"1" 1
"2" {:commando/from ["1"]}
"3" {:commando/from ["WRONG" "PATH"]}}
:errors
[{:message "Point dependency failed: key ':commando/from' references non-existent path [\"WRONG\" \"PATH\"]"
:path ["3"]
:command {:commando/from ["WRONG" "PATH"]}}]
:warnings
[{:message "Skipping sort-commands-by-deps"}
{:message "Skipping execute-commands!"}]
:successes
[{:message "Commands were successfully collected"}]
:stats [...]
:uuid "d4e5f6..."
:internal/cm-list #{...}
...}
Helper predicates for checking status:
(commando/ok? result) ;; => true | false
(commando/failed? result) ;; => true | false
commando/execute accepts an optional third argument — a configuration map. Config keys control execution behavior and are automatically inherited by nested execute calls (e.g. from :commando/macro or :commando/resolve). Inner calls can override specific keys — non-overridden keys come from the parent.
(commando/execute registry instruction
{:error-data-string false
:hook-execute-start (fn [status-map] ...)
:hook-execute-end (fn [status-map] ...)})
:error-data-string (boolean, default true):hook-execute-start (function) — called before execution begins, receives execution context map:hook-execute-end (function) — called after execution completes, receives execution context map with :stats and :instructionHooks allow you to observe or instrument the execution lifecycle — for example, to collect timing data, log nested executions, or build execution traces. See Debugging for a practical use via execute-trace.
:error-data-stringWhen :error-data-string is true, the :data key within serialized ExceptionInfo objects (processed by commando.impl.utils/serialize-exception) will contain a string representation of the exception's data. Conversely, if false, the :data key will hold the raw data structure (map). This setting is particularly useful for controlling the verbosity of error details, in example when examining Malli validation explanations etc.
(def value
(commando/execute [commands-builtin/command-from-spec]
{"a" 10
"ref" {:commando/from "BROKEN"}}))
(get-in value [:errors 0 :error])
;; =>
;; {:type "exception-info",
;; :class "clojure.lang.ExceptionInfo",
;; :message "Failed while validating params for :commando/from ...",
;; :stack-trace
;; [["commando.impl.finding_commands$instruction_command_spec$fn__14401" "invoke" "finding_commands.cljc" 65]
;; ["clojure.core$some" "invokeStatic" "core.clj" 2718]
;; ...
;; ...],
;; :cause nil,
;; :data "{:command-type :commando/from, :reason #:commando{:from [\"commando/from should be a sequence path to value in Instruction: [:some 2 \\\"value\\\"]\"]}, :path [\"ref\"], :value #:commando{:from \"BROKEN\"}}"}
(def value
(commando/execute [commands-builtin/command-from-spec]
{"a" 10
"ref" {:commando/from "BROKEN"}}
{:error-data-string false}))
(get-in value [:errors 0 :error])
;; =>
;; {:type "exception-info",
;; :class "clojure.lang.ExceptionInfo",
;; ...
;; ...
;; :data
;; {:command-type :commando/from,
;; :reason {:commando/from
;; ["commando/from should be a sequence path to value in Instruction: [:some 2 \"value\"]"]},
;; :path ["ref"],
;; :value {:commando/from "BROKEN"}}}
Commando is designed for high performance, using efficient algorithms for dependency resolution and command execution to process complex instructions swiftly.
All benchmarks were conducted on an Intel Core i9-13980HX. The primary metric for performance is the number of dependencies within an instruction.
The graph below illustrates the total execution time for instructions with a typical number of dependencies, ranging from 1,250 to 80,000. As you can see, the execution time scales linearly and remains in the low millisecond range, demonstrating excellent performance for common use cases.
To provide deeper insight, we've broken down the execution into five distinct steps:
The following graphs show the performance of each step under both normal and extreme load conditions.
Normal Workloads (100-200 dependecies)
Under normal conditions, each execution step completes in just a few milliseconds. The overhead of parsing, dependency resolution, and execution is minimal, ensuring a fast and responsive system.
Massive Workloads (up to 5,000,000 dependencies)
To test the limits of the library, we benchmarked it with instructions containing up to 5 million dependencies. The graph below shows that while the system scales, the find-commands (parsing) and build-deps-tree (dependency graph construction) phases become the primary bottlenecks. This demonstrates that the core execution remains fast, but performance at extreme scales is dominated by the initial analysis steps.
Libraries that explore related ideas — graph-based resolution, declarative data pipelines, or reactive maps:
{keyword → fnk}, compiled into an execution plan with automatic dependency resolution.We comply with: Break Versioning
<major>.<minor>.<non-breaking>[-<optional-qualifier>]
For version changes look to CHANGELOG
This project is licensed under the Eclipse Public License (EPL).
Can you improve this documentation? These fine people already did:
SerhiiRI, kaspazza, Mateusz Mazurczak & Serhii RiznychukEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |