Distributing Tasks in Things 3 with AppleScript

Stack of task cards on the left flowing into a calendar grid on the right, with one task card placed on each consecutive day, illustrating sequential daily task distribution

I have some pretty good systems for capturing action items; maybe they are too good. Pair that with the fact that no matter how solid my weekly review feels, it’s easy to skip the next one. Pretty quickly, the inputs outpace the outputs, and like the second page of Google search results, there are corners of my Things database I haven’t seen in quite some time.

To address those quiet corners, I’ve spent time working on an automation that puts tasks in front of me every day. While Claude and Gemini both helped me refine this script, what I’m sharing with you today is straight AppleScript; there is no AI required to run it.

So strap in and let me show you my latest creation.

Use Case

There are two versions of this script, each solving a different problem.

Chipping Away at Big Projects

The first version helps me systematically tackle large, non-urgent projects, like updating the WordPress categories and tags on my blog. The post you’re reading is the 144th on this site, and updating the metadata on those posts is tedious. However, there is no deadline, and I have no desire to grind in the WordPress admin panel for hours at a time.

So, I created a Things project for updating post metadata. I quickly automated the creation of individual action items for each of the 144 blog posts, and had a nice big project to track everything. But, that project would sit in an area of Things, completely undisturbed, so I created the Distributor script.

The script takes the selected items in Things and, starting with today, assigns a “When” date to each action item, one day at a time. My project to update all 144 blog posts starts today and continues for the next 143 days. To each their own, but I find it incredibly easy to knock out one of these with my morning coffee and eventually cross the finish line.

Dealing with Backlog

I’ve also found this extraordinarily helpful for dealing with accumulated task backlog. Instead of avoiding a weekly review because I want to dodge processing a massive list, I can select everything and run the distributor script. In seconds, I’ve broken up the Herculean task of the weekly review into tiny, manageable moments.

Working with Deadlines

The second version handles large parallel projects that carry the urgency of a deadline. This script asks for your target completion date, then takes your selected tasks and equally divides them across the days between today and your deadline. Unlike the native Deadline feature in Things, which only warns you when you’re late, this script creates a roadmap to get you there.

If there’s an uneven number of tasks, it front-loads the extras to give you wiggle room. If there are fewer tasks than available days, it spaces them out evenly so you complete everything on time but with breaks between tasks; there is no need to work consecutively.

How It Works

Both versions of the script respect any deadlines you’ve assigned and preserve all tags, area, and notes on your tasks. When you run either script, you get the option to preserve or overwrite any existing “When” dates. Any preserved dates or deadlines are reserved so a second task doesn’t get assigned to a day that already has something scheduled.

Both scripts are also contextually aware of tasks tagged with “work”; those tasks won’t be assigned to Saturdays or Sundays, keeping your weekends free from intrusion.

Code Share

Distributor (Sequential)

This version distributes tasks one per day, starting from today.

