My Guidelines on Tags with Org-Roam V2

The tag is a label that you can use to group your notes. Tags are useful when you want to filter and find your notes through tens, hundreds and thousands of them in your file system. The mechanism and what you want from it seem simple but tags can be troublesome. Here are my own guidelines that I follow for tags:

  • Do not use a lot of tags in one note; instead create topic notes (index or MoC notes) and add links to them
  • Do not define many tags; they grow out of control if you are not careful
  • Your tag system will evolve over time; don’t worry about changing it

These guidelines let me focus on writing and linking notes without the overhead of micro-managing the tags. And yet they still help me find and filter my notes even from year 2011, so the guidelines have served me well for these years (the guidelines have also taken some time to evolve).

Below I will describe how I use tags in detail but please read it as an account of another PKM (personal knowledge management) nerd sharing his experience. I hope it is useful in some way for you to form your own principles and guidelines. To assist you in your own endeavour, here is a bit more context. I use Org-roam only as an information repository and thinking tool[1]. The tags are used only to filter and search files in org-roam-node-find, in Emacs’s Dired, and in the file manager of the OS (Windows, macOS, or Linux). Another user @rodelrod also advocates sparse use of tags like I do. I suggest to read his discussions, which I find inspiring: here on his advice on the use of tags[2] and here on his workflow and file structure. For those who use Org mode or Org-roam for task management, I have some pointers in the footnote[3].

Now onto the detail. The screen image below shows what my minibuffer looks like when I call org-roam-node-find. The tag is not the only filtering mechanism and I combine it with subdirectories as another filtering criteria.


Figure 1. My use of tags in org-roam-node-find. See the bottom half of the screen. I use virtico-mode for my minibuffer completion

The first column of the minibuffer is the subdirectory under org-roam-directory – you see (articles) and (personal). To enable it, I use this code in the wiki: Filtering by subdirectory. Alternatively, Showing node hierarchy can be useful for you, too. These need a few lines of code but are an easy customization.

The second column is the tag of the note. By default, Org-roam does not show the tags in the minibuffer. Customize org-roam-node-display-template as shown in README and in the user manual (info "(org-roam) Customizing Node Completions"). Once you show the tags, they are prefixed with a hash character “#” by default. This is what user option org-roam-node-template-prefixes controls.

So I have only this to configure my tags (the backlinkscount part is not relevant for tags, but for completion, the configuration is mentioned in the wiki). And I have a capture template that lets me select one of the few possible tags (more on this below).

  (setq org-roam-node-display-template "${directories:10} ${tags:10} ${title:60} ${backlinkscount:6}")

  ;;;; Org-roam my tags
  (cl-defmethod org-roam-node-directories ((node org-roam-node))
    (if-let ((dirs (file-name-directory (file-relative-name (org-roam-node-file node) org-roam-directory))))
        (format "(%s)" (car (split-string dirs "/")))
      ""))

This is it with my Org-roam-specific customization for tags. But I have a long history with tags with note-taking applications. I have settled on how I use tags and file naming convention around 2011 before I started using Emacs and long before Roam Research, Org-roam, Logseq, Obsidian and other note-taking apps that have appeared in the PKM scene in recent years around the pandemic. The number of tags and what tags I use have evolved over the years, but the principle that governs my way has been unchanged. It is based on this old webpage written in 2009. The “infobase manager” would be now called a PKM tool. The links to the original discussions on Scrivener discussion boards that the author refers to are not correct any longer, but I have since found one of them archived in the new forum. It is still a good read.

When I talk about tags, I need to talk about the file naming convention because I embed the tag in the file name in addition to the note’s front matter. I assign one and only one tag (not empty and not more than one) to each of my Org-roam files. As a real example, the full file name of this file looks like this:

~/Documents/evergreen/2023-12-23T131154_C_tags-in-org-roam-v2.md

