org-roam

Table of Contents

Summary

org-roam is an Emacs package that implements a Zettelkasten-style knowledge graph over plain .org files, maintaining a SQLite database of nodes, tags, and links. ORE Studio uses org-roam as the backbone of its entire documentation system: every .org file in the repo is a node in the graph, cross-document references use [[id:UUID][Title]] links that survive file renames, and the graph is published as an interactive visualisation alongside the static documentation site. To make this work in a CI/batch context (no user home directory, no interactive Emacs), several non-obvious workarounds are required; they are described below.

Detail

What org-roam does

org-roam indexes every .org file under org-roam-directory into a SQLite database (.org-roam.db). Each file that carries a :ID: property in its :PROPERTIES: drawer becomes a node in the graph; headings can also be nodes. The database records the UUID, title, tags (from #+filetags:), and outgoing links of every node. This gives three things:

  1. Id-link navigation[[id:UUID][Title]] links resolve to the right file and heading regardless of where either file is located in the directory tree.
  2. Back-links — org-roam knows which nodes link to a given node, enabling the org-roam-buffer to show incoming references for any open file.
  3. Graph export — the link and tag tables in the SQLite database can be exported to JSON and rendered as an interactive force-directed graph via org-roam-ui.

How we use it

Every document in doc/ and every component overview in projects/*/modeling/ is an org-roam node. The ORE Studio conventions layer on top of org-roam: the #+type: and #+level: frontmatter fields are ORE Studio conventions, not org-roam fields, but org-roam indexes them as document properties and compass queries them via the same SQLite database. The full documentation graph is therefore traversable both by an interactive Emacs developer (via org-roam-buffer and org-roam-node-find) and by the LLM toolchain (via compass search/show/list).

The knowledge graph "hacks"

Making org-roam work in a reproducible batch build required several departures from the standard interactive setup. These are sometimes called "hacks" because they bypass assumptions baked into the org-roam ecosystem:

Repo-root org-id locations file

Standard org-roam stores the id-to-file mapping in ~/.emacs.d/.org-id-locations. In batch mode (CI runner, colleague's machine) that file either does not exist or maps a different checkout path. The site build script sets:

(setq org-id-locations-file (expand-file-name "./.org-id-locations-file"))
(org-id-update-id-locations (directory-files-recursively "." "\\.org$"))

This writes a fresh .org-id-locations-file at the repo root on every build, derived entirely from the files in the working tree. The file is .gitignore=d (it's machine-specific absolute paths) but is always regenerated before =org-publish-all so every [[id:UUID]] link resolves. The recursive scan covers the entire repo root; build/, .git/, and vcpkg/ directories rarely contain .org files so the scan is fast in practice. The subsequent org-roam-db-sync step applies its own org-roam-file-exclude-regexp to keep build artefacts out of the database.

Advice on org-html-headline for id-link anchors

Org's HTML exporter rewrites [[id:UUID]] links to href"#ID-UUID"= in the output HTML, but does not automatically add a corresponding id"ID-UUID"= attribute to the target heading. Browsers therefore cannot scroll to the target. The site build advises org-html-headline to prepend an empty anchor to every heading that carries an :ID: property:

(defun ores/org-html-headline-id-anchor (orig-fun headline contents info)
  (let ((rendered (funcall orig-fun headline contents info))
        (uuid (org-element-property :ID headline)))
    (if (and rendered uuid)
        (concat (format "<a id=\"ID-%s\" class=\"id-anchor\"></a>\n" uuid)
                rendered)
      rendered)))
(advice-add 'org-html-headline :around #'ores/org-html-headline-id-anchor)

The idea for this fix was adapted from community discussion in the org-roam forums and the cunene emacs config.

Batch SQLite graph export (no full org-roam load)

The org-roam-ui graph visualisation requires a JSON file (graphdata.json) describing every node and link in the database. Generating this normally requires a fully initialised org-roam session, which is expensive and fragile in batch mode. ores-org-roam-export.el instead reads the SQLite database directly using Emacs 29+'s built-in sqlite-open support — without loading org-roam itself — and emits the JSON in org-roam-ui's expected format. This code was originally lifted from the cunene emacs config and adapted to run as a standalone script. The approach works because the database schema is stable across org-roam minor versions.

Pre-built org-roam-ui static bundle

org-roam-ui is a React application that must be compiled from source. Checking a compiled build into external/org-roam-ui avoids needing Node.js or npm on the CI runner. The site build copies this pre-built bundle to the graph/ subdirectory of the site output (see the deploy_site row in Emacs for the full output path), patches the graph page's <head> to include the ORE Studio stylesheet and nav bar, and writes the generated graphdata.json alongside it.

Link types: id, file, proj

Which link type to use depends on whether the target is published by the site build:

[[id:UUID]]
the default for any org document in the graph — resolves locally, in the graph, and on the published site.
[[file:path]]
only for targets the site build publishes as-is: org documents (exported to .html) and attachments matching the publish components — images (png, jpe?g, gif, svg), css and pdf. Inline images must use file: to render as <img>.
[[proj:path/from/repo/root]]
everything else — source files, JSON, scripts, Markdown, directories. The site build does not copy these, so a file: link 404s on the published site; proj: links export to GitHub blob URLs instead (ores-build-site.el defines the type). Locally they do not resolve — that is the accepted trade-off.

See also

  • org-roam manual — full reference for org-roam commands, configuration, and concepts.
  • Zettelkasten — the note-taking method that motivates the id-link graph structure.
  • Emacs — the editor that hosts org-roam and drives all documentation builds.
  • ores.lisp — the ores-org-roam-export.el source and all other Emacs Lisp modules.

Emacs 29.1 (Org mode 9.6.6)