Dominik Honnef

My org-roam workflows for taking notes and writing articles

Published:
Last modified:
by

This article documents the entirety of my org-mode based workflows, from note-taking to generating a website. I’ll mention all relevant packages, applications, and configuration.

The core of the workflows

My workflows are based around org-roam, a personal knowledge management system built atop org-mode. At its core, org-roam implements an index of notes, using SQLite. This allows org-roam to provide fast auto-completion of notes, ID-based links, backlinks, and metadata-based search. It was written to implement Luhmann’s Zettelkasten method in Emacs, but it is flexible enough to be used for any kind of personal knowledge management.

I use org-roam for the majority of my Org files: articles, notes, facts, literature notes, inventories, etc. The only exception is the inbox.org file, which contains hastily taken notes and to-do items that will either be turned into proper notes later, or will be deleted. Everything else is a note in org-roam.

The note taking system

I’m not sticking to a strict system like Luhmann’s Zettelkasten. Notes may be short or long; they may contain original ideas, facts, or entire articles.

I use one file per note, primarily because I find it less overwhelming when a buffer is limited to a single note and don’t feel comfortable working with giant trees of notes.

I borrow some terms from Ahrens’s exposition of the Zettelkasten system [1]. Notes are divided into three kinds: Fleeting notes, literature notes, and permanent notes. The definition of fleeting notes matches Ahrens’s: they’re the kind of unstructured, everyday notes we are used to taking when a thought pops into our head [1, Chap. 2.1]. Literature notes are notes you take while reading literature, or more generally consuming content. Permanent notes are all other notes: original ideas, facts, journal entries, articles, etc. This differs from Ahrens, where permanent notes are meant exclusively for your own ideas, and the Zettelkasten explicitly shouldn’t be treated as a wiki or encyclopedia ( it would be rather misleading to think of his slip-box as a personal Wikipedia or a database on paper [1, Chap. 1.3].) I do not differentiate between permanent notes and project notes.

Fleeting notes

When I’m at my computer, I use org-capture and the following capture template to add fleeting notes to ~/notes/inbox.org:

("f" "Fleeting note" entry  (file "~/notes/inbox.org") "* TODO %^{Note title}\nContext: %a\n%?" :empty-lines-before 1 )

The %a verb expands to a link to where one was when they called org-capture. This might, for example, be another file or an email.

inbox.org is an ordinary Org file and not part of org-roam’s database. Fleeting notes are short-lived and will be deleted once they’ve been acted on. I might use them to write permanent notes, or they might be simple reminders of things to do.

Literature notes and citations

I (try to) take literature notes whenever I consume content that may contain helpful knowledge, be it papers, books, or videos.

The first step is to import the content into my reference management software of choice, Zotero. This guarantees that I have a record of all important metadata, a copy of the PDF/website/video, and a unique ID (the citation key) to refer to it.

In most cases, I use Zotero’s browser plugin, which automates most parts of the import, although some cleanups are often required, such as:

For text documents, if Zotero hasn’t already downloaded a full PDF automatically, I use the Zotero SciHub plugin to fetch one. This usually works. Videos have to be downloaded manually using yt-dlp. For websites, the browser plugin automatically takes a snapshot that contains all of the styles and images necessary to render the page. Having a copy of everything ensures that the referenced content won’t change or disappear.

Once all the metadata is correct, I use the ZotFile plugin to rename all attachments to match the citation key. This will later allow opening the file corresponding to a citation key. All of the files are stored in ~/notes/pdfs. I also use Better BibTeX to pin the citation key so that it doesn’t accidentally change in the future.

Zotero is configured to automatically export a BibLaTeX bibliography, which is placed in ~/notes/references.bib. This file is read by org-cite and citar to resolve citation keys.

Citar provides auto-completion for entries from the bibliography. I use it together with vertico and marginalia for my auto-completion setup. Additionally, citar provides functionality that integrates with org-mode and org-cite. I use the following customization options:

