TLDR: The Things MCP (the server that lets an AI agent drive Things 3, my task manager) never returns the UUID it just created. Fabricate one and your move calls will quietly succeed while doing nothing. Stale reads make it worse. There is one escape hatch.
the setup
I was doing a big reorg of a law firm client project — a practice management system I built for my business partner — inside Things 3.
Lots of tasks. I wanted them pulled into a fresh project and sorted properly.
So Apollo (my AI assistant system) called add_project. Things confirmed it. Apollo moved on.
where it went sideways
Here is the problem: add_project returns a confirmation string.
That is it. No UUID.
No UUID means no list_id to pass to update_todo. So Apollo — trying to be helpful — fabricated one.
Forty-two update_todo(list_id=...) calls followed.
Every single one came back "success".
Every single one did nothing.
That is the part that got me. Not an error — a cheerful, confident, LYING success message. update_todo with an invalid list_id silently moves nothing, reports success, and expects you to trust it.
I didn't notice until I opened Things and the project looked exactly as I'd left it.
the obvious fix that didn't work
OK, fine — fetch the real UUID via get_projects. Get the actual ID. Re-run the moves.
Except get_projects was serving a stale cached snapshot for the entire session.
The project existed. Things 3 knew about it. But get_projects kept returning the old list — new project nowhere in sight. And get_areas(include_items=true) lists project titles but not their UUIDs.
Dead end.
what finally worked
update_todo(list="<exact project title>")
Move by title. It's in the MCP. It is not obvious — everything points you toward list_id. But when get_projects is stale and you cannot get a UUID, moving by exact title is the rescue.
One requirement: the title has to be unique in your Things database. Two projects with the same name and it is ambiguous — rename the duplicate first, then run it.
Forty-two re-runs, all by title. Done.
why this matters
Any ID a write operation does not hand you back is an ID you do not have.
The Things MCP is sneaky here because it does not error — it silently succeeds while doing nothing. That is the trap for any agent workflow: HTTP 200 and a success string are not the same as the thing actually happened.
If you are building agents that touch Things 3, wire these in:
- Never fabricate a UUID after a create — always re-fetch before acting on the new object
- Don't trust
get_projectsfor a project you just created —get_todosandget_areastend to be fresher - Keep
update_todo(list="<title>")ready — it is the escape hatch when the ID world fails you
Forty-two silent successes. The lesson was free, technically.