Bullet Journaling In Org Mode

I promised a guide to setting up bullet journaling with Emacs and org-mode. Here is that guide. If you want to know about my journey in note-taking apps and strategies, I'll refer you to my previous post here.

Getting started. Deciding on what to install.

There's lots of systems out there that this section could get infinite. I will not do that. Instead I'll tell you what I did in my case that:

  • I work on a recent M3 MacBook Pro
  • I'm already comfortable with vim, so an Emacs flavor that has evil-mode as a default fits me well.
  • My deep tinkerer spirit never died, but I have things to do here. So I need something a bit put together.

There's also some other decisions I took along the way. I'll add them here for completion.

  • I'm interested in org-roam but I'm not sure yet. So I won't marry to it nor use org-roam-dailies for the journal.
  • I want the task-carry-over to be my main magic move. This has to work well. And well for me means that every day the extant tasks get ported over and the original ones are marked as POSTPONED, not deleted.

With these requirements in mind we decide on:

Installation

This is the easy part. We install Emacs

brew tap d12frosted/emacs-plus
brew install emacs-plus

The installation will take a while as it'll have to compile a bunch of things. But make sure to pay attention to the end. It'll tell you if you need to run any additional commands, e.g. to symlink the Emacs.app to the right place so you can open emacs with Spotlight or by navigating to the Applications folder as usual. It'll also tell you that you can start the Emacs service. Turns out Emacs does Make A Computer Slow and therefore has to run in the background as a daemon. If you don't start it it'll auto-start when you open it first, but if you're EmacsMaxxing you may consider enabling this to happen on startup.

With Emacs installed, we install the Doom Emacs distribution

git clone --depth 1 https://github.com/doomemacs/doomemacs ~/.config/emacs
~/.config/emacs/bin/doom install

Once we've installed Doom Emacs, we can jump straight to configuring it, but it'd make sense to do so in Emacs itself, wouldn't it? Then you need to sync your Doom configuration onto Emacs. This is how Doom works: we don't edit the actual Emacs config directly. We edit Doom-controlled files and then we apply those to Emacs. This is how you do it:

doom sync

Now, finally, you can CMD+Space or whatever it is you do in your machine and run Emacs. And we're going to jump straight to configuring it.

Installing packages

Doom Emacs has a pretty neat configuration system that allows us to pretty much do anything within the comforts of their little sanitized configuration system. It's not like we cannot run arbitrary Emacs code in our config, we surely can. But if we use their macros we're assured to get it working as the Doom devs intended. So we may as well do that.

The first place we encounter this is in the init.el file. Open it by, in normal-mode, doing SPC ..

That's pressing the space bar and then the dot. The space bar is your leader key in Doom and all their commands start with SPC (or with M-SPC, that is, ALT + Space Bar, if you're in Edit mode).

Then navigate to ~/.config/doom/init.el using your hacker skills and press enter. You're now editing your initialization file for Doom Emacs. Congrats!

You'll see some informational comments up top. Read them. It'll tell you two critical things:

  • Doing SPC h d h we can see the Doom documentation.
  • And with K over a package name, its documentation.

With these two you can start spelunking and deciding what to enable. But for now I'll tell you what to do so you get my same initial config, which essentially means enabling org-mode.

Find ;;org commented at some point of the file and replace that line with:

(org +journal +roam2 +pretty)

What we're asking is to have org-mode enabled plus the journal, Roam-equivalent and to be prettier.

With this done we should now save (:wq, of course) and re-sync the Doom config. We don't have to do this every single time, but we do if we've enabled/disabled packages. Drop to a shell and do doom sync.

Configuring Emacs

For configuration of our stuff we go to a different file in the same directory: config.el. Navigate to it (you know how now!) and have a look.

As before, you'll be graced with some amazing documentation up top. Read it. Then we start configuring.

Configuring org-mode

We start by defining a things. To do so, in elisp, you setq. For example, the directories where we'll store things. The directory for org-mode files and the one specific for journal entries.

(setq org-directory "~/org/" ;; This probably is already defined in your config
      org-journal-dir (expand-file-name "journal/" org-directory))

Keeping with configuring org-mode, we now configure the TODO states. You can do any set you like, but for me the sweet spot is TODO, DOING and DONE for the usual flow, POSTPONED for tasks left for another day or carried over and CANCELLED for those that I decided against doing.

I also like that when a task is marked DONE the current timestamp is recorded.

We use a Doom macro, after!, to ensure this configuration runs after org itself loads.

