It was September 2014, and I needed to procrastinate a University exam. Looking for something I could mentally categorize under “useful”, I happened upon Taskwarrior, and it has been running my life ever since.

In this post, I will describe what Taskwarrior is and how I’m using it, as well as my migration from Taskwarrior 2.6 and Taskd to the new Taskwarrior 3.4 and taskchampion-sync-server.

In short, Taskwarrior is a command line task management system.

Before I go into more detail, allow me to go on a short tangent: Taskwarrior contains my first open source contribution, in the form of a bug report. Many years back in 2016, I found that TW was getting slower and slower while I was setting up a complex dependency graph for a larger project. Looking into the code, I found that the circular dependency check did not include a mechanism to make sure that a task wasn’t looked at multiple times. So it could happen that the same task and all of its dependencies were checked multiple times while checking whether a single task could be added with a given set of dependencies without creating a circular graph.

I reported the issue and did some more elaborate explanation, leading to this commit to the project. I’m even still in the AUTHORS file. 🙂 The original task even still exists here. The frustrating thing right now: I cannot remember whether I also provided the code to fix the implementation or if I just provided the bug report? Argh.

But enough personal Michael lore. To introduce you to what Taskwarrior looks like, accompany me for a little story.

A story

So let’s imagine you’ve just run the Friday evening host update on a Wednesday for a change and suddenly, your task management stops working. You were vaguely aware that there was a new major release, but you decided to ignore it - mucking around with your task management is scary!

But now, the time has come. You roll back the change, but you know that it’s finally time to take the plunge and update Taskwarrior to the new 3.0 version. As is so often the case, you start with making sure you don’t forget:

$ task add "Migrate Taskwarrior to 3.0" project:admin.tools.tw-demo +admin +tools +current +taskwarrior
Created task 1.

You then go and admire your handiwork:

task proj:admin.tools.tw-demo

A screenshot of a terminal showing the output of the previous command. It shows a task table with the following columns: ID, Age, Project, Tag, Description and Urg. The ID is 1, the Age 54s, the Project is 'admin.tools.tw-demo', the Urg '2' and the description is 'Migrate Taskwarrior to 3.0'. The task has the following tags: admin, current, tools

Our first task.

(I will be using screenshots instead of pasting the content into a code block because I find that Taskwarrior’s CLI is actually quite nice to look at.)

And there we are, the project is basically half done already. 😁 To celebrate your accomplishment, you go and fetch another coffee. While you prepare your artisinal brew garnered with herbs from a secret valley deep in the snow-crowned heights of the Alps, you suddenly realize: You forgot to tag the task with taskwarrior to make clear which tool it is about!

You remedy it with a quick flourish of confident keystrokes:

task 1 mod +taskwarrior
Modifying task 1 'Migrate Taskwarrior to 3.0'.
Modified 1 task.

A bit rattled by that oversight, you decide to take a closer look at the task to make sure you did not forget anything else:

task 1 info
Another terminal screenshot, this time showing detailed info about the task. The base info is still the same as in the previous screenshot, so I will concentrate on the added elements. First, there is now a 'status' given as 'Pending'. There are also two dates, one for when the task was created and one for when it was last modified. There are also a number virtual tags shown: LATEST, PENDING, PROJECT, READY, TAGGED, UNBLOCKED. In addition to showing the urgency as '2' again, there is also a short table showing how this value was computed. 1 urgency was coming from the fact that the task has a project, and the other 1 urgency from the fact that it has at least one tag set. Then at the bottom of the output is a list of changes done to the task, first showing the initial creation and then the deletion and re-adding of the 'taskwarrior' tag. I did a little oopsie there.

More detailed info on the first task.

Your sense of accomplishment not quite satisfied yet, you decide to create some more tasks:

task add "Migrate data on desktop" proj:admin.tools.tw-demo +taskwarrior +current +tools +admin +desktop
task add "Migrate data on laptop" proj:admin.tools.tw-demo +taskwarrior +current +tools +admin +laptop
task add "Deploy TaskChampion on k8s cluster" proj:homelab.services.tw +taskwarrior +current +homelab +services
task add "Do first TW sync from desktop" proj:admin.tools.tw-demo +taskwarrior +current +admin +tools +desktop
task add "Document TaskChampion setup" proj:homelab.docs +taskwarrior +current +homelab +docs

Slowly stroking the white cat impatiently waiting for you to return to the “Conquer a small country that can’t possibly defend itself” part of the evening, you take stock of your heroic labor:

I will spare you the preamble from here-on out: All of the pictures in the rest of this post will be terminal screenshots. This one shows a task list with the same columns as the previous ones, showing all of the tasks created up to now. Important here is the order: 'Migrate Taskwarrior to 3.0', 'Migrate data on desktop', 'Migrate data on laptop', 'Deploy TaskChampion on k8s cluster', 'Do first TW sync from desktop', 'Document TaskChampion setup'. All of the tasks show an urgency of '2'.

Our current task list

You stare at the task list. Something is off. Something feels wrong. Like when the Homelab next to your desk suddenly sounds different because one of your Fediverse posts went Fedi-viral and all of the fans ramp up to cool down the queues. There it is: The documentation task really should not be at the bottom of the pile. Documentation is important!

You launch your favorite editor, Emacs, of course, and point it at ~/.taskrc. You enter the following, to make sure that documentation tasks are always taken seriously:

urgency.user.tag.docs.coefficient=+2

Now that’s looking better:

Again the same task list, but with a slightly changed order: The 'Document TaskChampion setup' task is now first in the list, instead of last. Its urgency has changed to '4', while all other tasks are still at the previous '2'.

Documentation is now the top priority.

But there’s still some visual distinguishing missing, so you head back to your terminal to have a look at TW’s colors:

task colors
A screenshot of Taskwarrior's color chart. It shows the effects available for coloring TW's output, ranging from effects like underlining or bolding over changing the background color to a 215 color grid for the font and background colors. At the bottom is also a gray-scale.

Taskwarrior’s color documentation.

With a nice dark pink chosen, you open up the .taskrc file again and add the following line:

color.tag.docs=rgb205
The same task list as before, but now the 'Document TaskChampion' setup task is colored in a dark pink.

Task lists, now with more color!

So far, so good, you think to yourself. But you’re a technical lead in your secret other life. And you know the drill: A project is not properly setup before everything is put into a, preferably acyclic, dependency graph.

task 6 mod dep:4
task 3 mod dep:2,4,5
task 5 mod dep:4,2
task 1 mod dep:2,3,4,5

This ends up changing your task list to look like this:

The same task list as before, but now with some coloring and ordering changes. The two tasks at the top are now 'Migrate data to desktop' and 'Deploy TaskChampion on k8s cluster', both with an urgency of 10. They also have a white background now, indicating that they're blocking tasks. There is also a new column in the list now, showing the IDs of the tasks the given task depends on. The next two tasks are 'Migrate data on laptop' and 'Do first TW sync from desktop', both with an urgency of 5. They also changed appearance, gaining a gray background. Next comes the documentation task, still in pink, but also with a gray background and an urgency of -1. The last task is the original 'Migrate Taskwarrior to 3.0' task, with an urgency of -3.

Tasks with dependencies set.

With that, only the two tasks which can actually be started are at the top, now clearly marked with a white background. All of the tasks which have unfinished dependency got a gray background.

Alright, that looks pretty acceptable. But there has to be more procrastination material in here. Taking another look, you decide that you don’t really need the individual tags of the tasks shown in your ’next’ report. You use the task show command to grab the default value for the next report’s columns:

task show report.next

report.next.columns     id,start.age,entry.age,depends,priority,project,tags,recur,scheduled.countdown,due.relative,until.remaining,description,urgency
report.next.context     1
report.next.description Most urgent tasks
report.next.filter      status:pending -WAITING limit:page
report.next.labels      ID,Active,Age,Deps,P,Project,Tag,Recur,S,Due,Until,Description,Urg
report.next.sort        urgency-

