DEV Community

Jeremy Friesen
Jeremy Friesen

Posted on • Originally published at takeonrules.com on

Org Mode Capture Templates and Time Tracking

Sharing My Workflow and Tooling

In this post I’ll share my workflow and the Emacs functions I use to facilitate my workflow. One highlight is better understanding how to use org-capture\’s (file+function "filename" function-finding-location) target element.

Context

One of my administrative tasks for my role at Software Services by Scientist.com is time tracking. In my role I’m both coding and helping get team members unstuck on their tasks. This means on a given day, I jump between 5 to 7 projects. To help me with keeping track of all of my hours and I work, I have begun leveraging even more of Emacs’s Org-Mode; a framework and toolkit for note taking and time tracking (and so much more).

My Current Workflow

At the start of my day, I review my existing todo items. This helps me remember where to start.

As I work on a todo item, I record time and take notes; which involves links and capturing further general documentation. As I start a new project:

  • I start tracking my time.
  • Write a bit about what I’m working on.
  • And start taking notes.
  • For tasks that I don’t complete, I mark as todo.

As I wrap up a project’s task I go back to my todo items. At the end of the month I then go through my projects and record that time. I do all of this in my org-mode agenda file.

Code Supporting My Workflow

Before I started down this path I spent a month exploring, noting, and adjusting my workflow. As the month closed, I started to see the pattern I could use to extend my existing toolkit to better help my emerging workflow.

This section goes into the technical implementation.

Here’s my org-capture-templates. There are two of them:

project
The client’s project that I’m working on.
task
The task within a project.
For those reading along, here’s the documentation for org-mode’s the [capture template documentation](https://orgmode.org/manual/Template-elements.html).

Due to a present implementation constraint, which I’ll get to later, I need to first create the project’s parent day node. That is done via the file+olp+datetree directive; But I’m getting a bit ahead of myself.

(setq org-capture-templates
      '(;; Needed for the first project of the day; to ensure the datetree is
    ;; properly generated.
    ("p" "Project"
     entry (file+olp+datetree jf/primary-agenda-filename-for-machine)
     "* %(jf/org-mode-project-prompt) :project:\n\n%?"
     :empty-lines-before 1
     :empty-lines-after 1)
    ("t" "Task"
     ;; I tried this as a node, but that created headaches. Instead I'm
     ;; making the assumption about project/task depth.
     plain (file+function jf/primary-agenda-filename-for-machine jf/org-mode-find-project-node)
     ;; The five ***** is due to the assumptive depth of the projects and tasks.
     " ***** TODO %? :task:\n\n"
     :empty-lines-before 1
     :empty-lines-after 1)
    ))

Enter fullscreen mode Exit fullscreen mode

Anywhere in Emacs I can call org-capture (e.g. C-c c in Emacs dialect).

Begin Capturing Notes for the Project

The capture for the project positions the content in the following headline tree:

  • Year (e.g. 2022)
    • Month (e.g. 2022-09 September)
    • Day (e.g. 2022-09-03 Friday)
      • Project

The capture template for the project is (e.g. * %(jf/org-mode-project-prompt) :project:\n\n%?).

For the Project capture template, this:

  • creates a headline
  • prompts for the project
  • tags the node as a :project:
  • positions the cursor to begin note taking.

The following function prompts me to select an existing project or allows me to enter a new one.

