A small Go CLI for Things3 on macOS. Reads
tasks, projects, areas and tags straight from the Things3 SQLite database
(read-only) and writes via the things:/// URL scheme and AppleScript — so the
app stays the source of truth and your data never leaves the machine.
things # today's tasks
things inbox -j | jq # JSON for piping into anything
things add "Buy milk" --when today --tags errand
things edit 3 --add-tags urgent --deadline 2026-05-01
things complete "Pay rent" # by title, with interactive disambig
things search migrate # full-text across titles + notes
things open --project "Launch" # reveal in the Things appWhat it does:
- List & inspect every built-in view (
today,inbox,upcoming,anytime,someday,logbook,trash,deadlines) plus projects, areas, tags, and full-text search - Create tasks and projects with notes, schedules, deadlines, tags, checklists, and headings
- Edit anything mutable via
things:///update— only the flags you pass are sent, so unset fields stay untouched - Complete, cancel, log, or reveal items in the app
- Import Things JSON URL scheme payloads in bulk
- JSON everywhere — every command supports
-j/--jsonfor clean piping intojq, agents, or scripts
Teach your agent to drive it. A bundled skill ships in the binary —
install it once and your agent knows when to reach for things instead
of guessing at AppleScript:
things skill install claude # also: codex, piFor other agents, things skill show prints the neutral source so you can
append it to whatever your agent reads for instructions (e.g. a project
AGENTS.md).
By default output is plain text formatted for humans. Pass -j / --json
for structured JSON suitable for piping into jq or another tool. List
commands assign each result a stable index (1, 2, 3, …) you can use
in follow-up commands like show, edit, complete, and cancel.
| Flag | Description | Default |
|---|---|---|
-j, --json |
Output as JSON instead of plain text | false |
--db PATH |
Override the Things3 SQLite database path | auto-detected |
-v, --version |
Print version, commit, and build date and exit (same as things version) |
— |
things <view> prints a built-in list. With no arguments, things prints
today. View names take precedence over project names — a non-view
argument is treated as a project name (things "Weekly Review"), so a
project literally called Inbox would need things -p Inbox.
| View | Description |
|---|---|
today |
Tasks scheduled for today (default) |
inbox |
Inbox |
upcoming |
Scheduled tasks and deadlines |
anytime |
Anytime list |
someday |
Someday list |
logbook |
Completed tasks |
trash |
Trashed tasks |
deadlines |
Tasks with a deadline |
Filters (combine freely with any view):
| Flag | Description |
|---|---|
-p, --project NAME |
Filter by project name or UUID |
-a, --area NAME |
Filter by area name or UUID |
-t, --tag NAME |
Filter by tag name |
Examples:
things # today (default)
things inbox
things upcoming -t urgent
things "Weekly Review" # tasks in a project by name
things -a Work # tasks in an areaOutput groups by project or area; numeric indices are stable for follow-up commands until the next listing:
$ things
Launch v2
1. [ ] Draft release notes [docs] today
2. [ ] Cut RC build due:2026-04-30
Errands
3. [ ] Buy milk [shopping]
4. [x] Pick up dry cleaning
| Command | Description |
|---|---|
things show <task> |
Show a task's detail (with checklist) |
things projects [--area NAME] [--completed] |
List projects |
things areas |
List areas |
things tags |
List tags |
things search <query> |
Full-text search across titles and notes |
<task> accepts a UUID, a numeric index from the last list, or a title
substring. When a title matches multiple tasks, an interactive prompt picks
between them; non-TTY callers get the match list as an error.
things show 3 # task #3 from the last list
things show "Pay rent" # by title (interactive disambig)
things search migrate # full-text search$ things show 2
Title: Cut RC build
UUID: 8K3FpQ2eRtNbHwpNiM71Eu
Status: Open
Project: Launch v2
Tags: release
Deadline: 2026-04-30
Created: 2026-04-12 09:14
Notes:
Coordinate with marketing before tagging.
Checklist:
[x] Bump version
[ ] Update changelog
[ ] Tag and push
things projects renders a one-line-per-project list; the leading glyph
shows completion progress (○ empty, ◔ ◑ ◕ partial, ● done, ◌
cancelled):
$ things projects
◑ Launch v2 Work
◔ Migrate API Work
○ Garden plan Home
● Spring cleaning Home
things add <title> creates a task. things project add <title> creates a
project.
| Flag | add |
project add |
Description |
|---|---|---|---|
--notes TEXT |
✓ | ✓ | Free-form notes |
--when VALUE |
✓ | ✓ | Schedule (see Date values) |
--deadline DATE |
✓ | ✓ | Deadline date |
--tags LIST |
✓ | ✓ | Comma-separated tags |
--checklist ITEMS |
✓ | — | Newline-separated checklist items |
--todos ITEMS |
— | ✓ | Newline-separated initial to-dos |
--project NAME |
✓ | — | Project to add the task into |
--heading NAME |
✓ | — | Heading within the project |
--list NAME |
✓ | — | List (project or area) name |
--area NAME |
— | ✓ | Area to file the project under |
Examples:
things add "Buy milk" --when today --tags errand,shopping
things add "Ship v2" --project "Launch" --deadline 2026-04-30
things project add "Launch site" --area Work --deadline 2026-05-01things edit <task> updates a task via things:///update.
things project edit <project> updates a project via
things:///update-project. Only the flags you pass are sent — unset fields
stay untouched. An empty value clears the field (e.g. --deadline "").
Prerequisite:
edit,project edit, andimportpayloads withoperation: updaterequire the Things auth token. Enable it once via Things → Settings → General → Enable Things URLs. Without it, writes fail withupdate: auth token is required — enable Things URLs in Things → Settings → General ….
| Flag | edit |
project edit |
Description |
|---|---|---|---|
--title TEXT |
✓ | ✓ | Replace title |
--notes TEXT |
✓ | ✓ | Replace notes |
--prepend-notes TEXT |
✓ | ✓ | Prepend to notes |
--append-notes TEXT |
✓ | ✓ | Append to notes |
--when VALUE |
✓ | ✓ | Reschedule (see Date values) |
--deadline DATE |
✓ | ✓ | Set deadline |
--tags LIST |
✓ | ✓ | Replace all tags (comma-separated) |
--add-tags LIST |
✓ | ✓ | Add tags without replacing existing |
--checklist ITEMS |
✓ | — | Replace checklist (newline-separated) |
--prepend-checklist ITEMS |
✓ | — | Prepend checklist items |
--append-checklist ITEMS |
✓ | — | Append checklist items |
--list NAME |
✓ | — | Move to list/project by name |
--list-id UUID |
✓ | — | Move to list/project by UUID |
--heading NAME |
✓ | — | Set heading within project by name |
--heading-id UUID |
✓ | — | Set heading within project by UUID |
--area NAME |
— | ✓ | Move project to area by name |
--area-id UUID |
— | ✓ | Move project to area by UUID |
--complete |
✓ | ✓ | Mark as completed |
--cancel |
✓ | ✓ | Mark as canceled |
--duplicate |
✓ | ✓ | Duplicate before applying edits |
--reveal |
✓ | ✓ | Reveal in Things after editing |
Examples:
things edit 3 --title "New title" --when tomorrow
things edit "Buy milk" --add-tags urgent --deadline 2026-05-01
things edit "Old idea" --deadline "" # clear the deadline
things project edit "Launch" --append-notes "Beta cut on Friday"| Command | Description |
|---|---|
things complete <task> |
Mark a task or project as completed (project completion is confirmed interactively) |
things cancel <task> |
Cancel a task |
things log |
Move today's done/cancelled items to the Logbook (Items → Log Completed) |
log is the housekeeping action; logbook (above) is the view of
already-archived tasks.
things complete 3
things cancel "Old idea"
things logthings open brings Things3 forward and reveals a list, item, or quick-find
result. Pass exactly one of:
| Flag / Argument | Description |
|---|---|
<ref> |
Built-in list name (today, inbox, …), task UUID, numeric list index, or title |
-p, --project NAME |
Open a project by name or UUID |
-a, --area NAME |
Open an area by name or UUID |
-t, --tag NAME |
Open a tag by name or UUID |
-q, --query TEXT |
App-side quick find |
Additional flags:
| Flag | Description |
|---|---|
--filter TAGS |
Tag filter on the shown list (comma-separated) |
--background |
Don't bring Things to the foreground |
Examples:
things open today
things open "Pay rent"
things open --project "Launch"
things open --query stagingthings import forwards a Things JSON URL scheme
payload — a
batch of to-do, project, heading, and checklist-item items, each
with operation and attributes. The CLI validates the payload is
syntactically valid JSON, then forwards it verbatim. The auth token is
attached automatically (required for operation: update items, harmless
for create-only payloads).
| Flag | Description |
|---|---|
-f, --file PATH |
Read JSON payload from this file instead of stdin |
--reveal |
Reveal the first created/updated item in Things after import |
things import < payload.json
things import --file payload.json --revealNote: macOS open has a URL length limit; split very large payloads.
--when accepts:
| Form | Example |
|---|---|
| Keyword | today, tomorrow, evening, anytime, someday |
| Date | 2026-05-01 |
| Time | HH:MM (21:30) or H:MMam / H:MMpm (9:30PM) |
| Date + time | 2026-05-01@09:30 |
| RFC3339 | 2026-05-01T09:30:00Z (rewritten to YYYY-MM-DD@HH:MM; offset preserved as wall-clock, no conversion to local time) |
| Natural language | friday, next monday (English locales only; passed through verbatim) |
Inputs within edit distance 2 of a known keyword are rejected client-side
as likely typos (e.g. tommorrow, evning) with a "did you mean" hint.
--deadline accepts a YYYY-MM-DD date or an English natural-language
phrase. Keywords are not accepted.
project add accepts --notes, --when, --deadline, --tags, --area
and --todos (newline-separated initial to-dos).
import accepts a JSON array on stdin (or via --file) matching the
Things JSON URL scheme payload
— a batch of to-do, project, heading, and checklist-item items, each
with operation and attributes. The CLI validates the payload is
syntactically valid JSON, then forwards it verbatim. The auth token is
attached automatically (required for operation: update items, harmless for
create-only payloads). Pass --reveal to jump to the first created item.
Note: macOS open has a URL length limit; split very large payloads.
project edit updates an existing project via the things:///update-project
URL scheme. Only flags you pass are sent. Supported flags: --title,
--notes, --prepend-notes, --append-notes, --when, --deadline,
--tags (replace), --add-tags, --area / --area-id, --complete,
--cancel, --duplicate, --reveal. An empty value clears the field
(e.g. --deadline ""). Requires the Things auth token, same as edit.
edit updates an existing task via the things:///update URL scheme. Only
flags you pass are sent, so unset fields stay untouched. Supported flags:
--title, --notes, --prepend-notes, --append-notes, --when,
--deadline, --tags (replace), --add-tags, --checklist,
--prepend-checklist, --append-checklist, --list / --list-id,
--heading / --heading-id, --complete, --cancel, --duplicate,
--reveal. An empty value clears the field (e.g. --deadline ""). Requires
the Things auth token — enable Things → Settings → General → Enable Things
URLs.
things-cli bundles an agent skill that teaches Claude Code, OpenAI's Codex
CLI, the Pi coding agent, and other compatible agents how to drive the CLI.
Install it once and the agent will know when to reach for things instead
of guessing.
| Command | Description |
|---|---|
things skill list |
Show supported agents and install status |
things skill install <agent> |
Install the skill for an agent (claude, codex, pi) |
things skill uninstall <agent> |
Remove the installed skill |
things skill show |
Print the neutral skill source |
things skill show <agent> |
Print the files that would be installed for that agent |
Default install paths:
| Agent | Path |
|---|---|
claude |
~/.claude/skills/things-cli/ |
codex |
~/.codex/skills/things-cli/ |
pi |
~/.pi/agent/skills/things-cli/ |
install and uninstall accept:
| Flag | Description |
|---|---|
--path DIR |
Install or uninstall under a custom directory (e.g. project-local .claude/skills/ or .agents/skills/) |
-y, --yes |
Skip the overwrite/removal prompt |
The skill body is internal/skill/SKILL.md,
embedded in the binary — so a plain things upgrade refreshes it; re-run
skill install to pick up the new version.
- Reads go through
modernc.org/sqlite(pure Go, no cgo) withPRAGMA query_only = ON, so the CLI cannot mutate the Things database. - Writes go through the official
things:///addandthings:///updateURL schemes for creating and editing tasks, and through AppleScript for completing and cancelling them. This is the same interface Things exposes to Shortcuts and automation tools. - Task resolution accepts a UUID, a title (with interactive disambiguation when multiple tasks match) or a numeric index into the last listing.
With Homebrew:
brew install ryanlewis/tap/thingsOr one-line install (downloads the latest release, verifies checksums,
installs to /usr/local/bin):
curl -fsSL https://raw.githubusercontent.com/ryanlewis/things-cli/main/install.sh | shOverride the destination with INSTALL_DIR or pin a version with VERSION:
curl -fsSL https://raw.githubusercontent.com/ryanlewis/things-cli/main/install.sh \
| INSTALL_DIR="$HOME/bin" VERSION=v0.1.0 shOr download a prebuilt binary manually from the
latest release
(darwin_arm64 for Apple Silicon, darwin_amd64 for Intel):
tar -xzf things_*_darwin_arm64.tar.gz
mv things /usr/local/bin/ # or ~/bin, etc.
things versionOr install with go install:
go install github.com/ryanlewis/things-cli/cmd/things@latestOr build from source:
make build # produces ./things
# or
go build -o things ./cmd/thingsRequires macOS with Things3 installed. Go 1.26 or later when building from source.
cmd/things/ CLI entry point (alecthomas/kong)
internal/model/ Shared types + date codecs (ThingsDate, Core Data time)
internal/db/ SQLite queries, read-only
internal/things/ URL scheme + AppleScript writers
internal/output/ JSON and plain-text rendering
internal/cache/ Last-list UUID cache for numeric references