Then you go back in to add the following lines to .taskrc:

report.next.columns=id,start.age,entry.age,depends,priority,project,recur,scheduled.countdown,due.relative,until.remaining,description,urgency
report.next.labels=ID,Active,Age,Deps,P,Project,Recur,S,Due,Until,Description,Urg

This makes for a neater format of the task list, emphasizing the task description:

Yet again the same task list, but now the 'Tag' column is removed, making the overall format a bit neater.

The task list after the tags column has been removed.

Better, but there’s still some clutter in the form of the tasks which you cannot work on right now, namely the ones currently blocked. That can be remedied by using a different Taskwarrior report:

task ready

This only shows the tasks which can currently be acted upon:

Again a task list, but this time only the previously top two tasks are shown, the ones without dependencies: 'Migrate data on desktop' and 'Deploy TaskChampion' on k8s cluster.

Only showing actionable tasks.

But even this little detour has not quieted the siren song of procrastination. Casting about for more ways to put the actual execution of the project off, your well-trained brain offers up a rationalization: It’s the middle of the week! You cannot possibly put your fingers into your task management system in the middle of a work week. A small misstep would wreak havoc upon your routine.

And so you decide to put the project off until Sunday. For good measure, you also put Sunday as the due date, just to make sure your brain doesn’t try to weasel out again:

task 2,4 mod wait:sunday due:sunday

And with that, the task ready report is blissfully empty. Definitely high time for an eight year Lagavulin after this much highly productive work.

Come Sunday, the tasks appear in the list again, and you start with the first one:

task 2 start
The same list with only the 'Migrate data on desktop' and 'Deploy TaskChampion on k8s cluster' tasks. But this time, the 'Migrate data on desktop' task has an orange instead of a white background, and its urgency shot up to 23.2.

Starting the first task.

Once you finished the task and migrated the desktop TW install to 3.0, you set the task to done:

task 2 done

Now, task ready only shows the k8s TaskChampion setup as still actionable. So you get into your best robes, have The Acolyte bring in the big “How to Deploy a New Service in the k8s Cluster Without Bringing About The End of Creation” tome and begin to summon forth reams of YAML from the Void. You also sacrifice a CPU to the kubectl, an ancient Pentium 1 no less - you really need this project to go well.

It all goes well. The Acolyte breaths a sigh of relieve. He is weak. You knew you needed to replace him when he started babbling about how you hadn’t updated Kubernetes in almost two years. He just can’t seem to enjoy the sweet nectar of pressure, worry, anxiety and bliss that is the price to be paid for practicing the dark arts of YOLO.

After setting that task to done as well, the previously blocked documentation and first sync tasks show up in task ready now:

A task list containing the 'Do first TW sync from desktop' and 'Document TaskChampion setup' tasks.

New tasks became available.

You decide that The Acolyte needs a bit of practice in forming full sentences and send him off to write the documentation on the new service deployment.

You yourself type a quick task sync, and lo and behold, it works. The tasks are flowing off to the servers. Your task migration was successful. You finish the remaining tasks in a haze of keystrokes, dark black coffees and pipe smoke.

Even The Acolyte finished his task, and he even used punctuation almost everywhere! You decide not to get rid of him quite so soon. Perhaps corrupting him into loving the eldritch joys of YOLO might be a fun pastime?

task ready
No matches.

Writer’s note

Okay, that was pretty amusing for myself. 😁 But for the deeper technical explanation I will return to my normal writing style. But I will definitely experiment a bit with getting The Acolyte into my posts. I have to figure out how to do that without obscuring the technical info with too much prose first, though. The above tour through Taskwarrior’s usage lend itself nicely to putting a bit of creative writing around it. Definitely nicer than my first dry “Type this, then type this, and if you now type this that happens” draft.

Urgency in Taskwarrior

A task’s urgency is arguably Taskwarrior’s core feature. It determines the ordering of tasks in a number of reports, and I’d like to talk a bit about it.

