Org Roam, Emacs, and Ever Refining the Note Taking Process

Always Be Refining Your Config

I want to write about my third iteration on an org-roam. It’s goal is to address use-cases that I’ve encountered while moving more of my note-taking with org-roam.

One use-case is when I’m running or playing in an Role Playing Game (RPG 📖) session. During those sessions, when I create/find/insert nodes, I almost want to leverage the same tags. That can be in my capturing of nodes or in my searching for nodes. This is something I observed while running my 13 session “Thel Sector” campaign.

A second use-case is when I’m writing notes or thoughts related to work. In a past life, I might have written notes for either my employer or Samvera (a community in which I participated). Those notes might overlap but rarely did.

While I’m writing those notes, if I’m developing out concepts, I might want to filter my captures and searches to similar tags.

Another use case is less refined, namely I’m writing but am not “in” a specific context.

However, v2 of my org-roam structure, didn’t quite get out of the way. Iterating on my v2 org-roam setup was critical in learning more about Emacs. I will certainly reference my v2 org roam configuration as I continue my Emacs usage. I never quite got to the speed of note taking that I had for the original Thel Sector campaign.

What follows builds on Jethro Kuan’s How I Take Notes with Org-roam. Reading Jethro Kuan’s post helped me see how I could do this.


The jf/org-roam-capture-templates-plist variable defines the possible org-roam capture templates that I will use. I have chosen to narrow these to three types:

References to other people’s thoughts.
My thoughts, still churning, referencing other thoughts.
My thoughts, published and ready to share. Referencing any thoughts I’ve captured (and probably more).

Note: I chose to go with 4 character types to minimize it’s impact on rendering “type” in the search results (4 characters requires less visual space than 10 characters).

(defvar jf/org-roam-capture-templates-plist
   ;; These are references to "other people's thoughts."
   :refs '("r" "refs" plain "%?"
	   :if-new (file+head "refs/%<%Y%m%d>---${slug}.org" "#+title: ${title}\n#+FILETAGS:\n")
	   :unnarrowed t)
   ;; These are "my thoughts" with references to "other people's thoughts."
   :main '("m" "main" plain "%?"
	   :if-new (file+head "main/%<%Y%m%d>---${slug}.org"
			      "#+title: ${title}\n#+FILETAGS: ${auto-tags}\n")
	   :immediate-finish t
	   :unnarrowed t)
   ;; These are publications of "my thoughts" referencing "other people's thoughts".
   :pubs '("p" "pubs" plain "%?"
	   :if-new (file+head "pubs/%<%Y%m%d>---${slug}.org" "#+title: ${title}\n#+FILETAGS:\n")
	   :immediate-finish t
	   :unnarrowed t))
  "Templates to use for `org-roam' capture.")

The jf/org-context-plist defines and names some of the contexts in which I might be writing. Each named context defines the associated tags. These are the tags that all nodes will have when they are written in the defined context.

Loosely related is the jf/org-auto-tags--current-list; Contexts are a named set of tags. However, other functions don’t operate based on context. They instead operated based on the tags.

(defvar jf/org-context-plist
    :name "none"
    :tags (list))

    :name "burning-locusts"
    :tags '("burning-locusts"

    :name "forem"
    :tags '("forem"))

    :name "mistimed-scroll"
    :tags '("eberron"
    :name "thel-sector"
    :tags '("thel-sector"
	    "rpg" "swn")))
  "A list of contexts that I regularly write about.")

(defvar jf/org-auto-tags--current-list
  "The list of tags to automatically apply to an `org-roam' capture.")

I can use jf/org-auto-tags--set to create an ad hoc context, or perhaps a “yet to be named” context. I can use jf/org-auto-tags--set-by-context to establish the current context (or clear it).

(defun jf/org-auto-tags--set (tags)
  "Prompt user or more TAGS."
     "Tag(s): " (org-roam-tag-completions))))
  (setq jf/org-auto-tags--current-list tags))

