File note TODO states

I just learned about a few handy functions org-roam-node struct adds:
org-roam-node-todo, org-roam-node-scheduled, org-roam-node-properties, etc.

It allows you to read some info about an Org-roam-node, e.g.:
(org-roam-node-scheduled (org-roam-node-at-point))

How is this useful?
So, you can, for example, find all the nodes that are in “TODO”, like so:

(org-roam-node-find nil nil (lambda (node)
                              (string-equal "TODO" (org-roam-node-todo node))))

Now, the problem is: That works only for nodes that are Org headings.
It doesn’t seem to work with nodes that are note files.

I can’t figure out a way to toggle TODO state for the entire file, I tried adding the property:

:PROPERTIES:
:ID: 48632DFE-8945-48C4-B101-430179FC79AA
:TODO: TODO
:END:
#+title: Some title

That didn’t work because (org-entry-properties) doesn’t even see it. So my question is:

How do I set properties like TODO state, SCHEDULED and DEADLINE for the entire Org-roam node?

You cannot, unfortunately. These are exclusively for headline nodes, since Org doesn’t provide syntax for this in the top-level files.

There is a blog post about managing TODO items in org-roam files that might help.

2 Likes

Whoa, this is perfect. Basically, one can use #+filetags: TODO and the little snippet I posted has to be updated, and you can find all the nodes (either file notes or headings) with TODO, like so:

(org-roam-node-find
 nil nil
 (lambda (node)
   (or (string-equal "TODO" (org-roam-node-todo node))
       (seq-contains-p (org-roam-node-tags node) "TODO"))))
2 Likes

I’m glad you’re finding use for it. I’d love to hear of any other improvements and adjustments you make.

1 Like

I’d love to hear of any other improvements and adjustments you make

Of course. Here’s what I did:

  • I wrote a function to toggle TODO states in note-files
  • Advised org-todo function, now, it is possible to toggle TODO state with C-c C-t, and not only in the headings, but also the note-files
  • Improved “find-nodes-with-todo-state”

Here’s the full set of functions, sorry @magthe, I slightly changed the names of those awesome helpers you wrote (for consistency with the rest of my config)

(require 'dash)

;; filetag helpers borrowed from:
;; https://magnus.therning.org/2021-07-23-keeping-todo-items-in-org-roam-v2.html

(defun org-roam/get-filetags ()
  (split-string (or (org-roam-get-keyword "filetags") "")))

(defun org-roam/add-filetag (tag)
  (let* ((new-tags (cons tag (org-roam/get-filetags)))
         (new-tags-str (combine-and-quote-strings new-tags)))
    (org-roam-set-keyword "filetags" new-tags-str)))

(defun org-roam/kill-filetag (tag)
  (let* ((new-tags (seq-difference (org-roam/get-filetags) `(,tag)))
         (new-tags-str (combine-and-quote-strings new-tags)))
    (org-roam-set-keyword "filetags" new-tags-str)))

(defun org-roam/org-todo-keywords->list ()
  (let ((lst (-partition-by
              (lambda (s) (string-equal s "|"))
              (seq-map
               (lambda (s) (replace-regexp-in-string "\(.*" "" s))
               (cl-rest (car org-todo-keywords))))))
    `(,(car lst) ,(caddr lst))))

(defun org-roam/find-nodes-with-todo-state (todo-state)
  "Finds all nodes with selected TODO state.
   For headings that would be regular Org-mode TODO cookie,
   for file notes it's managed with TODO filetag."
  (interactive "P")
  (let ((todo-state (or todo-state
                        (completing-read
                         "Choose TODO state"
                         (-flatten (org-roam/org-todo-keywords->list))))))
    (org-roam-node-find
     nil nil
     (lambda (node)
       (or (string-equal todo-state (org-roam-node-todo node))
           (seq-contains-p (org-roam-node-tags node) todo-state)
           (seq-contains-p (org-roam-node-tags node) todo-state))))))

(defun org-roam/toggle-todo-state-in-node (&optional new-state)
  "Togggle TODO filetag in Org-roam node file."
  (interactive "P")
  (when (and (not (active-minibuffer-window))
             (org-roam-file-p))
    (let* ((all-states (-flatten (org-roam/org-todo-keywords->list)))
           (new-state (or new-state
                          (completing-read "Choose TODO state" all-states)))
           (current (seq-find
                     (lambda (tag)
                       (seq-contains-p all-states tag))
                     (org-roam/get-filetags))))
      (when current (org-roam/kill-filetag current))
      (when (not (string-equal current new-state))
        (org-roam/add-filetag new-state)))))

(defun org-todo--around (old-fn &rest args)
  (if (and (not (active-minibuffer-window))
           (org-roam-file-p)
           (not (ignore-errors (org-get-outline-path 'with-self?))))
      (funcall 'org-roam/toggle-todo-state-in-node)
    (apply old-fn args)))

(advice-add 'org-todo :around #'org-todo--around)

Thanks to your initial idea, I was able to take my TODO-fu to another level :slight_smile:

This entire snippet is “fresh out of the press”; please let me know if I’ve missed something (most likely I have)

1 Like

Before this, I used a few Org-roam Nodes named: TODO 1, ONGOING, DONE, CANCELLED, and I’d add a link to a node (to designate its todo state).
And then, to find all nodes that are in “TODO” state, I would navigate to the * TODO heading and toggle backlinks buffer.
That’s pretty straightforward, simple, very “Org-Roamy” and has some benefits, like for example you can see all the items that are in todo state as a sub-section in the graph.

But this new approach with filetags and vanilla Org todo states feels a bit more ergonomic.

Of course, the austere simplicity of Org mode allows you to automate it and use both these ways, i.e., whenever you toggle TODO state (with C-c C-t) - it would add/remove/change link to the relevant todo state Org-Roam node.

I’m not sure now if I end up using both ways, or would stick to one. But it’s nice to have different ways.


  • 1 Actually, Org wouldn’t let me create a heading named “TODO”, so I used a different name and addded :roam_alises: TODO