There are a lot of sources of both urgency increases and decreases in Taskwarrior, and they’re almost entirely configurable to fit your approach to task management.

Let’s have a look at one of my tasks, for the regular Homelab host updates. The task info output contains a detailed table describing how the task’s urgency is computed:

An example urgency table. It shows how the urgency of the task is computed. In this case, the task has an overall urgency of 10.43. The task gets 1 urgency from having a project, -3 from currently being in waiting state, 1 from having at least one tag, 2.4 from having a due date, 0.033 from its age and finally 9 from having the 'current' tag.

Example urgency table

The simplest values are coming from whether the task has certain attributes, getting 1 urgency from having at least one tag and another 1 from having a project set.

Then there’s the age of the task. This urgency value slowly converges towards 2 while the task’s age converges towards a configurable number of days. The default is 365 days, so when the task is 365 days old, it will get 2 urgency from age. This value won’t increase any more after that.

Urgency can also be reduced, for example when the task is currently waiting or when it is blocked. This makes sure that even when not filtering for actionable tasks, as I did in the examples above, the non-actionable tasks will appear behind actionable tasks in the list.

In addition to these mechanisms, you can also configure specific tags or certain projects granting an amount of urgency, to prioritize them. I will go into a bit more detail on my personal setup in the next section.

There is also an urgency change based on the due date, and how close that date is. Similar to the age-related urgency, a task’s urgency increases the closer it gets to its due date.

And finally, there’s also a helper for dependency hierarchies. By default, tasks which block other tasks get an urgency boost, but its a fixed value. But there’s also an option to increase the urgency of a blocking task by an amount of urgency depending on the urgency of the tasks it blocks.

How I’m using Taskwarrior

As TW is a highly configurable tool, I think it’s useful to show you how I’m working with it.

To start with, here is my task stats output:

A table with statistics information. It shows: 1404 pending tasks, 83 waiting, 60 recurring, 20096 completed, 5124 deleted, 26767 total. There are 31935 annotation, 489 unique tags, 554 projects, 584 blocked tasks, 482 blocking tasks, 5 undo transactions, 66 sync backlog transactions. 99.7% of tasks are tagged. The oldest task is from 2014-09-29-01:15 and the newest task from today. Task was used for 11.1 years, with a task added on average every 3h and completed every 4h. A task is deleted on average every 19 hours, with the average time spend pending being 5 months and an average description length of 36 characters.

My task stats over the last 11 years.

I think the average time spend pending for a task, 5 months, shows a bit about my usage. I usually don’t just add tasks for the next week or so, but I tend to do a planning phase for larger projects and then add pretty specific tasks for everything that needs to be done. And then I’m slowly working my way through them.

For task creation, I’ve got two modes. This is as good a place as any to mention that while Taskwarrior is a great CLI tool, there was never any Android app I liked. I tend to have relatively deep project structures and quite a few tags on my tasks. And I haven’t seen any good Taskwarrior app which makes entering tasks even remotely as comfortable and fast as entering tasks on the terminal with autocomplete for tags and projects. So when I need to add some task on the go, I just jot it down in my notes app and enter it into TW the next time I’m in front of a terminal.

But back to new tasks. Most of the time, I’m entering them “complete” already, setting the right project and tags when creating them. Sometimes, when I just want to jot something down, I use the capture tag. It’s configured like this:

urgency.user.tag.capture.coefficient=8.0
color.tag.capture=rgb405

This puts it pretty high on the task list and colors it in pink, which I don’t use for any other coloring. This way, I will see the task the next time I’m running task and can then take care of setting the project and tags and dependencies properly.

I have several systems for prioritizing tasks. The most important one consists of the three tags current, following, soon. They are configured like this:

color.tag.soon=rgb150
color.tag.current=rgb035
color.tag.following=rgb043
urgency.user.tag.current.coefficient=+9
urgency.user.tag.following.coefficient=+6
urgency.user.tag.soon.coefficient=+3