(defun jf/org-mode-project-prompt ()
    "Prompt for project based on existing projects in agenda file.

Note: I tried this as interactive, but the capture templates
insist that it should not be interactive."
    (completing-read
     "Project: "
     (sort
     (-distinct
      (org-map-entries
       (lambda ()
     ;; Get the entry's title
     (org-element-property :title (org-element-at-point)))
       ;; By convention project’s are:
       ;; - a level 4 headline
       ;; - tagged with :project:
       "+LEVEL=4+project"
       ;; Look within all agenda files
       'agenda))
     #'string<)))

Enter fullscreen mode Exit fullscreen mode

When I started I thought I would need to create a local variable for projects. But I use org-map-entries to dynamically query the document for existing projects.

I also spent some time on the prompting function; in part because I thought it needed to be interactive. It does not.

Begin “Capturing” Notes for the Task

The “Task” capture template uses the file+function directive to find where in the document to insert the task.

The first parameter (e.g. jf/primary-agenda-filename-for-machine) specifies the agenda file for my machine. The second parameter (e.g. jf/org-mode-find-project-node) is defined below; it finds and positions the cursor at the end of the given project within the give date.

;; Inspiration from https://gist.github.com/webbj74/0ab881ed0ce61153a82e
(cl-defun jf/org-mode-find-project-node (&key
                       (project (jf/org-mode-project-prompt))
                       ;; The `file+olp+datetree` directive creates
                       ;; a headline like “2022-09-03 Saturday”.
                       (within_headline (format-time-string "%Y-%m-%d %A")))
    "Find and position the cursor at the end of
the given PROJECT WITHIN_HEADLINE."
    ;; Ensure we’re using the right agenda file.
    (with-current-buffer (find-file-noselect jf/primary-agenda-filename-for-machine)
      (let ((existing-position (org-element-map
                   (org-element-parse-buffer)
                   'headline
                 ;; Finds the end position of:
                 ;; - a level 4 headline
                 ;; - that is tagged as a :project:
                 ;; - is titled as the given project
                 ;; - and is within the given headline
                 (lambda (hl)
                   (and (=(org-element-property :level hl) 4)
                    ;; I can't use the :title attribute as it is a
                    ;; more complicated structure; this gets me
                    ;; the raw string.
                    (string= project (plist-get (cadr hl) :raw-value))
                    (member "project" (org-element-property :tags hl))
                    ;; The element must have an ancestor with a headline of today
                    (string= within_headline
                         (plist-get
                          ;; I want the raw title, no styling nor tags
                          (cadr (car (org-element-lineage hl))) :raw-value))
                    (org-element-property :end hl)))
                 nil t)))
    (if existing-position
        ;; Go to the existing position for this project
        (goto-char existing-position)
      (progn
        ;; Go to the end of the file and append the project to the end
        (end-of-buffer)
        (insert (concat "\n ****" project " :project:\n\n")))))))

Enter fullscreen mode Exit fullscreen mode

Current Implementation Constraint

My workflow does not need the “Project” capture. However the “Task” capture needs the headline structure that the “Project” capture creates.

My Daily Task Sheet

Last the org-clock-report function provides a plain text tabular breakdown of my work days. Below is an anonymized example:

Headline Time
Total time 14:30
--------------------------------------------- --------- ------- ------ ------
\_ 2022-09 September 14:30
\_ 2022-09-01 Thursday 7:45
\_ Client 1 0:30
\_ Merge and Backport... 0:30
\_ Client 2 2:15
\_ Get Bitnami SOLR Blocking Done 2:15
\_ Learning Time 1:30
\_ Adjusting Time Tracking Automation... 0:30
\_ Submit Proposal for Responsible... 0:30
\_ Show and Tell 0:30
\_ Client 3 1:45
\_ Pairing with A regarding Workflows 1:45
\_ Client 4 1:15
\_ Pairing on #138 1:00
\_ Reviewing... 0:15
\_ Client 5 0:30
\_ Pairing with B on Collection Slugs 0:30
\_ 2022-09-02 Friday 6:45
\_ Client 6 0:15
\_ Pairing with C regarding rebase... 0:15
\_ Learning Time 0:15
\_ Writing About Emacs and Org-Mode Time... 0:15
\_ Client 1 2:15
\_ Working on troubleshooting upstream... 0:30
\_ Work on Documenting Hyrax’s IIIF... 1:45
\_ Samvera 0:15
\_ Reviewing PR for a Hyrax app without... 0:15
\_ Client 2 1:30
\_ Working on getting SOLR up and... 1:30
\_ Client 7 1:15
\_ Client 7 Upgrade Estimate 1:15
\_ Client 5 1:00
\_ Universal Viewer Overview 0:45
\_ Working with D on Collections 0:15

In the actual time sheet each of those lines link to the corresponding headline. The provides another way to navigate.

Conclusion

I never quite realized that I would appreciate time tracking. It helps me ensure that I’m not working more hours than I should. At other places, I’d work more hours. Here the time sheet helps set clear boundaries.

This workflow also helps me recover from context shifting. I want to help people get unstuck, but jumping in and out of that context does come with a cognitive cost. The underlying technical workflow provides the ritual/habit for re-orienting to what comes next.

As I mentioned earlier, my agenda file becomes a source for knowledge sharing; either with my future self or as a blog post. This article began as a quick note in my agenda file. And in that agenda file I’ve linked to this article.

Now to write a function to generate my daily stand-up “what did I do”; it should be rather straightforward based on my well structured time sheet and notes.

And as always, you can look to my dotemacs repository for even more regarding my Emacs configuration.

Top comments (0)