Timekeeping with Emacs and Org-Mode

Although I have been an Emacs user for 15 years, for the first 13 of those years I only used a handful of commands and one or two “modes”. A couple years ago I went through the Emacs tutorial (within Emacs, type C-h r) to see if I was missing anything useful. I was not disappointed! Since that time, I have gone through the entire Emacs manual, made full use of Elpy to create a rich Python IDE, adopted Magit to speed up my version control workflow, and more!

In this post, I want to talk about how I use org-mode for timekeeping. I only recently started doing this, so my workflow likely will change, but I’m reasonably happy with it today. It is easy to “clock-in” to a particular task by positioning point within an item and typing C-c C-x C-i and clocking out with C-c C-x C-o. See the documentation for details.

But the thing that really impressed me is the flexible reporting framework. Whether you need to report hourly time as an independent contractor, or simply want to review how much time you’re spending where (in meetings for example), org-mode’s clocktable functionality has you covered!

I’m doing a contract with Calypso AI (which is doing some really cool work in the trusted AI space) and need to report weekly invoices with services provided by day. I created two clocktables: one on a daily granularity just for the current week, the other on a weekly granularity since I started the contract.

Since I am using one projects.org file for everything, I have a top-level item for Calypso and limit reporting to that subtree. I define my hourly rate as a property at that level, which inherits down to any subitems. Some items can also be tagged as @overhead for items that logically belong in this subtree but that I don’t charge for (like preparing the invoice itself). I still care about tracking time there, so I make sure I’m not wasting too much time on overhead! I customized the org-duration-format variable in my init.el file as (org-duration-format (quote (("h" . t) (special . 2)))) to print all times as hours with 2 decimal points. (The default was to print durations as DD:HH:MM which isn’t convenient for me.)

My weekly clocktable definition is:

+BEGIN: clocktable
    :scope subtree              ;; Limit reporting to this item
    :maxlevel 2                 ;; Level of detail
    :properties ("HOURLY_RATE") ;; My hourly rate
    :inherit-props t            ;; Hourly rate cascades down
    :tstart "<2020-xx-yy Mon>"  ;; Contract start date
    :tend "<tomorrow>"          ;; Report up to and including today
    :step week                  ;; Weekly granularity
    :link t                     ;; Link to items in table
    :sort (4 . ?N)              ;; Sort descendingly on time
    :tags "-@overhead"          ;; Omit overhead
    :formula "$5='
        (* (string-to-number @3$1)
           (+ (string-to-number (replace-regexp-in-string \"[\*h]\" \"\" $3))
              (string-to-number (replace-regexp-in-string \"[\*h]\" \"\" $4))
           )
        );%.2f
        ::@1$5=string(\"Total\")"

I have split the definition over multiple lines and added comments for readability, but in org-mode the entire definition has to be on one line. So if you are copy-pasting, remove all the comments and put it all on one line.

Before going over the definition, let’s see what the result looks like. We get separate tables for each week. Here is an example:

Weekly report starting on: [2020-xx-yy Mon]
| HOURLY_RATE | Headline           | Time     |        |   Total |
|-------------+--------------------+----------+--------+---------|
|             | *Total time*       | *40.00h* |        |      ZZ |
|-------------+--------------------+----------+--------+---------|
|       X     | \_  Meetings       |          | 10.00h |      ZZ |
|       X     | \_  Task A         |          | 10.00h |      ZZ |
|       X     | \_  Task B         |          | 10.00h |      ZZ |
|       X     | \_  Task C         |          |  5.00h |      ZZ |
|       X     | \_  Task D         |          |  5.00h |      ZZ |
|       X     | Company            |   40.00h |        |      ZZ |

Notably, this isn’t exactly what I want. My hourly rate becomes the first column in the table, which isn’t aesthetically satisfying, but that’s what I get. Since I have two hierarchical levels I get two columns for time reporting. I would prefer to only report on the second hierarchy, but that would require a :minlevel property, which doesn’t exist. I have limited control over the sorting: ideally I would sort first by column 3 and then by column 4, but I can only sort on a single column. Thus, the overall total line which should be first, ends up being last. But I’m reasonably happy with it, considering how little recurring effort is needed to regenerate it!

Going into the definition a bit more, I had to specify “tomorrow” as the end date, because when I specified “today”, the report didn’t actually include today. By specifing link = t, I can position point over one of the items in the table and type C-c C-o to navigate to the corresponding item for more details. The formula is a little kludgey but basically strips the asterisks and units from the durations, converts to numbers, adds columns 3 and 4 as a rudimentary COALESCE, multiplies the hourly rate times the duration, and formats with two decimals of precision as a currency.

For completeness, here is the definition for my daily view:

+BEGIN: clocktable
    :scope subtree
    :maxlevel 2
    :properties ("HOURLY_RATE")
    :inherit-props t
    :block thisweek               ;; Only show this week
    :step day                     ;; Daily granularity
    :stepskip0 t                  ;; Don't show days with zero time reported
    :link t
    :tags "-@overhead"
    :formula "$5='
        (* (string-to-number @3$1)
           (+ (string-to-number (replace-regexp-in-string \"[\*h]\" \"\" $3))
              (string-to-number (replace-regexp-in-string \"[\*h]\" \"\" $4))
           )
        );%.2f
        ::@1$4=string(\"Total\")"

The only differences are that I don’t do any sorting and I do daily granularity, just for the present week.

Even as a salaried employee, I can see this being useful as a way of tracking how much time I spend in meetings, dealing with email, dealing with Slack, researching, and programming. Having recently read “Deep Work” by Cal Newport, I believe having a quick way of tracking how much time I’m spending on shallow work versus deep work would likely be insightful!

Subscribe to Adventures in Why

* indicates required
Bob Wilson
Bob Wilson
Data Scientist

The views expressed on this blog are Bob’s alone and do not necessarily reflect the positions of current or previous employers.

Related