Post

Organizing my todo lists with emacs org mode

Introduction to EMACS

I know a few people that use emacs for note-taking or as an IDE and recently I decided to give it a try. Instead of diving into vanilla Emacs, I picked up spacemacs. As a former Obsidian user, I wanted to rebuild my note-taking workflow using Org mode and Org Roam.

At first glance, Org appears to be just a markup language for notes and todos. But it’s much more than that: it’s a flexible system where plain text, task management, and executable code all coexist. You can write a source block in almost any language, run it, and get the results inline as text, a table, or even an image. It’s like Jupyter notebooks, except it’s not limited to one language or context. Everything is just text, and everything composes naturally. And before I knew it, nearly everything I was working on ended up in Org files.

Since Emacs is highly extendable, I gradually settled into a structure that feels natural to me. I have separate files for quick notes, meetings organized as a datetree, a diary, and ideas that haven’t yet taken concrete form. Larger task lists live in their own files. This separation keeps everything readable while maintaining the feeling that it all belongs to the same system.

One of Org mode’s many strengths is capture. You can quickly store thoughts without breaking your flow—and let the system decide where they belong.

To support that, I wrote a bit of Lisp to organize my captures exactly for my needs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
(setq org-default-notes-file (concat org-directory "/notes.org"))
(setq org-default-meeting-file (concat org-directory "/meetings.org"))
(setq org-default-diary-file (concat org-directory "/diary.org"))
(setq org-default-ideas-file (concat org-directory "/ideas.org"))

(defun my/org-capture-get-topic (file)
  "Return the point of a topic headline in FILE, creating it if needed."
  (let ((company (read-string "Topic: ")))
    (with-current-buffer (find-file-noselect file)
      (goto-char (point-min))
      (unless (re-search-forward
               (format "^\\*+ %s$" (regexp-quote company)) nil t)
        (goto-char (point-max))
        (unless (bolp) (insert "\n"))
        (insert "* " company "\n"))
      (goto-char (point-min))
      (re-search-forward (format "^\\*+ %s$" (regexp-quote company)))
      (point))))

(setq org-capture-templates
      `(
        ("t" "ToDo" entry
         (file+function ,org-default-notes-file
                        (lambda () (my/org-capture-get-topic org-default-notes-file)))
         "** TODO %?\n%u\n%a\n"
         :clock-in t :clock-resume t)

        ("m" "Meeting" entry (file+datetree org-default-meeting-file)
         "* MEETING %? :MEETING:\n%t"
         :clock-in t :clock-resume t)

        ("d" "Diary" entry (file+datetree org-default-diary-file)
         "* %?\n%U\n"
         :clock-in t :clock-resume t)

        ("i" "Idea" entry
         (file+function ,org-default-ideas-file
                        (lambda () (my/org-capture-get-topic org-default-ideas-file)))
         "* %? :IDEA:\n%t"
         :clock-in t :clock-resume t)
        ))

With this setup, I don’t have to decide much in the moment. I trigger capture, type what’s on my mind, maybe assign a topic, and move on. The system takes care of putting it somewhere reasonable. And amazing upgrade to obsidian (although with some extensions you may be able to replicate this exa

That alone removed a surprising amount of friction.

Building Small Tools Around Org

Eventually, I wanted more than just storing information; I wanted to extract it in useful ways.

One example was collecting all entries with a specific tag across multiple files. Instead of manually searching, I wrote a small helper:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
(defun my/org-collect-tagged-subtrees (tag)
  "Collect all subtrees with TAG from multiple Org files."
  (interactive "sTag (without colons): ")
  (let* ((files (list (concat org-directory "/notes.org")
                      (concat org-directory "/meetings.org")
                      (concat org-directory "/ideas.org")
                      (concat org-directory "/main/todo-arbeit.org")
                      (concat org-directory "/main/todo-persönlich.org")
                      (concat org-directory "/main/todo.org")))
         (output-buffer (get-buffer-create (format "*Org tag export: %s*" tag)))
         (tag-query tag))
    (with-current-buffer output-buffer
      (erase-buffer)
      (org-mode)
      (insert (format "#+TITLE: All entries tagged :%s:\n\n" tag))
      (dolist (file files)
        (when (file-exists-p file)
          (with-current-buffer (find-file-noselect file)
            (org-map-entries
             (lambda ()
               (let ((subtree (org-copy-subtree t)))
                 (with-temp-buffer
                   (org-paste-subtree)
                   (insert (format "\n#+BEGIN_QUOTE\nFrom: %s\n#+END_QUOTE\n\n" file))
                   (append-to-buffer output-buffer (point-min) (point-max)))))
             tag-query)))))
    (switch-to-buffer output-buffer)))

To keep everything manageable, I load my configuration from a single Org file:

1
2
(defun dotspacemacs/user-config ()
  (org-babel-load-file "~/Documents/org/main/config.org"))

This way, my configuration becomes part of my note-taking system, as they are just lisp source blocks as well. Everything lives in one place, written in the same format I use every day. Since it’s all plain text, it fits naturally into a Git repository, giving me versioned snapshots independent of system backups. As a result, I’m a lot happier with my workflow.

This post is licensed under CC BY 4.0 by the author.