These three tags, with current having blue font color, following being dark green and soon being bright green, do a general categorization of tasks. current is what I’m currently working on, the current project for example, or a blog post I want to start. In task next output, there are only ever at most three or four of those, with the rest of the current project’s tasks being dependent on those actionable tasks and not seen in the task next output because they get their urgency reduced by a lot. following is then the category of tasks I’d like to start next. These might be new Homelab projects for example. And soon is then the next-lower prio, which I give to task I don’t want to do as the next project, but I also don’t want to completely vanish into the morass of tasks which have none of the three tags.

For categorization of tasks within one of those rough levels I’m using priorities, which I’m configuring like this:

urgency.uda.priority.H.coefficient=1.5
urgency.uda.priority.M.coefficient=0.0
urgency.uda.priority.L.coefficient=-0.5

Note how the three different urgency levels are each 3 apart from each other, while the priorities here add 1.5 urgency at maximum. This allows me to prioritize within an urgency level. E.g. even a soon tagged task with priority:H won’t get more urgency than a following tagged task with pri:L.

Another little wrinkle I’ve added to my workflow is the short tag. It’s configured like this:

urgency.user.tag.short.coefficient=1.4

Its intention is to highlight short tasks, which in my head is anything up to about 30 minutes. These are for when I’ve only got 30 minutes left before I need to do something else. Or for days where I want to feel really productive and want to close a whole bunch of tasks. 😁

When I’m starting to work on a task, I use the task start command, and task stop when I stop working on it. This increases the urgency of the task so that it shows up at the very top of the task next list and puts the task’s row in the output onto an orange background. I use the mechanism to indicate which of the current tasks I’m actively working on. The feature also has a time journalling functionality, as each time task start or task stop are called on a task, an annotation for the start/stop time is added to the task. These annotations could theoretically be used to compute the time spend on the task if you diligently run start/stop each time you start or stop working on it. I’m not using the feature, as I don’t start/stop a task only because I’m making dinner, for example. I’m using timewarrior for time tracking.

As I’ve mentioned in the introduction, I’m using Taskwarrior for personal and work task, on the same database. To separate the two, I’m using Taskwarrior’s context feature. It’s configured like this:

context.work.read=project:work
context.work.write=
context.private.read=project.not:work or +home
context.private.write=
context=private

The context=private setting configures the context when running with this .taskrc file to private. As you can see in the context.private.read filter, this excludes the work project from all read operations. Meaning that when I call task next on my private desktop or laptop, I will never get a work task shown, and it’s the other way around at work, where I’ve set context=work and don’t get any private tasks shown.

For completeness’ sake, here is my full .taskrc, with some annotations:

data.location=~/.task

include ~/.task/dark-256.theme
# Mostly the default value, but I've switched it around a bit so that `due.today`
# appears before `overdue`. Otherwise, the coloring rules will mark tasks as
# overdue once their due-time is passed, which I generally don't want, because
# shorthands like due:tomorrow set the due time to "00:00" for the next day,
# so tasks would immediately show as overdue, instead of "due today".
rule.precedence.color=deleted,completed,active,keyword.,project.,due.today,overdue,scheduled,due,blocked,tag.,blocking,recurring,tagged,uda

# Enabling task's "hooks", although I'm not currently using any
hooks=1
# Configuring tab completion to complete all tags
complete.all.tags=1
list.all.projects=1
# Disabling purging, so my older completed/deleted tasks are kept for historical
# purposes.
purge.on-sync=0

# Some date formatting configs
dateformat=Y.M.D-H:N
dateformat.holiday=Y.M.D
dateformat.report=Y.M.D
dateformat.annotation=Y.M.D-H:N
weekstart=Monday

# Re-jigging the columns and column order in the "next" report a bit to fit
# my personal taste.
report.next.columns=id,project,priority,due,start.active,entry.age,urgency,description.count
report.next.labels=ID,Proj,Pri,Due,A,Age,Urg,Description