(*
Things Task Date Distributor
Version: 1.6
Created by: Mike Burke (https://www.themikeburke.com), Claude (Anthropic), and Gemini
Last Updated: 2025-02-09

License: MIT
Copyright (c) 2025 Mike Burke
Permission is hereby granted to use, copy, modify, and share this script freely,
provided the above copyright and attribution notices are preserved.
Full license text: https://opensource.org/licenses/MIT

Description:
This AppleScript enhances Things 3's task management by automatically distributing selected tasks across multiple days. 
It helps break down large projects into manageable daily tasks while respecting work/personal boundaries and existing due dates.

Functionality:
1. Processes selected tasks in Things 3 and distributes them across multiple days
2. For each task, it:
   a. Checks for existing due dates OR activation ("When") dates to use as anchor points
   b. Identifies work vs. personal tasks via tags (case-insensitive)
   c. Preserves and maintains:
      - Project assignment
      - Area
      - Tags
      - Notes
      - Original dates (used as anchor points)
   d. Handles scheduling based on task type:
      - Work tasks: Scheduled only on weekdays (Locale-safe logic)
      - Personal tasks: Can be scheduled any day
   e. Distributes tasks around anchor points:
      - Respects existing due dates so you don't double-book
      - Prevents task clustering
   f. Updates each task with new scheduled date

Usage:
1. Select multiple tasks in Things 3
2. Run this script (typically via Stream Deck + Keyboard Maestro)
3. Confirm the number of tasks to be distributed
4. Tasks will be automatically scheduled across available days

Note: Requires Things 3 for macOS. Works best when triggered via Keyboard Maestro 
and Stream Deck for efficient workflow integration. Tasks tagged with "work" 
will only be scheduled on weekdays.
*)

use scripting additions
use application "Things3"

-- =============================================================================
-- Main Handler
-- Purpose: Entry point for the script that processes selected tasks and distributes them
-- Operations:
--   1. Validates task selection
--   2. Detects existing "When" dates and asks user preference
--   3. Sorts tasks by existing dates (using efficient Insertion Sort)
--   4. Distributes undated tasks across available days
-- =============================================================================
on run
	tell application "Things3"
		-- Get and validate selected to-dos
		-- This ensures we have something to work with before proceeding
		set selectedTodos to selected to dos
		if selectedTodos is {} then
			display dialog "Please select some to-dos first." buttons {"OK"} default button "OK" with title "No Tasks Selected"
			return
		end if
		
		set taskCount to count of selectedTodos
		
		-- Check for existing "When" dates (Activation Dates)
		-- Sometimes tasks have a start date but no deadline. We need to know if we should move these.
		set hasActivationDates to false
		repeat with todoItem in selectedTodos
			if activation date of todoItem is not missing value then
				set hasActivationDates to true
				exit repeat
			end if
		end repeat
		
		-- Ask user preference regarding conflicts
		-- If tasks are already scheduled, we shouldn't overwrite them without permission
		set respectWhenDates to false
		if hasActivationDates then
			set whenDatePrompt to display dialog "Some selected tasks already have scheduled dates. How should the script handle them?" buttons {"Cancel", "Overwrite All", "Respect Existing"} default button "Respect Existing" cancel button "Cancel" with title "Existing Dates Detected"
			
			if button returned of whenDatePrompt is "Cancel" then return
			if button returned of whenDatePrompt is "Respect Existing" then set respectWhenDates to true
		else
			-- Standard confirmation if no conflicts detected
			set dialogResult to display dialog "Distribute " & taskCount & " tasks sequentially starting from today?" buttons {"Cancel", "Continue"} default button "Continue" cancel button "Cancel" with title "Distribute Tasks"
			if button returned of dialogResult is "Cancel" then return
		end if
		
		-- Initialize arrays for dated and undated tasks
		-- Dated tasks act as "anchors" that block those specific days
		set todosWithDates to {}
		set todosWithoutDates to {}
		
		-- Sort tasks into dated and undated arrays
		-- We check both Due Dates (Deadlines) and Activation Dates (Start Dates)
		repeat with todoItem in selectedTodos
			set shouldAnchor to false
			
			-- Priority 1: Check Due Date (Deadline)
			-- We normalize the time to 0 (midnight) to ensure accurate date comparisons
			set todoDue to due date of todoItem
			if todoDue is not missing value then
				set time of todoDue to 0
				set end of todosWithDates to {todo:todoItem, dueDate:todoDue}
				set shouldAnchor to true
			end if
			
			-- Priority 2: Check Activation Date
			-- Only check this if we are respecting existing dates and the task isn't already anchored by a deadline
			if not shouldAnchor and respectWhenDates then
				set todoActivation to activation date of todoItem
				if todoActivation is not missing value then
					set time of todoActivation to 0
					set end of todosWithDates to {todo:todoItem, dueDate:todoActivation}
					set shouldAnchor to true
				end if
			end if
			
			-- If no relevant dates found, mark for distribution
			if not shouldAnchor then
				set end of todosWithoutDates to todoItem
			end if
		end repeat
		
		-- Sort dated tasks chronologically
		-- This helps the algorithm "see" upcoming blocks in the schedule
		set todosWithDates to my sortTodosByDate(todosWithDates)
		
		-- Initialize date counter from today
		-- We start scheduling from today and move forward sequentially
		set currentDate to current date
		set time of currentDate to 0
		
		-- Process and distribute undated tasks
		-- This assigns each undated task to the next available slot
		repeat with todoItem in todosWithoutDates
			-- Check for work tag (Optimized)
			-- AppleScript's 'contains' operator is case-insensitive by default.
			-- This efficiently catches "work", "Work", or "WORK" without a loop.
			set tagNames to name of tags of todoItem
			set isWorkTask to (tagNames contains "work" or tagNames contains "Work")
			
			-- Get next available date
			-- This function skips weekends (for work tasks) and existing anchor dates
			set nextDate to my findNextAvailableDate(currentDate, isWorkTask, todosWithDates)
			
			-- Update Things 3
			schedule todoItem for nextDate
			
			-- Advance the counter
			-- We move to the day AFTER the one we just assigned
			set currentDate to nextDate + (1 * days)
		end repeat
		
	end tell
end run

-- =============================================================================
-- Helper Function: findNextAvailableDate
-- Purpose: Determines the next available date for task scheduling
-- Parameters:
--   startDate: The date to start checking from
--   isWorkTask: Boolean indicating if task is work-related
--   anchorTodos: List of existing dated tasks to work around
-- Returns: Date object for next available scheduling slot
-- =============================================================================
on findNextAvailableDate(startDate, isWorkTask, anchorTodos)
	set candidateDate to startDate
	
	-- Loop until we find a valid date
	-- We use a 'repeat' loop instead of recursion to prevent stack overflow errors on large lists
	repeat
		set isAvailable to true
		
		-- Weekend Check (International Safe)
		-- We use integer comparison instead of text names ("Saturday").
		-- In AppleScript: 1 is ALWAYS Sunday, 7 is ALWAYS Saturday.
		-- This ensures the script works on computers in Germany, Spain, Japan, etc.
		if isWorkTask then
			set theWeekday to weekday of candidateDate as integer
			if theWeekday is in {1, 7} then set isAvailable to false
		end if
		
		-- Anchor Check
		-- If the day isn't a weekend, check if it's blocked by an existing task
		if isAvailable then
			repeat with anchorRecord in anchorTodos
				if (dueDate of anchorRecord) is equal to candidateDate then
					set isAvailable to false
					exit repeat -- Stop checking anchors if we found a conflict
				end if
			end repeat
		end if
		
		-- If passed all checks, return this date
		if isAvailable then return candidateDate
		
		-- Otherwise, advance to the next day and try again
		set candidateDate to candidateDate + (1 * days)
	end repeat
end findNextAvailableDate

-- =============================================================================
-- Helper Function: sortTodosByDate
-- Purpose: Sorts a list of todo records by their due dates
-- Algorithm: Insertion Sort
-- Note: Insertion sort is significantly faster than Bubble Sort for this use case
--       because task lists are often "partially" sorted already.
-- =============================================================================
on sortTodosByDate(todoList)
	set n to count of todoList
	if n < 2 then return todoList -- No need to sort 0 or 1 items
	
	repeat with i from 2 to n
		set currentItem to item i of todoList
		set currentDate to dueDate of currentItem
		set j to i - 1
		
		-- Shift elements that are greater than currentDate to the right
		repeat while j > 0
			set comparisonItem to item j of todoList
			if (dueDate of comparisonItem) ≤ currentDate then exit repeat
			
			set item (j + 1) of todoList to comparisonItem
			set j to j - 1
		end repeat
		
		-- Insert current item at its correct position
		set item (j + 1) of todoList to currentItem
	end repeat
	
	return todoList
end sortTodosByDate

To Use

  1. Select the tasks you want to distribute in Things.
  2. Run the script.
  3. Confirm the distribution.
  4. Tasks are scheduled starting today, one per day.

Distributor with Deadline

This version distributes tasks based on a target completion date.

(*
Things Task Deadline Distributor
Version: 1.6
Created by: Mike Burke (https://www.themikeburke.com), Claude (Anthropic), and Gemini
Last Updated: 2025-02-09

License: MIT
Copyright (c) 2025 Mike Burke
Permission is hereby granted to use, copy, modify, and share this script freely,
provided the above copyright and attribution notices are preserved.
Full license text: https://opensource.org/licenses/MIT

Description:
This AppleScript helps manage deadline-driven projects by evenly distributing selected tasks 
across all available days until a target completion date. It respects work/personal boundaries 
and existing due dates while front-loading tasks when distribution is uneven.

Functionality:
1. Processes selected tasks in Things 3 and distributes them toward a deadline
2. Prompts for target completion date (Supports ISO 8601 YYYY-MM-DD input)
3. For each task, it:
   a. Preserves tasks with existing due dates as immovable anchors
   b. Identifies work vs. personal tasks via tags (case-insensitive)
   c. Maintains:
      - Project assignment
      - Area
      - Tags
      - Notes
      - Original due dates (for anchor tasks)
   d. Handles scheduling based on task type:
      - Work tasks: Scheduled only on weekdays (Locale-safe logic)
      - Personal tasks: Can be scheduled any day
   e. Distributes undated tasks evenly toward deadline:
      - Front-loads when distribution is uneven (more tasks earlier)
      - Schedules tasks to complete by day BEFORE target date
      - Respects anchor dates that can't be used
   f. Updates each task with new scheduled date

Usage:
1. Select multiple tasks in Things 3
2. Run this script (typically via Stream Deck + Keyboard Maestro)
3. Enter target completion date in YYYY-MM-DD format
4. Confirm distribution plan
5. Tasks will be automatically scheduled across available days

Note: Requires Things 3 for macOS. Works best when triggered via Keyboard Maestro 
and Stream Deck for efficient workflow integration. Tasks tagged with "work" 
will only be scheduled on weekdays.
*)

use scripting additions
use application "Things3"

-- =============================================================================
-- Main Handler
-- Purpose: Entry point for the script that processes selected tasks and 
--          distributes them toward a deadline
-- Operations:
--   1. Validates task selection
--   2. Prompts for ISO 8601 target date
--   3. Separates tasks into anchored (with dates) and undated
--   4. Calculates available days
--   5. Distributes undated tasks with front-loading logic
-- =============================================================================
on run
	tell application "Things3"
		-- Get and validate selected to-dos
		set selectedTodos to selected to dos
		if selectedTodos is {} then
			display dialog "Please select some to-dos first." buttons {"OK"} default button "OK" with title "No Tasks Selected"
			return
		end if
		
		-- Prompt for target completion date
		-- Using ISO 8601 format (YYYY-MM-DD) for unambiguous date entry
		set datePrompt to display dialog "Enter target completion date (YYYY-MM-DD):" default answer "" buttons {"Cancel", "Continue"} default button "Continue" cancel button "Cancel" with title "Set Deadline"
		if button returned of datePrompt is "Cancel" then return
		
		set dateInput to text returned of datePrompt
		
		-- Parse and validate date
		-- Uses a robust parsing handler that prevents locale and rollover errors
		try
			set targetDate to my parseDate(dateInput)
		on error
			display dialog "Invalid date format. Please use YYYY-MM-DD." buttons {"OK"} default button "OK" with title "Invalid Date"
			return
		end try
		
		-- Set actual deadline to the day BEFORE the target
		-- This creates a safety buffer. If you say "Due Friday", the script aims to finish Thursday.
		set actualDeadline to targetDate - (1 * days)
		
		-- Validate that deadline is in the future
		set today to current date
		set time of today to 0
		if actualDeadline < today then
			display dialog "Target date must be in the future (at least 2 days from now to allow for the buffer)." buttons {"OK"} default button "OK" with title "Invalid Date"
			return
		end if
		
		-- Check for existing "When" dates (Activation Dates)
		set hasActivationDates to false
		repeat with todoItem in selectedTodos
			if activation date of todoItem is not missing value then
				set hasActivationDates to true
				exit repeat
			end if
		end repeat
		
		-- Ask user preference for handling existing dates
		set respectWhenDates to false
		if hasActivationDates then
			set whenDatePrompt to display dialog "Some tasks already have scheduled dates. How should we handle them?" buttons {"Cancel", "Overwrite All", "Respect Existing"} default button "Respect Existing" with title "Existing Dates Detected"
			if button returned of whenDatePrompt is "Cancel" then return
			if button returned of whenDatePrompt is "Respect Existing" then set respectWhenDates to true
		end if
		
		-- Initialize arrays for anchor tasks and undated tasks
		set anchorTodos to {}
		set undatedTodos to {}
		
		-- Separate tasks into anchored and undated
		-- We preserve due dates and (optionally) activation dates as immovable blocks
		repeat with todoItem in selectedTodos
			set shouldAnchor to false
			
			-- Priority 1: Check Due Date
			set todoDue to due date of todoItem
			if todoDue is not missing value then
				set time of todoDue to 0
				set end of anchorTodos to {todo:todoItem, dueDate:todoDue}
				set shouldAnchor to true
			end if
			
			-- Priority 2: Check Activation Date (if user opted to respect them)
			if not shouldAnchor and respectWhenDates then
				set todoActivation to activation date of todoItem
				if todoActivation is not missing value then
					set time of todoActivation to 0
					set end of anchorTodos to {todo:todoItem, dueDate:todoActivation}
					set shouldAnchor to true
				end if
			end if
			
			if not shouldAnchor then
				set end of undatedTodos to todoItem
			end if
		end repeat
		
		-- Sort anchor tasks chronologically
		set anchorTodos to my sortTodosByDate(anchorTodos)
		
		-- Separate undated tasks by type (work vs personal)
		-- We use case-insensitive tag checking for robustness
		set workTasks to {}
		set personalTasks to {}
		
		repeat with todoItem in undatedTodos
			set tagNames to name of tags of todoItem
			-- 'contains' handles "work", "Work", "WORK" automatically
			if tagNames contains "work" or tagNames contains "Work" then
				set end of workTasks to todoItem
			else
				set end of personalTasks to todoItem
			end if
		end repeat
		
		-- Calculate available days
		-- This scans the calendar and removes weekends (for work) and anchor dates
		set workDays to my calculateAvailableDays(today, actualDeadline, true, anchorTodos)
		set personalDays to my calculateAvailableDays(today, actualDeadline, false, anchorTodos)
		
		set workTaskCount to count of workTasks
		set personalTaskCount to count of personalTasks
		set workDayCount to count of workDays
		set personalDayCount to count of personalDays
		
		-- Sanity Check: Is it physically possible?
		if (workTaskCount > 0 and workDayCount = 0) or (personalTaskCount > 0 and personalDayCount = 0) then
			display dialog "Impossible Schedule: You have tasks to do but 0 available days (due to weekends or anchor conflicts)." buttons {"OK"} default button "OK" with title "Impossible Schedule"
			return
		end if
		
		-- Generate Preview
		-- Calculating average tasks per day helps set user expectations
		set previewMessage to "Distribution Plan:" & return & return
		if workTaskCount > 0 then
			set avgWork to (round ((workTaskCount / workDayCount) * 10)) / 10
			set previewMessage to previewMessage & "• Work: " & workTaskCount & " tasks / " & workDayCount & " days (~" & avgWork & "/day)" & return
		end if
		if personalTaskCount > 0 then
			set avgPers to (round ((personalTaskCount / personalDayCount) * 10)) / 10
			set previewMessage to previewMessage & "• Personal: " & personalTaskCount & " tasks / " & personalDayCount & " days (~" & avgPers & "/day)"
		end if
		
		set confirmResult to display dialog previewMessage buttons {"Cancel", "Distribute"} default button "Distribute" with title "Confirm Schedule"
		if button returned of confirmResult is "Cancel" then return
		
		-- Distribute Tasks
		if workTaskCount > 0 then my distributeTasksToDays(workTasks, workDays)
		if personalTaskCount > 0 then my distributeTasksToDays(personalTasks, personalDays)
		
	end tell
end run

-- =============================================================================
-- Helper Function: parseDate
-- Purpose: Safely converts YYYY-MM-DD string to AppleScript date object
-- Parameters:
--   dateString: String in YYYY-MM-DD format
-- Returns: Date object
-- Note: This function avoids common AppleScript date bugs by not using 'current date'
-- =============================================================================
on parseDate(dateString)
	-- Split the string
	set AppleScript's text item delimiters to "-"
	set dateComponents to text items of dateString
	set AppleScript's text item delimiters to ""
	
	if (count of dateComponents) is not 3 then error "Invalid date format"
	
	set theYear to item 1 of dateComponents as integer
	set theMonth to item 2 of dateComponents as integer
	set theDay to item 3 of dateComponents as integer
	
	-- Create a fresh date object based on a known safe constant (Jan 1, 2000)
	-- This prevents inheriting the current time of day
	set resultDate to date "Saturday, January 1, 2000 at 12:00:00 AM"
	
	-- Critical: Set Day to 1 BEFORE changing month
	-- If today is Jan 31st and we set month to Feb, it will auto-roll to March 3rd.
	-- Setting day to 1 first prevents this "rollover" bug.
	set day of resultDate to 1
	set year of resultDate to theYear
	set month of resultDate to theMonth
	set day of resultDate to theDay
	set time of resultDate to 0
	
	return resultDate
end parseDate

-- =============================================================================
-- Helper Function: calculateAvailableDays
-- Purpose: Generates list of available dates between start and end
-- Parameters:
--   weekdaysOnly: Boolean, true for work tasks (skip weekends)
-- Note: Uses Integer check for weekends to ensure international compatibility
-- =============================================================================
on calculateAvailableDays(startDate, endDate, weekdaysOnly, anchorTasks)
	set availableDays to {}
	set currentDate to startDate
	
	repeat while currentDate ≤ endDate
		set isAvailable to true
		
		-- Weekend Check
		-- AppleScript Constant: 1 is ALWAYS Sunday, 7 is ALWAYS Saturday.
		-- This works on US, UK, ISO, and German/French systems universally.
		if weekdaysOnly then
			if (weekday of currentDate as integer) is in {1, 7} then set isAvailable to false
		end if
		
		-- Anchor Check
		-- If not a weekend, check if blocked by anchor
		if isAvailable then
			repeat with anchor in anchorTasks
				if (dueDate of anchor) is equal to currentDate then
					set isAvailable to false
					exit repeat
				end if
			end repeat
		end if
		
		if isAvailable then set end of availableDays to currentDate
		
		set currentDate to currentDate + (1 * days)
	end repeat
	
	return availableDays
end calculateAvailableDays

-- =============================================================================
-- Helper Function: distributeTasksToDays
-- Purpose: Distributes tasks across available days with smart distribution
-- Operations:
--   - When tasks >= days: Front-loads (div/mod logic)
--   - When tasks < days: Evenly spaces tasks (interval logic)
-- =============================================================================
on distributeTasksToDays(taskList, dayList)
	set taskCount to count of taskList
	set dayCount to count of dayList
	
	tell application "Things3"
		
		-- SCENARIO A: Front-Loading (More Tasks than Days)
		-- We need to "pack" tasks. Some days get 2, some get 1.
		if taskCount ≥ dayCount then
			-- Integer division (div) gives the base number
			-- Modulo (mod) gives the remainder
			set baseTasksPerDay to taskCount div dayCount
			set remainderTasks to taskCount mod dayCount
			set currentTaskIndex to 1
			
			repeat with dayIndex from 1 to dayCount
				set currentDay to item dayIndex of dayList
				
				-- If we have remainders, add them to the earliest days (front-loading)
				if dayIndex ≤ remainderTasks then
					set tasksForThisDay to baseTasksPerDay + 1
				else
					set tasksForThisDay to baseTasksPerDay
				end if
				
				-- Assign the batch for this day
				repeat tasksForThisDay times
					if currentTaskIndex ≤ taskCount then
						schedule (item currentTaskIndex of taskList) for currentDay
						set currentTaskIndex to currentTaskIndex + 1
					end if
				end repeat
			end repeat
			
			-- SCENARIO B: Spacing Out (More Days than Tasks)
			-- We need to spread them out. 5 tasks over 10 days = 1 task every 2 days.
		else
			set interval to dayCount / taskCount
			
			repeat with i from 1 to taskCount
				-- Logic: Calculate the exact position on the timeline
				-- We add 0.5 via 'round ... as taught in school' to handle spacing predictably
				-- This prevents clustering at the very end
				set rawIndex to ((i - 1) * interval)
				set dayIndex to (round (rawIndex) rounding as taught in school) + 1
				
				-- Safety clamps to ensure we don't crash by going out of bounds
				if dayIndex > dayCount then set dayIndex to dayCount
				if dayIndex < 1 then set dayIndex to 1
				
				schedule (item i of taskList) for (item dayIndex of dayList)
			end repeat
		end if
	end tell
end distributeTasksToDays

-- =============================================================================
-- Helper Function: sortTodosByDate
-- Algorithm: Insertion Sort
-- Note: Faster than bubble sort for typical task lists
-- =============================================================================
on sortTodosByDate(todoList)
	set n to count of todoList
	if n < 2 then return todoList
	
	repeat with i from 2 to n
		set currentItem to item i of todoList
		set currentDate to dueDate of currentItem
		set j to i - 1
		
		repeat while j > 0
			set comparisonItem to item j of todoList
			if (dueDate of comparisonItem) ≤ currentDate then exit repeat
			
			set item (j + 1) of todoList to comparisonItem
			set j to j - 1
		end repeat
		
		set item (j + 1) of todoList to currentItem
	end repeat
	
	return todoList
end sortTodosByDate

To Use

  1. Select the tasks you want to distribute in Things.
  2. Run the script.
  3. Enter your target completion date (YYYY-MM-DD format).
  4. Confirm the distribution plan.
  5. Tasks are distributed evenly toward your deadline.

Keyboard Maestro Integration

To make this easier to use, I’ve created two Keyboard Maestro macros in my Things macro group. When I press Hyper + Left Bracket (I map Caps Lock to Hyper), a conflict palette appears letting me choose between the sequential distributor or the deadline distributor.

Each macro runs the AppleScript using Keyboard Maestro’s Execute AppleScript action, so the code is embedded directly in the macro; there are no separate script files to manage.

Keyboard Maestro Editor window showing the 'Things Task Date Distributor' macro selected in the macro list. The macro configuration panel displays a hotkey trigger (Control-Option-Shift-[) and an Execute AppleScript action containing the script's header with version information and description.

My Workflow

  1. Select action items in Things.
  2. Hit the macro hotkey.
  3. Choose from the conflict palette.
  4. Respond to the prompt.
  5. The Distributor runs.

Within two seconds, I go from an amorphous pile of possibilities to successive steps toward success.

Impact

The impact on my work is significant: I’m happily chipping away at tasks I haven’t seen in months, and I’m completing work ahead of schedule, even without my fancy project templates. It removes the friction of “planning” so I can get straight to “doing.”

Outro

In the end, this is a script that helps me deal with my imperfect self. As awesome as a weekly review is, I’ll likely work through dozens of tasks this way before my next one; that is okay. You have to work with the motivation you’ve got.

I also want to acknowledge that these scripts never would have been created without the assistance of LLMs, particularly Claude and Google Gemini. These types of custom scripts are still my absolute favorite use case for AI.

I encourage you to grab the code above and adapt it to your situation however you see fit. Everything is commented thoroughly enough that anyone interested in AppleScript, or any LLM, should be able to adapt this to fit their specific needs.

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.