Org-roam Basics: How org-roam-capture-templates Work

This is a re-post of my article I published on my website.

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.

  1. Create a new file with find-file.
  2. Add a title and ID as metadata to an Org buffer.
  3. 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: How can I create a random note with a random unique file name in one go? - #2 by nobiot

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

  1. Key (“d”): A shortcut for selecting this template.
  2. Description (“default”): The label shown in prompts.
  3. Type (plain): The type of content to insert.
  4. Template (“%?”): The text content to insert.
  5. Target: The file name and header content.
  6. 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+headoption (Exhibit 11):

  1. ❺-a Type of target specification (file+head).
  2. ❺-b Target file name: Includes a timestamp (%<%Y%m%d%H%M%S>) and slug (${slug}).
  3. ❺-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-stringfor 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 with org-roam-node- using the describe-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.

2 Likes

Hi @nobiot great article as always. Wanted to ask a small qs regarding setting the capture variable. In your website and here when you show the different ways to setting the variable, that is, add-to-list, push and setopt, you show to use add-to-list inside setopt too, is this an oversight?


(add-to-list 'org-roam-capture-templates
             '("n" "new" ...))
;; or

(push
 '("n" "new" ...)
 org-roam-capture-templates)

(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

This is one way to set this user option without overriding the default or any configuration done prior to this form. Yes, it feels redundant because add-to-list already changes the user option. As customizing does more than setting the value, I think it is a good practice to use setopt consistently.

1 Like

Hi, thanks for mentioning find-file for debugging purposes at the start of the article.

I had this bug with double titles that did not make sense when I was capturing a new note. I realised I had to disable the file-template module in doom emacs’ init.el. Both org-roam’s template and the file-template module snippet system were adding a title each.

A big thanks for the article

1 Like