# Increasing urgency of "started" tasks
urgency.active.coefficient=6.0
# Setting urgency for annotations to 0, because whether a task has annotations
# or not doesn't matter.
urgency.annotations.coefficient=0.0
# Sharply reducing blocked task's urgency, so they go to the bottom of task lists
urgency.blocked.coefficient=-11.0
# Only very slightly increasing urgency of blocking tasks. I use dependencies a
# lot, and I've found that whether a task is blocking or not doesn't always matter
# to me, so they only get a small boost.
urgency.blocking.coefficient=0.5
# Prio configuration
urgency.uda.priority.H.coefficient=1.5
urgency.uda.priority.M.coefficient=0.0
urgency.uda.priority.L.coefficient=-0.5
urgency.user.tag.backlog.coefficient=-10.0
# Increasing bug's urgency a little bit, so errors get fixed before I start
# something new.
urgency.user.tag.bug.coefficient=2.0
urgency.user.tag.capture.coefficient=8.0
# Tag of FOSS contributions, which also get a small boost in prio
urgency.user.tag.contrib.coefficient=0.5
urgency.user.tag.current.coefficient=+9
# I also note down TV shows, movies and games I'd like to watch/play, but they
# should not show up on the "normal" task list. So they get a special tag with
# a reduced urgency.
urgency.user.tag.entertain.coefficient=-9.0
urgency.user.tag.following.coefficient=+6
# Domestic chores. I should really bite the bullet and get a housekeeper. If
# only that didn't feel so incredibly decadent.
urgency.user.tag.haushalt.coefficient=0.5
# Bills get a high urgency too, so they don't get lost in the sea of other tasks
urgency.user.tag.rechnung.coefficient=10.0
urgency.user.tag.short.coefficient=1.4
urgency.user.tag.soon.coefficient=+3
urgency.waiting.coefficient=-3.0

uda.priority.default=M

context.work.read=project:work
context.work.write=
context.private.read=project.not:work or +home
context.private.write=
context=private

One last comment I would like to make: Taskwarrior is very much a task management software, not a project planning tool. I use it together with Emacs' org mode. It does task management really well, but doesn’t really have note taking capabilities. There are annotations which can be added like this:

task 123 annot "Some note"

But these are really only intended for short notes. I only use them if I want to store relevant links to websites for example.

Import/Export

One last important feature to note is Taskwarrior’s import/export functionality. Your tasks are not walled off in Taskwarrior. I will talk a bit more about its storage formats when I talk about the migration to 3.0, but it has a nice feature for importing and exporting tasks in JSON format.

For example, let’s look at the task for this blog post:

task 1016 export

It results in this JSON:

[
{"id":1016,
"description":"Blog: Taskwarrior",
"entry":"20230519T165524Z",
"modified":"20251110T191504Z",
"priority":"M",
"project":"blog",
"start":"20251110T191504Z",
"status":"pending",
"uuid":"c3bd5a56-684a-48e8-b54e-fa0267f34247",
"annotations":[
  {
    "entry":"20251110T191504Z",
    "description":"Started task"
  }
],
"tags":["blog","current","tools"],
"depends":["4511952a-a314-4cae-9b92-9ef15b14a188","f771863d-a079-4c33-aeb3-71790ee7272d"],
"urgency":19
}
]

A JSON in similar format can be used to import tasks as well. So switching to Taskwarrior or switching away from Taskwarrior is doable, with a bit of scripting to translate the JSON to whatever format the previous/new tool supports.

The migration to Taskwarrior 3.0

In March 2024, Taskwarrior 3.0 was released. This happened after a pretty long phase of the project being dormant. That dormancy wasn’t actually a bad thing. It continued working perfectly fine through all of those years, with perhaps one release per year on average. It’s a command line tool, and it only ever had three dependencies: libc, libgnutls and libuuid.

The syncing was really the only issue, as it was a bit cumbersome, having been based on TLS client certificates, and the sync server implementation has had its last release in 2022, and no really regular releases before that. And that was potentially a publicly accessible server.

