I want to create a new org-roam (v2) file (i.e., an org-roam node that is a file…) from an elisp script. I know what I want the content of that file to be, and I want the file to be created and the content to be inserted without any user interaction.

Over at org-roam's slack, someone recommended something like this to me:

(setq org-roam-capture-templates
    `(("d" "dummy" plain "The content I want to see in the file \n%?"
                         :target (file+head "/tmp/myfile.org" "My Heading"))))

They told me that they have something similar working, and while this is hijacking a 'private' part of org-roam (org-roam-capture- does not look like 'public' API), it works for them. However, if I execute this, I get this error in the debugger:

Debugger entered--Lisp error: (wrong-type-argument org-roam-node nil)
  signal(wrong-type-argument (org-roam-node nil))
  org-capture(nil "d")
  eval((org-roam-capture-) nil)

The problem seems to be that the symbol org-roam-capture--node is not set. Indeed, if you look at org-roam-capture (the 'public' counterpart to org-roam-capture-), it does:

  (let ((node (org-roam-node-read nil filter-fn)))
    (org-roam-capture- :node node […]))

So apparently, org-roam-capture- wants to be passed an org-roam-node object that describes the node to capture into. However, when I change my code above to

(setq org-roam-capture-templates
    `(("d" "dummy" plain "The content I want to see in the file \n%?"
                         :target (file+head "/tmp/myfile.org" "My Heading"))))

(note the org-roam-capture instead of org-roam-capture-), I am always prompted (via Ivy, in my case) for the node to capture into. That's also not what I want. Also I don't understand why org-roam-capture asks for a node when my template contains a :target that already specifies a node?

Is there a way of creating a new org-roam file (/node) in a given path with a given content without any user interaction?

1 Answer 1


I made something similar that might help:

    (defun org-roam-fast-make-link (s)
    "Make an org-roam node with title S and return a link to it.

    We eschew the usual org-capture approach for a fast, non-interactive result."
    (let* ((slug (org-roam-node-slug (org-roam-node-create :title s)))
           (filename (format "%s/%d-%s.org"
                 (expand-file-name org-roam-directory)
                 (time-convert (current-time) 'integer)
           (org-id-overriding-file-name filename)
        (insert ":PROPERTIES:\n:ID:        \n:END:\n#+title: "
        (goto-char 25)
        (setq id (org-id-get-create))
        (write-file filename)
        (org-roam-db-update-file filename)
        (format "[[id:%s][%s]]" id s))))

It is a bit of a hack but...

  • Hacky or not, works like a charm for me :)
    – willem
    Commented Nov 6, 2022 at 10:53
  • Ok, ALMOST works. Is there an easy way to tweak the above to return a link to an existing node, if one with the same title already exists?
    – willem
    Commented Nov 6, 2022 at 11:05
  • Must be possible: do a search for existing nodes before embarking on the with-temp-buffer form. That is an exercise for the interested reader! Commented Nov 6, 2022 at 11:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.