Ignore the directory part and file extension (I use Org-roam with markdown files), and look at the file base name: 2023-12-23T131154_C_tags-in-org-roam-v2.

  • 2023-12-23T131154 is the ID, which consists of the file’s creation date and time
  • C is the tag
  • tags-in-org-roam-v2 is the sluggified title “Tags in Org-roam V2”
  • I concatenate these parts with an underscore “_”

“C” is the capitalized first letter of the tag “creation”. This tag denotes the “type” of the note-file. For each file, I will ever assign only one of the types because I have made them mutually exclusive. Currently I have 3 type-tags “(C)reation” “(R)eference” “(I)ndex”. These types will not overlap. Creation notes are my thoughts. Reference notes are notes on work by someone else. Typically they contain direct quotes from a book or article on the web with my notes around them. “Index” is a collection of links for a topic. Some people may call it a map of contents (MoC). Below are examples of other types. You will see what sort of notes are reference and index types.

  • Reference file: 2020-03-14T151100_R_file-system-infobase-manager.md
  • Index file: 2022-05-31T192524_I_note-taking.md

You may think the boundary between creation and reference can be blurry. A reference note often contains my comments, which have elements of my original creation, and therefore these types do not seem mutually exclusive. My rule is simple: if I take notes directly for a reference with quotes or with paraphrased sentences, that’s a reference note-file; if I start typing my thoughts with no reference, then that’s my creation. This article is written in Org-roam and started as a creation note (I started this note with no reference).

You can see my file naming convention is related to the first two guidelines:

  1. Do not use a lot of tags in one note. I use only one
  2. Do not define many tags. I have only three tags

Now onto the last guideline: Your tag system will evolve over time. The snippet below reveals my dirty secret (that even I was not aware of). Evaluating the snippet lists the tags used and their count.

(org-roam-db-query [:select [tag (funcall count tag)] :from tags :group-by tag])

⇒ (("bib" 2) ("classic" 2) ("creation" 113) ("index" 5) ("literacture" 1) 
   ("org-transclusion" 1) ("reference" 116) ("test" 6) ("to-read" 1) ("wip" 1))

I use other tags than the main three I said I would only use. I am only a human… org-transclusion and classic are the name of my projects. test should be obvious and they can be deleted once the test is done (whatever it is). bib and literature are both part of my experiment with Bibtex to manage bibliographic information. to-read and wip are another new experiment, which may prove useful and extend my conception of what tags should be – they are temporary status. In addition to the type of the notes, adding tags that denote the note’s temporary-status may be useful to me. It is another common use of tags in the wild, but so far I haven’t been using them this way. I will see how this goes.

So you see that I change how I use tags over time. You don’t have to feel that your choice in the beginning is the ultimate commitment that you cannot get out of. Go ahead and experiment. I’d love to see you share your experiences with tags.


  1. My use of Org-roam is as a thinking tool and information repository (see my other comment post for these three modes of usage: 1. task management; 2. information repository; 3. thinking tool). Org-roam is a flexible system, and I can see from this forum that many of us use it for a combination of the three modes (some as all the modes in one). ↩︎

  2. This thread on tags in Org-roam is old and referring to the V1 feature with #+ROAM_TAGS, which is no longer present in V2. Tags are now represented by Org’s standard #+filetags metadata. The discussion itself is still useful. ↩︎

  3. Tags used in Task Management. Personally I find the use of @people-name tag interesting (as sort of a personal customer relationship management system).

    ↩︎
2 Likes

@akashp , as promised :slight_smile:

1 Like

Learnt a lot from this post. Will try to implement these guidelines - with your provided clue so as how to do tag queries - I came up with a few extensions to the tag system; I prepend all my tags with :@: so that its easily searchable and to navigate to them using ripgrep.

I use d12frosted (Boris Buliga) · GitHub 's Org-agenda modification that saves me not adding all my files to org-agenda - it uses and automatically prepends #+filetags: :project: to any org file having TODO while saving to selectively add files to org-agenda and also - i use :attached: tag for my org-attachments - so making provisions for them - I created my extension

