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.
things:///is fire-and-forget, sothre-reads the database after every write and exits non-zero if the task isn't there. An agent can't report a task it never created.- A title that matches two tasks stops the command and prints both, instead of acting on the wrong one.
- Every read command takes
--json, and every listing prints ids. - There is no API key, no token, no network and no account, so there is nothing to put in a config file and nothing to leak.
- The database is opened read-only, and writes go through Things' own APIs, so Things can undo them.
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:
- Reads come from the local SQLite database Things keeps.
thqueries it read-only. - Writes go through the official
things:///URL scheme, which creates tasks, and AppleScript, which changes them.
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
| Command | What it does | Options |
|---|---|---|
th today | Tasks scheduled for today, plus anything overdue | --json --tag |
th inbox | Tasks sitting in the Inbox | --json --tag |
th upcoming | Tasks 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 logbook | Recently completed or canceled tasks | --days N (7) --json --tag |
th areas | List your areas | --json |
th projects | List active projects | --json |
th tags | List 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
| Command | What it does | Options |
|---|---|---|
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, orYYYY-MM-DD.--deadline <value>YYYY-MM-DD;editalso takestodayandtomorrow.--tags a,b- Comma-separated. On
editthis replaces the existing tags; pass''to clear them. --notes <text>- Task notes. On
edit, replaces them; pass''to clear. --dry-run- On
add: print thethings:///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
addfails butdoneworks- 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=1gives the traceback.