This is the instruction manual for org-node. The README is separate, but you should not need it anymore.
Assuming your package manager gets recipes from MELPA, add the following initfile snippet.
(use-package org-mem
:config
(org-mem-updater-mode))
(use-package org-node
:init
;; Optional key bindings
;; Tip: Try changing these to just "M-o"!
(keymap-global-set "M-o n" org-node-global-prefix-map)
(with-eval-after-load 'org
(keymap-set org-mode-map "M-o n" org-node-org-prefix-map))
:config
(org-node-cache-mode))If you’re coming here from org-roam, try this instead:
(use-package org-mem
:config
(setq org-mem-watch-dirs
(list "~/org/")) ;; Your org-roam-directory here
(org-mem-updater-mode))
(use-package org-node
:init
;; Optional key bindings
;; Tip: Try changing these to just "M-o"!
(keymap-global-set "M-o n" org-node-global-prefix-map)
(with-eval-after-load 'org
(keymap-set org-mode-map "M-o n" org-node-org-prefix-map))
:config
(org-node-cache-mode)
(org-node-roam-accelerator-mode)
(setq org-node-creation-fn #'org-node-new-via-roam-capture)
(setq org-node-file-slug-fn #'org-node-slugify-like-roam-default)
(setq org-node-file-timestamp-format "%Y%m%d%H%M%S-"))See at the end of this readme: How to rollback
If you’re new to these concepts, fear not. The main things for day-to-day operation are two commands:
org-node-find(M-o n f)- Tip: Consider binding a short key like
M-o f
- Tip: Consider binding a short key like
org-node-insert-link(M-o n i)- Tip: Consider binding a short key like
M-o i
- Tip: Consider binding a short key like
Tip: There’s no separate command for creating a new node! Reuse one of the commands above, and type the name of a node that doesn’t exist. Try it and see what happens!
To see a list of all commands, enter an Org buffer and type M-o n <f1>.
To browse config options, type M-x customize-group RET org-node RET (or M-o n x o).
Backlinks are the butter on the bread that is your notes. If you’ve ever seen a “What links here” section on some webpage, that’s exactly what it is.
You do not have to create backlinks yourself, they are autogenerated. The following sections outline two basic ways this can work.
- Solution 1A: Reuse the org-roam buffer
-
You can use the org-roam buffer without the rest of org-roam! Enable the following mode.
Then to learn how to invoke the buffer, see their documentation at info:org-roam#The Org-roam Buffer or online website.
(org-node-roam-accelerator-mode)
- Solution 1B: Use the org-node-context buffer
-
Org-node ships a rewrite of the org-roam buffer, in the included extension org-node-context.el.
Try the command
org-node-context-dwim(M-o n b).If you like it, consider binding it to an easily typed key, such as
M-o M-b. Or you can let it keep itself updated, reflecting where point is at all times, by enabling this mode:;; Tip: if you're like org-node's author and constantly lose your window ;; configuration, the Emacs 30 command `toggle-window-dedicated' (C-x w d) ;; can help, or simply invoking `org-node-context-dwim' a lot. ;; The latter strategy tends to make this mode superfluous. (org-node-context-follow-mode)
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.
A complementary solution, which can also stand alone, is to have the backlinks written into the file, on an Org property line or in a drawer.
To be clear, this solution never generates new IDs (and neither would Solution 1). That’s your own business. This only adds/edits :BACKLINKS: properties or drawers.
Note
A difference between org-node-context (solution 1) and org-node-backlink-mode (solution 2) is that the latter only shows backlinks corresponding to links where the original site already has or inherits some ID. The former additionally shows backlinks to places that have no ID at all (since v3.14).
- Solution 2A: Automatic
:BACKLINKS:property line -
Add to initfiles:
(setq org-node-backlink-do-drawers nil) (org-node-backlink-mode)
For a first-time run, type
M-x org-node-backlink-mass-update-props. (Don’t worry if you change your mind; undo withM-x org-node-backlink-mass-delete-props.)
- Solution 2B: Automatic
:BACKLINKS:...:END:drawer -
Same as Solution 2A, but uses a multiline drawer.
Add to initfiles:
(setq org-node-backlink-do-drawers t) (org-node-backlink-mode)
For a first-time run, type
M-x org-node-backlink-mass-update-drawers. (Don’t worry if you change your mind; undo withM-x org-node-mass-delete-drawers.)
- Solution 2C: Semi-automatic
:BACKLINKS:...:END:drawer -
If you were previously using org-super-links, you can continue letting it manage its drawers, and leave org-node out of the matter.
Do not enable `org-node-backlink-mode` at all, just add to initfiles:
(add-hook 'org-node-insert-link-hook #'org-super-links-convert-link-to-super)
You may find these tools useful:
- 1. You can list any dead forward-links to fix them manually:
M-x org-node-list-dead-links - 2. You can add all missing backlinks in bulk:
M-x org-node-backlink-mass-update-drawers
The second command may be useful as a starting point if you’re new to org-super-links, pre-populating the notes you already have.
However, when you have pre-existing drawers… make a full backup before trying it!
Org-node has a different usage in mind than org-super-links. You may be accustomed to having old manually formatted and sorted drawers.
Running aforementioned command may re-sort your backlinks and re-format their appearance into something you don’t want; double-check the following options:
org-node-backlink-drawer-sorterorg-node-backlink-drawer-formatter
Finally, lines that contain no Org link such as
[[id:1234][Title]]are deleted, which would mean destroying any other info within. Same if a backlink is stale and no longer valid. - 1. You can list any dead forward-links to fix them manually:
You may have heard that org-roam has a set of meta-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.
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). Examples:
(setq org-capture-templates
'(("e" "Capture entry into ID node"
entry (function org-node-capture-target) "* %?")
("p" "Capture plain text 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
:prepend t
:immediate-finish t
:jump-to-captured t)
;; Sometimes handy after `org-node-insert-link', to
;; make a stub you plan to fill in later, without
;; leaving the current buffer for now
("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)That last optional functionality may only confuse you more if I try to describe it in words – better you give it a spin and see if you like.
For deeper hacking, see wiki.
One user had over a thousand project-nodes, but only just began to do a knowledge base, and wished to avoid seeing the project nodes.
This could work by—for example—excluding anything with the Org tag :project: or perhaps anything that has a TODO state. Here’s a way to exclude both:
(setq org-node-filter-fn
(lambda (node)
(not (or (member "project" (org-mem-tags node))
(org-mem-todo-state node)))))Or you could go with a whitelist approach, to show only nodes from a certain directory we’ll call “my-personal-wiki”:
(setq org-node-filter-fn
(lambda (node)
(string-search "/my-personal-wiki/" (org-mem-file node))))Since org-mem only looks at saved files on disk, new notes only “appear” after save. So it can make for a smoother experience to enable auto-save-visited-mode.
That is also the usage that I test most – I’ve had the following config for years:
(setq auto-save-visited-interval 2)
(auto-save-visited-mode)FWIW, org-node used to try to be smarter and also cache notes in unsaved buffers, but that quickly became difficult to reason about, especially wrt. deciding whether to update a backlink or not.
A global minor mode to complete words at point into known node titles:
(org-node-complete-at-point-mode)
(setq org-roam-completion-everywhere nil) ;; Stop org-roam equivalent.(Analogue to org-roam-node-display-template, for those of you who know what that is)
To customize how the nodes look in the minibuffer, configure org-node-affixation-fn:
M-x customize-variable RET org-node-affixation-fn
A related option is org-node-alter-candidates, which lets you match against the annotations as well as the title if set to t:
(setq org-node-alter-candidates t)A useful way to sort completions is to base it on a :TIME_MODIFIED: property under each of your ID-nodes.
First, of course, a few nodes must have such a property. Start generating them by adding this hook:
(setq org-node-property-mtime "TIME_MODIFIED")
(add-hook 'org-node-modification-hook
#'org-node-update-mtime-property)Then to start actually sorting nodes by their :TIME_MODIFIED: values:
(setq org-node-display-sort-fn
#'org-node-sort-by-mtime-property)I want to see :TIME_CREATED: right above :TIME_MODIFIED:.
So I re-sort the entire properties drawer on save:
(add-hook 'org-node-modification-hook
(defun my-sort-properties ()
(org-back-to-heading-or-point-min)
(re-search-forward org-property-drawer-re (org-entry-end-position))
(let ((end (pos-bol))
(beg (progn (goto-char (match-beginning 0))
(forward-line 1)
(point))))
(sort-lines nil beg end)))
50)
;; The above is so the following properties will come in predictable order.
(setq org-node-property-crtime "TIME_CREATED")
(setq org-node-property-mtime "TIME_MODIFIED")If you have Ripgrep installed on the computer, and Consult installed on Emacs, you can use this command to grep across all your Org files at any time.
org-node-grep(M-o n g)
This can be a power-tool for mass edits. Say you want to rename some Org tag :math: to :Math: absolutely everywhere. Then you could follow a procedure such as:
- Use
org-node-grepand type:math: - Use
embark-export(from package Embark) - Use
wgrep-change-to-wgrep-mode(from package wgrep) - Do a query-replace (
M-%) to replace all:math:with:Math: - Type
C-c C-cto apply the changes
(For background, see What are ROAM_REFS? at the end of this README.)
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 with files over TRAMP is unsupported, because org-mem works in parallel subprocesses which do not inherit your TRAMP setup.
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. Much can be said for embracing the uniqueness constraint, and org-node will print messages about collisions.
Anyway… there’s a workaround. Assuming you leave org-node-affixation-fn at its default setting, adding this to initfiles tends to do the trick:
(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.
NB: for users of org-node-complete-at-point-mode, this workaround won’t help those completions. With some luck you’ll link to the wrong one of two homonymous nodes, but it’s worth being aware. (#62)
Org-node supports the Org 9.5 @citations, but not fully the aftermarket org-ref &citations that emulate LaTeX look-and-feel.
What works is bracketed Org-ref v3 citations that start with “cite”, e.g. [[citep:...]], [[citealt:...]], [[citeauthor:...]], since org-mem-parser.el is able to pick them up for free.
What doesn’t work is e.g. [[bibentry:...]] since it doesn’t start with “cite”, nor plain citep:... since it is not wrapped in brackets.
If you need more of Org-ref, you have at least two options:
- Use org-roam - see discussions on boosting its performance here and here
- Contribute to org-mem, see function
org-mem-parser--scan-text-until.
(Note that you can view this same list of commands in Emacs. Assuming that M-o n was where you bound org-node-org-prefix-map, type M-o n <f1>.)
Basic commands:
org-node-findorg-node-insert-linkorg-node-insert-into-relatedorg-node-insert-transclusionorg-node-insert-transclusion-as-subtreeorg-node-visit-randomorg-node-context-dwimorg-node-set-tagsorg-node-add-aliasorg-node-add-reforg-node-refile- Does two technically different things: either move a subtree into some node it prompts you for, or if you press RET with no input, extract the subtree into a new file (similarly to
org-roam-extract-subtree)
- Does two technically different things: either move a subtree into some node it prompts you for, or if you press RET with no input, extract the subtree into a new file (similarly to
org-node-seq-dispatch- Browse node series – see README
org-node-nodeify-entry- Give an ID to the entry at point, and run the hook
org-node-creation-hook
- Give an ID to the entry at point, and run the hook
org-node-grep- (Requires consult) Grep across all known Org files.
org-node-card-view- Try it and, uh, see.
Rarer commands:
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
#+titleor first heading- Can be run manually or placed on
after-save-hook! When run as a hook, it is conservative, doing nothing until you configureorg-node-renames-allowed-dirs. - Please note that if your filenames have datestamp prefixes, like org-roam’s default behavior of making filenames such as
20240831143302-node_title.org, it is important to getorg-node-file-timestamp-formatright or it may clobber a pre-existing datestamp.A message is printed about the rename, but it’s easy to miss.
- Can be run manually or placed on
- Auto-rename the file based on the current
org-node-list-dead-links- List links where the destination ID could not be found
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 Org syntax problems, for you to correct manually.
Org-node assumes all files have valid syntax, but many of the reported problems are survivable.
- Can help you fix a broken setup: it runs org-lint on all known files and generates a report of Org syntax problems, for you to correct manually.
org-node-list-reflinks- List all links that aren’t
id:links. Also includes citations, even though they are technically not links.
- List all links that aren’t
org-node-list-feedback-arcs- (Requires GNU R, with R packages stringr, readr and igraph)
Explore feedback arcs in your ID link network. Can work as a sort of occasional QA routine.
- (Requires GNU R, with R packages stringr, readr and igraph)
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: It prompts you for a certain root directory, and then only looks for links in there, and in sub and sub-subdirectories and so on – but won’t find a link elsewhere.
Like if you have Org files under /mnt linking to assets in /home, then those links won’t be updated. Or if you choose ~/org/some-subdir as the root directory, then links in ~/org/file.org will not update. So choose ~/org as the root even if you are renaming something in a subdir.
- NOTE: It prompts you for a certain root directory, and then only looks for links in there, and in sub and sub-subdirectories and so on – but won’t find a link elsewhere.
- Interactively rename an asset such as an image file and try to update all Org links to them. Requires wgrep.
Rarer commands for org-node-backlink-mode:
org-node-backlink-mass-update-drawersorg-node-backlink-mass-update-propsorg-node-backlink-mass-delete-drawersorg-node-backlink-mass-delete-propsorg-node-backlink-fix-buffer
Do you already know about “daily-notes”? Then get started as follows:
- Configure variable
org-node-seq-defs; see wiki for premade examples - Enable
(org-node-seq-mode) - Try the command
org-node-seq-dispatch(M-o n s)- Tip: Consider binding a shorter key
M-o s
- Tip: Consider binding a shorter key
It’s easiest to explain node sequences if we use “daily-notes” (aka “dailies”) as an example.
Org-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. A node sequence. You could have sequences for, let’s say, historical events, Star Trek episodes, your school curriculum…
API cheatsheet between org-roam and org-node.
| Action | org-roam | org-node |
|---|---|---|
| Get ID near point | (org-roam-id-at-point) | (org-entry-get-with-inheritance "ID") |
| Get node at point | (org-roam-node-at-point) | (org-node-at-point) |
| Prompt user to pick a node | (org-roam-node-read) | (org-node-read) |
| Get node by ID | (org-mem-entry-by-id ID) | |
| Get list of files | (org-roam-list-files) | (org-mem-all-files) |
| Get backlink objects | (org-roam-backlinks-get NODE) | (org-mem-id-links-to-entry NODE) |
| Get reflink objects | (org-roam-reflinks-get NODE) | (org-mem-roam-reflinks-to-entry NODE) |
| Get title | (org-roam-node-title NODE) | (org-mem-entry-title NODE) |
| Get title of file where NODE is | (org-roam-node-file-title NODE) | (org-mem-file-title NODE) |
| Get title or name of file where NODE is | (org-mem-file-title-or-basename NODE) | |
| Get full path to file where NODE is | (org-roam-node-file NODE) | (org-mem-entry-file NODE) |
| Get ID | (org-roam-node-id NODE) | (org-mem-entry-id NODE) |
| Get tags | (org-roam-node-tags NODE) | (org-mem-entry-tags NODE) |
| Get tags (local only) | (org-mem-entry-tags-local NODE) | |
| Get tags (inherited only) | (org-mem-entry-tags-inherited NODE) | |
| Get outline level | (org-roam-node-level NODE) | (org-mem-entry-level NODE) |
| Get char position | (org-roam-node-point NODE) | (org-mem-entry-pos node) |
| Get line number | (org-mem-entry-lnum NODE) | |
| Get properties | (org-roam-node-properties NODE) | (org-mem-entry-properties NODE) |
| Get subtree TODO state | (org-roam-node-todo NODE) | (org-mem-entry-todo-state NODE) |
| Get subtree SCHEDULED | (org-roam-node-scheduled NODE) | (org-mem-entry-scheduled NODE) |
| Get subtree DEADLINE | (org-roam-node-deadline NODE) | (org-mem-entry-deadline NODE) |
| Get subtree CLOSED | (org-mem-entry-closed NODE) | |
| Get subtree priority | (org-roam-node-priority NODE) | (org-mem-entry-priority NODE) |
| Get outline-path | (org-roam-node-olp NODE) | (org-mem-entry-olpath NODE) |
Get ROAM_REFS | (org-roam-node-refs NODE) | (org-mem-entry-roam-refs NODE) |
Get ROAM_ALIASES | (org-roam-node-aliases NODE) | (org-mem-entry-roam-aliases NODE) |
Get ROAM_EXCLUDE | (org-mem-entry-property "ROAM_EXCLUDE" NODE) | |
| Ensure fresh data | (org-roam-db-sync) | (org-node-cache-ensure t t) |
Instructions to downgrade to an older version, let’s say 1.6.2.
With Quelpa:
(use-package org-node
:quelpa (org-node :fetcher github :repo "meedstrom/org-node"
:branch "v1.6"))With vc-use-package on Emacs 29:
(use-package org-node
:vc ( :fetcher github :repo "meedstrom/org-node"
:branch "v1.6"))With built-in :vc on Emacs 30+ (but note default value of use-package-vc-prefer-newest means you never update, since it is not aware of Git tags):
(use-package org-node
:vc ( :url "https://github.com/meedstrom/org-node"
:branch "v1.6"))With Elpaca as follows. Note that recipe changes only take effect after you do M-x elpaca-delete and it re-clones – the idea is that Elpaca users will prefer to do it manually with M-x elpaca-visit and using Magit to switch branch.
(use-package org-node
:ensure ( :fetcher github :repo "meedstrom/org-node"
:branch "v1.6"))…Elpaca can also target an exact version tag! Package manager of the future, it is:
(use-package org-node
:ensure ( :fetcher github :repo "meedstrom/org-node"
:tag "1.6.2"))With Straight:
(use-package org-node
:straight (org-node :type git :host github :repo "meedstrom/org-node"
:branch "v1.6"))Org-roam shipped the optional (require 'org-roam-export), a patch to fix id: links in HTML export.
Good news, upstream fixed the root of the issue in 5e9953fa0! Update Org to 9.7+ (comes with Emacs 30), then set this.
(setq org-html-prefer-user-labels t)
Here’s the start of one of my note files. Note the :ROAM_REFS: line.
:PROPERTIES:
:CREATED: [2023-09-11 Mon 12:00]
:ID: 3bf9opc0tik0
:ROAM_REFS: https://www.greaterwrong.com/s/pFatcKW3JJhTSxqAF https://mindingourway.com/guilt/
:END:
#+filetags: :pub:
#+options: toc:t
#+title: Replacing Guilt
Takeaways from Nate Soares' excellent "Replacing Guilt" series.
...
An explanation: think of them as like IDs. While org-node is built around the ID property because it acts as a singular identifier, the concept can be generalized.
In another universe, ROAM_REFS might have been called EXTRA_IDS because in many ways it is just a list of additional IDs for the same node.
For performance reasons, not just any string of text is accepted – it must have valid links per Org syntax, such as [[https://gnu.org][GNU Website]] or https://gnu.org. That is because the org-mem library searches for links anyway in all body text, making it cheap to see after-the-fact where else this same “extra ID” may have been mentioned, and generate a backlink!
Org-roam calls such backlinks reflinks. In my view, adding a new word for such a similar concept just increases the air of mystery. That’s why in org-node’s context buffer, they’re just called “ref backlinks” – as opposed to “ID backlinks”.
People often use it to write notes about a specific web-page or PDF file, and call it a ref-node for that resource.
As a special case, citation keys such as “@ioannidis2005” also work in ROAM_REFS, corresponding to Org citations like [cite:@ioannidis2005].