I found org-roam too slow, so I made quickroam. And that idea spun off into this package, a standalone thing. I hope it’s also easier to pick up than org-roam.
- If you were using org-roam, there is nothing to migrate. You can use both packages. It’s exactly the same on-disk format: “notes” are identified by their org-id.
With optional shims, you can even skip syncing the org-roam DB and continue using its rich backlinks buffer and org-roam-capture!
In pursuit of being “just org-id”, this package has no equivalent setting to
org-roam-directory
– it just looks uporg-id-locations
. - If you were not using org-roam, maybe think of it as somewhat like org-recent-headings beefed-up to the extent that you won’t need other methods of browsing.
If you were the sort of person to prefer ID-links over file links or any other type of link, you’re in the right place! Now you can rely on IDs, and—if you want—stop worrying about filenames, directories and subtree hierarchies. As long as you’ve assigned an ID to a heading, you can find it later.
My life can be divided into two periods ”before org-roam” and ”after org-roam”. I crossed a kind of gap once I got a good way to link between my notes. It’s odd to remember when I just relied on browsing subtrees and folders – what a strange way to work!
I used to lose track of things I had written, under some forgotten heading in a forgotten file in a forgotten directory. The org-roam method let me find and build on my own work, instead of recreating my work all the time.
At the core, all the “notetaking packages” (orgrr/zk/zetteldeft/org-roam/denote/howm/minaduki/…) try to help you with this: make it easy to link between notes and explore them.
Right off the bat, that imposes two requirements: a method to search for notes, since you can’t link to something you can’t search for, and a design-choice about what kinds of things should turn up as search hits. What’s a “note”?
Just searching for Org files is too coarse. Just searching for any subtree anywhere brings in too much clutter.
Here’s what org-roam invented. It turns out that if you limit the search-hits to just those files and subtrees you’ve deigned to assign an org-id – which roughly maps to everything you’ve ever thought it was worth linking to – it filters out the noise excellently.
Once a subtree has an ID you can link to, it’s a “node” because it has joined the wider graph, the network of linked nodes. I wish the English language had more distinct sounds for the words “node” and “note”, but to clarify, I’ll say “ID-node” when the distinction matters.
A comparison of three similar systems, which permit relying on org-id and don’t lock you into the all-too-seductive concept of “one-note-per-file”.
Feature | org-roam | org-node | org-super-links |
---|---|---|---|
Backlinks | yes | yes | yes |
Node search and insert | yes | yes | – (suggests org-ql) |
Node aliases | yes | yes | – |
Node exclusion | yes | limited | not applicable |
Rich backlinks buffer | yes | yes (org-roam’s) | – |
Customize how backlinks shown | yes | yes (org-roam’s) | yes |
Reflinks | yes | yes (as backlinks) | – |
Ref search | yes | yes (as aliases) | not applicable |
Org 9.5 @citekeys as refs | yes | yes | not applicable |
Support org-ref v2 and v3 | yes | limited | not applicable |
Work thru org-roam-capture | yes | yes | ? |
Work thru org-capture | – | yes | ? |
Daily-nodes | yes | yes | – |
Node series | – | yes | – |
Show backlinks in same window | – | yes | yes |
Cooperate with org-super-links | – | yes | not applicable |
Fix link descriptions | – | yes | – |
List dead links | – | yes | – |
Rename file when title changes | – | yes | – |
Warn about duplicate titles | – | yes | – |
Principled “related-section” | – | – | yes |
Refile | yes | – | – |
Untitled notes | – | – | – |
Support roam: links | yes | – (WONTFIX) | not applicable |
Can have separate note piles | yes | – (WONTFIX) | not applicable |
Some query-able cache | EmacSQL | hash tables | – |
Async cache rebuild | – | yes | not applicable |
Time to cache my 3000 nodes | 2m 48s | 0m 02s | not applicable |
Time to save file w/ 400 nodes | 5–10s | instant | ? |
Time to open minibuffer | 1–3s | instant | not applicable |
This package is finally on MELPA! Assuming your package manager knows about it, add this initfile snippet:
(use-package org-node
:after org
:config (org-node-cache-mode))
If you are an org-roam user, you’ll want the org-node-fakeroam module as well. It is not on MELPA yet, so use straight.el or similar and add an init snippet like this:
(use-package org-node-fakeroam
:straight (org-node-fakeroam
:type git :host github :repo "meedstrom/org-node"
:files ("org-node-fakeroam.el")
:branch "melpa")
:after org-node
:requires org-node)
If you’re new to these concepts, fear not. The main things for day-to-day operation are two verbs: “find” and “link”.
Pick some short keys and try them out.
(keymap-set global-map "M-s M-f" #'org-node-find)
(keymap-set org-mode-map "M-s M-i" #'org-node-insert-link)
To browse config options, type M-x customize-group RET org-node RET
.
Final tip: there’s no separate command for creating a new node! Reuse one of the commands above, then type the name of a node that doesn’t exist. Try it and see what happens!
These settings help you feel at home using both packages side-by-side:
(setq org-node-creation-fn #'org-node-new-via-roam-capture)
(setq org-node-slug-fn #'org-node-slugify-like-roam-actual)
(setq org-node-datestamp-format "%Y%m%d%H%M%S-")
If you’ve struggled in the past with big files taking a long time to save, consider these org-roam settings:
(setq org-roam-db-update-on-save nil) ;; don't update DB on save, not needed
(setq org-roam-link-auto-replace nil) ;; don't look for "roam:" links on save
And if the backlinks buffer is laggy:
(org-node-fakeroam-fast-render-mode) ;; build the Roam buffer faster
Finally, make sure the underlying org-id knows about the files org-roam knows. You’d think it would, but that isn’t a given! Do either of these:
- Run
M-x org-roam-update-org-id-locations
after every Emacs restart - Edit the following setting so it includes your
org-roam-directory
. Supposing that is “~/org/”, set this:
(setq org-node-extra-id-dirs '("~/org/"))
With that done, try out the commands we went over in Quick start. There’s more under Toolbox. Enjoy!
Backlinks are the butter on the bread of your notes. If you’ve ever seen a “What links here” section on some webpage, that’s exactly what it is. Imagine seeing that, all the time. The below sections outline two general ways to do so.
As a Roam user, you can just keep using M-x org-roam-buffer-toggle
, but you get some new ways to keep its data fresh, circumventing Roam’s autosync mode.
Let org-roam manage its own DB.
If you didn’t have laggy saves, this is fine. In other words, keep variable org-roam-db-update-on-save
at t.
Tell org-node to write to the org-roam DB.
(setq org-roam-db-update-on-save nil)
(org-node-fakeroam-db-feed-mode)
Unfortunately, it is still a bit slow when I save a file with 400 nodes. Maybe 100x faster than org-roam, but still a noticeable delay. It appears to be all down to EmacSQL – if someone could help optimize how we call EmacSQL, that’d be much appreciated. Until then, I recommend Option 1C.
Related: if you often have reason to full-reset the DB, there is a faster command than C-u M-x org-roam-db-sync
! Try M-x org-node-fakeroam-db-rebuild
. Benchmark:
C-u M-x org-roam-db-sync
! 179 secondsM-x org-node-fakeroam-db-rebuild
: 6 seconds
This is also a matter where I request help, since 6 seconds seems about a thousand times slower than it needs to be. I envision running it on every save – that’s obviously not possible yet.
Cut out the DB altogether.
No more battling SQLite! Type M-x org-node-fakeroam-jit-backlinks-mode
, then see what populates your Roam buffer henceforth. Hopefully you see the same links as before.
If you’re happy with the result, you can disable org-roam-db-autosync-mode
entirely in favour of the slimmer M-x org-node-fakeroam-redisplay-mode
. As an init snippet:
(org-roam-db-autosync-mode 0)
(org-node-fakeroam-jit-backlinks-mode) ;; no use sqlite to build buffer
(org-node-fakeroam-fast-render-mode) ;; build the buffer fast
(org-node-fakeroam-redisplay-mode) ;; always show relevant backlinks
I rarely have the screen space to display a backlink buffer. Because it needs my active involvement to keep visible, I go long periods seeing no backlinks. This solution can be a great complement (or even stand alone).
Let org-node add a :BACKLINKS: property to all nodes.
For a first-time run, type M-x org-node-backlink-fix-all
. (Don’t worry, if you change your mind, you can undo with M-x org-node-backlink-regret
.)
Then start using the minor mode org-node-backlink-mode
, which keeps these properties updated. Init snippet:
(add-hook 'org-mode-hook #'org-node-backlink-mode)
NOTE: I hadn’t even realized this, but people who don’t use visual-line-mode may not find this solution very scalable, since Org properties must stay on one line. This marks the second time I distribute software that assumes visual-line-mode :-)
Let org-super-links manage a :BACKLINKS:…:END: drawer under all nodes.
I think the following should work. Totally untested, let me know!
(add-hook 'org-node-insert-link-hook #'org-node-convert-link-to-super)
Bad news: this is currently directed towards people who used org-super-links from the beginning, or people who are just now starting to assign IDs, as there is not yet a command to add new BACKLINKS drawers in bulk to preexisting nodes. (org-super-links#93)
You may have heard that org-roam has its own special set of capture templates: the org-roam-capture-templates
.
People who understand the magic of capture templates, they may take this in stride. Me, I never felt confident using a second-order abstraction over an already leaky abstraction I didn’t fully understand.
Can we just use vanilla org-capture? That’d be less scary. The answer is yes!
The secret sauce is (function org-node-capture-target)
:
(setq org-capture-templates
'(("i" "Capture into ID node"
plain (function org-node-capture-target) nil
:empty-lines-after 1)
("j" "Jump to ID node"
plain (function org-node-capture-target) nil
:jump-to-captured t
:immediate-finish t)
;; Sometimes handy after `org-node-insert-link' to
;; make a stub you plan to fill in later
("q" "Make quick stub ID node"
plain (function org-node-capture-target) nil
:immediate-finish t)))
With that done, you can optionally configure the everyday commands org-node-find
& org-node-insert-link
to outsource to org-capture when they try to create new nodes:
(setq org-node-creation-fn #'org-capture)
Do you already know about “daily-notes”? Then get started with a keybinding such as:
(keymap-set global-map "M-s s" #'org-node-series-dispatch)
and configure org-node-series-defs
if needed. See wiki.
It’s easiest to explain series if we use “daily-notes” (or “dailies”) as an example of a series.
Roam’s idea of a “daily-note” is the same as an org-journal entry: a file/entry where the title is just today’s date.
You don’t need software for that basic idea, only to make it extra convenient to navigate them and jump back and forth in the series.
Thus, fundamentally, any “journal” or “dailies” software are just operating on a sorted series to navigate through. I was resisting having to implement an equivalent to the Org-roam option org-roam-dailies-directory
, so I ended up generalizing the concept and it’s really powerful!
You could have series about, let’s say, historical events, Star Trek episodes, your school curriculum…
Define more series in the variable org-node-series-defs
.
Already included is a definition that approximates the org-roam-dailies defaults, but I encourage you to override it to suit your tastes.
You may be taken aback that defining a new series requires writing 5 lambdas, but once you get the hang of it, you can often reuse those lambdas.
I find unsatisfactory the config options in org-id (Why? See Taking ownership of org-id), so org-node gives you an extra way to feed data to org-id, making sure we won’t run into “ID not found” situations.
Example setting:
(setq org-node-extra-id-dirs
'("/home/kept/org/"
"~/Syncthing/project2/"
"/mnt/stuff/"))
It’s not for everyone, but there is the option:
(org-node-complete-at-point-mode)
Say there’s a link to a web URL, and you’ve forgotten you also have a node listing that exact URL in its ROAM_REFS
property.
Wouldn’t it be nice if, clicking on that link, you automatically visit that node first instead of being sent to the web? Here you go:
(add-hook 'org-open-at-point-functions
#'org-node-try-visit-ref-node)
Working over TRAMP is untested, but I suspect it won’t work. Org-node tries to be very fast, often nulling file-name-handler-alist
, which TRAMP needs.
The best way to change this is to file an issue to show you care :-)
If two ID-nodes exist with the same title, one of them disappears from minibuffer completions.
That’s just the nature of completion. Other packages such as Roam have the same limitation. Much can be said for embracing the uniqueness constraint, and org-node will print messages telling you about title collisions.
Anyway… there’s a workaround. Assuming you leave org-node-affixation-fn
at its default setting, just add to initfiles:
(setq org-node-alter-candidates t)
This lets you match against the node outline path and not only the title, which resolves most conflicts given that the most likely source of conflict is subheadings in disparate files, that happen to be named the same. Some people make this trick part of their workflow.
NB: this workaround won’t help the in-buffer completions provided by org-node-complete-at-point-mode
, but with a light peppering of luck this isn’t something you’ll ever have to notice. (Work on Issue 38 for principled fix)
The option org-node-filter-fn
works well for excluding TODO items that happen to have an ID, and excluding org-drill items and that sort of thing, but beyond that, it has limited utility because unlike org-roam, child ID nodes of an excluded node are not excluded!
So let’s say you have a big archive file, fulla IDs, and you want to exclude all of them from appearing in the minibuffer. Putting a :ROAM_EXCLUDE: t
at the top won’t do it. As it stands, what I’d suggest is to use the file name.
While a big selling point of IDs is that you avoid depending on filenames, it’s often pragmatic to let up on purism just a bit :-) It works well for me to filter out any file or directory that happens to contain “archive” in the name – see the last line here:
(setq org-node-filter-fn
(lambda (node)
(not (or (org-node-get-todo node) ;; Ignore headings with todo state
(member "drill" (org-node-get-tags node)) ;; Ignore :drill:
(assoc "ROAM_EXCLUDE" (org-node-get-properties node))
(string-search "archive" (org-node-get-file-path node))))))
We support the builtin @citations, not (yet) org-ref &citations, since it slows down my scan about 50% if I amend org-link-plain-re
to match them. There’s a commented-out block of code in the file org-node-parser.el if you feel like getting your elbows dirty.
You can still find nodes with e.g. :ROAM_REFS: &citekey
, just not see backlinks.
I hear that on Termux on Android, filesystem access can be so slow that it’s a pain to cycle dailies with org-roam (11 seconds just to goto next daily!). A Redditor also said Apple NFS is not ideal for Emacs.
Good news. You can speed up some functions by making them look up org-node tables and avoid the filesystem:
(advice-add #'org-roam-list-files :override
#'org-node-fakeroam-list-files)
(advice-add #'org-roam-dailies--list-files :override
#'org-node-fakeroam-list-dailies)
(advice-add #'org-roam-dailies--daily-note-p :override
#'org-node-fakeroam-daily-note-p)
This has nothing to do with org-node, but I actually didn’t know this for ages.
If you don’t hard-wrap but prefer visual-line-mode or similar (org-roam#1862), you have to enable such modes yourself – it sensibly doesn’t inherit your Org hooks:
(add-hook 'org-roam-mode-hook #'visual-line-mode)
Basic commands:
org-node-find
org-node-insert-link
org-node-insert-transclusion
org-node-insert-transclusion-as-subtree
org-node-visit-random
org-node-series-dispatch
- Browse node series – see README
org-node-extract-subtree
- A bizarro counterpart to
org-roam-extract-subtree
. Export the subtree at point into a file-level node, leave a link where the subtree was, and show the new file as current buffer.
- A bizarro counterpart to
org-node-nodeify-entry
- (Trivial) Give an ID to the subtree at point, and run the hook
org-node-creation-hook
- (Trivial) Give an ID to the subtree at point, and run the hook
org-node-insert-heading
- (Trivial) Shortcut for
org-insert-heading
+org-node-nodeify-entry
- (Trivial) Shortcut for
org-node-grep
org-node-fakeroam-show-roam-buffer
- A different way to invoke the Roam buffer: display the buffer or refresh it if it was already visible. And a plot twist, if it was not visible, do not refresh until the second invocation.
- Useful if you have disabled the automatic redisplay, because the Roam command
org-roam-buffer-toggle
is not meant for that.
- Useful if you have disabled the automatic redisplay, because the Roam command
- A different way to invoke the Roam buffer: display the buffer or refresh it if it was already visible. And a plot twist, if it was not visible, do not refresh until the second invocation.
Rarer commands:
org-node-lint-all-files
- Can help you fix a broken setup: it runs org-lint on all known files and generates a report of syntax problems, for you to correct at will. Org-node assumes all files have valid syntax, though many of the problems reported by org-lint are survivable.
org-node-list-dead-links
- List links where the destination ID could not be found
org-node-list-reflinks
- List citations and non-ID links
org-node-rewrite-links-ask
- Look for link descriptions that got out of sync with the corresponding node title, then prompt at each link to update it
org-node-rename-file-by-title
- Auto-rename the file based on the current
#+title
- Works as an after-save-hook! Does nothing until you configure
org-node-renames-allowed-dirs
.
- Works as an after-save-hook! Does nothing until you configure
- Auto-rename the file based on the current
org-node-backlink-fix-all
- Update
BACKLINKS
property in all nodes
- Update
org-node-list-feedback-arcs
- (Requires GNU R) Explore feedback arcs in your ID link network. Can be a nice occasional QA routine.
org-node-rename-asset-and-rewrite-links
- Interactively rename an asset such as an image file and try to update all Org links to them. Requires wgrep.
- NOTE: For now, it only looks for links inside the root directory that it prompts you for, and sub and sub-subdirectories and so on – but won’t find a link outside that root directory.
Like if you have Org files under /mnt linking to assets in /home, those links won’t be updated. Neither if you choose ~/org/subdir as the root directory will links in ~/org/file.org be updated.
- NOTE: For now, it only looks for links inside the root directory that it prompts you for, and sub and sub-subdirectories and so on – but won’t find a link outside that root directory.
- Interactively rename an asset such as an image file and try to update all Org links to them. Requires wgrep.
API cheatsheet between org-roam and org-node.
Action | org-roam | org-node |
---|---|---|
Get ID near point | (org-roam-id-at-point) | (org-id-get nil nil nil t) |
Get node at point | (org-roam-node-at-point) | (org-node-at-point) |
Get list of files | (org-roam-list-files) | (org-node-list-files) |
Prompt user to pick a node | (org-roam-node-read) | (org-node-read) |
Get backlink objects | (org-roam-backlinks-get NODE) | (org-node-get-backlinks NODE) |
Get reflink objects | (org-roam-reflinks-get NODE) | (org-node-get-reflinks NODE) |
Get title | (org-roam-node-title NODE) | (org-node-get-title NODE) |
Get title of file where NODE is | (org-roam-node-file-title NODE) | (org-node-get-file-title NODE) |
Get title or name of file where NODE is | (org-node-get-file-title-or-basename NODE) | |
Get name of file where NODE is | (org-roam-node-file NODE) | (org-node-get-file-path NODE) |
Get ID | (org-roam-node-id NODE) | (org-node-get-id NODE) |
Get tags | (org-roam-node-tags NODE) | (org-node-get-tags NODE) , no inheritance |
Get outline level | (org-roam-node-level NODE) | (org-node-get-level NODE) |
Get whether this is a subtree | (zerop (org-roam-node-level NODE)) | (org-node-get-is-subtree NODE) |
Get char position | (org-roam-node-point NODE) | (org-node-get-pos NODE) |
Get properties | (org-roam-node-properties NODE) | (org-node-get-properties NODE) , no inheritance |
Get subtree TODO state | (org-roam-node-todo NODE) | (org-node-get-todo NODE) |
Get subtree SCHEDULED | (org-roam-node-scheduled NODE) | (org-node-get-scheduled NODE) |
Get subtree DEADLINE | (org-roam-node-deadline NODE) | (org-node-get-deadline NODE) |
Get subtree priority | (org-roam-node-priority NODE) | (org-node-get-priority NODE) |
Get outline-path | (org-roam-node-olp NODE) | (org-node-get-olp NODE) |
Get ROAM_REFS | (org-roam-node-refs NODE) | (org-node-get-refs NODE) |
Get ROAM_ALIASES | (org-roam-node-aliases NODE) | (org-node-get-aliases NODE) |
Get ROAM_EXCLUDE | (assoc "ROAM_EXCLUDE" (org-node-get-properties NODE)) , no inheritance | |
Ensure fresh data | (org-roam-db-sync) | (org-node-cache-ensure t t) |