Library / SDK
nextjournal/clerk avatar
nextjournal/clerk

Clerk: Clojure notebooks that stay plain namespaces

⚡️ Moldable Live Programming for Clojure

2,086 stars86 forksClojureISC

At a glance

What is it?
Clerk renders Clojure namespaces as notebooks while keeping them as ordinary .clj files in your editor, evaluating strictly top to bottom. The README labels it alpha and expects breaking changes, and the built-in server runs your code on port 7777 with no documented authentication.
Who is it for?
Clerk fits Clojure developers who already live in an editor and a REPL and want narrated, versioned notebooks without giving up either. It does not fit teams that want an independent notebook runtime, a Jupyter kernel protocol, or a server that is safe to expose, since Clerk evaluates inside your own process and the README documents no authentication for port 7777.
Can I use it commercially?
Yes. ISC is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 6 days ago.
What is it written in?
Mainly Clojure, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 6, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Why Clerk drops the notebook cell model

Jupyter arrived with a promise that mixing prose and code would let people argue from evidence. Clerk takes that promise and removes the parts it considers mistakes, and the README is unusually direct about which ones. Its stated pain points are that notebooks feel less helpful than an editor, that notebook code is hard to reuse, that out-of-order execution creates reproduction problems, and that archiving notebooks into source control is awkward.

The response is subtraction. Clerk has no editing environment of its own; you keep the editor you already use. There is no new file format: a Clerk notebook is either a regular Clojure namespace with markdown in comments, or a regular markdown file with Clojure in fenced code blocks. Both are meant to live in git, which removes the archival problem and simultaneously makes diffs readable.

The reuse problem falls out of that choice. A namespace already has forms that other namespaces can require, so nothing about a Clerk notebook is quarantined from the rest of the program. The cost is equally concrete: you lose the cell abstraction that lets a notebook declare dependencies on cells out of order, and you inherit Clojure's compile-time model instead.

Top-to-bottom evaluation over a graph of Clerk vars

The interesting mechanism is not the browser rendering, it is what happens when you save a file. Clerk has no out-of-order execution. Notebooks always evaluate from top to bottom, and to keep that honest while staying fast, Clerk builds a dependency graph of Clojure vars and recomputes only the parts that a change actually invalidated.

That is the same idea as a build system applied to a document. Change a helper def near the bottom and the graph knows which downstream results depend on it, so the page updates without re-running an hour of data munging from the first cell. Change a markdown comment and nothing numeric recomputes at all.

The graph is described in terms of Clojure vars, not cells, which is the design decision everything else follows from. Vars are what Clojure already tracks for compilation, so Clerk gets caching from the language rather than inventing a cell protocol on top of it. It also means a notebook that hides expensive work inside a side effect rather than a def can fool that reasoning, since there is nothing in the graph to record the dependency.

No external process: Clerk runs inside your Clojure JVM

Clerk runs inside your own Clojure process, and the README lists this as a deliberate choice: it gives Clerk access to all code on the classpath. Nothing is serialised across a process boundary, so what a notebook shows is what your program actually computed, including private and internal code that a separate kernel could not reach.

The web side is a React application with CodeMirror editors, KaTeX for mathematics, and a markdown pipeline built on markdown-it with toc, footnotes, block images and TeX math plugins. The dependency list pins React at 18.2.0 and KaTeX at 0.12.0, and includes lezer-clojure at 1.0.0-rc.2 together with a matching @nextjournal/lezer-clojure package, which is the parser layer that makes Clojure inside markdown fences highlightable rather than a flat string.

The trade-off is that there is no isolation story. Whatever your notebook evaluates, it evaluates with the full permissions of your application process, and the server is the same process. Bind it to localhost and treat it that way.

Adding Clerk to deps.edn and serving the first notebook

Clerk needs Java 11+ and a working clojure command. The dependency goes into deps.edn with the coordinate io.github.nextjournal/clerk and the current version 0.18.1158:

edn
{:deps {io.github.nextjournal/clerk {:mvn/version "0.18.1158"}}}

Then require it and start the built-in webserver from your start-up code, for example user.clj. The metadata flag on the form tells Clerk to render the code listing above each output:

```clojure {:nextjournal.clerk/code-listing true} (require '[nextjournal.clerk :as clerk])

(clerk/serve! {:browse true})

code

The server listens on port 7777 and, with :browse true, opens a browser once it is ready. There are several ways to use it: clerk/show! on one notebook file, clerk/serve! with :watch-paths to reload on change, and a :show-filter-fn so pinning only applies to notebooks. With a filter, only paths under "notebooks" get pinned while "src" is watched for recompilation:

clojure (clerk/serve! {:watch-paths ["notebooks" "src"] :show-filter-fn #(clojure.string/starts-with? % "notebooks")}) ```

The /notebooks folder in the repository is the fastest place to look, and the README uses notebooks/rule_30.clj as its running example throughout, which doubles as the smallest interesting target for a first render.

Triggering clerk/show! from Emacs, Cursive and Conjure

The intended workflow is save the buffer, then show it, so the editor integration matters more than usual. The README recommends binding clerk/show! to a shortcut. In Emacs, save the buffer and evaluate the show call with cider-interactive-eval:

elisp
(defun clerk-show ()
  (interactive)
  (when-let
      ((filename
        (buffer-file-name)))
    (save-buffer)
    (cider-interactive-eval
     (concat "(nextjournal.clerk/show! \"" filename "\")"))

(define-key clojure-mode-map (kbd "<M-return>") 'clerk-show)

The elisp builds the form by string concatenation, which is simple and also fragile: a path containing a double quote or a backslash will produce a form that does not read. It also evaluates in whatever namespace is current, so the fully qualified call in the snippet is doing real work.

Cursive takes the same idea through its REPL command settings, adding a command of the form (show! "~file-path") that must execute in the nextjournal.clerk namespace, then assigning a shortcut in Settings, Keymap. Conjure users get a vimscript function that writes the buffer and evaluates the qualified show form:

vimscript
function! ClerkShow()
  exe "w"
  exe "ConjureEval (nextjournal.clerk/show! \"" . expand("%:p") . "\")"
endfunction

nmap <silent> <localleader>cs :execute ClerkShow()<CR>

Three editors, one convention: the file path is passed as a string argument, and the notebook is identified by path, not by a notebook id.

Static HTML with build! and development through Babashka

Notebooks do not have to be served. clerk/build! renders the given notebooks to HTML files, and the README points at the docstring for the full option list rather than enumerating them:

clojure
(clerk/build! {:paths ["notebooks/rule_30.clj"]})

That matters for sharing and for archiving: a rendered artifact can go to a static host while the source stays a namespace in git, and the two never drift because the same form is both.

Working on Clerk itself takes Babashka. With bb installed, one command brings up everything the project needs and opens a browser, and trailing arguments are forwarded to clojure -X and clerk/serve!:

bash
bb dev :browse true

Omit :browse true if you would rather connect an editor over nREPL yourself. The repository also carries build.clj, bb.edn, a shadow-cljs.edn for the front end, a CHANGELOG.md, and a chinook.db sample database used by the demo namespace, so a database-backed notebook has a fixture available from day one.

The gaps: alpha status, an unauthenticated server, and deferred known issues

The README states the status in two words: ALPHA, expect breaking changes. Everything else in the document is written around that caveat. An artifact version of 0.18.1158 with that many revisions is a project that has been changing shape continuously, and a Clojure library that depends on evaluation order is exactly the kind of code where a refactor upstream changes output downstream without any compile error in your notebook.

Second, the server. clerk/serve! starts a webserver on port 7777 inside a JVM that can execute arbitrary code from the classpath, and the README documents no authentication, no token and no configuration for either. It also documents no way to change the bind address. The honest reading is that this server is a localhost development tool, not something to tunnel, and a team deploying shared notebooks will need to put a real boundary in front of it.

Third, the known issues are not in the README at all. They live in notebooks/onwards.md, so you discover them by opening a notebook in the repository rather than reading the documentation. That is a defensible choice for a project whose whole point is that documentation is executable, but it does mean an evaluation will not tell you what is currently broken. There is a related gap on citing: the BibTeX entry offered for research use still points at February 2023, which is old for a project shipping weekly changes.

Alternatives: a separate Jupyter kernel, or no notebook at all

The natural comparison is Jupyter itself, and the README does not hide it. It links two critiques of traditional notebooks before arguing for its own design, and the difference is architectural rather than cosmetic. A Jupyter kernel is another process with its own language runtime and its own copy of your code, reached through a wire protocol. Clerk has no external process: the notebook evaluates in the JVM you already started, against the classpath you already built, which is why reuse works and why there is no kernel protocol to implement.

That same difference cuts the other way. If your analysis needs libraries that only work in a separate runtime, or you want colleagues without a Clojure toolchain to open and run your work, a kernel-based notebook serves them and Clerk does not. Clerk's output is only reproducible by someone who can start your Clojure project.

The other alternative is not using a notebook. Keep a plain namespace, keep the README prose, and put the conclusions in comments you maintain by hand. Clerk's value is that the narrative and the computation cannot silently diverge, because the narrative is evaluated in the same pass. If you never narrate results in your code, that benefit is theoretical.

Editorial conclusion

Clerk fits Clojure developers who already live in an editor and a REPL and want narrated, versioned notebooks without giving up either. It does not fit teams that want an independent notebook runtime, a Jupyter kernel protocol, or a server that is safe to expose, since Clerk evaluates inside your own process and the README documents no authentication for port 7777. Verify three things before adopting it. Pin the dependency, because the README states ALPHA and expects breaking changes and the current artifact is a long 0.18.1158 that moves often. Check whether your notebooks must be plain text, since the markdown path relies on fenced clojure blocks and the dependency list pins a lezer-clojure release candidate at 1.0.0-rc.2. And decide where notebooks/onwards.md fits your process, because the known issues are deliberately kept out of the README and live in a notebook inside the repository. Start by adding io.github.nextjournal/clerk to deps.edn, running clerk/serve! with :browse true, and editing one of the notebooks in the /notebooks folder.

Frequently asked questions

How do I add Clerk to a Clojure project?

Add io.github.nextjournal/clerk to deps.edn, requiring Java 11+ and clojure, then require nextjournal.clerk and call clerk/serve! with :browse true. The server starts on port 7777.

Can I write a Clerk notebook as markdown instead of Clojure?

Yes. The README states that a Clerk notebook is either a regular Clojure namespace with markdown in comments, or a regular markdown file with Clojure in fenced code blocks, and that both are meant to be stored in source control.

Does Clerk support running notebook cells out of order?

No, and that is a stated design point. The README says Clerk notebooks always evaluate from top to bottom, using a dependency graph of Clojure vars to recompute only what a change invalidated, in order to avoid the reproduction problems of out-of-order execution.

Is the Clerk server safe to expose on a network?

The README does not document any authentication for the built-in webserver on port 7777, and it runs inside the Clojure process with access to the whole classpath. Treat it as a localhost development server.

How do I develop Clerk itself?

Install Babashka and run bb dev :browse true, which starts everything needed and opens a browser, connecting an editor over nREPL if you omit the browse argument. Any trailing arguments are forwarded to clojure -X and clerk/serve!.

Official sources

  1. Issues
  2. License: ISC
  3. nextjournal/clerk on GitHub
  4. Project website
  5. README