The main change in the 3.0 release was a change of the data storage backend and sync features. Both of those were rewritten in Rust.

The data storage backend was also changed from plain text files to an SQLite database. This is, at least from my PoV, a slight step back. I liked the fact that the data was just stored in text files. I was able to make use of that fact a while ago when I needed to fix an issue in the data files due to my home partition filling up and a task update only being written back partially. At least from my PoV, this wasn’t a performance problem. The previous file based implementation written in C++ was perfectly workable even with a task database of my size. The performance definitely got worse with the SQLite DB, especially at work, where our home directories are stored on NFS mounts.

The rewrite of the sync backend was a pure win, though. Where before it was raw TCP, it now became HTTP, meaning I was able to put the new sync server behind Traefik, and I didn’t have any issues anymore with reaching the instance in my Homelab from work, which I had done with a complicated SSH tunnel contraption because I couldn’t set up VPN access to my Homelab from the work machine.

The migration itself was also done very well. Not only is there a tool to migrate the TW v2 file-based storage to the v3 SQLite DB, but they also left the frontend entirely untouched, so my day-to-day usage was not impacted at all.

Migrating desktop data

My migration was from the 2.6.2 version all the way up to v3.4.1. This isn’t the latest release, there’s already v3.4.2, but that hasn’t made its way into the Gentoo packages yet.

My first step was seeing whether 3.4.1 even build in my work environment, as it wouldn’t have been useful to just migrate my personal use. And it did, due to its relatively few dependencies. Or well, its relatively few dependencies on the C++ side. Rust pulled in over a page of dependencies, of course.

With that verified, I backed up my local data and ran the migration script:

task import-v2

That went through without a hitch. Then I cautiously typed in task to show the default next report. And then I waited. While it wasn’t really slow, there was a measurable delay between me hitting enter and the report appearing. I then tried a few other things, and while any read operations, like showing reports, still were fast, any write operations were slow as molasses. A simple task start 1536 took about a minute:

time task start 1536
Starting task 1536 'Taskwarrior: Update on Desktop and migrate data'.
Started 1 task.
You have more urgent tasks.
Project 'admin.tools' is 51% complete (13 of 27 tasks remaining).

real    0m54,508s
user    0m54,460s
sys     0m0,027s

That was not at all the perf I was used to. That command was instantaneous with the older versions. After some digging, I luckily came upon this ticket, which seemed to describe a similar perf issue, resolved by disabling the project progress report at the end of the output. I tried that by adding the following to my .taskrc:

# Removed "project" because the computation was exceedingly expensive
# Added context and removed filter to restore 2.6 behavior
verbose=blank,header,footnote,label,new-id,news,affected,edit,special,sync,override,recur,context

Notably, I removed the project entry from the verbose list. After that, the performance returned to what I was used to. In the same step, I also removed the filter value from the verbose list, as now every command was showing the filter I had applied, which isn’t something I needed.

I also added this setting:

purge.on-sync=0

This is a setting to prevent automated purging of completed and deleted tasks. This is supposed to keep performance up by cleaning older tasks which shouldn’t be needed anymore. But to me, my older deleted and completed tasks are a slice of personal history I would like to keep. I’m for example working on a history series about my Homelab, from before I started this blog. And because that was also before I had any configs version controlled, my task list is the only record I have from before approximately 2020 when it comes to the Homelab.

With that done, I looked at the new sync setup.

Migrating sync

As I’ve said above, the sync backend was also changed. There are now multiple backend available, Google Cloud, AWS S3 and the taskchampion-sync-server. All options are described in the TW docs and the sync man page.

I initially thought to use S3, but the implementation seems to be geared towards using AWS S3, as there’s for example no config option for enabling path-style buckets or setting an endpoint URL. In addition, I figure if there are interesting new sync-related features, they will likely be implemented in the taskchampion-sync-server, so I went with that.

The server supports both, SQLite and Postgres. I decided to go with the SQLite option, because having a full Postgres cluster felt a bit like overkill.

