# Loom: a graph library for Clojure and ClojureScript

> Loom is an open source graph library for Clojure and ClojureScript that builds undirected, directed and weighted graphs, runs traversal, pathfinding and network flow algorithms on them, generates random graphs, and reads or writes graphs in external formats. Graphs are immutable records, so every operation returns a new graph, and the whole thing installs from Clojars with one dependency line.

**clj-commons/loom** — Graph library for Clojure

- Repository: https://github.com/clj-commons/loom
- Website: http://clj-commons.org/loom/
- Stars: 893 · Forks: 108
- Language: Clojure
- License: EPL-1.0
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/clj-commons-loom

## One dependency line, two build tools

Loom is published to Clojars under the coordinate aysylu/loom, and the installation is a single line in whichever build tool you use. For the tools.deps style deps.edn:

```clojure
aysylu/loom {:mvn/version "1.2.3"}
```

and for Leiningen:

```clojure
[aysylu/loom "1.2.3"]
```

The dependency list beyond Clojure itself is short: data.priority-map for the Clojure side and cljs-priority-map for the ClojureScript side, plus optional support for visualisation through GraphViz. The project lives under the clj-commons organisation, and the licence is EPL-1.0, a weak copyleft that suits a library. The stack badges list Clojure, ClojureScript, Leiningen and deps.edn, which tells you both a classic project.clj and a modern deps.edn are supported. The repository carries both, alongside a CHANGELOG, a CONTRIBUTING file, a CODE_OF_CONDUCT, a scripts directory, a src directory and separate test and test-cljs directories, which is the layout of a library that takes both runtimes seriously.

## A graph is a record, and every operation returns a new one

The core representation decision is that graphs are immutable records, and the documentation is explicit that the add and remove functions therefore return new graphs rather than mutating anything. That is the natural fit for Clojure and it has a visible consequence in the API: you thread graphs through a pipeline instead of calling mutators, and the accessor functions are pure queries. The constructors are flexible, since a graph can be initialized with edges, adjacency lists, nodes or other graphs:

```clojure
;; Initialize with any of: edges, adacency lists, nodes, other graphs
(def g (graph [1 2] [2 3] {3 [4] 5 [6 7]} 7 8 9))
(def dg (digraph g))
(def wg (weighted-graph {:a {:b 10 :c 20} :c {:d 30} :e {:b 5 :d 5}}))
(def wdg (weighted-digraph [:a :b 10] [:a :c 20] [:c :d 30] [:d :b 10]))
(def rwg (gen-rand (weighted-graph) 10 20 :max-weight 100))
(def fg (fly-graph :successors range :weight (constantly 77)))
```

The last two are the interesting ones for testing. A random weighted graph with a bounded weight range, and a fly graph where the successors and the weight are supplied as functions, which lets you build a graph of any shape on demand. Inspection functions answer the obvious questions, with nodes, edges, successors, predecessors, in-degree and out-degree, and the predicates graph?, directed? and weighted? let you test a graph's kind in a single pass.

## Traversal returns lazy sequences, and that distinction is documented

The traversal functions are annotated with whether they are lazy, and the difference shows up in the results. Breadth first traversal from the example graph gives a sequence of nodes, and it can be seeded from a particular node. Preorder and postorder traversals on a directed weighted graph give different orders, and postorder is the one marked as not lazy:

```clojure
(bf-traverse g) ;lazy
=> (9 8 5 6 7 1 2 3 4)

(bf-traverse g 1)
=> (1 2 3 4)

(pre-traverse wdg) ;lazy
=> (:a :b :c :d)

(post-traverse wdg) ;not lazy
=> (:b :d :c :a)

(topsort wdg)
=> (:a :c :d :b)
```

For a library that people will run over large graphs, the lazy annotations are not decoration. A lazy traversal can be consumed with take and stopped early, while a non-lazy one will compute the whole thing first. Topological sort is there too, which is the operation you reach for when the graph is a dependency structure. The pathfinding functions follow the same pattern, with breadth first paths, a bidirectional variant, and Dijkstra paths and path distances for weighted graphs, where the distance variant returns the path and the total weight together in a vector.

## Spans and connected components answer structural questions

Beyond single paths, the library computes structures that describe a whole region of the graph. A breadth first span is a map from each reachable node to the nodes discovered directly from it, a preorder span is a map from each node to the node that reached it, and the Dijkstra span carries the running distances with each edge weight. Connected components break a graph into its separate pieces, which for the example graph gives four of them, including two isolated nodes. These are the operations that answer questions like which nodes can reach which others, and they are all expressed as plain Clojure data, so a map of a span is something you can inspect, filter and feed into further transformations. The fact that a span is a map rather than a special object means the library composes with the rest of the language instead of closing you inside its own world.

## Attributes attach metadata to nodes and edges

The attribute subsystem is what lets a graph carry more than topology, and it works on both nodes and edges with separate functions for each. You attach with add-attr for a single node, add-attr-to-nodes for a set of nodes, and add-attr-to-edges for edges identified by their node pairs, and you read back with attr for a single value and attrs for the whole map. Asking for an attribute that does not exist returns nil rather than throwing, which keeps the accessors safe to use in pipelines. Removing an attribute is a remove-attr call that returns a new graph, consistent with the immutability rule. The example builds a graph with a label on two nodes, a parity attribute on two others, and an edge label on two edges, then reads them back:

