Org-roam Basics: How org-roam-capture-templates Work
Introduction
In this article, I will explain the basics of org-roam-capture-templates
by walking you through the default template. This guide is aimed at Org-roam users who have read the “Templating System” chapter of the user manual but find the system overwhelming or confusing. Through a step-by-step guided tour of the default template, you’ll gain the understanding needed to customize capture templates for your workflows.
Advanced topics, such as prompting for metadata (e.g., author, book title) and reusing responses during the capture process, will be covered in a follow-up article. This primer provides the foundational knowledge to help you confidently explore and customize Org-roam capture templates. Feel free to follow along with your setup as you read.
Creating Notes without Capture Templates
Creating a notes file in Org-roam is straightforward, and in fact, you do not need to use the capture system.
- Create a new file with
find-file
. - Add a title and ID as metadata to an Org buffer.
- Save the buffer to a file in your
org-roam-directory
.
If org-roam-db-auto-sync-mode
is enabled, the file is automatically added to the database and will appear when you call org-roam-node-find
. You can create an ID using org-id-get-create
or manually add one. While capture templates are powerful, understanding manual file creation can help you troubleshoot or customize your workflow. See an example in the screenshot below.
Is this Useful?
While manual note creation might seem unnecessary given Org-roam’s powerful capture templates, it offers flexibility for crafting your own workflow. For instance, you can create a simple command (Exhibit 1 below) to generate notes with randomized file names. This approach lets you skip capture templates and create notes quickly.
(defun my/create-new-note ()
"Create a new note file in `org-roam-directory'."
(interactive)
(let* ((time-string (format-time-string "%Y%m%dT%H%M%S"))
(extension "org")
(file-base-name (concat time-string "." extension))
(file-name (expand-file-name file-base-name org-roam-directory)))
(find-file file-name)
;; In the new buffer
(insert (format "#+title: %s\n\n" time-string))
(org-id-get-create)
(end-of-buffer)))
Exhibit 1. Custom my/create-new-note
command
Source: https://org-roam.discourse.group/t/how-can-i-create-a-random-note-with-a-random-unique-file-name-in-one-go/3655/2
How to Add a New Template to org-roam-capture-templates
org-roam-capture-templates
is a list of capture templates, each of which is also a list of elements (Exhibit 2). You typically use add-to-list
or push
to add your custom capture templates. You can also use setopt
(Exhibit 3). I personally prefer setopt
and add-to-list
, but you can choose any approach to work with a list.
○ - org-roam-capture-templates
│
└──○ - default capture-template
│
│──○ key
│──○ description
└──○ other elements of the default capture template
Exhibit 2. Diagram for the structure of org-roam-capture-templates
(add-to-list 'org-roam-capture-templates
'("n" "new" ...))
;; or
(push
'("n" "new" ...)
org-roam-capture-templates)
;; or
(setopt org-roam-capture-templates
(add-to-list 'org-roam-capture-templates
'("n" "new" ...)))
Exhibit 3. Example code to add a new template to org-roam-capture-templates
Understanding the Default Capture Template
To understand how org-roam-capture-templates
work, let’s examine the default capture template (Exhibit 4). This example demonstrates the structure and purpose of the six elements (Exhibit 5), which we’ll explore step by step.
("d" "default" plain "%?"
:target (file+head "%<%Y%m%d%H%M%S>-${slug}.org"
"#+title: ${title}\n")
:unnarrowed t)
Exhibit 4. Default capture template
("d" ;; ➊
"default" ;; ➋
plain ;; ➌
"%?" ;; ➍
:target (file+head "%<%Y%m%d%H%M%S>-${slug}.org" ;; ➎
"#+title: ${title}\n")
:unnarrowed t) ;; ➏
Exhibit 5. Default capture template with its numbered elements
- Key (“d”): A shortcut for selecting this template.
- Description (“default”): The label shown in prompts.
- Type (plain): The type of content to insert.
- Template ("%?"): The text content to insert.
- Target: The file name and header content.
- Optional Properties (:unnarrowed t): Additional capture behaviors.
➊ key and ➋ description
These two elements, key and description, are straightforward. The key (“d”) is a shortcut for selecting the template, while the description (“default”) is displayed in the capture prompt when you have more than one capture template. For example, in the screenshot below, the keys (“d” and “e”) and their descriptions are shown at the bottom of the capture selection prompt for you to choose from.
➌ type and ➍ template
The type (plain) is the type of captured content to insert, while the template ("%?") defines the text content to insert. The “plain” type inserts text content as-is. The default template “%?” has no text content, but the “%?” tells the capture to place the cursor at this point after the capture process. The “entry” type adds text content as a child heading under an existing node. For example, the following capture template adds a new heading to a node titled ‘My Philosophy Log’ (Exhibit 6).
(push
'("l" "log" entry
"* ${title}\n\n%?"
;; Ensure to have a single node with this title/alias below.
;; ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
:target (node "My Philosophy Log")
:unnarrowed t
:prepend t
:empty-lines 1)
org-roam-capture-templates)
Exhibit 6. Sample capture template of type entry
that uses node
as the target
If the node is a file, the new heading will be a level-1 heading, and if the node is a heading-node, the new one will be its direct child heading. If you want to try this example, use org-roam-capture
and not org-roam-node-find
. To get it to work in your Emacs, replace the target node with a title, alias, or ID that actually exists in your Org-roam directory.
Using file
for your template
In some cases, you might want to insert predefined structured content into each new node. Using a text file as your template ensures consistency and makes it easy to maintain reusable structures. Here’s an example that swaps the inline template with content from an external file (Exhibit 7).
;; Optionally, reset `org-roam-capture-templates` by setting it to nil. You
;; don't need to do this, but makes the variable clean for next experiments.
(setq org-roam-capture-templates nil)
(push
'("l" "log" entry
(file "~/tmp/philosophy-log-capture.org")
;; Ensure to have a single node with this title/alias below.
;; ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
:target (node "My Philosophy Log")
:unnarrowed t
:prepend t
:empty-lines 1)
org-roam-capture-templates)
Exhibit 7. Sample capture template of type entry
that uses file
for template
The template file ~/tmp/philosophy-log-capture.org
looks like this (Exhibit 8).
* ${title}
** What are you thinking?
%?
** How do you feel about it?
** Any other comments?
Exhibit 8. Content of file ~/tmp/philosophy-log-capture.org
➎ :target
property
The target (Exhibit 10) specifies where the captured content will be placed. In the default capture template, it has three components with the file+head
option (Exhibit 11):
- ➎-a Type of target specification (
file+head
). - ➎-b Target file name: Includes a timestamp (%<%Y%m%d%H%M%S>) and slug (${slug}).
- ➎-c Head content: Adds metadata like #+title.
:target (file+head "%<%Y%m%d%H%M%S>-${slug}.org" ;; ➎
"#+title: ${title}\n")
Exhibit 10. The :target
property of default capture template
(file+head ;; ➎-a
"%<%Y%m%d%H%M%S>-${slug}.org" ;; ➎-b
"#+title: ${title}\n") ;; ➎-c
Exhibit 11. Elements of the :target
property
➎-a Type of target specification
The number of elements in ➎ target varies depending on ➎-a type of target specification. We have file+head
in the example, and therefore two elements follow it: ➎-b and ➎-c. The docstring lists available options for ➎-a such as file
, file+olp
, and node
, and they have one, two, and one elements to follow respectively.
➎-b Target file name
"%<%Y%m%d%H%M%S>-${slug}.org" ;; ➎-b
Exhibit 12. ➎-b Target file name
➎-b is the target file name. You can have an absolute or relative file name. If relative, it’s relative toorg-roam-directory
. With this default example, the target file name starts with a percent symbol ("%") followed by an angle bracket “<”. What does this mean? Consult the docstring, and you will see this part.
Furthermore, the following %-escapes will be replaced with content and expanded
Exhibit 13. Docstring on %-escapes
So it is one of the pre-defined “%-escapes”, which get expanded and replaced with something else. You will also see a list of available %-escapes you can use, and the one starting with an angle bracket is this.
%<...> The result of ‘format-time-string’ on the ... format specification.
Exhibit 14. %<…> format-time-string
How do we use it? The angle brackets in the default template contain this string %Y%m%d%H%M%S
, which is a common convention to format a time string in programming in general. Refer to the docstring of the function format-time-string
for more detail. You can test the function and the time string like this below. Here you are evaluating an Emacs Lisp form or expression.
(format-time-string "%Y%m%d%H%M%S" (current-time))
e.g. ⇒ "20241020112615"
Exhibit 15. Code snippet to test format-time-string
The timestamp is then followed by a hyphen “-”, which is used as a separator between the timestamp and the title slug. The hyphen may be buried in all the symbols like “%”, “>”, “$” and “{”, all of which have certain syntactic significance, but the hyphen does not, and thus will be inserted literally as it is as a character.
The ${slug}
part uses an Org-roam specific convention. Toward the end of the docstring, you will see this mentioned.
Org-roam supports additional substitutions within its templates. "${foo}" will look for the foo property in the Org-roam node (see the ‘org-roam-node’).
Exhibit 16. Docstring excerpt on Org-roam’s “${foo}” convention
This may not be easy to understand with technical precision in the beginning. Do not worry too much about it for now. The following should be enough to get you going – I am adding some contextual information that is not explicit in the excerpt of the docstring above (Exhibit 16).
-
You can use properties of the node being captured throughout the current capture process (refer to the user manual for a full list.
(info "(org-roam) Node Properties")
). -
You can access each property value by calling a function
org-roam-node-foo
(but note that “foo” is a fictitious example and is not defined in Org-roam by default). -
There are several properties defined by default such as
slug
,id
,title
and so on. See functions whose name starts withorg-roam-node-
using thedescribe-function
command (bound to “C-h f” by default).
When you are creating a new node in the capture process, the title is the text you enter in the beginning for the new node, and you can use ${slug}
in your capture-template. Internally, the capture process calls the function org-roam-node-slug
to return a slug based on the title. You can play with a simple example below to see how it works more concretely.
(let* ((new-title (read-string "Enter title: "
"This is the Title of the Node"))
(node (org-roam-node-create :title new-title)))
(org-roam-node-slug node))
e.g. ⇒ "this_is_the_title_of_the_node"
Exhibit 17. Code snippet to test org-roam-node-slug
Now that’s the${slug}
part. After this, the file name extension.org
is added to the end of the file name simply as it is.
We will come back to the ${slug}
convention for more, because it makes org-roam-capture-templates
distinct from its Org counterpart, org-capture-templates
. We can add our custom properties to the node with this facility, but that’s for a bit later.
➎-c Head content
➎-c is the head content. It is inserted into the file only when it is created for the first time.
"#+title: ${title}\n") ;; ➎-c
Exhibit 18. ➎-c head content in the default template
The first part #+title:
is literally added. We’ll discuss the ${title}
part in a minute but just to quickly point out one thing about the last part \n
. This adds a newline character in Emacs, meaning it serves as a line break. With \n
, we are simply telling the capture process to end the line at this point, as if you pressed the return or enter key (↵ ) on your keyboard.
Note the ${title}
part. It’s the same technique we have seen used with ${slug}
in the ➎-b target file name part above. This way, you can insert a node’s property named title
and slug
during the capture process. See the example below (Exhibit 19).
(let* ((new-title (read-string "Enter title: "
"This is the Title of the Node"))
(node (org-roam-node-create :title new-title)))
(org-roam-node-title node)) ;; <= This time, this part accesses the title slot of node
e.g. ⇒ "This is the Title of the Node"
Exhibit 19. Code snippet to test org-roam-node-title
➏ :unnarrowed and other optional properties
Optional properties, like :unnarrowed
, provide fine-grained control over the capture process. For example, :unnarrowed
ensures the capture buffer isn’t restricted to a narrowed view, allowing you to see the full context of your note. Refer to the docstring of org-roam-capture-templates
for a full list and experiment with these options to tailor the capture behavior to your workflow.
Summary
Now you’ve seen all the six elements of a capture template and how they work together to let you capture your notes. You have also seen how you can add a new capture template to the org-roam-capture-templates
user option. Try experimenting with your custom ones to fit your workflow.