I’ve played around with lots of different task management solutions over the years. Too many, to be honest. I've even written my own todo utility. But while it's fun to try out a new tool and a potentially new way of thinking about managing tasks, in the end, the important part is doing the tasks, not managing them.

So after reflecting on all the tools I had tried in the past, I went back to an old friend that I had first used over 10 years ago: Things. Things is an intuitive, powerful, and polished app with many features, but most importantly, it has the features that meet my needs nicely and has a great user interface which is a pleasure to use.

However, there was one other app that I really wanted to like but it just fell short in a few areas so I gave up on it. That app was Sorted3 (extra points for having a superscript in your app name). As a pure task management solution, it wasn't quite as nice as Things, but it had a different perspective on task management that I found compelling. Sorted3 is built around a concept it calls hyper-scheduling or otherwise referred to as time-blocking. This is the idea of scheduling out all your activities throughout the day, like a daily plan on a calendar. Sorted3 took this idea one step further with its “Auto Schedule” feature, which schedules out your day in time blocks integrating your calendar appointments and the task items that you had planned for the day. It presents you with a detailed schedule for all your day’s activities that is realistic since it takes into account the time you have free and the estimated time to complete each task (you have to assign time estimates to tasks for this to work, more on that later).

Sorted<sup>3</sup>'s Schedule view
Sorted3's Schedule view

Sorted3 and its Auto Schedule feature addressed one problem I had always grappled with: signing up for more tasks in a day than I could actually get done. That’s easy to do when you just have a list of tasks staring you in the face. But when you lay them out on a timeline that includes other commitments, you immediately see whether you have enough time to finish the things you need to. If you’ve over-scheduled yourself, you can review your items and potentially defer some to the next day and refresh the schedule to get an updated plan for the day.

This approach to scheduling tasks was very appealing to me when I was working in a corporate environment and had a very busy calendar. It helped me prioritize my important tasks and find slivers of time throughout the day to get the right things done.

Things Schedule

I no longer work in an environment that is calendar-driven so I thought I wouldn’t miss the scheduling feature when I switched back to Things. But recently, I’ve been running into the same problem where I have more tasks on my list than I can get done in a day so I wanted to figure out a “sanity check” for my Things Today list that I could use to plan and review my work each day.

One stumbling block is that Things doesn’t have the notion of a time estimate for completing a task. Instead, I needed to create a convention that I could use for that purpose. Thing does support arbitrary tags for tasks, though, and it turned out that could work quite well.

Tags in Things for estimated times are easily applied with a keyboard shortcut
Tags in Things for estimated times are easily applied with a keyboard shortcut

I use tags such as “5m”, “15m” and “60m” to estimate a task's time and then assign convenient shortcuts to each tag so, for example, I can just press Ctrl-4 to set a task's estimated time to 45 minutes.

Now that I had a way to add time estimates to my tasks, I needed to retrieve the tasks and process them, creating the daily schedule. When Things 3 launched back in 2017, they introduced comprehensive AppleScript support to the app, and using AppleScript, it’s fairly straightforward to get a list of tasks out of Things. Here’s the code I use in my shortcut.

on run {input, parameters}
    set todoList to {}
    set text item delimiters to "
"
    tell application "Things3"
        set theTodos to to dos of list "Today"
        repeat with aToDo in theTodos
            set theTitle to name of aToDo
            set tagNames to tag names of aToDo
            set theTask to "title:" & theTitle & " tags:" & tagNames
            copy theTask to the end of todoList
            -- display dialog theTask
        end repeat
    end tell
    set tasks to todoList as text
    return tasks
end run

It runs through the tasks in the “Today” list and creates a text representation of the task in this format

title:Pick up milk on the way home tags:15m

and returns all of the tasks as one text block with one task per line.

The real work is done in a Python script which receives this block of text, parses it into tasks with their time and creates a schedule in HTML and opens it in a browser. A link to the complete script is included at the end of this post, but I’ll just describe the interesting parts below. The parse_things_tasks() routine below reads the tasks passed to it from the AppleScript action and parses them into two lists: one of timed_tasks (tuples of a duration and the task title) and one of untimed_tasks (that have no time estimate tag on them).

def parse_things_tasks(default_duration: Optional[TD]) -> Tuple[List[Tuple[TD, str]], List[str]]:
    timed_tasks, untimed_tasks = [], []
    for task in sys.stdin:
        m = re.match(r'^title:(.*) tags:(.*)$', task)
        if m:
            title = m.group(1)
            est_mins = parse_estimated_minutes(m.group(2).split(', '))
            if est_mins > 0:
                timed_tasks.append((datetime.timedelta(minutes=est_mins), title))
            elif est_mins == NO_TIME_ESTIMATE and default_duration is not None:
                timed_tasks.append((default_duration, title))
            else:
                untimed_tasks.append(title)
        else:
            print(f'unmatched: {task}')

    return timed_tasks, untimed_tasks

...

def parse_estimated_minutes(tags: List[str]) -> int:
    for tag in tags:
        if re.match(r'\d+m', tag):
            return int(tag[:-1])
    return NO_TIME_ESTIMATE

The function reads from sys.stdin so you need to set "Pass Input" to "to stdin" in the “Run Shell Script" action in Shortcuts. It then looks for lines matching the format described above (which should be every line) and extracts the title and duration. The function parse_estimated_minutes() looks for the first tag matching a number followed by m and returns that value or a special NO_TIME_ESTIMATE value if none is found, in which case the tasks is considered an untimed task and won’t be included in the scheduled portion of the report.

The other interesting part of the script is the build_schedule() function which takes the untimed_tasks returned from above and builds a daily schedule from it.

def build_schedule(timed_tasks: List[Tuple[datetime.timedelta, str]], start: datetime.datetime, buffer: datetime.timedelta):
    today = datetime.date.today()
    midnight = datetime.datetime(today.year, today.month, today.day, 23, 59)

    time = start
    schedule = []
    while time.date() == today and timed_tasks:
        if timed_tasks:
            sdt = midnight
            dur, text = timed_tasks[0]
            # add the task if it will fit in before the next event
            if time + dur + buffer <= sdt:
                schedule.append((time, dur, text, 'task'))
                timed_tasks.pop(0)
                time += dur + buffer
                continue
        # didn't add either, tick forward by 5 minutes
        time += datetime.timedelta(minutes=5)
    return schedule

Since I’m not including calendar events, this logic is pretty simple. I just need to run through the list of timed tasks assigning a start time to each one and then stepping forward in time by the duration of the task. The one case to consider is if there is enough time left in the day (before midnight) to fit the task. When either all the tasks are scheduled, or we spill over to the next day, we are done scheduling the tasks.

The Final Shortcut

The final shortcut is really simple utilizing just two actions to do all the work. This shortcut doesn’t benefit much from being in the Shortcuts app but it’s nice to be able to say “Hey Siri, Things Schedule” and see the page pop up. And by using Shortcuts, it would be easy to extend it, say, by running each morning and emailing the report to me.

The final shortcut uses just two actions to combine the scripts
The final shortcut uses just two actions to combine the scripts

The complete AppleScript and Python source code are available here.