(cl-defun jf/org-context-list-completing-read
  "Create a list of contexts from the CONTEXT-PLIST for completing read.

       The form should be '((\"forem\" 1) (\"burning-loscusts\" 2))."
  ;; Skipping the even entries as those are the "keys" for the plist,
  ;; the odds are the values.
  (-non-nil (seq-map-indexed
	     (lambda (context index)
	       (when (oddp index)
		 (list (plist-get context :name) index)))

(cl-defun jf/org-auto-tags--set-by-context
     (context-plist jf/org-context-plist))
  "Set auto-tags by CONTEXT.

   Prompt for CONTEXT from CONTEXT-PLIST."
     "Context: " (jf/org-context-list-completing-read))))
  (setq jf/org-auto-tags--current-list
	  context-plist (intern (concat ":" context)))

With the jf/org-auto-tags--current-list variable set, I want a function to inject those tags onto my captures. Looking at the org-roam docs on template expansion, I want to create a function named org-roam-node-auto-tags.

(cl-defun org-roam-node-auto-tags
     (tag-list jf/org-auto-tags--current-list))
  "Inject the TAG-LIST into the {auto-tags} region of captured NODE.

  (if (and tag-list (> (length tag-list) 0))
      (concat ":" (s-join ":" tag-list) ":")

And finally, we have functions to use for establishing what templates are available based on the context, as well as what to setup as the default filter-fn for org-capture.

In other words, when I have set one or more tags, I want to use the templates appropriate for those tags and filter my org-roam-nodes so that only those nodes that have all of the tags are candidates.

(cl-defun jf/org-roam-templates-list
     (template-plist jf/org-roam-capture-templates-plist))
  "List of `org-roam' capture templates based on the given TEMPLATE.

     Searches the TEMPLATE-PLIST for the templates.

     Note, the :all template assumes we use the whole list."
  (if (eq template :all)
	(lambda (tmp index)
	  (when (oddp index)
    (list (plist-get template-plist template))))

(cl-defun jf/org-roam-templates-context-fn
     (tag-list jf/org-auto-tags--current-list))
  "Returns a set of templates based on TAG-LIST.

     A key assumption is that if there's a default tag list, use the
     :main template."
  (if (and tag-list (> (length tag-list) 0))
      (jf/org-roam-templates-list :main)
    (jf/org-roam-templates-list :all)))

(cl-defun jf/org-roam-filter-context-fn
     (tag-list jf/org-auto-tags--current-list))
  "Determine TAG-LIST is subset of NODE's tags."
  ;; gnus-subsetp is a more "permissive" version of subsetp.  It doesn't
  ;; consider order.  And looks at strings as equal if their values are the
  ;; same.
  (gnus-subsetp tag-list (org-roam-node-tags node)))


I wrote three functions to mirror three core functions of org-mode:

  • jf/org-roam-capture: find or create a node and file it away.
  • jf/org-roam-node-insert: find or create a node and insert a link to that node. This is my “take notes quick” function.
  • jf/org-roam-find-node: find a node and open that node in the frame.

For each of those functions, I establish the filter based on the current context and/or tags. I also limit the available capture templates based on the context.

(defun jf/org-roam-capture
  "Call `org-roam-capture' based on set tags."
  (interactive "P")
   :filter-fn 'jf/org-roam-filter-context-fn
   :templates (jf/org-roam-templates-context-fn)))

(defun jf/org-roam-node-insert ()
  "Call `org-roam-node-insert' based on set tags."
   :templates (jf/org-roam-templates-context-fn)))

