th
A tiny Things 3 CLI (macOS).
today       tasks scheduled for today or overdue
inbox       tasks in the Inbox
areas       list areas
projects    list active projects
tags        list tags
list        tasks in an area or project
search      full-text search of titles and notes
logbook     recently completed or canceled tasks
upcoming    tasks scheduled in the next N days
add         create a task (via the Things URL scheme)
edit        change an existing task (by uuid or title)
done        complete one or more tasks (by uuid or title)
cancel      cancel one or more tasks (by uuid or title)
trash       move one or more tasks to Things' Trash
show        reveal a task in the Things app

Built for agents

Things has no API an agent can call, so th is built to be one. Claude Code and other local agents can run your task manager.

It works fine by hand too, or in a shell script, and --json pipes straight into jq.

Why this exists

Things has no public REST API. Everything you need is already on your Mac:

One Python file, standard library only. Other CLIs wrap this in a compiled language and need Xcode to build.

Not a public API

th is unofficial. It is not affiliated with, endorsed by, or supported by Cultured Code.

Their SQLite file is private and undocumented, and can change in any update. But th only ever reads it, so the worst case is a crash, never lost data.

Install

git clone https://github.com/prstn/th ~/code/th
ln -s ~/code/th/th ~/.local/bin/th    # or any directory on your PATH
th today

Needs macOS, Things 3, and Python 3, which is already on your Mac.

One setting

th add will not work until you turn this on.

Things → Settings → General → enable “Things URLs”

It ships disabled. Reads work either way, and your first write triggers a one-time macOS permission prompt.

Commands

Anything taking <ident> takes a uuid or a title. If a title is ambiguous, th refuses it instead of guessing.

Reading

CommandWhat it doesOptions
th todayTasks scheduled for today, plus anything overdue--json --tag
th inboxTasks sitting in the Inbox--json --tag
th upcomingTasks scheduled in the next N days--days N (7) --json --tag
th list <name>Tasks in an area or project--json --tag
th search <query>Full-text search across titles and notes--json --tag
th logbookRecently completed or canceled tasks--days N (7) --json --tag
th areasList your areas--json
th projectsList active projects--json
th tagsList your tags--json
$ th inbox
Inbox (2)
  • Reply to the landlord
      B1
  • Book a dentist appointment
      B2

$ th upcoming --days 7            # `on` is scheduled, `due` is the deadline
Upcoming (next 7 days) (3)
  • Sketch the new homepage  [Website redesign]  on 2026-07-30
      C1
  • Order packing boxes  [Move apartment]  on 2026-07-31  due 2026-08-06
      D1
  • Audit the old CSS  [Website redesign]  on 2026-08-02
      C2

$ th list "Website redesign"      # an area or a project
Website redesign (2)
  • Sketch the new homepage  [Website redesign]
      C1
  • Audit the old CSS  [Website redesign]
      C2

$ th search invoice               # titles and notes
Search: 'invoice' (1)
  • Send the invoice  [Work]  due 2026-07-29
      A2

$ th logbook --days 7
Logbook (last 7 days) (3)
  • Renew the domain  [Work]  done 2026-07-27
      E1
  • File the expense report  [Work]  done 2026-07-25
      E2
  • Cancel the old gym plan  [Personal]  done 2026-07-24
      E3

$ th tags                         # unused tags too — the spelling --tag needs
Tags (3)
  • work
  • errand
  • reading

$ th today --tag reading          # empty is never an error, never silent
Today (0)
  (nothing)

$ th today --json | jq -r '.[] | select(.deadline) | .title'
Send the invoice

Writing

CommandWhat it doesOptions
th add <title> Create a task via the URL scheme --list --when --deadline --notes --tags --dry-run
th edit <ident> Change an existing task --title --notes --list --when --deadline --tags
th done <ident>…Complete one or more tasks
th cancel <ident>…Cancel one or more tasks
th trash <ident>…Move tasks to Things' Trash (recoverable)
th show <ident>Reveal a task in the Things app
$ th add "Buy milk" --list Personal --when today --tags errand
added: Buy milk

$ th add "Buy milk" --when today --dry-run
things:///add?title=Buy%20milk&when=today

$ th edit A1 --deadline 2026-08-14   # only the flags you pass are touched
edited: A1  (deadline)

$ th trash B1 C2
trashed: B1
trashed: C2

When a write fails

th re-reads the database and fails loudly rather than lying.

$ th add "Buy milk"
th: issued the add for 'Buy milk' but could not confirm it in Things. It may NOT have been created.
    If every add fails this way, the URL scheme is probably off:
    Things → Settings → General → enable 'Things URLs'.
$ echo $?
1

$ th done "Review"
th: 'Review' is ambiguous — pass a uuid:
  A1  Review the Q3 draft
  A9  Review pull requests

Option reference

--list <name>
Target area or project. On edit, moves the task there.
--when <value>
today, tomorrow, evening (add only), anytime, someday, or YYYY-MM-DD.
--deadline <value>
YYYY-MM-DD; edit also takes today and tomorrow.
--tags a,b
Comma-separated. On edit this replaces the existing tags; pass '' to clear them.
--notes <text>
Task notes. On edit, replaces them; pass '' to clear.
--dry-run
On add: print the things:/// URL instead of opening it.
THINGS_DB
Override the path to main.sqlite.

Repeating tasks

done, cancel and trash act on the occurrence, not the series. Repeat rules aren't reachable from the URL scheme or AppleScript, so edit those in Things.

Recurring routines therefore need something to create the next occurrence. th ships a Claude Code skill that drives the CLI:

cp .claude/skills/things-routines/routines.example.md \
   .claude/skills/things-routines/routines.md
# edit routines.md, then in Claude Code:  "run my daily review"

Your routines live in routines.md, which is gitignored, so they never land in a commit.

Troubleshooting

add fails but done works
The Things URL scheme is off. Turn on Settings → General → “Things URLs”.
AppleEvent timed out (-1712)
Your terminal lacks Automation permission for Things. Run it from a normal terminal and approve the prompt.
th: cannot read the Things database
Usually Things is mid-write, so try again. If it persists, please open an issue; TH_DEBUG=1 gives the traceback.