(after! org
  (setq org-todo-keywords
        '((sequence
           "TODO(t)" "DOING(i)" "|" "DONE(d)"
           "POSTPONED(p)" "CANCELLED(c)"))
        org-todo-keyword-faces
        '(("TODO" . +org-todo-active)
          ("DOING" . +org-todo-onhold)
          ("POSTPONED" . warning)
          ("CANCELLED" . shadow)))

  ;; Log state changes in a drawer, time on DONE
  (setq org-log-into-drawer t
        org-log-done 'time
        org-log-reschedule 'note
        org-log-redeadline 'note))

With this org-mode itself is configured.

Configuring org-journal

Finally we reach the meat of the guide: setting up org-journal. This package, by default, does what we want: carrying over pending tasks from day to day. However it does it in a way that's not exactly what I expected:

  • It carries over only TODO items, but not e.g. DOING.
  • Fully deletes the tasks from the previous day.

I don't like either. Luckily we can configure both. First, the list of tasks we want to carry over is defined by the org-journal-carryover-items. And for the carryover operation, we can set the org-journal-handle-old-carryover variable to a function that will receive the list of carried over items and you can essentially do anything with it. Let's start by defining that function.

;; Carryover handler: mark originals as POSTPONED
(defun my/org-journal-postpone-old-carryover (entries)
  "Flip TODO/DOING → POSTPONED in *source* regions that were carried.
Handles both ((BEG END) …) and ((BEG (END . TEXT)) …) shapes and edits bottom-up."
  (save-excursion
    (let ((inhibit-read-only t))
      ;; Normalize to list of (BEG . END) markers and sort descending by BEG.
      (let* ((regions
              (mapcar (lambda (e)
                        (let* ((beg (car e))
                               (end-spec (cadr e))
                               (end (if (consp end-spec) (car end-spec) end-spec)))
                          (cons (copy-marker beg) (copy-marker end))))
                      entries))
             (regions (sort regions (lambda (a b) (> (car a) (car b))))))
        (dolist (r regions)
          (let ((beg (car r)) (end (cdr r)))
            (save-restriction
              (narrow-to-region beg end)
              (goto-char (point-min))
              (while (re-search-forward org-heading-regexp nil t)
                (let ((st (org-get-todo-state)))
                  (when (or (string= st "TODO") (string= st "DOING"))
                    (org-todo "POSTPONED")))))))))))

With the function defined, now we use another Doom macro, use-package!, to define this variable and a few more. Feel free to investigate what each one does and configure to your liking. They're quite self-explanatory, really.

(use-package! org-journal
  :after org
  :init
  (setq org-journal-date-prefix "#+title: "
        org-journal-file-type 'daily
        org-journal-file-format "%Y-%m-%d.org"
        org-journal-date-format "%Y-%m-%d"
        org-journal-time-format "(%H:%M)"
        org-journal-enable-agenda-integration t
        org-journal-find-file 'find-file)
  :config
  (setq org-journal-carryover-items "TODO=\"TODO\"|TODO=\"DOING\"" ;; Here we're saying both TODO and DOING are carried over
        org-journal-handle-old-carryover #'my/org-journal-postpone-old-carryover ;; Here we're referencing our function
        ;; This next one is the template that new files will have. You can do anything. Mine is tailored for a more complex org-roam setup.
        org-journal-file-header ":PROPERTIES:\n:ID: %Y-%m-%d\n:END:\n#+title: %Y-%m-%d\n#+startup: show2levels\n\n* Morning\n\n* Day log\n")
)

And voilà, our setup is ready.

Finishing touches

At this point we can start using our setup. To apply the config, you can evaluate the forms or just save and do SPC h r r which will reload the whole config for you. Pretty neat.

I have however added a couple of custom keybinds to help me. One, running the carryover manually. Useful for debugging but also for when you retroactively change a previous day and want to re-run the carryover for any reason.

;; Bind [SPC n j c] to manual carryover
(map! :leader
      :desc "Manual journal carryover"
      "n j c"
      (cmd! (require 'org-journal)
            (call-interactively 'org-journal--carryover)))

And another one for a pretty specific need. When you use the normal command to create/open today's note, it'll automatically add a timestamped section at the end, with the goal of taking timestamped notes. However I many times want to quickly go to the current journal entry just to look at the tasks. So I've added a binding for that in SPC n j o.

;; [SPC n j o] will open the current day note without adding a timestamp
(map! :leader :desc "Open today's journal (no new entry)"
      "n j o" #'org-journal-open-current-journal-file)

Using your new journaling system

Using this setup can't be easier. Once you open Emacs, SPC n j j will put you into today's note. Write as you please (read the org manual for that) and add your tasks. Use S-Right and S-Left to easily cycle between task states.

The next day when you start a new journal entry, yesterday's outstanding tasks will be automatically carried over.

Further reading

Use your daily journaling as an excuse to learn Emacs. By simply editing your tasks every day you'll gain muscle memory both from the vim idioms provided by evil-mode as well as for the specific idiosyncrasies of Emacs. You'll get comfortable with M-x'ing to search for commands, memorize the Doom specific bindings and become fluent at authoring org-mode files just as you may already be at doing Markdown.

And this will open the doors of the complete ecosystem to you. In my short (literally a few days!) journey I've already started publishing this blog entirely through Emacs using ox-publish. And I've started taking all my notes and organizing them in a very Roam/Obsidian way with org-roam.

Give in and have fun!