Commands
Read commands (list/views, projects, areas, tags, show, search)
accept -j / --json for structured output. Run things --help or
things <subcommand> --help for the full flag list.
In JSON, status, type and start are string enums rather than the raw
Things integers. status is "open", "completed" or "cancelled", and
appears on tasks, projects and checklist items. type is "task" or
"project", and appears on task rows only — projects, areas and tags
rows carry no type. Headings are never returned by any command, so the
third Things type never reaches the output.
start is "inbox", "anytime" or "someday", and appears on task and
project rows. It is the list an item falls back to when it carries no date,
so it does not on its own say which list the app shows the item in: a dated
"anytime" row is in Today, a dated "someday" row is in Upcoming, and
only an undated one is in Someday.
In v0.7.0 and earlier type and start were both integers, so a caller
matching on .type==1 has to become .type=="project", and one matching
on .start==2 has to become .start=="someday". startBucket alongside
them is still an integer: 1 is the app’s This Evening section and 0 is
everything else. Only the first of those has a name in Things’ own
vocabulary, so naming the pair would have meant inventing a word for 0.
The type values a listing reports are not the ones an import payload
takes: that payload is Things’ own JSON URL scheme, which spells a task
"to-do". The payload is the one place these pages use Things’ word rather
than the CLI’s, because it is passed through untouched. Do not copy .type
from a listing into an import item.
Listing
things # today's tasks (default view)
things list <view> # explicit form — see views below
things <view> # shortcut: things inbox, things today, etc.
Available views: today, inbox, upcoming, anytime, someday,
repeating, logbook, trash, deadlines.
Every view above except inbox and anytime lists projects as well as tasks,
marked (project) in plain output and "type": "project" in JSON; --area
and --tag match a project, --project never does, and things projects is
how to sweep projects on their own. The bundled agent skill states the rule
and the reasoning in full — things skill show.
today, anytime and someday are arranged the way the app arranges them —
unfiled items first, then areas, and inside an area its own loose tasks before
its projects’ — so plain output prints each project name once as a group header
above its tasks. anytime carries no project rows of its own because every
active project is trivially “anytime”: listing them all would bury the tasks,
so the app uses each project as a group header instead. today and someday
group the same way but do list their project rows, because a project put in
Today or Someday has actually been put somewhere. someday reaches only the
first half of the arrangement: it carries no task with a parent project, so it
ends at unfiled items and then areas.
today then orders within a group by the position Things keeps for the day,
which leaves an item closed today where it was rather than moving it to the
end. upcoming reads by date instead, the way the app’s own Upcoming does.
logbook is everything closed, not just everything finished. Cancelling a
task or a project logs it under its stop date beside the completed ones, the
way the app’s Logbook shows both, so the view returns cancelled rows too.
status tells them apart — "completed" or "cancelled" in JSON, [x] and
[~] in plain output — so filter on it when you mean finished rather than
closed: things logbook -j | jq '.[] | select(.status=="completed")'.
An item you tick off in Today is not in logbook yet. Things keeps it under
Today for the rest of the day and files it into the Logbook when the day rolls
over, or sooner if you run things log — the app’s “Log Completed Now”, which
files the day’s closed items straight away. things today --include-completed
shows the ones still waiting. anytime behaves the same way and takes the
same flag, because the app goes on showing a just-closed item there too.
Everything else closed goes to logbook at once, today’s closes included: a
task ticked off in the Inbox, or ahead of its date in Upcoming, is under
neither list, so nothing holds it back. A closed item whose project is still
open is therefore either in logbook or in a list still showing it, never both
and never neither — but today and anytime overlap each other, since a task
scheduled for today is in the Anytime bucket too, so sweeping both means
merging them on uuid. One closed inside a project that is itself closed or
trashed is in none of the three unfiltered lists — not logbook, not today,
not anytime — for the reason the next paragraph gives. Naming that project
brings it back. --include-completed works on today and anytime; with a
filter, name the view: things today -p "Launch v2" --include-completed
returns the tasks of a closed “Launch v2” that closed today, rather than
nothing.
A closed project is one row in logbook, not a row plus its contents. The
app folds a closed project’s tasks into the project’s own row and lists none
of them separately, and trash does the same for a trashed project. To reach
those tasks, name the project: things --project <uuid> on a closed or
trashed project returns its contents whatever their status, which is what the
app answers for the same question. Naming the project works inside a view as
well as in that bare form: things anytime --project <uuid> on a trashed
project lists its open tasks, and naming a closed project on today or
anytime with --include-completed lifts the fold there too, so a slice of
those contents is reachable without leaving the view. A task you threw away
out of a project is the exception — it keeps its own trash row, because it
is in the Trash on its own account rather than through its project. A task
thrown away out of a project that is itself in the Trash is reachable nowhere,
as in the app.
someday is the app’s Someday list: the deferred things you have not filed
under a project. A task inside a project stays inside it however it is
deferred, so someday returns Someday projects and unparented Someday tasks,
not the deferred tasks of an Anytime project. Open the project to see those —
things --project "Name" — or use anytime, which carries the project itself.
Because nothing in someday has a parent project, --project there could
never match; the CLI rejects the combination rather than print an empty list.
repeating lists repeating task and project templates. The items a template
generates are ordinary tasks and projects and appear in today, upcoming,
things projects and the rest; the template itself appears only here — plus
trash or logbook for a template that ends up there, since those two report
what the database holds. Both carry projects, so a trashed or logged project
template shows there.
Project templates are marked (project) in plain output and carry
"type": "project" in JSON.
The tasks inside a project template are hidden along with it, since they
would otherwise list against a project things projects does not report.
trash and logbook still show them once they are trashed or closed.
Naming a project template with --project therefore lists nothing on those
views, and the CLI prints a one-line note on stderr saying so. Under --json
the note stays on stderr, so stdout is still an empty array. On trash,
logbook and repeating, which keep templates, there is no note: an empty
listing there means nothing has been trashed or closed yet.
things search is a lookup rather than a view, so it returns templates too.
Results carry "repeating": true.
Filter any list with -p/--project, -a/--area, or -t/--tag. On their
own the filters cover everything open in the project, area, or tag — so
things -a Work lists that area’s own projects as well as its tasks, while
-p still returns a project’s contents rather than the project row. Add a
view and the filter applies within it, with the view named in the output:
things -p "Launch v2" # every open task in the project
things today -p "Launch v2" # today's slice of it, labelled "view: today"
things upcoming -t urgent
things anytime --area "Side projects"
things --json list today | jq '.[] | .title'
Tasks filed under a project heading belong to that project, so they appear
under -p and under the project’s area.
things projects, things areas, and things tags list the
collections themselves. things projects accepts --area and
--completed.
Projects are scheduled the same way tasks are, and things projects -j
reports that with the same field names and encodings: start,
startBucket, startDate and deadline. A caller can tell a scheduled
project from an anytime one without a things show per project.
things projects -j also reports two counts per project. taskCount is
every untrashed task in the project; openCount is the ones still open.
The difference is the ones no longer open, which means completed or
cancelled. Tasks filed under a project heading count towards both; the
heading rows themselves never do, and neither do trashed tasks or
checklist items. Both numbers are Things’ own bookkeeping, read straight
from the database rather than recounted by the CLI.
That makes it one call to find projects whose work has landed but which are still open:
things projects -j | jq '.[] | select(.openCount == 0 and .taskCount > 0)'
taskCount > 0 keeps out empty projects, which have nothing done rather
than everything done. It does not tell done from cancelled: a project
whose tasks were all cancelled matches the same filter. Plain output
marks the same projects with a filled ● progress icon, and under
--completed that icon also marks every completed project, including
empty ones. A project holding a repeating task never appears while the
repeat is live: Things counts the hidden template row itself as an open
task, and a template never completes. things list -p <project> hides
that template, so it can report no open tasks for a project whose
openCount is 1.
Inspecting a task
things show 3 # by index from the last plain list
things show <uuid> # by Things3 UUID
things show "Buy milk" # by title (interactive disambiguation)
things show 3 --agent # Markdown brief for handing to an agent
A title reference is matched as a substring, literally and
case-insensitively. There is no wildcard syntax: % and _ are characters
to find, so things show "20_30 review" finds the task spelled with an
underscore and not the one spelled with a colon.
A title matching more than one item is reported rather than guessed at: an interactive run prints the candidates and asks which one, and a non-TTY run returns them as an error. That holds for an exact title too — a project and a to-do that share one are both offered, instead of the lookup picking whichever sorts first.
After any plain list or search, numeric indices stay valid until the
next one. A listing’s order is fixed, so the same list run twice numbers
the same items the same way — but the numbers still move as items are
added, closed or rescheduled, so re-read the list rather than reusing an
index from an earlier one.
A --json listing is the exception: it prints no numbers and records
none, so it leaves your indices pointing where they did. That keeps a
script or an agent running --json in another window from renumbering
the list you are reading. Agents should act on the uuid rather than an
index — see Working with agents.
Handing a task to an agent
things show <ref> --agent prints a Markdown brief written for an agent,
with the commands that act on the item. See
Working with agents.
Searching
things search "milk"
things search "release" --json
The query matches titles and notes literally and case-insensitively. There is
no wildcard syntax: % and _ are characters to find, so
things search "50%" returns the items that say “50%”.
Capturing
things add "Buy milk"
things add "Ship the thing" --when today --tags work,urgent
things add "Pay invoice" --deadline 2026-06-01 --notes "Send PDF"
things add "Review PR" --project "things-cli"
things add "Plan offsite" --list "Open source" # --list takes a project or area; it overrides --project if both are given
things add "Groceries" --checklist "Milk\nBread\nEggs"
--when accepts a keyword (today, tomorrow, evening, anytime,
someday), a date YYYY-MM-DD, a time HH:MM, a date+time
YYYY-MM-DD@HH:MM, or an RFC3339 timestamp. --deadline accepts a
YYYY-MM-DD date only.
things project add creates a new project with the same flag set
(--notes, --when, --deadline, --tags, --area, --todos).
Editing
things edit 3 --title "Buy oat milk"
things edit 3 --tags shopping # replace all tags
things edit 3 --add-tags urgent # additive
things edit 3 --deadline 2026-05-15
things edit 3 --when tomorrow
things edit 3 --notes "From Holland & Barrett"
things edit 3 --append-checklist "Almond too"
things edit 3 --complete # also: --cancel, --duplicate, --reveal
edit is for tasks only. A reference that resolves to a project is refused
before anything is written, because things:///update cannot address one —
use things project edit instead. project edit refuses a task the same
way, pointing back at things edit.
things project edit takes most of the same flags (--title, --notes,
--prepend-notes/--append-notes, --when, --deadline, --tags,
--add-tags, --complete, --cancel, --duplicate, --reveal) plus
--area/--area-id to move the project. It has no checklist or
heading flags.
Tags must already exist
Things applies only tags that already exist and drops the rest without
saying so. Every write that carries tags (add, project add, edit,
project edit, import) checks them against the database first and warns:
things add "Review the flags" --tags "Work,cifas-auto-reject"
# warning: these tags do not exist in Things and will be ignored: cifas-auto-reject
The write still happens. Add --create-tags to create the missing tags
first so the write applies them all, or --strict-tags to fail and write
nothing instead. The two contradict each other and are rejected together.
Creating tags
things tag add focus "deep work"
things tag add Work # skipped, it already exists
things tag add focus --json # {"created": [...], "skipped": [...]}
Names that already exist are skipped rather than duplicated, matched
case-insensitively as Things matches them. Creation goes through
AppleScript, so Things3 must be running; the tag list is read back
afterwards to confirm it landed, which --no-verify skips.
Completing and cancelling
things complete 3
things cancel 3
things complete "Launch v2" --yes # skip the project confirmation
Completing or cancelling a project also completes or cancels every task
in it, so it asks first. A run that cannot prompt — piped stdin, or
--json — declines instead of guessing; --yes (-y) answers the
question up front, which is how project completion works from a script.
assume_yes = true in the config file sets it every time, and --yes
still decides each run.
Both go through AppleScript so Things3 records the change in its
activity log. Task creation (add) and edits go through the
things:/// URL scheme; the CLI never writes to the database directly.
Logbook and import
things log # move all of Today's completed items into the Logbook
things import < payload.json # batch create/update via the Things JSON URL scheme
import payload is the array
documented by Cultured Code.
Items with "operation": "update" go through the same repeating check as
edit: if any of them carries when, deadline, completed or canceled
for a repeating task or project, the whole import is refused before anything
is sent, and the error names every offending item. The status fields are
two-way, so false is refused as readily as true. Update items that set
completed or canceled are read back from the database afterwards, and any
that Things dropped are reported one per line with a non-zero exit.
Opening in the app
things open today # built-in views
things open inbox
things open <uuid> # specific task or project
things open "Weekly Review" # task or project by title
things open --area "Side projects" # an area (bare titles never resolve areas)
things open --tag urgent # a tag
Agent skill
things skill install <agent> installs the bundled skill for Claude Code,
Codex or Pi. See Working with agents.
Shell completions
things completions <shell> prints a completion script for bash, zsh,
or fish:
things completions zsh > ~/.things-completions.zsh # then source it from ~/.zshrc
Homebrew-cask installs wire these up automatically.
Version
things version # or: things --version / things -v
Prints the version, commit, and build date.
Configuration
A TOML file at ~/.config/things-cli/config.toml supplies defaults for
the flags above, so you can set them once instead of typing them every
run. Precedence is flag > config file > built-in default.
things config init # write a commented template
things config path # the file in use, and whether it exists
things config show # the defaults it establishes
See Configuration for the full key
table, an annotated example file, and what happens when the file is
wrong. The three config subcommands keep working against a file the
CLI cannot use — they are how you find out what is wrong with it.
Caching
things caches the last list it printed in
$HOME/Library/Caches/things-cli/last-list so that numeric indices
(things show 3) work across invocations. Clear it by deleting that
file or by running any plain list command, which overwrites it.
The cache records when the listing ran and which command printed it. A row number is good for four hours; past that a numeric reference is refused rather than acted on, because the rows behind it have probably moved. The error names the listing to re-run:
$ things complete 2
Error: task #2 comes from a stale list cache: the rows were listed over 2 days ago, older than the 4 hours a row number is good for. Re-run `things today` and use the new row number, or pass the task's uuid.
Re-running that listing renumbers the rows and clears the refusal. A
UUID is never refused, and neither is a title. A cache file written by
a version before 0.8.0 records no time, so the first numeric reference
after upgrading is refused until you list again. The named listing
carries --db when the flag supplied one, so it re-reads the database
the rows came from.
A --json listing never writes the cache. JSON output carries no row
numbers, so it has nothing to record, and the file is one shared cache
per machine rather than one per shell — writing it from a scripted run
would move the numbers a person is reading from another window.