(org-cite-global-bibliography '("~/notes/references.bib"))
(org-cite-insert-processor 'citar)
(org-cite-follow-processor 'citar)
(org-cite-activate-processor 'citar)
(citar-bibliography '("~/notes/references.bib"))
(citar-notes-paths '("~/notes/references"))
(citar-symbols
 `((file ,(all-the-icons-faicon "file-pdf-o" :face 'all-the-icons-green :v-adjust -0.1) . " ")
   (note ,(all-the-icons-material "speaker_notes" :face 'all-the-icons-blue :v-adjust -0.3) . " ")
   (link ,(all-the-icons-octicon "link" :face 'all-the-icons-orange :v-adjust 0.01) . " ")))
(citar-symbol-separator "  ")

This configures org-cite to rely on citar for inserting, viewing, and following citations. It also makes citar’s auto-completion prettier, using fancy icons.

org-cite is a minimal frontend for handling citations in Org files and is included with org-mode. It introduces cite: links, which are citations. By using the #+print_bibliography: instruction, a bibliography will be included when exporting from Org to other formats. Citations will also be formatted according to the configured citation style. Citar builds on org-cite and expands on the possible actions on citations, such as jumping to the corresponding literature notes in org-roam or opening the referenced content.

To actually take literature notes, I use the citar-open function and select the relevant bibliography entry. This will present me with multiple options, one of which is to open or create a note. Because I am using citar-org-roam and have enabled the citar-org-roam-mode minor mode, this will create a new org-roam note (instead of an ordinary Org file). The new note will have the ROAM_REFS property populated with the citation key, allowing org-roam to connect cite: links with the literature notes, tracking backlinks as it does for other notes. See org-roam’s manual for more information about ROAM_REFS.

To revisit these notes later, I can use citar-open again or follow cite: links in other notes that reference the content. Both of these also allow me to open the file (such as a PDF) corresponding to a key. In theory, I should configure citar-library-paths which lets citar look up files from citation keys. However, the exported BibTeX file contains absolute paths to these files, which citar apparently can use.

Literature notes go in the ~/notes/references subdirectory. This behavior is controlled by the citar-org-roam-subdir variable, which defaults to "references".

I want to include BibTeX entries with my literature notes so that notes published on my website are self-contained. Unfortunately, citar-org-roam doesn’t allow customizing the capture template being used. Instead, I redefine citar-org-roam--create-capture-note. I also copy citar--insert-bibtex and modify it to return a string instead of inserting into a buffer.

;; citar-org-roam only offers the citar-org-roam-note-title-template variable
;; for customizing the contents of a new note and no way to specify a custom
;; capture template. And the title template uses citar's own format, which means
;; we can't run arbitrary functions in it.
;;
;; Left with no other options, we override the
;; citar-org-roam--create-capture-note function and use our own template in it.
(defun dh/citar-org-roam--create-capture-note (citekey entry)
  "Open or create org-roam node for CITEKEY and ENTRY."
  ;; adapted from https://jethrokuan.github.io/org-roam-guide/#orgc48eb0d
  (let ((title (citar-format--entry
                citar-org-roam-note-title-template entry)))
    (org-roam-capture-
     :templates
     '(("r" "reference" plain "%?" :if-new
        (file+head
         "%(concat
 (when citar-org-roam-subdir (concat citar-org-roam-subdir \"/\")) \"${citekey}.org\")"
         "#+title: ${title}\n\n#+begin_src bibtex\n%(dh/citar-get-bibtex citekey)\n#+end_src\n")
        :immediate-finish t
        :unnarrowed t))
     :info (list :citekey citekey)
     :node (org-roam-node-create :title title)
     :props '(:finalize find-file))
    (org-roam-ref-add (concat "@" citekey))))

;; citar has a function for inserting bibtex entries into a buffer, but none for
;; returning a string. We could insert into a temporary buffer, but that seems
;; silly. Plus, we'd have to deal with trailing newlines that the function
;; inserts. Instead, we do a little copying and implement our own function.
(defun dh/citar-get-bibtex (citekey)
  (let* ((bibtex-files
          (citar--bibliography-files))
         (entry
          (with-temp-buffer
            (bibtex-set-dialect)
            (dolist (bib-file bibtex-files)
              (insert-file-contents bib-file))
            (bibtex-search-entry citekey)
            (let ((beg (bibtex-beginning-of-entry))
                  (end (bibtex-end-of-entry)))
              (buffer-substring-no-properties beg end)))))
    entry))

(advice-add #'citar-org-roam--create-capture-note :override #'dh/citar-org-roam--create-capture-note)

Permanent notes

In my system, permanent notes are the opposite of fleeting notes. They are written more deliberately, in a way that’s understandable without further context. They will usually not be deleted, although I may sometimes merge multiple notes into a bigger note. Permanent notes can contain original ideas, observations, facts, or any other piece of information.

Working with permanent notes follows basic org-mode and org-roam workflows, namely:

For grouping notes by category, I use a :context: property that links to notes representing the categories. The backlinks of a category note act as an automatic index of notes in that category. Backlinks can be viewed in the org-roam buffer, which can be displayed with org-roam-buffer-toggle. The benefit of using notes for categories, instead of tags, is that I can trivially describe a category simply by filling out its note.

The :previous: property denotes that a note is part of a series by linking to the previous note. The links allow walking the series backwards, and automatic backlinks allow walking it forwards.

Articles

Articles are permanent notes that are published on my blog. They have the following additional metadata:

There are no fundamental differences between “normal” permanent notes and articles. Articles are simply notes I deem interesting enough to be listed on the website’s front page. Of course, articles get more thought put into them and will be edited to a higher standard than random notes.

Website generation

As the articles on my website are org-roam notes, I need a way to turn them into something that can be uploaded and rendered in a browser. I prefer the static site approach for static content and am already using Hugo for other projects. The problem, then, is to convert Org files to Hugo-compatible Markdown, generating valid front matter and converting links between notes to links that will work online. ox-hugo does all of that.

All that is needed is some glue that filters out all articles and exports them:

(defun dh/org-cite-export-bibliography-advice (fn keyword _ info)
  (if (org-cite-list-keys info)
      (funcall fn keyword nil info)))

;; The CSL style we use causes an error when trying to export an empty bibliography. Wrap the relevant function to
;; prevent that from happening.
(advice-add #'org-cite-export-bibliography :around #'dh/org-cite-export-bibliography-advice)

(defun dh/org-roam-node-directory (node)
  (string-remove-suffix
   "/"
   (string-remove-prefix
    "/"
    (string-remove-prefix
     org-roam-directory
     (file-name-directory (org-roam-node-file node))))))

(defun dh/org-roam-articles ()
  (cl-remove-if-not
   (lambda (node)
     (string= "article" (cdr (assoc-string "KIND" (org-roam-node-properties node)))))
   (org-roam-node-list)))

(defun dh/org-roam-to-hugo (section files)
  "Call `org-hugo-export-to-md' on all Org FILES.
All files have to be in `org-roam-directory'. Output is written
relative to SECTION in `org-hugo-base-dir'. Org files in
subdirectories of `org-roam-directory' will get matching
subdirectories underneath SECTION."
  (mapcar
   (lambda (node)
     (with-current-buffer (find-file-noselect (org-roam-node-file node))
       (let ((org-hugo-section (file-name-concat section (dh/org-roam-node-directory node))))
         (org-hugo-export-to-md))))
   files))

(defun dh/org-insert-date-keyword ()
  (org-roam-set-keyword "date" (format-time-string "[%Y-%m-%d %a]" (current-time))))

(defun dh/org-export-before-parsing (backend)
  (when (string= backend "hugo")
    (org-roam-set-keyword
     "hugo_lastmod"
     (format-time-string "%Y-%m-%d" (file-attribute-modification-time (file-attributes (buffer-file-name)))))))

(dh/org-roam-to-hugo "articles" (dh/org-roam-articles))

Additionally, some properties are set in ~/notes/.dir-locals.el:

((org-mode . ((org-hugo-base-dir . "~/prj/honnef.co")
              (org-cite-export-processors . ((t csl "~/notes/ieee.csl")))
              (eval . (setq-local
                       org-export-before-parsing-functions
                       (append org-export-before-parsing-functions '(dh/org-export-before-parsing)))))))

org-cite-export-processors configures how in-text citations and bibliographies should be rendered. I use the CSL processor and the IEEE style. To be able to use CSL, the citeproc package needs to be installed.

The function that’s added to org-export-before-parsing-functions ensures that the Hugo front matter includes the lastmod property, set to the Org file’s modification time. This way, lastmod updates whenever the file gets saved, or when it gets edited with external tools.

The same approach could be used to set other Hugo-specific front matter without having to “pollute” the Org files.

For ox-hugo’s output to render correctly in Hugo, “unsafe” markup has to be permitted. Otherwise, its Markdown parser will reject HTML inside Markdown files:

[markup.goldmark.renderer]
unsafe = true

Because I am already generating my website this way, I might as well publish non-article notes, as a sort of braindump. Most notes will not be useful to a large audience, but Google will be able to index them nevertheless, and if only one person finds a note helpful, that’ll be enough to make it worth it. However, some of my notes are private and should not be published. These notes have a :private: attribute in their property drawer and get filtered out:

(defun dh/org-roam-public-notes ()
  (cl-remove-if
   (lambda (node)
     (or
      (cdr (assoc-string "PRIVATE" (org-roam-node-properties node)))
      (string= "article" (cdr (assoc-string "KIND" (org-roam-node-properties node))))))
   (org-roam-node-list)))

(dh/org-roam-to-hugo "notes" (dh/org-roam-public-notes))

Public notes may contain links to private notes, which would result in links Hugo can’t resolve. By advising org-hugo-link, one can filter out such links. Going a step further, if the private note has a ROAM_REFS property that is a URL, the link to the note can be replaced with a link to the URL. This might happen, for example, if there are private notes about websites. This idea was based on code from [2].

(defun dh/org-link-advice (fn link desc &rest rest)
  (if (string= "id" (org-element-property :type link))
      (dh/org-link-by-id fn link desc rest)
    (apply fn link desc rest)))

(defun dh/org-link-by-id (fn link desc rest)
  (let ((node (org-roam-node-from-id (org-element-property :path link)))
        (protocols '("http://" "https://" "ftp://")))
    (if (assoc-string "PRIVATE" (org-roam-node-properties node))
        ;; The linked-to note is private. If it has a ROAM_REFS property with a URL in it, link to that URL, otherwise
        ;; only insert the link description, but no target.
        (if-let ((url (seq-find (lambda (arg) (cl-some (lambda (p) (string-prefix-p p arg)) protocols))
                                (split-string-and-unquote (or (cdr (assoc-string "ROAM_REFS" (org-roam-node-properties node))) "")))))
            (format "[%s](%s)" desc url)
          desc)
      ;; Note isn't private, use original FN.
      (apply fn link desc rest))))

(advice-add #'org-hugo-link :around #'dh/org-link-advice)

On the Hugo side, I use the following partial for rendering backlinks to notes, which was adapted from [3].

{{ $re := $.File.BaseFileName }}
{{ $backlinks := slice }}
{{ range .Site.AllPages -}}
   {{ if and (findRE $re .RawContent) (not (eq $re .File.BaseFileName)) }}
      {{ $backlinks = $backlinks | append . }}
   {{ end -}}
{{ end }}

{{ if gt (len $backlinks) 0 }}
<h2 id="backlinks"><a class="heading-anchor" href="#backlinks">##</a> Links to this note</h2>
<ul>
  {{ range $backlinks }}
  <li><a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
  {{ end }}
</ul>
{{ end }}

There are several other options for generating websites from org-roam (and org-mode in general) files. I’ve arrived at this setup after reading several articles by other org-roam users [4], [5], [6], [7].

Common elements

I want all of my notes to have a #+date property, set to the creation time of the note. Instead of having to modify all templates to include it, I add a hook to org-roam-capture-new-node-hook that inserts the property whenever a new note is created.

(defun dh/org-insert-date-keyword ()
  (org-roam-set-keyword "date" (format-time-string "[%Y-%m-%d %a]" (current-time))))

(add-hook 'org-roam-capture-new-node-hook #'dh/org-insert-date-keyword)

I use org-roam-ui to get an overview of my notes, spot clusters of links, and quickly navigate related notes. You could consider this the digital equivalent of browsing the notes in your slip box.

To make it easier to capture notes and work with literature, I define my own transient menu that is bound to the F1 key:

(transient-define-prefix dh-do-stuff ()
  ""
  ["Org"
   [("oc" "org-capture" org-capture)
    ("ol" "org-store-link" org-store-link)
    ("ornf" "org-roam-node-find" org-roam-node-find)
    ("orni" "org-roam-node-insert" org-roam-node-insert)
    ("ci" "Insert citation" citar-insert-citation)
    ("co" "citar-open" citar-open)]])

(global-set-key (kbd "<f1>") #'dh-do-stuff)

Summary

I use the following Emacs packages:

I use the following external software:

References

[1]
S. Ahrens, How to take smart notes: one simple technique to boost writing, learning and thinking, 2nd ed. Hamburg, Germany: Sönke Ahrens, 2022.
[2]
[3]
B. Mezger, “Export org-roam backlinks with Gohugo,” Mar. 07, 2021. https://seds.nl/notes/export_org_roam_backlinks_with_gohugo/ (accessed Jan. 10, 2023).
[4]
N. Mather, “How I publish my org-roam wiki with org-publish,” Aug. 21, 2020. https://doubleloop.net/2020/08/21/how-publish-org-roam-wiki-org-publish/ (accessed Jan. 06, 2023).
[5]
N. Mather, “Options for publishing an org-roam-based digital garden.” https://commonplace.doubleloop.net/options-for-publishing-an-org-roam-based-digital-garden (accessed Jan. 06, 2023).
[6]
J. Kuan, “Jethro’s braindump,” Jan. 02, 2023. https://github.com/jethrokuan/braindump (accessed Jan. 06, 2023).
[7]
H. Cisneros, “My org roam notes workflow,” Jun. 15, 2021. https://hugocisneros.com/blog/my-org-roam-notes-workflow/ (accessed Jan. 06, 2023).