org-roam
Stability: stable (org-extensions-stability)
A network of linked notes, modelled on Emacs org-roam v2. Every .org file under directory is indexed. A node is a file with a file-level :ID: property, or a headline with an :ID:; nodes link to each other with id: links. The file format is org-roam's, so a directory can be shared with Emacs.
require("org").setup({
extensions = { roam = { directory = "~/org/roam" } },
})
A node's title is #+title (else its path relative to directory) or the headline text. Other properties it reads:
ROAM_ALIASESmore titles, e.g.Alpha "The A"(quote spaces)ROAM_REFSURLs and citation keys the node is about:https://...,@key,[cite:@key]ROAM_EXCLUDEany value: the headline (or file) is not a node
Tags are #+filetags for a file node, and a headline's tags (inherited too, see use_tag_inheritance) for a headline node.
The index
The index is a JSON file (index_file, by default in stdpath("data")) that holds every file's nodes, links and citations with its mtime. Each command that reads it first re-parses the files that changed, and with update_on_save a roam file is re-indexed when it is written, by :w or by org itself, say a capture into it (the JSON file itself is rewritten a second later, once for all the saves in between, and on exit). :Org roam_db_sync updates it by hand, :Org roam_db_sync force rebuilds it. As in org-roam, links in verbatim blocks, comment and fixed-width lines, ROAM_REFS and #+transclude: are not indexed. Node IDs are also added to the ID locations (org-links), so id: links into roam files work from anywhere.
Commands
Keys are under <prefix>m, because <prefix>r is refile (which-key labels the group "roam", and <prefix>md "roam dailies"). Global:
<prefix>mfroam_node_find: find a node by title or alias and visit it. The "+ New node" candidate (or:Org roam_node_find Titlefor a missing title) creates one with a capture template.<prefix>miroam_node_insert: insert[[id:...][Title]]at the cursor, creating the node when it is new. In Visual mode the selection is the default title and the description, and is replaced by the link. Mapped in Insert mode, e.g.vim.keymap.set("i", "<C-c>ni", "<Cmd>Org roam_node_insert<CR>"), typing goes on after the link.<prefix>mlroam_buffer_toggle: toggle the backlinks window.<prefix>mcroam_capture: choose a node (or name a new one) and capture into it with a template.<prefix>mrroam_node_random: visit a random node.<prefix>mgroam_graph: show the node graph (see below).
In org buffers, acting on the node at point (the nearest headline with an ID, else the file):
<prefix>maroam_alias_add (and roam_alias_remove)<prefix>mRroam_ref_add (and roam_ref_remove)<prefix>mtroam_tag_add (and roam_tag_remove):#+filetagsfor a file node, the headline's tags otherwise<prefix>mxroam_extract_subtree: move the subtree at point into a new file (extract_new_file_path) whose properties,#+titleand#+filetagscome from the headline<prefix>mwroam_refile: move the subtree (or Visual lines) under a node, at the end of a file node or as the last child of a headline node
The :Org forms of these take the value as an argument, e.g. :Org roam_alias_add The A.
Choosing a node
picker sets how nodes are chosen:
"auto" the picker of org's picker option (org-pickers), whose
"auto" takes LazyVim's picker, else snacks.nvim's first
(default)
"snacks" fuzzy search; confirming a query that matches no node creates a
node with that title (like completing-read in Emacs)
"fzf-lua", "telescope", "mini"
the same with that picker, previewing the node (mini.pick:
"+ New node: <text>", below the matches, takes the typed text)
"select" vim.ui.select(), with a "+ New node" candidate that asks for
the title
"input" type the title, <Tab> completes titles and aliases; a title
that is no node's creates one
A Visual selection given to roam_node_insert is the starting query.
Capture templates
capture_templates are org-capture-templates whose target says where the node goes, as a file relative to directory with head (written into a new file), olp (headlines created when missing) and datetree fields, or as one of org-roam's :target forms:
{ "file", path }{ "file+head", path, head }{ "file+olp", path, { "Heading", "Sub" } }{ "file+head+olp", path, head, { "Heading", "Sub" } }{ "file+datetree", path, tree_type }"day" (default), "week" or "month"{ "node", title_or_id }capture under an existing node
The default:
capture_templates = {
d = {
description = "default",
type = "plain",
template = "%?",
target = "%<%Y%m%d%H%M%S>-${slug}.org",
head = "#+title: ${title}\n",
unnarrowed = true,
},
},
${title}, ${slug} (the title lower-cased, with _ for anything but letters and digits), ${id} and ${file} come from the node; any other ${key} or ${key=default} is asked for once. % escapes work as in capture. The capture location (the file, the olp headline or the date tree entry) gets the node's :ID:. An aborted capture leaves no new file behind, and a node with no text beyond its head is stored.
roam:Title links open the node with that title or alias, and create it when there is none (roam_links). With link_auto_replace (the default), saving a roam file (by :w or by org itself) and following such a link turn roam: links to existing nodes into id: links (outside verbatim blocks); :Org roam_link_replace_all does it by hand.
The backlinks window
roam_buffer_toggle opens a side window (buffer.position, width, height) listing the backlinks of the node at point (id: links to it) and its reflinks (links and citations to one of its ROAM_REFS), grouped by the node they come from, with the outline path and a few lines of context (buffer.preview_lines), links shown as their descriptions. Add "unlinked" to buffer.sections for unlinked references: whole-word mentions of the node's title or aliases, ignoring case, outside brackets and outside the node's own file. It follows the cursor. <CR> opens a link's location in the window it was toggled from (a new one beside it when it is the only window), r refreshes, <Esc> closes.
org-protocol
While the extension is on, two org-protocol sub-protocols are handled:
org-protocol://roam-ref?template=r&ref=URL&title=TITLE&body=TEXTcapture a note about a web page withcapture_ref_templates; the new node getsROAM_REFS: URL, and a page that already is a node's ref is captured into that node.${ref}and${body}fill from the URL, and%a,%:link,%iwork as in a capture.org-protocol://roam-node?node=IDvisit the node with that ID.
The bookmarklet from the org-roam manual works unchanged:
javascript:location.href='org-protocol://roam-ref?template=r&ref='
+ encodeURIComponent(location.href) + '&title='
+ encodeURIComponent(document.title) + '&body='
+ encodeURIComponent(window.getSelection())
The graph
:Org roam_graph (<prefix>mg) draws the nodes and the links between them with Graphviz (graph.executable, "dot") into graph.filetype ("svg") and opens it with graph.viewer (default vim.ui.open()). With a count N (:Org roam_graph N) it shows only the nodes within N links of the node at point; :Org roam_graph local (or a count of 0) its whole connected component. Nodes link to org-protocol://roam-node, so with an org-protocol handler clicking one opens it. Without Graphviz the .dot file is written to stdpath("cache")/org. graph.extra_config, edge_extra_config, node_extra_config, link_hidden_types, max_title_length, shorten_titles and link_builder are as in org-roam-graph.
Daily notes
One file per day in dailies.directory (default "daily/" under directory), made from dailies.capture_templates:
dailies = {
directory = "daily/",
capture_templates = {
d = {
description = "default",
type = "entry",
template = "* %?",
target = "%<%Y-%m-%d>.org",
head = "#+title: %<%Y-%m-%d>\n",
},
},
},
<prefix>mdtroam_dailies_goto_today (also _goto_yesterday and _goto_tomorrow,<prefix>mdyand<prefix>mdT; a count moves that many days)<prefix>mddroam_dailies_goto_date: a date from the calendar or the argument<prefix>mjroam_dailies_capture_today (also _capture_yesterday, _capture_tomorrow and _capture_date)<prefix>mdnroam_dailies_goto_next_note (<prefix>mdpthe previous)
roam_dailies_find_directory opens the directory.
Options
directorynotes directory ("~/org/roam")excludeEmacs regexps (orfunction(relpath, path)) of paths relative todirectorythat are not indexed ({ "data/" }); hidden files and directories are always skippedindex_filethe JSON index (stdpath("data")/org/roam-index.json)update_on_savere-index a roam file when it is written (true)picker"auto", "snacks", "fzf-lua", "telescope", "mini", "select" or "input" (see above)sortcandidates: "mtime" (recent files first), "title" or "none"display_olpshow the outline path of headline nodes (false)node_displayfunction(node, name)giving a candidate's textlink_descriptionfunction(node, name)giving an inserted link's description (default the chosen title or alias)capture_templatessee abovecapture_ref_templatesroam-ref captures (a "r" template making "${slug}.org" with#+title: ${title})protocol_store_linksalso store a roam-ref page's link (false)extract_new_file_path"%<%Y%m%d%H%M%S>-${slug}.org"roam_linksfollowroam:links (true)link_auto_replaceturnroam:links intoid:links (true)buffer{ position, width, height, sections, preview_lines }dailies{ directory, capture_templates }graphsee "The graph"
Differences from Emacs org-roam
- The index is JSON, not SQLite, so there is no
org-roam-db-queryor SQL; the Lua API (require("org.extensions.roam.db"):nodes(),node(id),by_title(),by_ref(),backlinks(),reflinks(),links()) covers the lookups. Changes are found by file mtime, not content hash. - Two nodes with the same ID: the first file (in path order) keeps it, and
:checkhealth orglists the others (org-roam's database refuses the second). - Unlinked references are found without ripgrep, and the backlinks window has no collapsible sections. org-roam-ui (a separate package) is not included.
- With vim.ui.select() (
picker = "select") a new node is made with the "+ New node" candidate, since that picker can't return typed text. - Symlinked directories inside
directoryare not followed (symlinked files are). - Only
.orgfiles are indexed: there is noorg-roam-file-extensions, so encrypted.org.gpgnotes are left out. - Titles are not completed while typing text (
org-roam-completion-everywhere);roam:links complete through org-links insertion.