Let a coding agent turn a conversation into a Things project

By

Ask Claude Code, Codex or Cursor to turn a planning chat into a Things 3 project with Things CLI, review the plan first, check the import, and undo it.

You’ve spent half an hour planning something with a coding agent. The chat now holds the tasks, the order, and the reasons behind each decision. Then the window closes, and the plan lives nowhere you’ll look at tomorrow.

You want the agent to put that plan in Things 3 for you: one project, tasks grouped under headings, the background in the project notes. And you want to see it before it lands, so a half-baked list doesn’t clutter your Today view.

Things CLI gives the agent a things command that creates projects and to-dos, reads them back as text or JSON, and moves them to the Trash. It ships with an Agent Skill that tells the agent how to use it safely. If you only want to add tasks yourself from the terminal, start with Add tasks and projects to Things 3 from the terminal. This tutorial is about the agent doing it, with you reviewing each step.

By the end, you’ll have a repeatable prompt, a draft file you can read before import, a way to check what the agent created, and a clean undo.

What you need

Things CLI runs on macOS with Things 3 and Node.js 20 or later, as the app page says. Things 3 is a separate purchase from Cultured Code. The CLI has no npm dependencies and doesn’t need the Things auth token.

To get it, download the source zip from your Polar receipt (the purchase email or the Polar customer portal) and unzip it. The receipt always has the latest version, so you download it again from there for updates. Then link the command from the unzipped folder:

npm link

This puts a things command on your PATH.

You also need a coding agent that runs shell commands: Claude Code, Codex or Cursor all work, as long as the agent can call things in a terminal.

The first time the agent reads from Things, macOS may ask whether your terminal or agent app can control Things. Say yes, or reads fail.

For the preflight check, ask the CLI for its version. This one doesn’t touch Things at all:

things --version
things 0.2.0

Version 0.2.0 is the one with delete and delete-project, which the undo section below needs. If you see 0.1.1, download the latest zip from your receipt and run npm link again.

Give the agent the Things skill

An agent that has never seen things will guess. It might write AppleScript by hand, build things:/// URLs itself, or run a command that matches the wrong to-do. The skill fixes that. It’s a short SKILL.md that lists the commands and a safe workflow: read before you update, pass --json when a program reads the output, verify every write with things show, and only delete what the user asked for. Without it, the agent still works, but you’ll spend your review time correcting its approach instead of its plan.

The skill lives in the source at .agents/skills/things/SKILL.md. An agent working inside that folder finds it on its own. To use it from your other projects, put the things skill folder where your agent loads skills. Find where each coding agent loads skills from on your Mac lists those folders.

The agent can also ask the CLI what it does, without opening Things:

things capabilities
things 0.2.0
Add, read, update and delete projects, to-dos and notes in Things 3.

What it can do:
  Add a to-do to a project with notes, schedule and tags
    $ things add "Renew the domain" --project "Website" --when today --tags work
...

things capabilities --json prints the same list as JSON, with a description and a command for each entry. That’s handy when the agent’s skill folder is missing, since one command tells it everything.

If you want to know how skills get picked up in the first place, I wrote about how to write an Agent Skill.

Ask for a draft before anything touches Things

Things CLI creates projects through the Things URL scheme. Once things import runs, the project exists, and every import creates a new one. So the cheapest place to fix a plan is before the import, in a plain Markdown file. If the agent imports straight away, a typo in a heading or a wrong grouping means a second project, a cleanup, and more noise in your lists.

The README suggests a prompt along these lines, and I add one more sentence to stop before importing:

Create a Things project from the plan we just agreed on. Keep the
background and the decisions in the project notes. Group the work
under headings. Write it to /tmp/bakery-project.md in the things
import format and show me the file. Don't import it yet.

Example 1: the bakery plan, end to end. The agent writes the file and prints it. Here’s the plan from a fictional bakery website chat. The names are examples:

# Bakery website relaunch
Relaunch the Crumb & Co site before the spring menu.
Keep the current domain and hosting. Photos come from Marta.

## Content
- Rewrite the About page
  Lead with the sourdough story.
- Photograph the spring menu
  Marta shoots on Thursday morning.

## Launch
- Point the domain at the new site
- Tell the regulars in the newsletter

The format is small. The first # line is the project name. Text after it, up to the first heading or task, becomes the project notes. ## lines are headings, - or * lines are to-dos, and lines indented under a to-do become that to-do’s notes.