(defun jf/org-roam-find-node
  "Call `org-roam-node-find' based on set tags."
  (interactive current-prefix-arg)
   :templates 'jf/org-roam-templates-context-fn))

And with all of that, let’s get into the org-roam configuration.

(use-package org-roam
  :straight t
  ;; I encountered the following message when attempting to export data:
  ;; => "org-export-data: Unable to resolve link: EXISTING-PROPERTY-ID"
  ;; See for details
  (defun jf/force-org-rebuild-cache ()
    "Call some functions to rebuild the `org-mode' and `org-roam' cache."
    ;; Note: you may need `org-roam-db-clear-all' followed by `org-roam-db-sync'
  (org-roam-directory (file-truename "~/git/org"))
   (concat "${type:4}   ${title:*} "
	   (propertize "${tags:40}" 'face 'org-tag)))
  (org-roam-capture-templates (jf/org-roam-templates-list :all))
  :bind (("C-s-f" . jf/org-roam-find-node)
	 ("C-s-c" . jf/org-roam-capture))
  :bind (:map org-mode-map
	       ("C-s-;" . org-roam-buffer-toggle)
	       ("s-i" . jf/org-roam-node-insert)))
  ;; Help keep the `org-roam-buffer', toggled via `org-roam-buffer-toggle', sticky.
  (add-to-list 'display-buffer-alist
		 (side . right)
		 (slot . 0)
		 (window-width . 0.33)
		 (window-parameters . ((no-other-window . t)
				       (no-delete-other-windows . t)))))
  ;; When t the autocomplete in org documents will query the org roam database
  (setq org-roam-completion-everywhere t)
  (setq org-roam-v2-ack t)

;; This needs to be after the `org-roam’ declaration as it is dependent on the
;; structures of `org-roam'.
(cl-defmethod org-roam-node-type ((node org-roam-node))
  "Return the TYPE of NODE."
  (condition-case nil
	  (org-roam-node-file node)
    (error "")))

All told, the past experience when running New Vistas in the Thel Sector // Take on Rules informed how I thought about my note taking.

Other Contexts

Try as I may, based on my configuration, I can’t get org-protocol to work. So I’ve opted to take a different path; write some Emacs functions instead.

  • jf/org-roam-capture-ref: Capture a “refs” context org-roam-node for the given title and url.
  • jf/menu-dwim--org-capture-elfeed-show: Capture an RSS entry.
  • jf/menu-dwim--org-capture-firefox: Capture the active tab of Firefox.
  • jf/menu-dwim--org-capture-safari: Capture the active tab of Safari.

These tie into my the context and auto-tags.

(cl-defun jf/org-roam-capture-ref (&key title url)
  "Capture the TITLE and URL in the `org-roam' :refs template"
  ;; If your installation of org-roam includes the fix fore
  ;; then you can leave the
  ;; below commented out.
  ;; This looks a bit odd, but to capture the :ref we need the callback from org-roam.
  ;; (require 'org-roam-protocol)
   :keys "r"
   ;; TODO: I would love to get tags working but I'm missing something
   :node (org-roam-node-create :title title)
   :info (list :ref url)
   :props '(:immediate-finish nil)
   :templates (jf/org-roam-templates-list :refs)))

(cl-defun jf/menu-dwim--org-capture-elfeed-show (&key (entry elfeed-show-entry))
  "Create an `org-roam-node' from elfeed ENTRY."
  (let ((url (elfeed-entry-link entry))
	(title (elfeed-entry-title entry)))
    (jf/org-roam-capture-ref :url url :title title)))

(defun jf/menu-dwim--org-capture-firefox ()
  "Create an `org-roam-node' from Firefox page.

  Depends on the `grab-mac-link' package."
  (let* ((link-title-pair (grab-mac-link-firefox-1))
	 (url (car link-title-pair))
	 (title (cadr link-title-pair)))
    (jf/org-roam-capture-ref :url url :title title)))

(defun jf/menu-dwim--org-capture-safari ()
  "Create an `org-roam-node' from Safari page.

  Depends on the `grab-mac-link' package."
  (let* ((link-title-pair (grab-mac-link-safari-1))
	 (url (car link-title-pair))
	 (title (cadr link-title-pair)))
    (jf/org-roam-capture-ref :url url :title title)))

(defun jf/menu-dwim--org-capture-eww ()
  "Create an `org-roam-node' from `eww' data"
  (let* ((url (plist-get eww-data :url))
	 (title (plist-get eww-data :title)))
    (jf/org-roam-capture-ref :url url :title title)))


This is the core of my note taking engine. It builds on the idea that I want to reduce the number of decisions I make. This is extremely important when I’m writing session notes.

While I’m playing in a session, my entire context ideally collapses to the relevant tags that I’ve established at the beginning of the session. That way I’m certain that I’m filing away notes to their proper location.