一键导入
babashka-tasks
// Write idiomatic bb.edn tasks: thin wrappers delegating to scripts/*.clj modules. Use when: creating or modifying a bb task, babashka tasks, editing bb.edn or {scripts,bb}/*.clj files, using Backseat Driver tools with Babashka.
// Write idiomatic bb.edn tasks: thin wrappers delegating to scripts/*.clj modules. Use when: creating or modifying a bb task, babashka tasks, editing bb.edn or {scripts,bb}/*.clj files, using Backseat Driver tools with Babashka.
| name | babashka-tasks |
| description | Write idiomatic bb.edn tasks: thin wrappers delegating to scripts/*.clj modules. Use when: creating or modifying a bb task, babashka tasks, editing bb.edn or {scripts,bb}/*.clj files, using Backseat Driver tools with Babashka. |
Write idiomatic bb.edn tasks: thin declarative wrappers that delegate to well-structured scripts/*.clj modules.
Prerequisite: Always load the babashka skill. It covers REPL-driven development, REPL-loadable script patterns, shell vs process, data-oriented design, and namespace reference - all foundational to writing good task modules.
babashka skillbb.edn scripts/*.clj
:requires [ns] (ns my-module
task-name {:task (ns/fn args)} (:require [babashka.fs :as fs]))
(defn my-fn [opts] ...)
Tasks are thin. Modules hold logic.
For setting up a global task system (tasks available from any directory), see references/global-bbg-setup.md.
{:paths ["scripts"] ;; put modules on classpath
:deps {org.babashka/cli {:mvn/version "0.2.23"}}
:tasks
{:requires ([babashka.cli :as cli]
[my-module]) ;; top-level requires shared across tasks
my-task {:doc "What it does"
:task (my-module/start! (cli/parse-opts *command-line-args*
{:coerce {:port :int}}))}
-private-task {:doc "Internal helper (hidden from bb tasks listing)"
:task (do-something)}
compound-task {:doc "Runs sub-tasks in parallel"
:depends [-private-task]
:task (run '-compound-all {:parallel true})}
-compound-all {:depends [-private-task another-task]}}}
| Situation | Pattern |
|---|---|
| Single shell command | Inline: {:task (p/shell "cmd")} |
| 2-3 lines, no branching | Inline with do |
| Validation, branching, error handling | Delegate to scripts/*.clj |
| Reused across multiple tasks | Always delegate |
| CLI argument parsing beyond simple coerce | Delegate |
(ns my-module
(:require [babashka.cli :as cli]
[babashka.fs :as fs]
[babashka.process :as p]
[clojure.string :as str]))
;; ============================================================
;; Pure helpers (no side effects, defined before callers)
;; ============================================================
(defn- validate-args
"Gather facts, return {:valid? bool :errors [...] :config {...}}"
[opts]
(let [errors (cond-> []
(not (:port opts)) (conj "Missing --port")
(not (fs/exists? (:dir opts "."))) (conj "Directory not found"))]
{:valid? (empty? errors)
:errors errors
:config (merge {:port 8080} opts)}))
;; ============================================================
;; Side-effecting functions (edges only)
;; ============================================================
(defn start!
"Entry point called from bb.edn task.
Gather-then-decide: validate all inputs before acting."
[opts]
(let [{:keys [valid? errors config]} (validate-args opts)]
(if valid?
(do
(println (str "Starting on port " (:port config)))
(p/shell "my-server" "--port" (str (:port config))))
(do
(doseq [e errors] (println (str "Error: " e)))
(System/exit 1)))))
;; bb.edn - thin wrapper passes parsed opts (see babashka skill: CLI argument parsing)
my-task {:task (my-module/start!
(cli/parse-opts *command-line-args*
{:coerce {:port :int :verbose :boolean}
:alias {:p :port :v :verbose}}))}
;; Separating task args from forwarded args (e.g. to Playwright)
;; Use -- to separate: bb my-task --my-flag -- --forwarded-arg
my-task {:task (let [{:keys [args opts]} (cli/parse-args *command-line-args*
{:coerce {:shards :int}
:alias {:s :serial}})]
(my-module/start! args opts))}
Export a cli-spec from each module. Reuse it in bb.edn wiring and shell completions.
;; scripts/my_module.clj
(def cli-spec
{:coerce {:download :string :use :string :status :boolean}
:alias {:d :download :u :use :s :status}})
(defn exec! [opts]
(cond
(:download opts) (download! (:download opts))
(:use opts) (use-version! (:use opts))
:else (status!)))
;; bb.edn - task wires to module's cli-spec
my-task {:doc "Do something [--download <ref> | --use <ref> | --status]"
:task (my-module/exec! (cli/parse-opts *command-line-args*
my-module/cli-spec))}
A completion function that tab-completes task names and per-task options. Relies on a private helper task that reads cli-spec from each module.
# completions.zsh — source in .zshrc
_bb_complete() {
if (( CURRENT == 2 )); then
local tasks=(`bb tasks | tail -n +3 | cut -f1 -d ' '`)
compadd -a tasks
else
local task="${words[2]}"
local opts=(`bb -task-options "$task"`)
if (( ${#opts} )); then
compadd -a opts
fi
fi
}
compdef _bb_complete bb
;; bb.edn — private helper task that emits CLI options for a given task
-task-options {:task (let [task-specs {"my-task" my-module/cli-spec}]
(doseq [opt (-> (get task-specs (first *command-line-args*))
:coerce keys)]
(println (str "--" (name opt)))))}
Write output to .tmp/ files so AI agents can read results with read_file instead of parsing terminal output. Be sure to mention in the task's :doc string, and output, where results will be/are written.
(defn- write-output! [filename content]
(fs/create-dirs ".tmp")
(spit (str ".tmp/" filename) content))
;; In your task function
(let [result (run-tests!)]
(write-output! "test-output.txt" (:output result))
(println "Results written to .tmp/test-output.txt"))
| Mistake | Correction |
|---|---|
Putting logic in bb.edn :task | Delegate to scripts/*.clj module |
Missing :doc string on task | Always add :doc for discoverability |
| Forward declaring functions | Define before use - rearrange file structure |
| Validation interleaved with execution | Gather all facts first, display diagnostics, then act |
Using System/exit inside with-* wrappers | Return exit code, call System/exit after cleanup |
| Top-level side effects in modules | See the babashka skill: REPL-loadable scripts |
This workflow applies to planning AND implementation. When creating a plan document for a new task, use the REPL to verify API behavior, test glob patterns, and validate assumptions. Don't write a plan full of untested guesses and defer all exploration to the implementer.
babashka skill: REPL-driven development)(require '[my-module :as m] :reload) then evaluate:doc stringbb my-task to verify end-to-endbabashka.fs instead of shell commands for file operationsshell/process, not string interpolation:doc stringWrite idiomatic Babashka (bb) scripts and modules. Covers babashka.fs, babashka.process, babashka.cli, babashka.http-client, and built-in namespaces. Use when: writing bb scripts, creating or modifying a task, REPL-driven Babashka development, editing .clj files in directories with bb.edn or scripts/ folders, using Backseat Driver tools with Babashka.
Live tamper with web pages and write userscripts using Epupp (ClojureScript/Scittle in the browser). Use when: working with Epupp projects, browser tampering, userscripts, live REPL in browser tabs, or using Backseat Driver tools with Epupp.
Clojure development skill — any dialect, any runtime. Use for all Clojure work, regardless of dialect or runtime: planning, reading, reviewing, designing, coding, debugging, testing, refactoring. Use whenever you are using Backseat Driver tools.
Squint ClojureScript development — writing Squint code, compilation, REPL workflow, debugging, and tooling. Use when: working with Squint projects, planning, writing, or reviewing Squint code, .cljs files compiled to .mjs, Squint REPL sessions, debugging compiled output, checking whether clojure.core functions exist in Squint, or setting up Squint builds. Use whenever you are using Backseat Driver tools with Squint.