org-roam-tag-add is lacking in some respects - to add #+filetags one has to manually escape any heading that is a node because it adds tags to the current node -
secondly, to add tags to a heading - it has to be a node! It is impossible to add tags to a heading which is not a node, which is unnecessary !!

Thirdly there is no way to search for headings containing tags without bogging down one’s org-agenda (although filetags may be queried by customising the org-roam-format-template as Sir @nobiot has shown with so much care) - to overcome all of these - I created my personal implementation. As described above it uses an additional tag :@: to make tag searching possible - I have automated adding this special tag if an user adds filetags or heading tags through my functions

The following functions are defined

  1. org-roam-tag-search: This function allows you to search Org-roam tags and uses consult-ripgrep for searching. It prompts the user to select tags and constructs a search term based on the selected tags.

  2. org-roam-tag-add-current-heading: This function sets Org-roam tags for the current heading. It retrieves current tags, fetches Org-roam tags from the database, prompts the user to select tags, and then sets the tags for the current heading.

  3. org-roam-tag-add-filetags: This function sets Org-roam filetags. It retrieves current filetags, fetches Org-roam tags from the database, prompts the user to select filetags, and then sets the filetags for the file.

  4. org-roam-tag-actions: This function performs different tag actions based on the prefix argument. Without any prefix, it calls org-roam-tag-add-current-heading. With C-u 1, it calls org-roam-tag-add-filetags. With C-u 2, it calls org-roam-tag-search.

These functions are designed to be a one-stop solution to adding tags and working with them - I have not created any function to remove tags because it is fairly easy to manually do it. I don’t think there is a need.

Here is the elisp code that achieves this:

;; org-roam-tag-extensions.el

;; Dependencies

