Org-roam: custom linking during capture
After using plain Org Mode for a few years and a brief experiment with Logseq1, I finally settled with Org-roam for my work notes and task management.
Org-roam builds on Emacs’ org-mode, adding functionality known from tools like Roam Research or Obsidian. Notes can be linked with each other, creating a “knowledge graph”2.
After some initial hurdles, I ended up with leveraging the “dailies” feature. Daily note contains headings with tickets I work on, linked to their respective nodes.
With this method, it is easy to track (and report) my work by just
reviewing the journal files. There is a small downside - if I would
like to see my progress on a specific task, I need to use the
backlinking feature (with org-roam-buffer-toggle
in the task
node). It is nice for a quick glance, but for longer analysis and
reporting I found it tedious. I even wrote some ugly
Elisp
to generate reports, but it still felt off.
Few days ago, I finally found some time to work on this and gathered the following requirements:
- daily notes related to a task should land in its own node,
- task node should contain a
Log
heading, with timestamped subheadings linking to a daily node, - capturing a task entry should automatically add a link to the task in a daily node.
To implement this behavior, two entry points were used:
- function being called with
org-capture-after-finalize-hook
, that will call another capture, adding a link to the daily note, - a custom org-roam capture target, leveraging org-roam custom references to insert a heading with a link.
Starting with the second one:
(setq org-roam-capture-templates
'(
("t" "Task note" entry
"**** %?"
:target (file+head+olp "pages/${slug}.org"
"#+title: ${title}\n\n" ("Log" "${roam-daily-link}"))
:unnarrowed t
:prepend nil)
))
The ${roam-daily-link}
is a custom template expansion I added:
;; Temporary hook to close daily after capture:
(defun temp-save-close-daily-hook ()
(save-buffer)
(kill-buffer))
;;Use ${roam-daily-link} to insert a link to today's daily
(defun roam-daily-link (node)
(let* ((l/daily-title
(format-time-string "%Y-%m-%d %a" (current-time)))
(l/daily-node
(org-roam-node-from-title-or-alias l/daily-title)))
(save-excursion
(if (not (eq nil l/daily-node))
(format "[[id:%s][%s]]"
(org-roam-node-id l/daily-node)
l/daily-title)
(progn
(add-hook
'org-roam-dailies-find-file-hook
#'temp-save-close-daily-hook)
(org-roam-dailies-capture-today t "d")
(remove-hook
'org-roam-dailies-find-file-hook
#'temp-save-close-daily-hook)
(setq l/daily-node
(org-roam-node-from-title-or-alias l/daily-title))
(format "[[id:%s][%s]]"
(org-roam-node-id l/daily-node)
l/daily-title))))))
The problem with inserting a daily link is that it could not exist
yet. I might need to create it first (with
org-roam-dailies-capture-today
), but to avoid opening the daily
buffer I need to temporarily hook org-roam-dailies-find-file-hook
to
save and close the buffer.
The last missing part is the capture finalization hook:
(defun wigol/org-roam-capture-in-journal-finalize-hook ()
"Adds the captured task file to the daily as a link"
;; If:
;; - the capture was confirmed,
;; - we are not in internal capture,
;; - current template supports daily logging.
(unless (or org-note-abort
(string= (org-capture-get :key) "ATC")
(not
(member
(org-capture-get :key)
my-org-roam-capture-in-journal-supported-keys)))
(with-current-buffer (org-capture-get :buffer)
(let ((l/name (file-name-sans-extension (buffer-name)))
(l/link
(format "[[id:%s][%s]]"
(org-roam-id-at-point)
(file-name-sans-extension (buffer-name))))
(l/daily-file
(concat
(file-name-as-directory org-roam-directory)
(file-name-as-directory org-roam-dailies-directory)
(format-time-string "%Y_%m_%d" (current-time)) ".org"))
(l/already-filed nil)
(org-roam-dailies-capture-templates
'(("ATC" "After Task Capture" entry "* %c %?"
:target
(file+head
"%<%Y_%m_%d>.org" "#+title: %<%Y-%m-%d %a>")
:immediate-finish t))))
; Check if the top level heading already exists in the journal:
(if (file-exists-p l/daily-file)
(progn
(save-excursion
(with-current-buffer (find-file-noselect
l/daily-file)
(setq l/already-filed
(search-forward (format "* %s" l/link)
nil
t))))))
(if (eq l/already-filed nil)
(progn
(kill-new l/link)
(org-roam-dailies-capture-today nil "ATC")
;; Clean kill ring:
(when kill-ring
(setq kill-ring (cdr kill-ring)))
(when kill-ring-yank-pointer
(setq kill-ring-yank-pointer kill-ring))))))))
my-org-roam-capture-in-journal-supported-keys
is a list of template
keys, where I would like to have the daily logging behavior.
Since I do not want add a link to the daily every time I make a note in the task, I check first if it has already been logged at top level.
At least for now, I am satisfied with the result:
Org-roam supports querying, also for relations between nodes. I guess the next step would be to use that for reporting.
-
It was a slick experience, especially paired with some Elisp to make it work with org-roam (mostly for code blocks). In a recent roadmap update, however, they announced pulling the plug on org-mode format support. Back to Org-roam only, I guess… ↩︎
-
using excelent org-roam-ui package. ↩︎