Read the file the way you’d read a pull request. Are the task names ones you’ll recognise in a week? Is anything missing from the notes that you’ll need when you pick it up?

Example 2: a decision list that turns into tasks. Say the agent writes the decisions as a bullet list inside the notes:

# Bakery website relaunch
Relaunch the Crumb & Co site before the spring menu.
Decisions from the planning chat:
- Keep the current domain and hosting.

## Content
- Rewrite the About page

That bullet isn’t a note. Any line starting with - is a to-do, wherever it sits. The parser stops the notes there and creates a to-do at the top of the project, before the first heading. The payload the CLI builds shows it:

{
  "type": "project",
  "attributes": {
    "title": "Bakery website relaunch",
    "notes": "Relaunch the Crumb & Co site before the spring menu.\nDecisions from the planning chat:",
    "items": [
      { "type": "to-do", "attributes": { "title": "Keep the current domain and hosting." } },
      { "type": "heading", "attributes": { "title": "Content" } },
      { "type": "to-do", "attributes": { "title": "Rewrite the About page" } }
    ]
  }
}

So you’d get a to-do called “Keep the current domain and hosting.” that nobody will ever tick. Ask the agent to write decisions as plain sentences in the notes, which is why the first draft above has no bullets before ## Content.

Import the project and read it back

Once the draft looks right, the import is one command. It turns the Markdown into a JSON payload and sends it to Things in a single things:///json URL, so the project, its headings and its to-dos arrive together. The URL scheme can create things, but it can’t tell you what it created. That’s why the second half of this step matters: the read goes through JavaScript for Automation, which returns what’s really in Things. Without the read-back, “imported” only means the URL was opened.

From conversation to Things projectThe agent writes a Markdown draft, you review it, the agent imports it with things import, then reads it back with things show. If something is wrong, the project goes to the Trash with things delete-project and the draft is fixed.NoYesYesNoPlanning chatAgent writes /tmpdraftYou approve thedraft?things importthings showMatches the plan?Work from Thingsthingsdelete-project

Back to the bakery plan from Example 1. Tell the agent to go ahead:

The file looks good. Import it, then show me the project from Things.

The agent runs:

things import /tmp/bakery-project.md
Imported project "Bakery website relaunch" (4 to-dos, 2 headings)

The counts come from the file, not from Things. Check them against your draft: four tasks and two headings here. Then the agent reads the project back:

things show "Bakery website relaunch"
# Bakery website relaunch

Relaunch the Crumb & Co site before the spring menu.
Keep the current domain and hosting. Photos come from Marta.

- Rewrite the About page
    Lead with the sourdough story.
- Photograph the spring menu
    Marta shoots on Thursday morning.
- Point the domain at the new site
- Tell the regulars in the newsletter

The project name, the notes and every to-do with its notes are there. The text view doesn’t print headings, so look for them in the Things app. things open "Bakery website relaunch" shows the project there.

When the agent itself needs to check the result, it adds --json:

things show "Bakery website relaunch" --json
{
  "id": "P1",
  "name": "Bakery website relaunch",
  "notes": "Relaunch the Crumb & Co site before the spring menu.\nKeep the current domain and hosting. Photos come from Marta.",
  "status": "open",
  "tags": "",
  "area": null,
  "toDos": [
    {
      "id": "A",
      "name": "Rewrite the About page",
      "notes": "Lead with the sourdough story.",
      "status": "open",
      "tags": "",
      "dueDate": null,
      "activationDate": null,
      "project": "Bakery website relaunch",
      "area": null
    }
  ]
}

I trimmed the toDos array to one entry, and the IDs are placeholders: Things uses its own long IDs. area is null because the Markdown format has no area. Move the project into an area in the Things app if you want it there.

Keep the decisions in the project notes

A to-do title tells you what to do. It rarely tells you why. Two weeks later, “Point the domain at the new site” is clear, but the reason you chose to keep the old hosting is gone, unless it’s in the notes. Things CLI can set and append project notes, so the agent can keep the reasoning next to the tasks. Without this, the context stays in a chat transcript you’ll never reopen, and the next agent session starts from zero.

When a new decision comes up later in the conversation, ask the agent to append it:

things note "Bakery website relaunch" --append "Decided: launch on 3 March, after the menu is printed."
Updated notes of "Bakery website relaunch"

