emacs · org-roam

Task management with org-roam Vol. 2: Categories

Sharing how I improved the agenda view for org-roam notes by fixing category display. Rather than showing file IDs, we can now show meaningful categories based on note titles or explicit CATEGORY properties. I've included a function to automatically extract and format categories, plus options to handle long titles gracefully. The result is a cleaner, more readable agenda that better integrates with org-roam's note-taking approach.


In the previous article we set a ground for moving tasks to org-roam, and encountered an issue with visual garbage in the agenda buffer. Namely, org-roam file id as part of the category. In this article, we are going to explore the means to overcome this issue.

Change Log:

  • [2021-03-02 Tue]: Update category extraction function to use TITLE of the note and enforce length limit. Kudos to Tim Ruffing for the idea.
  • [2021-03-02 Tue]: Update naming convention to match personal configurations.
  • [2021-05-10 Mon]: Update post to reflect changes in org-roam v2. Previous version of this article is available on GitHub.

Related posts

  • Task management with org-roam Vol. 7: Capture – Sharing my org-roam capture workflow, focusing on quick task entry and efficient processing. I've set up a dedicated inbox file per machine to avoid sync issues, and use org-agenda with the REFILE tag to process captured items. For meetings, I've created a smarter capture template that automatically places one-on-one notes in the right person's file - a nice example of using org-roam's query capabilities to streamline capture.
  • Task management with org-roam Vol. 6: Select a person and view related tasks – Sharing a quick utility I wrote to view all tasks related to a specific person in org-roam. By combining vulpea's selection functions with org-agenda's tag matching, we can easily see everything tagged with a person's name (including their aliases). Just a few lines of code, but it makes a big difference in managing person-related tasks!
  • Task management with org-roam Vol. 5: Dynamic and fast agenda – Sharing a significant performance optimization I created for org-roam agenda generation. By dynamically tracking notes containing TODOs with a 'project' tag, I reduced agenda loading from over 50 seconds to under 1 second. The solution automatically updates tags when files are visited or saved, querying only relevant files when building the agenda view. I've included all the code and explained the implementation details, from checking for TODO entries to SQL queries.
  • Task management with org-roam Vol. 4: Automatic tagging – Sharing how I automated task tagging in org-roam with vulpea. When you mention someone in a task by linking to their note, the task automatically gets tagged with that person's name. I'm using vulpea-insert hooks to handle this cleanly, avoiding the need for advice on org-roam functions. The code includes checks to ensure we only add person tags to actual TODO items.
  • Task management with org-roam Vol. 3: FILETAGS – Sharing my approach to managing person-related tasks in org-roam notes. I explain how to use filetags to automatically tag all tasks in a person's note (like @FrodoBaggins), maintaining the tag inheritance functionality from regular org-mode while moving to individual roam notes. I've included code to automate the tagging process, making it easier to maintain consistency when adding new people notes.
  • Task management with org-roam Vol. 1: Path to Roam – Sharing my approach to task management in org-roam, showing how to organize tasks and projects across notes while maintaining compatibility with org-mode's agenda features. While org-roam favors smaller files over larger ones, we can still implement a familiar structure of tasks, projects, and meta-projects. The article explains the basic setup, though there are some visual quirks in the agenda buffer that we'll address in a follow-up post.

On of the simplest solutions is to mimic solution for headlines by setting CATEGORY property on the file level (manually or by using org-set-property).

:PROPERTIES: :CATEGORY: emacs-plus :END: #+title: emacs-plus ...
:PROPERTIES: :CATEGORY: blog :END: #+title: Blog ...
some random image you can find on barberry garden

While this works, it is a manual labor. And in most cases we want to use TITLE as CATEGORY, at least for agenda buffer. Fortunately, we can help agenda to properly parse the category by modifying the value of org-agenda-prefix-format, which allows to specify how to render each line in the different agenda buffers (e.g. regular agenda, in the list of todo tasks etc). We are looking for the capability to evaluate arbitrary lisp expressions. The default value of this variable is

((agenda . " %i %-12:c%?-12t% s") (todo . " %i %-12:c") (tags . " %i %-12:c") (search . " %i %-12:c"))

The interesting part is %-12:c which means:

  • Give the category (because of c) a 12 chars wide field, padded with whitespace on the right (because of -).
  • Append a colon if there is a category (because of :).
  • Finally, append the category of the item, or as given by the CATEGORY property, or derived from the file name.

Instead of c we can use any expression.

(setq org-agenda-prefix-format '((agenda . " %i %-12(vulpea-agenda-category)%?-12t% s") (todo . " %i %-12(vulpea-agenda-category) ") (tags . " %i %-12(vulpea-agenda-category) ") (search . " %i %-12(vulpea-agenda-category) "))) (defun vulpea-agenda-category () "Get category of item at point for agenda. Category is defined by one of the following items: - CATEGORY property - TITLE keyword - TITLE property - filename without directory and extension Usage example: (setq org-agenda-prefix-format '((agenda . \" %(vulpea-agenda-category) %?-12t %12s\"))) Refer to `org-agenda-prefix-format' for more information." (let* ((file-name (when buffer-file-name (file-name-sans-extension (file-name-nondirectory buffer-file-name)))) (title (vulpea-buffer-prop-get "title")) (category (org-get-category))) (or (if (and title (string-equal category file-name)) title category) "")))

In order to extract title, I am using vulpea-buffer-prop-get from vulpea library. It's defined as:

(defun vulpea-buffer-prop-get (name) "Get a buffer property called NAME as a string." (org-with-point-at 1 (when (re-search-forward (concat "^#\\+" name ": \\(.*\\)") (point-max) t) (buffer-substring-no-properties (match-beginning 1) (match-end 1)))))
some random image you can find on barberry garden

Now if we remove the manually set CATEGORY property from both files we will get the same result with nicely parsed categories. Please note that these two approaches can be mixed. For example, if you wish to override the category, just set this property explicitly and call it a day.

Additionally, it's easy to extend this function to truncate overly long categories (in the screenshot above, Some project with ridiculously long title and Frodo Baggins are examples of long categories). We will use s.el library to achieve this.

(setq org-agenda-prefix-format '((agenda . " %i %(vulpea-agenda-category 12)%?-12t% s") (todo . " %i %(vulpea-agenda-category 12) ") (tags . " %i %(vulpea-agenda-category 12) ") (search . " %i %(vulpea-agenda-category 12) "))) (defun vulpea-agenda-category (&optional len) "Get category of item at point for agenda. Category is defined by one of the following items: - CATEGORY property - TITLE keyword - TITLE property - filename without directory and extension When LEN is a number, resulting string is padded right with spaces and then truncated with ... on the right if result is longer than LEN. Usage example: (setq org-agenda-prefix-format '((agenda . \" %(vulpea-agenda-category) %?-12t %12s\"))) Refer to `org-agenda-prefix-format' for more information." (let* ((file-name (when buffer-file-name (file-name-sans-extension (file-name-nondirectory buffer-file-name)))) (title (vulpea-buffer-prop-get "title")) (category (org-get-category)) (result (or (if (and title (string-equal category file-name)) title category) ""))) (if (numberp len) (s-truncate len (s-pad-right len " " result)) result)))
some random image you can find on barberry garden

Now the agenda is clean.

In the next article we are going to talk about tagging tasks related to a person. Stay tuned and keep roaming!

References