Wout Neirynck - 2026-02-10
TL;DR: I built a CI/CD platform where you write your build scripts in Clojure instead of YAML. It's open source, uses an event-driven architecture for durability, and lets you test your pipelines like any other code. Looking for beta testers from the Clojure community.
You know the drill. Your project starts simple: run tests, maybe publish to Clojars. A few lines of YAML in .gitlab-ci.yml or .github/workflows/, and you're done.
Then reality hits:
# Six months later...
script:
- if [ "$CI_COMMIT_BRANCH" == "main" ] && [ "$DEPLOY_ENV" == "prod" ]; then
if grep -q "major" CHANGELOG.md; then
VERSION=$(cat VERSION | awk -F. '{$1++; $2=0; $3=0; print $1"."$2"."$3}')
elif grep -q "minor" CHANGELOG.md; then
# ... 50 more lines of bash-in-yaml-in-strings
We've all been there. You're writing bash scripts inside YAML strings, fighting with indentation, and crossing your fingers that the conditions work. No REPL. No tests. No way to run it locally without Docker gymnastics. I spent too many hours debugging builds at 3 AM, and every time I thought: "Why aren't we using an actual programming language for this?"
That's why I built MonkeyCI. The core idea is simple: your deployment pipeline is a program, so write it in a programming language you already know.
Here's what a MonkeyCI build script looks like:
(ns build
(:require [monkey.ci.api :as m]))
(def clojure-img "docker.io/clojure:tools-deps-trixie")
(def run-tests
(-> (m/container-job "test")
(m/image clojure-img)
(m/script ["clojure -X:test"])))
(def publish
(-> (m/container-job "publish")
(m/image clojure-img)
(m/script ["clojure -X:publish"])
(m/depends-on "test")))
;; Return a list of jobs (or a fn that returns jobs based on context)
[run-tests
publish]
This is a real Clojure project. The .monkeyci/ directory in your repo is just a Clojure project with a deps.edn. You can:
MonkeyCI supports two kinds of jobs:
Container Jobs: Traditional containerized execution for things like Docker builds, database migrations, or running in specific environments.
Action Jobs: Pure Clojure functions that run in your build script's JVM. No container overhead for simple tasks like posting to Slack, updating a database record, or even coordinating other jobs.
(m/action-job
"notify-slack"
(fn [ctx]
(let [url (get (m/build-params ctx) "slack-webhook")]
(slack/post! url
(format "Build %s completed!" (:build-id ctx))))))
A great way to reuse code is to use libraries. MonkeyCI supports this as well! You can include any Java or Clojure lib in your script and call its functions, including your own libs.
First add the dependency to the .monkeyci/deps.edn, in this case the clj plugin:
{:deps {com.monkeyci/plugin-clj {:mvn/version "0.4.0"}}}
Then your build script can become even simpler:
(ns build
(:require [monkey.ci.plugin.clj :as clj]))
;; Creates both a test and publish job. In this case with default settings.
(clj/deps-library)
Since a build script is just code, you can do just about anything. The sky is the limit!
Run your builds locally with the CLI:
monkeyci build run
If it works locally, it works remotely. No more "works on my CI" mysteries.
(when (= "main" (m/branch ctx))
(deploy-job))
Not if: ${{ github.ref == 'refs/heads/main' }}. Just Clojure.
[(test-frontend)
(test-backend)
(deploy-job ["test-frontend" "test-backend"])] ;; Waits for both tests
MonkeyCI builds a DAG and runs jobs in parallel where possible.
Under the hood, MonkeyCI uses an event system (NATS with JetStream) for durability. If an agent crashes, another picks up. If your infrastructure reboots, jobs resume. This isn't fire-and-forget; it's designed for reliability. This is all built on top of a custom event handling library called Mailman (I gave a talk about it at London Clojurians last year) that lets you declare event handlers like HTTP routes.
MonkeyCI is open source (under the GPLv3). A few reasons:
The repo is at github.com/monkey-projects/monkeyci.
MonkeyCI is probably overkill if you just need to run lein test and deploy a JAR. The existing tools may be good enough for you. But if you've ever:
...then this might be worth a look.
I'm actively looking for beta testers from the Clojure community. Here's how to get started:
Since my main goal is building a community, there is a free tier of 1.000 monthly credits to spend on your builds.
MonkeyCI is rough around the edges. Error messages could be better. Documentation could be more complete. Some features are planned but not implemented.
But the core idea works. I've been using it for my own projects for two years. My libraries build on MonkeyCI. It's event-driven and durable. And writing build scripts in Clojure feels right in a way that YAML never did.
If that resonates with you, I'd love your feedback. Even if it's just "this doesn't work" or "I tried to do X and got confused." That's valuable.
Q: Why not Babashka?
A: The build scripts run as full Clojure via the CLI, not Babashka. This avoids compatibility issues with libraries. MonkeyCI uses some Babashka libraries internally, but your scripts have access to the entire Clojure ecosystem. You can still use Babashka in container jobs, just like with any other CI/CD tool.
Q: Can I self-host?
A: It's possible, but this is not my main priority. In addition to NATS, MonkeyCI also supports in-memory eventing (mostly for dev/testing purposes) and JMS (e.g. ActiveMQ). But setting up your own build agents and connecting them to MonkeyCI is definitely on the roadmap.
Q: How does pricing work?
A: First I want to make MonkeyCI better before I charge money. But it will be probably something along the lines of $5/user/month for basic usage (e.g. startups, teams up to 3 people), and $30/user/month for larger enterprises (larger teams, more credits).
Q: What about [Feature X]?
A: Tell me about it! That's why I'm looking for beta testers.
Looking forward to hearing from the community. Let's build something better than YAML together.