The documentation for the sync server can be found here. It’s not a very complicated piece of software to set up, so I will spare you most of the YAML manifests.

But here is at least the Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: tc-sync
spec:
  replicas: 1
  securityContext:
    runAsNonRoot: true
  selector:
    matchLabels:
      homelab/app: tc-sync
  strategy:
    type: "Recreate"
  template:
    metadata:
      labels:
        homelab/app: tc-sync
    spec:
      automountServiceAccountToken: false
      securityContext:
        fsGroup: 1000
      containers:
        - name: tc-sync
          image: ghcr.io/gothenburgbitfactory/taskchampion-sync-server:{{ .Values.appVersion }}
          volumeMounts:
            - name: tc-data
              mountPath: {{ .Values.mountDir }}
          resources:
            requests:
              cpu: 200m
            limits:
              memory: 200Mi
          env:
            - name: DATA_DIR
              value: "{{ .Values.mountDir }}"
            - name: LISTEN
              value: "0.0.0.0:{{ .Values.port }}"
            - name: CLIENT_ID
              value: "f5ea2f03-2f6b-4fc8-bae9-38fd06c08c65"
            - name: CREATE_CLIENTS
              value: "true"
            - name: RUST_LOG
              value: "info"
          livenessProbe:
            httpGet:
              port: {{ .Values.port }}
              path: "/"
            initialDelaySeconds: 15
            periodSeconds: 60
          ports:
            - name: tc-sync-http
              containerPort: {{ .Values.port }}
              protocol: TCP
      volumes:
        - name: tc-data
          persistentVolumeClaim:
            claimName: tc-sync-volume

One noteworthy piece of information is the CLIENT_ID list together with the CREATE_CLIENTS environment variable. Clients are identified by their UUID, created with the uuidgen tool. “Client” is a synonym for data set here, so all clients using the same client ID will work on the same set of tasks.

The CREATE_CLIENTS variable controls whether new client IDs which have not been seen before lead to new clients being created or are rejected as unknown. If it is set to false, new clients need to be added to the database manually, there is currently no tooling around that. But what’s possible is to keep the variable set to true, and then setting CLIENT_ID to a comma-separated list of allowed IDs. Then, only clients sending those IDs are automatically created when first seen, and all other IDs are rejected.

Once the server was running, I only needed to add an encryption secret and the server URL to my .taskrc file. The encryption secret is used for encrypting the tasks before they get synced, so the server only ever sees encrypted tasks, which is a very nice touch. An example of the .taskrc settings would look like this:

sync.server.url=https://tw.example.com/
sync.server.client_id=f5ea2f03-2f6b-4fc8-bae9-38fd06c08c65
sync.encryption_secret=12345

The sync.server.client_id needs to be in the CLIENT_ID list of the server for this setup to work. Otherwise the server will return a 403 HTTP code.

With this setup above, I started syncing and everything worked pretty nicely, I was able to sync to both my laptop and my work machine without issue.

But then I made a small mistake: I committed the above lines into my dotfiles repository, including the sync.encryption_secret value. This was not good, of course, as that value should be secret. I just deleted the sync server’s DB and created a new UUID for good measure and tried syncing again. But nothing at all happened, besides the client throwing this error message:

Failed to synchronize with server: Server Error: https://tw.example.com/v1/client/add-version/789 responded with 500 Internal Server Error

I then found that there was no way to reset the syncing on the client side. So I ended up going into the local SQLite DB and running this SQL command:

UPDATE operations
SET synced = 0

I tried this just because I saw that the operations table had a synced column set to 1 for all entries. And that solved the issue, and the local client started a completely fresh sync.

This was all honestly a bit scary, because I rely a lot on Taskwarrior to organize my life, but in the end the team really did a great job with the rewrite in 3.0, and besides the performance issues I haven’t hit any other issues in the one week I’ve now been using the new version.

I hope you enjoyed this post and have perhaps even found a new task management tool to try.

Just remember: Applying really fancy tagging and coloring schemes to your task management isn’t actually bringing your project forward. 😉