--append adds a blank line and then the new text, so earlier notes stay. --set replaces the notes. For anything longer than a sentence, the agent should write a file and use --file, which avoids quoting problems in the shell:

things note "Bakery website relaunch" --file /tmp/bakery-notes.md

--file replaces the notes with the file’s contents, like --set. Tell the agent which one you want, and have it run things show afterwards to check.

Notes work on to-dos too. Pass --in with the project name so the agent changes the right one:

things note "Photograph the spring menu" --in "Bakery website relaunch" --append "Moved to Friday, the oven is being serviced."

Names are matched exactly, case included. Without --in, note looks for a project with that name first, then for any to-do, and the first match wins. That’s why the skill tells the agent to read before updating when a name might repeat.

Let the agent work through the project later

The project is useful only if you come back to it. A new chat with the agent doesn’t remember the old one, but Things does. The read commands give the agent the state of the project as JSON, so it can pick up where you left off: what’s open, what’s done, which notes explain the plan. The risk is an agent that reads and then “helpfully” changes things. So for a review, say explicitly that nothing changes. The README uses this exact pattern.

Read the Bakery website relaunch project from Things. Suggest the
next two tasks, but don't change anything.

The agent runs read-only commands like these:

things show "Bakery website relaunch" --json
things list today --json
things search "bakery" --json

list takes today (the default), inbox, upcoming, anytime, someday, logbook or trash. search looks through the names and notes of open to-dos.

When you finish a task, tell the agent, and it marks it done:

things done "Rewrite the About page" --in "Bakery website relaunch"
Completed "Rewrite the About page"

Completing is easy to reverse: untick the to-do in Things.

Undo an import

Sometimes the import is wrong anyway: you approved the draft too fast, or the agent imported twice. Things CLI can move a whole project, with its to-dos, to the Trash in Things. That’s the undo. Nothing is deleted for good: the CLI has no command that empties the Trash, on purpose, and you can drag the project back from Trash in the app. Without a clean undo, you’d be clicking through each to-do by hand, which is exactly what the agent was supposed to save you.

Moving to the Trash is still destructive for your lists, so have the agent confirm the exact name with you before it runs this:

things delete-project "Bakery website relaunch"
Moved project "Bakery website relaunch" to the Trash (4 to-dos)

The count tells you how many to-dos went with it. Check the Trash if you want to be sure:

things list trash

For a single wrong to-do, use delete with --in. It only looks at open to-dos and also moves them to the Trash.

The delete commands never guess. A name that matches several items, or none, is an error, and nothing moves. That shows up on a second run. Run things import twice on the same file and you get two projects with the same name, because every import creates a new project. Then delete-project refuses:

2 projects are named "Bakery website relaunch". Delete them in the app.

Delete one copy in Things by hand. With one project left, delete-project works again.

If something looks wrong

Example 3: the import stops on an unindented note. The agent writes a to-do’s note on the line below it, without the indent:

## Content
- Rewrite the About page
Lead with the sourdough story.

The import fails before anything reaches Things:

Cannot parse line: "Lead with the sourdough story."

Indent the note under its to-do with two spaces, as in the first draft, and run things import again. Since nothing was sent, there’s no duplicate to clean up.

  • If the import prints Project markdown needs a "# Title" line, the file has only headings and tasks. Add # Project name as its first line.
  • If things import prints a parse error like Cannot parse line: "Bakery website relaunch", the title line is missing its # . The parser expects the title before any task.
  • If an import prints the counts but nothing appears in Things, check that Things URLs are enabled in Things → Settings → General.
  • If reads fail after you clicked Don’t Allow, open System Settings → Privacy & Security → Automation and turn on Things under your terminal or agent app.
  • If things show prints Project not found: Bakery website relaunch, the name doesn’t match exactly. Matching is case sensitive. Run things projects and copy the name.
  • If things note prints Pass --set, --append or --file, the agent forgot to say how to change the notes. Add one of the three.
  • If things list prints Unknown list "someplace". Use one of: inbox, today, upcoming, anytime, someday, logbook, trash, use one of those built-in lists. list doesn’t read areas or custom lists.
  • If the shell says things: command not found, run npm link again in the unzipped folder and check that node --version prints 20 or newer.

Get Things CLI

Create, read and update Things 3 projects and tasks from your terminal or coding agent. A macOS command-line tool with full MIT source.

See Things CLI → · Read the docs · $20 including tax, with full source code.

More tutorials for my apps →

Want me to talk about your product? You can sponsor this site.