```clojure
(def attr-graph (-> g
                (add-attr 1 :label "node 1")
                (add-attr 4 :label "node 4")
                (add-attr-to-nodes :parity "even" [2 4])
                (add-attr-to-edges :label "edge from node 5" [[5 6] [5 7]])))

(attr attr-graph 1 :label)
=> "node 1"
```

There are also namespaces dedicated to labels and to attributes, and a derived namespace for building new graphs from existing ones with maps and filters, including node mapping and node filtering and a function that takes the subgraph reachable from a given node.

## Eleven namespaces, and you only pay for what you require

The namespace list is a map of the library's capabilities and it is worth reading as a table of contents. loom.graph holds the records and constructors, loom.alg the algorithms, and loom.alg-generic the algorithms that work on any type of graph, which is the interesting one for library design since it separates the algorithm logic from a specific graph representation. loom.flow holds the network flow algorithms, and loom.network-simplex holds the network simplex algorithm for network flow, which is the more involved of the two approaches to that problem. loom.gen holds the graph generators, loom.attr the graph attributes, loom.label the graph labels, loom.io reads, writes and views graphs in external formats, loom.derived derives graphs from existing graphs using maps and filters, and loom.dataflow is a dataflow analysis framework. The split between loom.alg and loom.alg-generic is the one that will shape how you use it, because requiring the generic namespace lets you run the same algorithm over your own record type if it happens to look like a graph.

## The test suite runs on both runtimes and on three Clojure versions

The testing section shows the level of rigour behind the library. There are four commands:

```bash
lein test          # default suite
lein test :all     # includes the slow generative specs
clojure -M:test-cljs # the suite under ClojureScript on Node (needs Node.js)
lein all test      # Clojure 1.10, 1.11 and 1.12
```

The generative specs are the interesting part. Property based testing is a natural fit for a graph library, because the properties are easy to state: a path between two connected nodes should have a certain length, the components of a graph should partition the nodes, a traversal should visit each reachable node once. The last line means the suite runs against three Clojure versions, which matters for a library whose users span that range, and the ClojureScript run on Node means the browser target is not an afterthought. The repository has a .clj-kondo directory for the linter configuration, a separate test-cljs directory, and a CI workflow badge, so all of this runs automatically on every change.

## Where Loom stops and other tools start

The honest limit is that this is a library and not a store. Loom keeps a graph in memory as a Clojure record, which is exactly what you want for an algorithm you run once over a structure you already have, and exactly what you do not want if you need to query a graph database that is larger than memory or shared between processes. There is also no query language and no persistence layer here, so if your problem is running pattern matching over a stored graph, this is the wrong tool and a graph database is the right one. The same applies to a very large graph analysed repeatedly, where recomputing traversal each time is wasteful. On the other hand, for a codebase that already treats data as immutable maps and wants the algorithms inline with the rest of its logic, an in-memory record is the correct representation, and a library that returns plain Clojure data for its results composes better with that style than a wrapper around a database would. The optional GraphViz integration fits the same pattern, giving you a way to look at a graph without the library owning any of the display.

## Conclusion

Loom suits a Clojure or ClojureScript project that needs graph algorithms without writing them, and its immutable record representation plus the separate generic algorithm namespace make it a comfortable fit for functional code. It does not suit a project that needs graph databases, persistence or a query language, since it is a library and not a store, and the namespace layout means you pull in only the parts you use. Before adopting it, check the version you depend on against the ClojureScript build, because the library targets both and the test suite runs the ClojureScript version on Node as well as the Clojure one, and note that the visualisation support is optional and depends on GraphViz being installed separately.

## FAQ

### How do I add Loom to a Clojure project?

Add the dependency to your build file. In deps.edn it is aysylu/loom {:mvn/version "1.2.3"}, and in project.clj for Leiningen it is [aysylu/loom "1.2.3"]. The library is published on Clojars and the only other dependencies are data.priority-map and cljs-priority-map.

### Which graph types does Loom support?

Undirected, directed and weighted variants, built with graph, digraph, weighted-graph and weighted-digraph. You can also generate them with gen-rand for random graphs and fly-graph when the successors and weights come from functions, and convert one kind to another by passing a graph to a constructor.

### Which algorithms are in Loom?

Breadth first and depth first traversal, topological sort, breadth first paths including a bidirectional variant, Dijkstra paths and distances, spans in breadth first, preorder and Dijkstra forms, connected components, network flow algorithms and the network simplex algorithm, plus a dataflow analysis framework.

### Does Loom work with ClojureScript?

Yes. The library supports Clojure and ClojureScript, and the test suite runs under ClojureScript on Node with the command clojure -M:test-cljs, which needs Node.js installed. The default suite runs with lein test, lein test :all adds the slow generative specs, and lein all test covers Clojure 1.10, 1.11 and 1.12.

## Sources

- [clj-commons/loom on GitHub](https://github.com/clj-commons/loom)
- [Issues](https://github.com/clj-commons/loom/issues)
- [License: EPL-1.0](https://github.com/clj-commons/loom/blob/master/LICENSE)
- [Project website](http://clj-commons.org/loom/)
- [README](https://github.com/clj-commons/loom/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/clj-commons-loom