(require 'org-roam)
(require 'consult)

;; Functions

(defun org-roam-tag-search ()
  "Query Org-roam tags and search using consult-ripgrep."
  (interactive)

  ;; Retrieve a list of tags from the Org-roam database.
  (let* ((org-roam-tags (org-roam-db-query [:select [tag] :from tags :group-by tag]))
	 ;; Format the tags, replacing "@" with "Show All."
	 (formatted-tags (mapcar (lambda (tag)
				   (if (string= (car tag) "@") "Show All" (car tag)))
				 org-roam-tags))
	 ;; Prompt the user to select multiple tags using completing-read-multiple.
	 (selected-tags (completing-read-multiple "Select tags to search (,comma-separated): " formatted-tags))
	 ;; Concatenate the selected tags into a space-separated string.
	 (tags-string (mapconcat 'identity selected-tags " "))
	 ;; Determine the search term based on the selected tags.
	 (tagged-search-term
	  (cond
	   ((equal tags-string "Show All") ":@: ")
	   ((equal tags-string "attached") ":attached:")
	   ((equal tags-string "project") ":project:")
	   (t (concat ":@: " tags-string))))
	 ;; Set the arguments for the consult-ripgrep command.
	 (consult-ripgrep-args "rg --null --ignore-case --type=org --color=never --no-heading --line-number"))
    ;; Call consult-ripgrep with the Org-roam directory and the formatted search term.
    (consult-ripgrep org-roam-directory (format "%s" tagged-search-term))))

(defun org-roam-tag-add-current-heading ()
  "Set Org-roam tags in the current heading."
  (interactive)

  ;; Save the cursor position to be restored later
  (save-excursion
    ;; Move the cursor to the beginning of the current heading
    (org-back-to-heading)
    ;; Define local variables
    (let* ((current-tags (org-get-tags)) ; get current tags of the heading
	   (contains-at (member "@" current-tags)) ; check if '@' is among current tags.

	   ;; Adjust current tags by removing '@' if it is present.
	   (formatted-current-tags (if contains-at
				       (delete "@" current-tags)
				     current-tags))

	   ;; Fetch Org-roam tags from the database, excluding some predefined tags.
	   (org-roam-tags (org-roam-db-query
			   [:select [tag]
			    :from tags
			    :where (not (in tag [
						 "@"
						 "attached"
						 "project"
						 ]
					    ))
			    :group-by tag]))

	   ;; Prompt the user to select tags from 'org-roam-tags list
	   (tags-string (completing-read-multiple
			 (format "Select tags to add in current heading (current tags: %s) (,comma-separated): "
				 (mapconcat 'identity formatted-current-tags ", ")) ; Format for showing current tags
			  org-roam-tags))

	  ;; Construct a new set of tags based on user selection, ensuring uniqueness
	   (tags (if contains-at
		     (mapconcat 'identity (seq-uniq (append '("@") formatted-current-tags tags-string)) ":")
		   (mapconcat 'identity (seq-uniq (append '("@") current-tags tags-string)) ":")))

	   ) ; variable definitions ends here

      ;; Set the tags of the current heading to the constructed list
      (org-set-tags tags)
      ;; Display a message showing the added tags
      (message "Tags added: :%s" tags))))

(defun org-roam-tag-add-filetags ()
  "Set Org-roam filetags."
  (interactive)

  ;; Define local variables
  (let* ((current-filetags (split-string (or (cadr (assoc "FILETAGS"
							   (org-collect-keywords '("filetags"))))
					    "")
					  ":" 'omit-nulls)) ; get current filetags from filetags keyword

	 (contains-at (member "@" current-filetags)) ; check if '@' is among current filetags.

	 ;; Adjust current filetags by removing '@' if it is present.
	 (formatted-current-filetags (if contains-at
					(delete "@" current-filetags)
				      current-filetags))

	 ;; Fetch Org-roam tags from the database, excluding some predefined tags.
	 (org-roam-tags (org-roam-db-query
			 [:select [tag]
			  :from tags
			  :where (not (in tag [
					      "@"
					      "attached"
					      "project"
					      ]
					  ))
			  :group-by tag]))

	 ;; Prompt the user to select filetags from 'org-roam-tags list
	 (filetags-string (completing-read-multiple
			   (format "Select filetags to add (current filetags: %s) (,comma-separated): "
				   (mapconcat 'identity formatted-current-filetags ", ")) ; Format for showing current filetags
			   org-roam-tags))

	 ;; Construct a new set of filetags based on user selection, ensuring uniqueness
	 (filetags (if contains-at
		      (mapconcat 'identity (seq-uniq (append '(":@") formatted-current-filetags filetags-string)) ":")
		    (mapconcat 'identity (seq-uniq (append '(":@") current-filetags filetags-string)) ":")))

	 ) ; variable definitions ends here

    ;; Set the filetags of the current heading to the constructed list
    (org-roam-set-keyword "filetags" filetags)
    ;; Display a message showing the added filetags
    (message "Filetags added: %s" filetags)))


(defun org-roam-tag-actions (arg)
  "Perform tag actions based on the prefix ARG.
Without any prefix, call `org-roam-tag-add-current-heading'.
With C-u 1, call `org-roam-tag-add-filetags'.
With C-u 2, call `org-roam-tag-search'.
"
  (interactive "P")
  (let ((numeric-arg (prefix-numeric-value arg)))
    (cond
     ((null arg) (call-interactively 'org-roam-tag-add-current-heading))
     ((= numeric-arg 1) (call-interactively 'org-roam-tag-add-filetags))
     ((= numeric-arg 2) (call-interactively 'org-roam-tag-search))
     (t (call-interactively 'org-roam-tag-add-current-heading)))))

;; Keybinding
(global-set-key (kbd "C-c f t") 'org-roam-tag-actions)

;;; Footer
(provide 'org-roam-tag-extensions)

Thanks so much for writing this - its very detailed.