Skip to content
ToDowl owl logo ToDowl Help centre
Open app

ToDowl API reference

Every MCP tool, every HTTP endpoint and every command line command, with the parameters the server actually accepts. Written from the code and checked against a live server.

41 MCP tools 7 HTTP endpoints 37 CLI commands

Checked against the live server on 2026-08-28

Open as Markdown

The whole reference as one Markdown document, ready to hand to an AI agent.

If this is your first time here

Three short guides come before this page. They explain the same three things in words.

Before you start

One address, one key

Everything outside the browser goes through https://mcp.todowl.com/: AI clients, the todowl command line tool, and anything you write yourself. There is no second API and no separate database endpoint.

A call proves who it is with a personal API token in the Authorization header. The server keeps no session between calls, so the header goes on every single request.

Every tool acts on the account the token belongs to. No tool takes a user id, and there is no way to reach data that is not yours.

curl https://mcp.todowl.com/ \
  -H "Authorization: Bearer todowl_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": { "name": "list_tasks", "arguments": { "status": "open", "limit": 5 } }
  }'

Dates are dates, not words

Every date is a plain calendar date, YYYY-MM-DD, and every time is 24-hour HH:MM. The server has no idea what timezone the caller is in, so words like tomorrow have to be turned into a date before the call.

Names are not ids. A request that mentions a project or a habit by name starts with list_projects or list_habits, and the id from that answer is what the next call carries.

What comes back

A tool answers with JSON inside the MCP text part, written as compactly as the wire allows. Every example on this page is laid out for reading.

Empty fields are dropped rather than sent as null. A task with no list simply has no list_id in the answer. false is kept, because not set and not done are different things.

A refusal is not an HTTP error. The call answers 200 with isError set and a single sentence in { "error": "…" }, which is the sentence the app would have shown.

Long text is trimmed on the way out: 300 characters per row in a list, 8000 when a single item was asked for. An image pasted into a description becomes [image], and a cut is marked, so nothing reads a truncated description as the whole thing.

Leave it, clear it, or replace it

An update tool reads a field three different ways depending on what you send. Leave the field out and it is untouched. Send a value and it replaces what was there. Send null and it is taken off, the way an empty box in the app would be.

Not every field can be cleared this way: a title has to say something, and a boolean already has an off value of its own. Where null is accepted, the parameter table on this page says so.

The project is the one thing that cannot be taken off. A task, a calendar entry and a note all live in a project, and a row without one appears on no screen in the app, so the way out of a project is into another one. A list can still be dropped, either with null or with move_task and its clear_list flag.

Deleting is not erasing

A delete tool moves the thing to Trash, exactly as the app does. list_trash, restore_from_trash and delete_from_trash_forever are the rest of that story.

Two exceptions, and they are the only ones: delete_from_trash_forever erases, and delete_habit takes a habit and its whole check-in history with it, because habits have no Trash.

Limits the server enforces

What Limit
Rows per call 50 by default, 200 maximum
Tasks per batch_create_tasks 20
Task and entry title 500
Task and entry description 20000
Note title 300
Note body 100000
Project and list name 200
Reminder, minutes before the start 0-10080
Request body 4 MB
Assistant messages per hour 60

The number after the type is what the server enforces: the length of a string, the value of a number, the number of items in a list. Every tool says what it does to the account: reads changes nothing, writes changes something, removes takes something away.

MCP tools

What an AI client can do in your account. Grouped the way the server code is, and named exactly as a client will name them.

Tasks

The nine tools behind every to-do: listing, searching, creating one or twenty, editing, ticking off, re-filing, and sending to Trash.

Written from: mcp/src/tools/tasks.ts

list_tasks

reads

List tasks

Your tasks, soonest first. This is what answers questions like what is due today, what is left in a project, what has no date at all. Filters all apply at once.

Parameters

NameTypeRequiredWhat it does
statusstring, one of open, completed, all, default openoptionalWhich tasks to include. 'open' (the default) hides finished ones, 'all' includes both.
datestring, YYYY-MM-DDoptionalOnly tasks due on exactly this date.
date_fromstring, YYYY-MM-DDoptionalOnly tasks due on or after this date.
date_tostring, YYYY-MM-DDoptionalOnly tasks due on or before this date.
no_datebooleanoptionalTrue to return only undated tasks (the inbox). Ignores the date filters.
project_idstring, UUIDoptionalOnly tasks in this project.
list_idstring, UUIDoptionalOnly tasks in this list.
prioritystring, one of high, medium, low, noneoptionalOnly tasks at this priority.
include_archivedboolean, default falseoptionalInclude archived tasks. Default false.
limitinteger, 1-200, default 50optionalMaximum rows to return. Default 50, hard maximum 200.
offsetintegeroptionalRows to skip, for paging through large results.

Example call

{
  "status": "open",
  "date_from": "2026-09-01",
  "date_to": "2026-09-07",
  "limit": 20
}

Answer

{
  "count": 2,
  "tasks": [
    {
      "id": "9b2c1f04-3a7e-4c51-9d18-6f0b2a7d4e33",
      "title": "Send the invoice",
      "date": "2026-09-01",
      "start_time": "10:00:00",
      "all_day": false,
      "completed": false,
      "priority": "high",
      "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5"
    },
    {
      "id": "b7419e60-8c25-4d13-a0f6-52d7c1b93e48",
      "title": "Water the plants",
      "date": "2026-09-03",
      "all_day": true,
      "completed": false,
      "priority": "none"
    }
  ]
}

Worth knowing

  • Dates are plain calendar dates. The server has no idea what tomorrow means, so work the date out first and pass YYYY-MM-DD.
  • A list answers with the short set of fields you see in the example. Everything else about a task, its description included, comes from get_task.
  • no_date wins over date, date_from and date_to.
  • Archived tasks are hidden unless you ask for them. Anything in Trash never comes back from here.
  • Descriptions are cut to 300 characters in a list, and an image pasted into a description is replaced by [image]. One page of tasks cannot swallow a whole model context that way.

search_tasks

reads

Search tasks

Find tasks by wording rather than by id or date. Case-insensitive substring match over titles and descriptions.

Parameters

NameTypeRequiredWhat it does
querystring, 1-200requiredText to look for in the title or description.
statusstring, one of open, completed, all, default openoptionalWhich tasks to include. 'open' (the default) hides finished ones, 'all' includes both.
include_archivedboolean, default falseoptionalInclude archived tasks. Default false.
limitinteger, 1-200, default 50optionalMaximum rows to return. Default 50, hard maximum 200.

Example call

{
  "query": "invoice",
  "status": "all",
  "limit": 10
}

Answer

{
  "count": 1,
  "tasks": [
    {
      "id": "9b2c1f04-3a7e-4c51-9d18-6f0b2a7d4e33",
      "title": "Send the invoice",
      "date": "2026-09-01",
      "all_day": false,
      "completed": false,
      "priority": "high"
    }
  ]
}

Worth knowing

  • Substring, not a search engine: no wildcards and no word stemming. The characters *, % and _ are stripped out of the query before it reaches the database.
  • Results come back most recently changed first.

get_task

reads

Get one task

One task by id, with everything on it: description, scheduling, colour, repeat rule, timestamps.

Parameters

NameTypeRequiredWhat it does
task_idstring, UUIDrequiredTask UUID.

Example call

{
  "task_id": "9b2c1f04-3a7e-4c51-9d18-6f0b2a7d4e33"
}

Answer

{
  "id": "9b2c1f04-3a7e-4c51-9d18-6f0b2a7d4e33",
  "type": "task",
  "title": "Send the invoice",
  "description": "August hours, the usual template.",
  "date": "2026-09-01",
  "start_time": "10:00:00",
  "all_day": false,
  "completed": false,
  "priority": "high",
  "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5",
  "reminder": 30,
  "is_archived": false,
  "is_deleted": false,
  "created_at": "2026-08-24T09:12:44.318Z",
  "updated_at": "2026-08-28T07:01:02.905Z"
}

Worth knowing

  • The description is capped at 8000 characters here rather than the 300 a list uses, because this call was made about that one task.
  • No task with that id in your account is an error, not an empty answer.

create_task

writes

Create a task

Create one task in a project. A task with no date goes to the inbox, but never with no project.

Parameters

NameTypeRequiredWhat it does
titlestring, 1-500requiredTask title, the one line shown in the list.
descriptionstring, ≤ 20000optionalLonger free-form notes attached to the task. Pass null to clear it.
datestring, YYYY-MM-DDoptionalDue date, YYYY-MM-DD. Omit for a task with no date (it lands in the inbox). Pass null to clear it.
start_timestring, HH:MMoptionalStart time, HH:MM, 24-hour, in the user local timezone. Pass null to clear it.
end_timestring, HH:MMoptionalEnd time, HH:MM, 24-hour. Pass null to clear it.
all_daybooleanoptionalTrue when the task occupies the whole day rather than a time slot.
prioritystring, one of high, medium, low, noneoptionalPriority: 'high', 'medium', 'low' or 'none'. Pass null to clear it.
project_idstring, UUIDoptionalProject UUID from list_projects. Leave it out only when the account has a single project, which is then used; with several projects and none named the call is refused and comes back with the list to choose from.
list_idstring, UUIDoptionalList UUID from list_lists. Pass null to clear it.
reminderinteger, 0-10080optionalReminder, in minutes before the start time. Pass null to clear it.
colorstring, ≤ 32optionalHex colour override, for example #6366f1. Pass null to clear it.

Example call

{
  "title": "Send the invoice",
  "date": "2026-09-01",
  "start_time": "10:00",
  "priority": "high",
  "reminder": 30,
  "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5"
}

Answer

{
  "id": "9b2c1f04-3a7e-4c51-9d18-6f0b2a7d4e33",
  "type": "task",
  "title": "Send the invoice",
  "date": "2026-09-01",
  "start_time": "10:00:00",
  "all_day": false,
  "completed": false,
  "priority": "high",
  "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5",
  "reminder": 30,
  "is_archived": false,
  "is_deleted": false,
  "created_at": "2026-08-28T07:01:02.905Z",
  "updated_at": "2026-08-28T07:01:02.905Z",
  "project": { "id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5", "name": "Work" }
}

Worth knowing

  • Every task lives in a project. There is no unfiled bucket: the app's task screens are queried by project, so a task written without one would exist in the database and appear nowhere. That is why the call is refused instead of guessing a project for you.
  • The answer carries the new id and the project by name, so you can tell the person where the task went instead of just saying it was added.
  • Calendar entries are not created here. A meeting or an event is create_event, and the type is what puts it on the calendar screen.

batch_create_tasks

writes

Create several tasks

Up to 20 tasks in one call. This is what to use when somebody dictates a list, instead of calling create_task over and over.

Parameters

NameTypeRequiredWhat it does
tasksobject[], 1-20requiredThe tasks to create, same fields as create_task.

Fields of each item: tasks

NameTypeRequiredWhat it does
titlestring, 1-500requiredTask title, the one line shown in the list.
descriptionstring, ≤ 20000optionalLonger free-form notes attached to the task. Pass null to clear it.
datestring, YYYY-MM-DDoptionalDue date, YYYY-MM-DD. Omit for a task with no date (it lands in the inbox). Pass null to clear it.
start_timestring, HH:MMoptionalStart time, HH:MM, 24-hour, in the user local timezone. Pass null to clear it.
end_timestring, HH:MMoptionalEnd time, HH:MM, 24-hour. Pass null to clear it.
all_daybooleanoptionalTrue when the task occupies the whole day rather than a time slot.
prioritystring, one of high, medium, low, noneoptionalPriority: 'high', 'medium', 'low' or 'none'. Pass null to clear it.
project_idstring, UUIDoptionalProject UUID from list_projects. The same rule as create_task, applied to the batch as a whole: one row without a project is enough to refuse the lot, so half a dictated list never goes missing.
list_idstring, UUIDoptionalList UUID from list_lists. Pass null to clear it.
reminderinteger, 0-10080optionalReminder, in minutes before the start time. Pass null to clear it.
colorstring, ≤ 32optionalHex colour override, for example #6366f1. Pass null to clear it.

Example call

{
  "tasks": [
    { "title": "Buy milk" },
    { "title": "Book the dentist", "date": "2026-09-04", "priority": "medium" }
  ]
}

Answer

{
  "created": 2,
  "tasks": [
    {
      "id": "1c8d43a7-6b02-4e59-9f31-7a4c0e2b6d95",
      "title": "Buy milk",
      "all_day": false,
      "completed": false,
      "priority": "none"
    },
    {
      "id": "4e70b915-2f38-4c6a-b8d0-19c53e7a4f26",
      "title": "Book the dentist",
      "date": "2026-09-04",
      "all_day": false,
      "completed": false,
      "priority": "medium"
    }
  ]
}

Worth knowing

  • Each item takes the same fields as create_task.
  • Twenty is a hard ceiling. A longer list has to be split into several calls.
  • They are written in one go, so either all of them appear or none of them do.

update_task

writes

Update a task

Change fields on a task that already exists. Only the fields you pass are touched.

Parameters

NameTypeRequiredWhat it does
task_idstring, UUIDrequiredTask UUID.
titlestring, 1-500optionalTask title, the one line shown in the list.
descriptionstring, ≤ 20000optionalLonger free-form notes attached to the task. Pass null to clear it.
datestring, YYYY-MM-DDoptionalDue date, YYYY-MM-DD. Omit for a task with no date (it lands in the inbox). Pass null to clear it.
start_timestring, HH:MMoptionalStart time, HH:MM, 24-hour, in the user local timezone. Pass null to clear it.
end_timestring, HH:MMoptionalEnd time, HH:MM, 24-hour. Pass null to clear it.
all_daybooleanoptionalTrue when the task occupies the whole day rather than a time slot.
prioritystring, one of high, medium, low, noneoptionalPriority: 'high', 'medium', 'low' or 'none'. Pass null to clear it.
project_idstring, UUIDoptionalMove the task to this project. It cannot be set to null: every task belongs to a project.
list_idstring, UUIDoptionalList UUID from list_lists. Pass null to clear it.
reminderinteger, 0-10080optionalReminder, in minutes before the start time. Pass null to clear it.
colorstring, ≤ 32optionalHex colour override, for example #6366f1. Pass null to clear it.
is_archivedbooleanoptionalTrue to archive the task (out of the lists, still searchable), false to bring it back.

Example call

{
  "task_id": "9b2c1f04-3a7e-4c51-9d18-6f0b2a7d4e33",
  "date": "2026-09-02",
  "priority": "medium"
}

Answer

{
  "id": "9b2c1f04-3a7e-4c51-9d18-6f0b2a7d4e33",
  "type": "task",
  "title": "Send the invoice",
  "date": "2026-09-02",
  "start_time": "10:00:00",
  "all_day": false,
  "completed": false,
  "priority": "medium",
  "is_archived": false,
  "is_deleted": false,
  "updated_at": "2026-08-28T07:04:19.442Z"
}

Worth knowing

  • Pass at least one field besides task_id, otherwise the call is refused rather than quietly doing nothing.
  • Send null on a field to take it off rather than an empty string; leaving a field out just means it stays as it was. The project is the exception, it can only be changed to another one; move_task with clear_list does the taking off for a list.
  • Ticking a task off is complete_task, re-filing it is move_task.

complete_task

writes

Complete or reopen a task

Tick a task off, or reopen it with completed=false.

Parameters

NameTypeRequiredWhat it does
task_idstring, UUIDrequiredTask UUID.
completedboolean, default trueoptionalTrue to complete (default), false to reopen.

Example call

{
  "task_id": "9b2c1f04-3a7e-4c51-9d18-6f0b2a7d4e33"
}

Answer

{
  "id": "9b2c1f04-3a7e-4c51-9d18-6f0b2a7d4e33",
  "type": "task",
  "title": "Send the invoice",
  "date": "2026-09-02",
  "completed": true,
  "priority": "medium",
  "is_archived": false,
  "is_deleted": false,
  "updated_at": "2026-08-28T07:06:00.117Z"
}

Worth knowing

  • Running it twice on the same task changes nothing the second time.

move_task

writes

Move a task

Re-file a task into another project or list, or pull it back out to the inbox.

Parameters

NameTypeRequiredWhat it does
task_idstring, UUIDrequiredTask UUID.
project_idstring, UUIDoptionalDestination project UUID.
list_idstring, UUIDoptionalDestination list UUID.
clear_listbooleanoptionalTrue to detach the task from its list, keeping it in its project.

Example call

{
  "task_id": "9b2c1f04-3a7e-4c51-9d18-6f0b2a7d4e33",
  "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5",
  "clear_list": true
}

Answer

{
  "id": "9b2c1f04-3a7e-4c51-9d18-6f0b2a7d4e33",
  "type": "task",
  "title": "Send the invoice",
  "date": "2026-09-02",
  "completed": true,
  "priority": "medium",
  "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5",
  "is_archived": false,
  "is_deleted": false,
  "updated_at": "2026-08-28T07:08:41.220Z"
}

Worth knowing

  • Name at least one destination: project_id, list_id or clear_list. Nothing named is an error.
  • A task is always in some project, so the way out of one is into another. There is no way to leave a task with no project: a row like that appears on no screen in the app.
  • The answer names the project the task ended up in, so you can repeat it back to the person.
  • The destination is checked against your own account first, so a task cannot land in somebody else's project.

delete_task

removes

Delete a task

Move a task to Trash. The same soft delete the app performs, so it can still be restored.

Parameters

NameTypeRequiredWhat it does
task_idstring, UUIDrequiredTask UUID.

Example call

{
  "task_id": "9b2c1f04-3a7e-4c51-9d18-6f0b2a7d4e33"
}

Answer

{
  "trashed": true,
  "task": {
    "id": "9b2c1f04-3a7e-4c51-9d18-6f0b2a7d4e33",
    "title": "Send the invoice",
    "is_deleted": true,
    "deleted_at": "2026-08-28T07:10:55.003Z"
  }
}

Worth knowing

  • Nothing is erased. restore_from_trash with kind=task brings it back, delete_from_trash_forever is what erases.
  • Only tasks. A meeting or an event is delete_event.

Calendar

Meetings and events, and the one tool that reads a whole date range at once, dated tasks included.

Written from: mcp/src/tools/calendar.ts

list_events

reads

List the calendar between two dates

Everything scheduled between two dates, inclusive: meetings, events and dated tasks alike, in the order they happen. This is the tool behind what does my week look like.

Parameters

NameTypeRequiredWhat it does
date_fromstring, YYYY-MM-DDrequiredFirst day of the range, YYYY-MM-DD.
date_tostring, YYYY-MM-DDrequiredLast day of the range, YYYY-MM-DD, inclusive.
typesstring[], one of task, meeting, eventoptionalEntry kinds to include. Defaults to all three.
include_archivedboolean, default falseoptionalInclude archived entries. Default false.
include_completedboolean, default trueoptionalInclude entries already ticked off. Default true.
limitinteger, 1-200, default 50optionalMaximum rows to return. Default 50, hard maximum 200.

Example call

{
  "date_from": "2026-09-01",
  "date_to": "2026-09-07",
  "types": ["meeting", "event"]
}

Answer

{
  "count": 1,
  "from": "2026-09-01",
  "to": "2026-09-07",
  "entries": [
    {
      "id": "e2f70a95-4d1c-4b83-97a6-0b5e8c3d21f7",
      "type": "meeting",
      "title": "Weekly sync",
      "date": "2026-09-02",
      "start_time": "11:00:00",
      "end_time": "11:30:00",
      "all_day": false,
      "completed": false,
      "priority": "none"
    }
  ]
}

Worth knowing

  • Tasks are included by default, because a day is a day. Pass types to narrow it down to meetings and events.
  • Archived entries are never returned here, with or without a flag, and neither is anything in Trash.
  • Ordered by date, then all-day entries, then by start time.
  • For to-do work use list_tasks: it has the filters a task list needs, this one has the date range a calendar needs.

create_event

writes

Create a calendar entry

Put a meeting or an event on the calendar.

Parameters

NameTypeRequiredWhat it does
titlestring, 1-500requiredWhat the entry is called.
datestring, YYYY-MM-DDrequiredThe day it happens, YYYY-MM-DD.
start_timestring, HH:MMoptionalStart time, HH:MM, 24-hour, local to the user.
end_timestring, HH:MMoptionalEnd time, HH:MM, 24-hour.
all_daybooleanoptionalTrue for an entry that takes the whole day.
typestring, one of meeting, event, default eventoptionalKind of entry. Defaults to 'event'.
descriptionstring, ≤ 20000optionalLonger free-form notes.
project_idstring, UUIDoptionalProject UUID: the calendar the entry sits on, and what gives it its colour. Leave it out only when the account has a single project; with several and none named the call is refused and comes back with the list to choose from.
colorstring, ≤ 32optionalHex colour override, for example #6366f1.
reminderinteger, 0-10080optionalReminder, in minutes before the start time.

Example call

{
  "title": "Weekly sync",
  "date": "2026-09-02",
  "start_time": "11:00",
  "end_time": "11:30",
  "type": "meeting",
  "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5"
}

Answer

{
  "id": "e2f70a95-4d1c-4b83-97a6-0b5e8c3d21f7",
  "type": "meeting",
  "title": "Weekly sync",
  "date": "2026-09-02",
  "start_time": "11:00:00",
  "end_time": "11:30:00",
  "all_day": false,
  "completed": false,
  "priority": "none",
  "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5",
  "is_archived": false,
  "is_deleted": false,
  "created_at": "2026-08-28T07:12:30.771Z",
  "updated_at": "2026-08-28T07:12:30.771Z"
}

Worth knowing

  • A to-do is create_task. Both live in the same place and the type is what decides which screen the entry belongs on.
  • A project_id that is not yours is refused before anything is written.

update_event

writes

Update a calendar entry

Reschedule or edit a meeting or an event: day, times, title, notes, colour, project. Only the fields you pass are touched.

Parameters

NameTypeRequiredWhat it does
event_idstring, UUIDrequiredEntry UUID from list_events.
titlestring, 1-500optionalNew title.
datestring, YYYY-MM-DDoptionalNew day, YYYY-MM-DD.
start_timestring, HH:MMoptionalNew start time, HH:MM. Pass null to clear it.
end_timestring, HH:MMoptionalNew end time, HH:MM. Pass null to clear it.
all_daybooleanoptionalTrue to make it an all-day entry.
descriptionstring, ≤ 20000optionalNew notes. Pass null to clear it.
project_idstring, UUIDoptionalMove the entry to this project. It cannot be set to null: every entry sits on some calendar.
colorstring, ≤ 32optionalHex colour override. Pass null to clear it.
reminderinteger, 0-10080optionalReminder, in minutes before the start. Pass null to clear it.

Example call

{
  "event_id": "e2f70a95-4d1c-4b83-97a6-0b5e8c3d21f7",
  "date": "2026-09-03",
  "start_time": "12:00",
  "end_time": "12:30"
}

Answer

{
  "id": "e2f70a95-4d1c-4b83-97a6-0b5e8c3d21f7",
  "type": "meeting",
  "title": "Weekly sync",
  "date": "2026-09-03",
  "start_time": "12:00:00",
  "end_time": "12:30:00",
  "all_day": false,
  "completed": false,
  "priority": "none",
  "is_archived": false,
  "is_deleted": false,
  "updated_at": "2026-08-28T07:15:02.610Z"
}

Worth knowing

  • Tasks are out of reach from here. Aiming this at a task id gets an error that points at update_task.
  • Pass at least one field besides event_id.
  • date is the one field here null does not reach: an entry always has a day, so it can be moved but never cleared.

delete_event

removes

Delete a calendar entry

Move a meeting or an event to Trash, the same soft delete the app performs.

Parameters

NameTypeRequiredWhat it does
event_idstring, UUIDrequiredEntry UUID from list_events.

Example call

{
  "event_id": "e2f70a95-4d1c-4b83-97a6-0b5e8c3d21f7"
}

Answer

{
  "trashed": true,
  "event": {
    "id": "e2f70a95-4d1c-4b83-97a6-0b5e8c3d21f7",
    "title": "Weekly sync",
    "is_deleted": true,
    "deleted_at": "2026-08-28T07:16:44.118Z"
  }
}

Worth knowing

  • restore_from_trash with kind=event brings it back.
  • Tasks are deleted with delete_task.

Projects and lists

The structure a task is filed under. Projects hold lists, lists hold tasks, and both can be archived instead of deleted.

Written from: mcp/src/tools/structure.ts

list_projects

reads

List projects

Every project in the account, with its id and colour. Call it first whenever a request names a project in words, so the right project_id can be passed on.

Parameters

NameTypeRequiredWhat it does
include_archivedboolean, default falseoptionalInclude archived projects. Default false.
limitinteger, 1-200, default 50optionalMaximum rows to return. Default 50, hard maximum 200.

Example call

{
  "include_archived": false
}

Answer

{
  "count": 2,
  "projects": [
    {
      "id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5",
      "name": "Work",
      "color": "#6366f1",
      "icon": "briefcase",
      "is_active": true,
      "is_default": false,
      "order": 1,
      "is_archived": false,
      "created_at": "2026-05-02T10:14:07.882Z"
    },
    {
      "id": "6b0a2e54-71d3-4f89-93c1-40b7e5a2c6d9",
      "name": "Home",
      "color": "#22c55e",
      "is_active": true,
      "is_default": true,
      "order": 2,
      "is_archived": false,
      "created_at": "2026-05-02T10:14:07.882Z"
    }
  ]
}

Worth knowing

  • Archived projects are hidden unless you ask for them, and projects in Trash never come back from here.
  • Ordered the way they sit in the sidebar, oldest first inside the same position.

create_project

writes

Create a project

Create a project. The answer carries the id to file tasks into.

Parameters

NameTypeRequiredWhat it does
namestring, 1-200requiredProject name.
descriptionstring, ≤ 2000optionalOptional one-liner about the project.
colorstring, ≤ 32optionalHex colour, for example #6366f1. Defaults to indigo.
iconstring, ≤ 64optionalOptional icon name.

Example call

{
  "name": "Work",
  "color": "#6366f1",
  "icon": "briefcase"
}

Answer

{
  "id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5",
  "name": "Work",
  "color": "#6366f1",
  "icon": "briefcase",
  "is_active": true,
  "is_default": false,
  "order": 1,
  "is_archived": false,
  "created_at": "2026-08-28T07:18:11.409Z"
}

Worth knowing

  • Without a colour the project comes out indigo, the same default the app uses.

update_project

writes

Update a project

Rename a project or change its colour, icon, description or archived state. Only the fields you pass are touched.

Parameters

NameTypeRequiredWhat it does
project_idstring, UUIDrequiredProject UUID from list_projects.
namestring, 1-200optionalNew project name.
descriptionstring, ≤ 2000optionalOne-liner about the project.
colorstring, ≤ 32optionalHex colour, for example #6366f1.
iconstring, ≤ 64optionalIcon name.
is_archivedbooleanoptionalTrue to archive the project, false to bring it back.

Example call

{
  "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5",
  "name": "Work 2026",
  "is_archived": false
}

Answer

{
  "id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5",
  "name": "Work 2026",
  "color": "#6366f1",
  "icon": "briefcase",
  "is_active": true,
  "is_default": false,
  "order": 1,
  "is_archived": false,
  "created_at": "2026-05-02T10:14:07.882Z"
}

Worth knowing

  • Archiving is how you hide a project without deleting it: it leaves the sidebar and its tasks stay where they are.
  • Pass at least one field besides project_id.

delete_project

removes

Delete a project

Move a project to Trash. Its tasks and lists keep pointing at it and come back with it.

Parameters

NameTypeRequiredWhat it does
project_idstring, UUIDrequiredProject UUID from list_projects.

Example call

{
  "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5"
}

Answer

{
  "trashed": true,
  "project": {
    "id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5",
    "name": "Work 2026",
    "is_deleted": true,
    "deleted_at": "2026-08-28T07:20:52.336Z"
  }
}

Worth knowing

  • The default project cannot be deleted, here or in the app. That is where unfiled work lands.
  • To hide a project without deleting it, archive it with update_project.

list_lists

reads

List lists

Lists, the sub-groups inside projects. Pass a project_id to see only one project's.

Parameters

NameTypeRequiredWhat it does
project_idstring, UUIDoptionalOnly lists belonging to this project.
include_archivedboolean, default falseoptionalInclude archived lists. Default false.
limitinteger, 1-200, default 50optionalMaximum rows to return. Default 50, hard maximum 200.

Example call

{
  "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5"
}

Answer

{
  "count": 1,
  "lists": [
    {
      "id": "7a4e9c31-2b60-4d8f-a1c5-3e97b6042f18",
      "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5",
      "name": "Backlog",
      "icon": "inbox",
      "order": 1,
      "is_archived": false,
      "created_at": "2026-05-02T10:16:33.501Z"
    }
  ]
}

Worth knowing

  • A list can also live outside any project, in which case project_id is absent from the row.
  • Archived lists are hidden unless you ask for them.

create_list

writes

Create a list

Create a list, inside a project or on its own.

Parameters

NameTypeRequiredWhat it does
namestring, 1-200requiredList name.
project_idstring, UUIDoptionalProject to create the list in.
iconstring, ≤ 64optionalOptional icon name.
colorstring, ≤ 32optionalHex colour, for example #6366f1.

Example call

{
  "name": "Backlog",
  "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5",
  "icon": "inbox"
}

Answer

{
  "id": "7a4e9c31-2b60-4d8f-a1c5-3e97b6042f18",
  "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5",
  "name": "Backlog",
  "icon": "inbox",
  "order": 1,
  "is_archived": false,
  "created_at": "2026-08-28T07:22:04.918Z"
}

Worth knowing

  • A project_id that is not yours is refused before anything is written.

update_list

writes

Update a list

Rename a list, change its icon or colour, move it to another project, or archive it.

Parameters

NameTypeRequiredWhat it does
list_idstring, UUIDrequiredList UUID from list_lists.
namestring, 1-200optionalNew list name.
iconstring, ≤ 64optionalIcon name.
colorstring, ≤ 32optionalHex colour, for example #6366f1.
project_idstring, UUIDoptionalMove the list into this project.
is_archivedbooleanoptionalTrue to archive the list, false to bring it back.

Example call

{
  "list_id": "7a4e9c31-2b60-4d8f-a1c5-3e97b6042f18",
  "name": "Later"
}

Answer

{
  "id": "7a4e9c31-2b60-4d8f-a1c5-3e97b6042f18",
  "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5",
  "name": "Later",
  "icon": "inbox",
  "order": 1,
  "is_archived": false,
  "created_at": "2026-05-02T10:16:33.501Z"
}

Worth knowing

  • Pass at least one field besides list_id.
  • Moving a list to a project that is not yours is refused.

delete_list

removes

Delete a list

Move a list to Trash. Tasks filed under it stay in their project and simply lose the list, exactly as in the app.

Parameters

NameTypeRequiredWhat it does
list_idstring, UUIDrequiredList UUID from list_lists.

Example call

{
  "list_id": "7a4e9c31-2b60-4d8f-a1c5-3e97b6042f18"
}

Answer

{
  "trashed": true,
  "list": {
    "id": "7a4e9c31-2b60-4d8f-a1c5-3e97b6042f18",
    "name": "Later",
    "is_deleted": true,
    "deleted_at": "2026-08-28T07:24:10.220Z"
  }
}

Worth knowing

  • restore_from_trash with kind=list brings it back.

Notes

Plain-text notes: list and search them, read one, write one, replace its body, send it to Trash.

Written from: mcp/src/tools/notes.ts

list_notes

reads

List notes

Note titles with their ids, pinned ones first and the rest by when they were last edited. Pass a query to search titles and body text.

Parameters

NameTypeRequiredWhat it does
querystring, 1-200optionalCase-insensitive substring to look for in the title or body.
include_archivedboolean, default falseoptionalInclude archived notes. Default false.
limitinteger, 1-200, default 50optionalMaximum rows to return. Default 50, hard maximum 200.

Example call

{
  "query": "invoice",
  "limit": 20
}

Answer

{
  "count": 1,
  "notes": [
    {
      "id": "c81b5d47-0e39-4b2a-9f64-5a7d31e8c026",
      "title": "Invoice template",
      "project_id": null,
      "is_pinned": true,
      "is_archived": false,
      "sort_order": 0,
      "created_at": "2026-06-11T08:20:15.004Z",
      "updated_at": "2026-08-20T19:41:03.772Z"
    }
  ]
}

Worth knowing

  • Bodies do not come back here. Fetch one with get_note.
  • The query looks in the title and in the body at once.

get_note

reads

Get one note

One note by id, with its body as plain text.

Parameters

NameTypeRequiredWhat it does
note_idstring, UUIDrequiredNote UUID.

Example call

{
  "note_id": "c81b5d47-0e39-4b2a-9f64-5a7d31e8c026"
}

Answer

{
  "id": "c81b5d47-0e39-4b2a-9f64-5a7d31e8c026",
  "title": "Invoice template",
  "project_id": null,
  "is_pinned": true,
  "is_archived": false,
  "sort_order": 0,
  "created_at": "2026-06-11T08:20:15.004Z",
  "updated_at": "2026-08-20T19:41:03.772Z",
  "content_text": "Hours, rate, bank details.

Send on the first working day."
}

Worth knowing

  • The body is capped at 8000 characters, and an image pasted into it is replaced by [image].

create_note

writes

Create a note

Create a note from plain text. Blank lines become paragraph breaks in the editor.

Parameters

NameTypeRequiredWhat it does
titlestring, ≤ 300optionalNote title. Leave empty for an untitled note.
contentstring, ≤ 100000optionalBody as plain text.
project_idstring, UUIDoptionalProject the note belongs to, the folder it appears in. Leave it out only when the account has a single project; with several and none named the call is refused and comes back with the list to choose from.
is_pinnedbooleanoptionalPin the note to the top of the list.

Example call

{
  "title": "Invoice template",
  "content": "Hours, rate, bank details.

Send on the first working day.",
  "is_pinned": true
}

Answer

{
  "id": "c81b5d47-0e39-4b2a-9f64-5a7d31e8c026",
  "title": "Invoice template",
  "is_pinned": true,
  "is_archived": false,
  "sort_order": 0,
  "created_at": "2026-08-28T07:26:47.512Z",
  "updated_at": "2026-08-28T07:26:47.512Z",
  "content_text": "Hours, rate, bank details.

Send on the first working day."
}

Worth knowing

  • Plain text only. Formatting, links and images are not part of what this tool writes, though a note created here opens and edits normally in the app.
  • A note with neither a title nor a body is allowed: the app shows it as untitled.

update_note

writes

Update a note

Change a note's title, body, project, pinned or archived state.

Parameters

NameTypeRequiredWhat it does
note_idstring, UUIDrequiredNote UUID from list_notes.
titlestring, ≤ 300optionalNew title.
contentstring, ≤ 100000optionalNew body as plain text, replacing what is there.
project_idstring, UUIDoptionalMove the note to this project. It cannot be set to null: every note lives in one.
is_pinnedbooleanoptionalPin or unpin the note.
is_archivedbooleanoptionalArchive the note or bring it back.

Example call

{
  "note_id": "c81b5d47-0e39-4b2a-9f64-5a7d31e8c026",
  "content": "Hours, rate, bank details.

Send on the first working day.

New: VAT line."
}

Answer

{
  "id": "c81b5d47-0e39-4b2a-9f64-5a7d31e8c026",
  "title": "Invoice template",
  "is_pinned": true,
  "is_archived": false,
  "sort_order": 0,
  "created_at": "2026-06-11T08:20:15.004Z",
  "updated_at": "2026-08-28T07:28:19.660Z",
  "content_text": "Hours, rate, bank details.

Send on the first working day.

New: VAT line."
}

Worth knowing

  • content replaces the whole body. To add to a note, read it with get_note first and send the old text plus the new.
  • Pass at least one field besides note_id.

delete_note

removes

Delete a note

Move a note to Trash, the same soft delete the app performs.

Parameters

NameTypeRequiredWhat it does
note_idstring, UUIDrequiredNote UUID from list_notes.

Example call

{
  "note_id": "c81b5d47-0e39-4b2a-9f64-5a7d31e8c026"
}

Answer

{
  "trashed": true,
  "note": {
    "id": "c81b5d47-0e39-4b2a-9f64-5a7d31e8c026",
    "title": "Invoice template",
    "is_deleted": true
  }
}

Worth knowing

  • restore_from_trash with kind=note brings it back.

Habits

Habits and their daily check-ins, including the history a streak question is answered from.

Written from: mcp/src/tools/habits.ts

list_habits

reads

List habits

The habits being tracked, with their ids. Call it before any check-in.

Parameters

NameTypeRequiredWhat it does
include_inactiveboolean, default falseoptionalInclude paused habits. Default false.
limitinteger, 1-200, default 50optionalMaximum rows to return. Default 50, hard maximum 200.

Example call

{
  "include_inactive": false
}

Answer

{
  "count": 1,
  "habits": [
    {
      "id": "5c9a11de-7f42-4e06-8b3d-1c60a95d7f84",
      "name": "Read 20 pages",
      "icon": "book",
      "color": "#f59e0b",
      "time_of_day": "evening",
      "is_active": true,
      "sort_order": 0,
      "created_at": "2026-07-01T06:30:11.204Z",
      "updated_at": "2026-08-27T20:02:48.911Z"
    }
  ]
}

Worth knowing

  • Paused habits are hidden unless you ask for them.

get_habit_checkins

reads

Get habit check-ins

Check-in history for one habit over a date range, newest first. This is what answers streak and consistency questions.

Parameters

NameTypeRequiredWhat it does
habit_idstring, UUIDrequiredHabit UUID from list_habits.
date_fromstring, YYYY-MM-DDrequiredFirst day of the range, YYYY-MM-DD.
date_tostring, YYYY-MM-DDrequiredLast day of the range, YYYY-MM-DD, inclusive.
limitinteger, 1-200, default 50optionalMaximum rows to return. Default 50, hard maximum 200.

Example call

{
  "habit_id": "5c9a11de-7f42-4e06-8b3d-1c60a95d7f84",
  "date_from": "2026-08-01",
  "date_to": "2026-08-31"
}

Answer

{
  "habit": "Read 20 pages",
  "habit_id": "5c9a11de-7f42-4e06-8b3d-1c60a95d7f84",
  "count": 2,
  "completed_count": 1,
  "checkins": [
    {
      "id": "8f2b6c05-14ad-4e73-9c81-2b407fd6a319",
      "habit_id": "5c9a11de-7f42-4e06-8b3d-1c60a95d7f84",
      "date": "2026-08-27",
      "completed": true,
      "created_at": "2026-08-27T20:02:48.911Z"
    },
    {
      "id": "3d5e9017-6b24-4c8f-91a0-7e13c5b8d024",
      "habit_id": "5c9a11de-7f42-4e06-8b3d-1c60a95d7f84",
      "date": "2026-08-26",
      "completed": false,
      "created_at": "2026-08-26T21:44:02.180Z"
    }
  ]
}

Worth knowing

  • completed_count counts the days actually done, count counts every row in the range, misses included.
  • A day with no row at all was never touched, which is different from a day recorded as a miss.

checkin_habit

writes

Check a habit in or out

Mark a habit done, or explicitly not done, on a given day.

Parameters

NameTypeRequiredWhat it does
habit_idstring, UUIDrequiredHabit UUID from list_habits.
datestring, YYYY-MM-DDrequiredThe day being checked in, YYYY-MM-DD.
completedboolean, default trueoptionalTrue for done (default), false to record a miss.

Example call

{
  "habit_id": "5c9a11de-7f42-4e06-8b3d-1c60a95d7f84",
  "date": "2026-08-28",
  "completed": true
}

Answer

{
  "habit": "Read 20 pages",
  "checkin": {
    "id": "b04c7e18-32f5-4a69-8d70-1e6b95c3f402",
    "habit_id": "5c9a11de-7f42-4e06-8b3d-1c60a95d7f84",
    "date": "2026-08-28",
    "completed": true,
    "created_at": "2026-08-28T07:30:55.744Z"
  }
}

Worth knowing

  • Running it again for the same day overwrites that day rather than adding a second entry.
  • The date is a plain calendar date, so today has to be worked out by the caller.

create_habit

writes

Create a habit

Start tracking a habit. The answer carries the id to check in against.

Parameters

NameTypeRequiredWhat it does
namestring, 1-200requiredHabit name, for example "Read 20 pages".
iconstring, ≤ 64optionalOptional icon name.
colorstring, ≤ 32optionalHex colour, for example #6366f1.
time_of_daystring, one of morning, afternoon, evening, anytime, default anytimeoptionalWhen in the day the habit belongs. Defaults to anytime.

Example call

{
  "name": "Read 20 pages",
  "time_of_day": "evening",
  "color": "#f59e0b"
}

Answer

{
  "id": "5c9a11de-7f42-4e06-8b3d-1c60a95d7f84",
  "name": "Read 20 pages",
  "color": "#f59e0b",
  "time_of_day": "evening",
  "is_active": true,
  "sort_order": 0,
  "created_at": "2026-08-28T07:32:12.008Z",
  "updated_at": "2026-08-28T07:32:12.008Z"
}

Worth knowing

  • Without time_of_day the habit is anytime.

update_habit

writes

Update a habit

Rename a habit or change its icon, colour, time of day, or whether it is running at all.

Parameters

NameTypeRequiredWhat it does
habit_idstring, UUIDrequiredHabit UUID from list_habits.
namestring, 1-200optionalNew name.
iconstring, ≤ 64optionalIcon name.
colorstring, ≤ 32optionalHex colour.
time_of_daystring, one of morning, afternoon, evening, anytimeoptionalWhen in the day it belongs.
is_activebooleanoptionalFalse pauses the habit, true resumes it.

Example call

{
  "habit_id": "5c9a11de-7f42-4e06-8b3d-1c60a95d7f84",
  "is_active": false
}

Answer

{
  "id": "5c9a11de-7f42-4e06-8b3d-1c60a95d7f84",
  "name": "Read 20 pages",
  "color": "#f59e0b",
  "time_of_day": "evening",
  "is_active": false,
  "sort_order": 0,
  "created_at": "2026-07-01T06:30:11.204Z",
  "updated_at": "2026-08-28T07:33:40.126Z"
}

Worth knowing

  • Check-ins are left alone, including when a habit is paused.
  • Pausing with is_active=false is the way to stop tracking without losing the history.

delete_habit

removes

Delete a habit

Delete a habit and its check-in history for good.

Parameters

NameTypeRequiredWhat it does
habit_idstring, UUIDrequiredHabit UUID from list_habits.

Example call

{
  "habit_id": "5c9a11de-7f42-4e06-8b3d-1c60a95d7f84"
}

Answer

{
  "deleted": true,
  "habit_id": "5c9a11de-7f42-4e06-8b3d-1c60a95d7f84",
  "name": "Read 20 pages"
}

Worth knowing

  • Habits have no Trash. This one cannot be undone.
  • To stop tracking and keep the history, pause the habit with update_habit instead.

Trash

One set of tools over everything that can be deleted: tasks, calendar entries, notes, projects and lists.

Written from: mcp/src/tools/trash.ts

list_trash

reads

List what is in Trash

Everything deleted and still restorable: tasks, calendar entries, notes, projects and lists. Pass kinds to look at one sort only.

Parameters

NameTypeRequiredWhat it does
kindsstring[], one of task, event, note, project, listoptionalKinds to include. Defaults to all of them.
limitinteger, 1-200, default 50optionalMaximum rows in the whole answer, across every kind. Default 50, hard maximum 200.

Example call

{
  "kinds": ["task", "note"],
  "limit": 20
}

Answer

{
  "count": 2,
  "items": [
    {
      "kind": "task",
      "id": "9b2c1f04-3a7e-4c51-9d18-6f0b2a7d4e33",
      "title": "Send the invoice",
      "date": "2026-09-02",
      "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5",
      "deleted_at": "2026-08-28T07:10:55.003Z"
    },
    {
      "kind": "note",
      "id": "c81b5d47-0e39-4b2a-9f64-5a7d31e8c026",
      "title": "Invoice template",
      "project_id": null,
      "updated_at": "2026-08-28T07:29:44.512Z"
    }
  ]
}

Worth knowing

  • limit applies to each kind separately, so asking for all five kinds with limit 50 can bring back up to 250 rows.
  • Each row carries the kind it is, and that is the value restore_from_trash and delete_from_trash_forever expect.
  • Habits are not here. They have no Trash at all.

restore_from_trash

writes

Restore something from Trash

Put a deleted task, calendar entry, note, project or list back where it was.

Parameters

NameTypeRequiredWhat it does
kindstring, one of task, event, note, project, listrequiredWhat is being restored, as reported by list_trash.
idstring, UUIDrequiredUUID of the item, as reported by list_trash.

Example call

{
  "kind": "task",
  "id": "9b2c1f04-3a7e-4c51-9d18-6f0b2a7d4e33"
}

Answer

{
  "restored": true,
  "kind": "task",
  "item": {
    "id": "9b2c1f04-3a7e-4c51-9d18-6f0b2a7d4e33",
    "title": "Send the invoice",
    "date": "2026-09-02",
    "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5",
    "deleted_at": null
  }
}

Worth knowing

  • kind and id have to be the pair list_trash reported. The wrong kind for a real id finds nothing.
  • Restoring a project brings its tasks and lists back with it.

delete_from_trash_forever

removes

Delete something from Trash for good

Erase an item that is already in Trash.

Parameters

NameTypeRequiredWhat it does
kindstring, one of task, event, note, project, listrequiredWhat is being erased, as reported by list_trash.
idstring, UUIDrequiredUUID of the item, as reported by list_trash.

Example call

{
  "kind": "task",
  "id": "9b2c1f04-3a7e-4c51-9d18-6f0b2a7d4e33"
}

Answer

{
  "deleted": true,
  "kind": "task",
  "item": {
    "id": "9b2c1f04-3a7e-4c51-9d18-6f0b2a7d4e33",
    "title": "Send the invoice",
    "date": "2026-09-02",
    "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5",
    "deleted_at": "2026-08-28T07:10:55.003Z"
  }
}

Worth knowing

  • This cannot be undone.
  • It only ever touches things already deleted once. Something still in use has to be deleted normally first, which is what keeps a single call from erasing live work.

Working together

Sharing a project and putting a task on somebody. These run as you, under the same rules the Share panel obeys: Pro to invite, owner to change anything.

Written from: mcp/src/tools/sharing.ts

list_project_members

reads

List the people in a project

Everyone with access to a project, with their role and the user_id needed to change a role, remove somebody or put a task on them.

Parameters

NameTypeRequiredWhat it does
project_idstring, UUIDrequiredProject UUID from list_projects.

Example call

{
  "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5"
}

Answer

{
  "count": 2,
  "members": [
    {
      "user_id": "a3d5f712-96b4-4c08-bd21-7e60f4c9a583",
      "role": "owner",
      "name": "Nick",
      "email": "nick@example.com",
      "added_at": "2026-05-02T10:14:07.882Z"
    },
    {
      "user_id": "0d8c41b6-73e5-4f92-a107-64b2e9d35c8a",
      "role": "editor",
      "name": null,
      "email": "sam@example.com",
      "added_at": "2026-08-14T12:03:51.117Z"
    }
  ]
}

Worth knowing

  • Only projects you belong to can be read.
  • A project shared with nobody answers with an empty list and a note saying so, not an error.
  • Names and email addresses come from profiles and can be absent for somebody who never set a name. The roles are the answer either way.

invite_to_project

writes

Invite somebody by email

Send an invitation to an email address. The person gets it by mail, and inside the app as well if that address already has a ToDowl account.

Parameters

NameTypeRequiredWhat it does
project_idstring, UUIDrequiredProject UUID from list_projects.
emailstring, emailrequiredWho to invite.
rolestring, one of editor, viewer, default vieweroptional'viewer' (default) or 'editor'.

Example call

{
  "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5",
  "email": "sam@example.com",
  "role": "editor"
}

Answer

{
  "invited": "sam@example.com",
  "role": "editor",
  "status": "pending",
  "has_account": true,
  "invite_id": "f13b7a29-0c64-4e85-9d31-58a70e6c4b12"
}

Worth knowing

  • Sharing needs Pro, and only the project owner can invite. Both refusals come back as a plain sentence rather than a database error.
  • A viewer can read, an editor can change things.
  • has_account in the answer says whether that address already had a ToDowl account, which is what decides how to word the next sentence to the person.

set_project_member_role

writes

Change what somebody may do

Move a member between viewer and editor.

Parameters

NameTypeRequiredWhat it does
project_idstring, UUIDrequiredProject UUID.
user_idstring, UUIDrequiredMember user_id from list_project_members.
rolestring, one of editor, viewerrequired'editor' or 'viewer'.

Example call

{
  "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5",
  "user_id": "0d8c41b6-73e5-4f92-a107-64b2e9d35c8a",
  "role": "viewer"
}

Answer

{
  "updated": true,
  "user_id": "0d8c41b6-73e5-4f92-a107-64b2e9d35c8a",
  "role": "viewer"
}

Worth knowing

  • Only the project owner can.
  • The user_id comes from list_project_members.

remove_project_member

removes

Remove somebody from a project

Take away a person's access. The project disappears from their sidebar and they keep nothing.

Parameters

NameTypeRequiredWhat it does
project_idstring, UUIDrequiredProject UUID.
user_idstring, UUIDrequiredMember user_id from list_project_members.

Example call

{
  "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5",
  "user_id": "0d8c41b6-73e5-4f92-a107-64b2e9d35c8a"
}

Answer

{
  "removed": true,
  "user_id": "0d8c41b6-73e5-4f92-a107-64b2e9d35c8a"
}

Worth knowing

  • Only the project owner can, and the owner cannot be removed.
  • Transferring ownership is deliberately not part of the API: it is the one sharing action the person who did it cannot undo.

assign_task

writes

Put a task on somebody

Assign a task in a shared project to one of its members, or take it off them with assigned=false.

Parameters

NameTypeRequiredWhat it does
task_idstring, UUIDrequiredTask UUID.
user_idstring, UUIDrequiredMember user_id from list_project_members.
assignedboolean, default trueoptionalTrue to assign (default), false to unassign.

Example call

{
  "task_id": "9b2c1f04-3a7e-4c51-9d18-6f0b2a7d4e33",
  "user_id": "0d8c41b6-73e5-4f92-a107-64b2e9d35c8a"
}

Answer

{
  "assigned": true,
  "task_id": "9b2c1f04-3a7e-4c51-9d18-6f0b2a7d4e33",
  "user_id": "0d8c41b6-73e5-4f92-a107-64b2e9d35c8a"
}

Worth knowing

  • The person has to be in the project already. Assigning is not a way to add somebody to it.
  • The user_id comes from list_project_members.

HTTP endpoints

The plain HTTP surface of mcp.todowl.com: the MCP transport itself, the endpoints that mint and revoke keys, and the two the in-app assistant uses.

The MCP endpoint

One address, Streamable HTTP, no session kept between calls. Every request carries the token, and every request is answered as the account that token belongs to.

Written from: mcp/src/index.tsmcp/src/server.tsmcp/src/auth.ts

POST /

API token

The MCP transport. Speaks JSON-RPC 2.0: initialize, tools/list, tools/call.

Request

POST https://mcp.todowl.com/
Authorization: Bearer todowl_YOUR_KEY
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2025-06-18

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "list_tasks",
    "arguments": { "status": "open", "limit": 5 }
  }
}

Answer

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      { "type": "text", "text": "{\"count\":1,\"tasks\":[…]}" }
    ]
  }
}

Worth knowing

  • /mcp is the same endpoint under a second name, for clients that insist on a path.
  • The answer arrives as an event stream, so a client that reads it by hand has to pull the JSON out of the last data: frame.
  • A tool answer is JSON inside the text field, written as compactly as possible. Examples on this page are laid out for reading; the wire never is.
  • A tool that refuses answers with isError: true and { "error": "…" } in the text, not with an HTTP error.
  • GET and DELETE on the same path are accepted and do nothing: there is no session to stream into or to close.
  • The request body is capped at 4 MB.

GET /health

no key

Is the server up. The one endpoint that needs no key.

Request

curl https://mcp.todowl.com/health

Answer

{
  "ok": true,
  "service": "todowl",
  "version": "0.1.0"
}

Personal tokens

Minting and revoking keys. These take a signed-in session rather than an API token: you have to already be in the account to hand out a key to it. This is what the settings page and todowl auth login call.

Written from: mcp/src/index.tsmcp/src/tokens.ts

POST /auth/token

signed-in session

Mint a personal API token. Answers 201 and shows the token once.

Body

NameTypeRequiredWhat it does
namestring, ≤ 120, default API tokenoptionalWhat to call the key, so you can tell it apart later. Longer names are cut to 120 characters.

Request

POST https://mcp.todowl.com/auth/token
Authorization: Bearer <session access token>
Content-Type: application/json

{ "name": "Claude on the laptop" }

Answer

{
  "token": "todowl_QY2fJ8sK…",
  "warning": "This is the only time the token is shown. Store it now; it cannot be retrieved again.",
  "id": "d4a91b76-30c8-4f52-9e17-6b03a5c2d84f",
  "name": "Claude on the laptop",
  "prefix": "todowl_QY2fJ8",
  "scopes": [],
  "created_at": "2026-08-28T07:40:11.284Z",
  "last_used_at": null,
  "revoked_at": null
}

Worth knowing

  • The token is shown here and nowhere else. Only its SHA-256 hash is stored, so nobody, us included, can read it back to you.
  • A token is todowl_ plus 43 letters and digits. Anything else is refused before the database is even asked.
  • Scopes are not a thing yet. A key carries the same access to the account as you have in the browser.

GET /auth/tokens

signed-in session

Every key of the account, newest first, up to a hundred.

Answer

{
  "tokens": [
    {
      "id": "d4a91b76-30c8-4f52-9e17-6b03a5c2d84f",
      "name": "Claude on the laptop",
      "prefix": "todowl_QY2fJ8",
      "scopes": [],
      "created_at": "2026-08-28T07:40:11.284Z",
      "last_used_at": "2026-08-28T09:12:03.660Z",
      "revoked_at": null
    }
  ]
}

Worth knowing

  • Revoked keys stay in the list with revoked_at filled in, so the history of what existed is not lost.
  • last_used_at is stamped at most once a minute per key, so a chatty client does not turn every tool call into an extra write.

DELETE /auth/tokens/:id

signed-in session

Revoke a key. It stops working immediately.

In the path

NameTypeRequiredWhat it does
idstring, UUIDrequiredId of the key, as listed by GET /auth/tokens.

Answer

{
  "revoked": true,
  "id": "d4a91b76-30c8-4f52-9e17-6b03a5c2d84f",
  "name": "Claude on the laptop",
  "prefix": "todowl_QY2fJ8",
  "scopes": [],
  "created_at": "2026-08-28T07:40:11.284Z",
  "last_used_at": "2026-08-28T09:12:03.660Z",
  "revoked_at": "2026-08-28T10:02:47.915Z"
}

Worth knowing

  • A key that is already revoked, or belongs to another account, answers 404. Nothing about the keys of another account leaks either way.
  • Revoking touches no data. Clients using that key start getting 401 mid-conversation.

The in-app assistant

What the assistant inside app.todowl.com talks to. Listed for completeness: it takes a browser session, not an API token, so it is not a way to build your own client.

Written from: mcp/src/index.tsmcp/src/assistant.ts

POST /assistant/chat

signed-in session

One turn of the conversation. The model picks tools, they run against your account, and the answer comes back with the receipts.

Body

NameTypeRequiredWhat it does
messagesobject[]requiredThe conversation so far, in OpenAI message shape. At least one message.
timezonestringoptionalIANA zone from the browser, so that tomorrow means tomorrow where the reader is.
localestringoptionalLanguage to answer in.
approvebooleanoptionalTrue when the person has just said yes to a deletion the model was holding.
resumebooleanoptionalTrue when the app is carrying on unfinished work rather than a person saying something new.

Answer

{
  "reply": "Added it for Tuesday.",
  "messages": [],
  "actions": [{ "tool": "create_task", "ok": true }],
  "cards": [
    {
      "kind": "task",
      "action": "created",
      "id": "9b2c1f04-3a7e-4c51-9d18-6f0b2a7d4e33",
      "title": "Send the invoice",
      "date": "2026-09-02"
    }
  ],
  "usage": { "promptTokens": 1840, "completionTokens": 96, "costUsd": 0.000412 },
  "model": "deepseek/deepseek-v4-flash"
}

Worth knowing

  • A turn that wants to delete something stops and comes back with pending instead of doing it. The app asks, and sends the same request again with approve.
  • continues: true means the turn ran out of steps rather than finished. The transcript goes straight back with resume and the work carries on.
  • 60 messages per account per hour, counted in memory, so a restart clears the count. Over that it is 429.
  • Without an assistant key configured the endpoint answers 503 and the rest of the server carries on as normal.

POST /assistant/title

signed-in session

A short name for a conversation, so the history list reads like something.

Body

NameTypeRequiredWhat it does
messagesobject[]requiredThe conversation to name. At least one message.
localestringoptionalLanguage of the title.

Answer

{
  "title": "Invoice for August",
  "subtitle": "Task added for Tuesday"
}

Worth knowing

  • Same gates as a turn: a session, the assistant enabled, and the hourly ceiling. It is the same money.

Command line

The todowl tool, command by command. Every one of them is a call to the tools above, which is why each block says which.

The tool itself

Where the token lives, how ids are shortened, and the options that work everywhere.

Written from: cli/src/index.tscli/src/config.tscli/README.md

todowl --help

The full command list, the same one this page is written from.

Options

NameTypeRequiredWhat it does
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.
--versionbooleanoptionalPrint the version and stop.
--helpbooleanoptionalSame as running the tool with no arguments.

Example call

todowl --help

Worth knowing

  • -h and -v are the short forms.
  • Every option has to be one the tool knows. An unknown option stops the command instead of being quietly swallowed.
  • The id can be shortened to the first characters todowl … list printed, six at the least. A prefix that matches more than one thing is an error, never a guess.

Signing in

Sign in once per machine. The token is kept in ~/.config/todowl/config.json, readable only by you, deliberately a file rather than an environment variable.

Written from: cli/src/commands/auth.tscli/src/config.ts

todowl auth login

Sign in and store a fresh personal token on this machine.

Options

NameTypeRequiredWhat it does
--provider <google|apple>string, one of google, apple, default googleoptionalWhich button to press in the browser.
--email <address>stringoptionalAsk for a password in the terminal instead of opening a browser. Useful over SSH.
--no-browserbooleanoptionalPrint the sign-in URL instead of opening it.
--name <name>stringoptionalWhat to call the token that gets created. Defaults to the name of this machine.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl auth login --provider apple

Worth knowing

  • The browser hands the session back on a loopback port the CLI opens for the length of the sign-in and closes straight after.
  • The session is exchanged for a long-lived token through POST /auth/token. It is the token, not the session, that is stored.
  • Five minutes with no answer from the browser and the command gives up.

todowl auth token <TOKEN>

Use a token you already have, instead of signing in. This is the one for scripts and CI.

Example call

todowl auth token todowl_QY2fJ8sK…

Worth knowing

  • Without the argument the tool asks for the token on the terminal.
  • The token is tried against the server before you are told it worked. A rejected token is not kept.

todowl auth status

Who this machine is signed in as, and whether the stored token still works.

Options

NameTypeRequiredWhat it does
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl auth status --json

Worth knowing

  • Shows the prefix of the token, never the token. It ends with a non-zero exit code when the token has stopped working.
  • todowl auth with nothing after it does the same thing.

todowl auth logout

Forget the token stored on this machine.

Example call

todowl auth logout

Worth knowing

  • It deletes the local file only. The token itself stays valid on the server, so revoke it in settings if it may have leaked.

Projects and lists

The structure tasks are filed under.

Written from: cli/src/commands/structure.tscli/src/commands/tasks.ts

todowl project list

Calls list_projects

Projects, with their ids and colours.

Also spelled: todowl projectstodowl project

Options

NameTypeRequiredWhat it does
--allbooleanoptionalInclude what is normally hidden: archived rows, and paused habits.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl project list --all

todowl project create --name "Work"

Calls create_project

Create a project.

Also spelled: todowl project add

Options

NameTypeRequiredWhat it does
--name <name>stringrequiredProject name.
--description <text>stringoptionalOne line about the project.
--color <hex>stringoptionalColour, for example #6366f1.
--icon <name>stringoptionalIcon name.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl project create --name "Work" --color "#6366f1" --icon briefcase

todowl project update <id>

Calls update_project

Rename a project or change its colour, icon, description or archived state.

Also spelled: todowl project edit

Options

NameTypeRequiredWhat it does
--name <name>stringoptionalNew name.
--description <text>stringoptionalNew description.
--color <hex>stringoptionalNew colour.
--icon <name>stringoptionalNew icon.
--archivebooleanoptionalMove it to the archive.
--unarchivebooleanoptionalBring it back out of the archive.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl project update 1f6d0b72 --name "Work 2026"

Worth knowing

  • At least one thing to change is required, otherwise the command says so and stops.
  • The id can be shortened to the first characters todowl … list printed, six at the least. A prefix that matches more than one thing is an error, never a guess.

todowl project delete <id>

Calls delete_project

Move a project to Trash.

Also spelled: todowl project rm

Example call

todowl project delete 1f6d0b72

Worth knowing

  • todowl trash restore --kind project <id> brings it back.

todowl list list

Calls list_lists

Lists, optionally only those of one project.

Also spelled: todowl liststodowl list

Options

NameTypeRequiredWhat it does
--project <id>stringoptionalProject, by id or by the first characters of it.
--allbooleanoptionalInclude what is normally hidden: archived rows, and paused habits.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl list list --project 1f6d0b72

todowl list create --name "Backlog"

Calls create_list

Create a list, inside a project or on its own.

Also spelled: todowl list add

Options

NameTypeRequiredWhat it does
--name <name>stringrequiredList name.
--project <id>stringoptionalProject, by id or by the first characters of it.
--icon <name>stringoptionalIcon name.
--color <hex>stringoptionalHex colour, for example #6366f1.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl list create --name "Backlog" --project 1f6d0b72

todowl list update <id>

Calls update_list

Rename a list, change its icon, move it to another project, or archive it.

Also spelled: todowl list edit

Options

NameTypeRequiredWhat it does
--name <name>stringoptionalNew name.
--icon <name>stringoptionalNew icon.
--color <hex>stringoptionalHex colour, for example #6366f1.
--project <id>stringoptionalProject, by id or by the first characters of it.
--archivebooleanoptionalMove it to the archive.
--unarchivebooleanoptionalBring it back out of the archive.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl list update 7a4e9c31 --name "Later"

todowl list delete <id>

Calls delete_list

Move a list to Trash. Its tasks stay in the project.

Also spelled: todowl list rm

Example call

todowl list delete 7a4e9c31

Tasks

The everyday half of the tool.

Written from: cli/src/commands/tasks.ts

todowl task list

Calls list_tasks

Tasks, filtered the same way the app filters them.

Also spelled: todowl taskstodowl task

Options

NameTypeRequiredWhat it does
--status <open|completed|all>string, one of open, completed, all, default openoptionalWhich tasks to include.
--project <id>stringoptionalProject, by id or by the first characters of it.
--list <id>stringoptionalList, by id or by the first characters of it.
--date <YYYY-MM-DD>string, YYYY-MM-DDoptionalOnly that day.
--from <YYYY-MM-DD>string, YYYY-MM-DDoptionalFrom that day on.
--to <YYYY-MM-DD>string, YYYY-MM-DDoptionalUp to and including that day.
--priority <high|medium|low|none>string, one of high, medium, low, noneoptionalOnly that priority.
--no-datebooleanoptionalOnly tasks with no date at all, the inbox.
--limit <n>integer, 1-200, default 50optionalHow many rows.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl task list --status open --from 2026-09-01 --to 2026-09-07

Worth knowing

  • The table shows the first eight characters of each id. --json prints them whole.

todowl task create --title "Call the bank"

Calls create_task

Create a task.

Also spelled: todowl task add

Options

NameTypeRequiredWhat it does
--title <text>stringrequiredWhat the task is called.
--description <text>stringoptionalLonger notes.
--project <id>stringoptionalProject, by id or by the first characters of it.
--list <id>stringoptionalList, by id or by the first characters of it.
--date <YYYY-MM-DD>string, YYYY-MM-DDoptionalDue date. Without it the task goes to the inbox.
--time <HH:MM>string, HH:MMoptionalStart time.
--priority <high|medium|low|none>string, one of high, medium, low, noneoptionalPriority.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl task create --title "Call the bank" --date 2026-09-01 --priority high

todowl task update <id>

Calls update_task

Change a task that already exists.

Also spelled: todowl task edit

Options

NameTypeRequiredWhat it does
--title <text>stringoptionalNew title.
--description <text>stringoptionalNew notes.
--date <YYYY-MM-DD>string, YYYY-MM-DDoptionalNew date.
--time <HH:MM>string, HH:MMoptionalNew start time.
--priority <high|medium|low|none>string, one of high, medium, low, noneoptionalNew priority.
--archivebooleanoptionalMove it to the archive.
--unarchivebooleanoptionalBring it back out of the archive.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl task update a1b2c3d4 --date 2026-09-02 --priority medium

todowl task move <id>

Calls move_task

Re-file a task into another project or list.

Options

NameTypeRequiredWhat it does
--project <id>stringoptionalProject, by id or by the first characters of it.
--list <id>stringoptionalList, by id or by the first characters of it.
--no-listbooleanoptionalDetach it from its list, keeping its project.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl task move a1b2c3d4 --project 7f3e1c02

Worth knowing

  • Name at least one destination, otherwise the command asks where to.
  • There is no way to leave a task without a project: such a task appears on no screen in the app. Move it to another project instead.

todowl task complete <id>

Calls complete_task

Tick a task off, or reopen it.

Also spelled: todowl task done

Options

NameTypeRequiredWhat it does
--reopenbooleanoptionalOpen the task again instead.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl task complete a1b2c3d4

todowl task delete <id>

Calls delete_task

Move a task to Trash.

Also spelled: todowl task rm

Example call

todowl task delete a1b2c3d4

Calendar

Meetings and events from a terminal.

Written from: cli/src/commands/content.ts

todowl event list

Calls list_events

What is on the calendar. Today, unless you say otherwise.

Also spelled: todowl eventstodowl event

Options

NameTypeRequiredWhat it does
--date <YYYY-MM-DD>string, YYYY-MM-DDoptionalOne day.
--from <YYYY-MM-DD>string, YYYY-MM-DDoptionalFirst day of the range.
--to <YYYY-MM-DD>string, YYYY-MM-DDoptionalLast day of the range.
--tasksbooleanoptionalInclude dated tasks, not only meetings and events.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl event list --from 2026-09-01 --to 2026-09-07

Worth knowing

  • Without --tasks the command asks for meetings and events only, which is the opposite of what the tool does on its own.

todowl event create --title "Standup" --date 2026-09-01

Calls create_event

Put a meeting or an event on the calendar.

Also spelled: todowl event add

Options

NameTypeRequiredWhat it does
--title <text>stringrequiredWhat the entry is called.
--date <YYYY-MM-DD>string, YYYY-MM-DDrequiredThe day it happens.
--time <HH:MM>string, HH:MMoptionalStart time.
--end <HH:MM>string, HH:MMoptionalEnd time.
--all-daybooleanoptionalAn entry that takes the whole day.
--type <meeting|event>string, one of meeting, event, default eventoptionalKind of entry.
--description <text>stringoptionalLonger notes.
--project <id>stringoptionalProject, by id or by the first characters of it.
--color <hex>stringoptionalColour override.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl event create --title "Standup" --date 2026-09-01 --time 11:00 --end 11:30 --type meeting

todowl event update <id>

Calls update_event

Reschedule or edit a calendar entry.

Also spelled: todowl event edit

Options

NameTypeRequiredWhat it does
--title <text>stringoptionalNew title.
--date <YYYY-MM-DD>string, YYYY-MM-DDoptionalNew day.
--time <HH:MM>string, HH:MMoptionalNew start time.
--end <HH:MM>string, HH:MMoptionalNew end time.
--all-daybooleanoptionalMake it an all-day entry.
--description <text>stringoptionalNew notes.
--project <id>stringoptionalProject, by id or by the first characters of it.
--color <hex>stringoptionalColour override.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl event update e2f70a95 --date 2026-09-03 --time 12:00

Worth knowing

  • The entry is looked up over a year either side of today, so you do not have to say which week it is in. --from and --to widen or narrow that window.

todowl event delete <id>

Calls delete_event

Move a calendar entry to Trash.

Also spelled: todowl event rm

Example call

todowl event delete e2f70a95

Notes

Plain text in, plain text out.

Written from: cli/src/commands/content.ts

todowl note list

Calls list_notes

Notes, pinned ones first.

Also spelled: todowl notestodowl note

Options

NameTypeRequiredWhat it does
--query <text>stringoptionalSearch titles and bodies.
--allbooleanoptionalInclude what is normally hidden: archived rows, and paused habits.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl note list --query invoice

todowl note get <id>

Calls get_note

Print one note, body and all.

Also spelled: todowl note show

Options

NameTypeRequiredWhat it does
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl note get c81b5d47

todowl note create

Calls create_note

Create a note from plain text.

Also spelled: todowl note add

Options

NameTypeRequiredWhat it does
--title <text>stringoptionalTitle. A note without one is untitled, which is allowed.
--content <text>stringoptionalBody.
--project <id>stringoptionalProject, by id or by the first characters of it.
--pinbooleanoptionalPin it to the top of the list.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl note create --title "Invoice template" --content "Hours, rate, bank details."

todowl note update <id>

Calls update_note

Change a note.

Also spelled: todowl note edit

Options

NameTypeRequiredWhat it does
--title <text>stringoptionalNew title.
--content <text>stringoptionalNew body. It replaces the old one whole.
--project <id>stringoptionalProject, by id or by the first characters of it.
--pinbooleanoptionalPin it.
--unpinbooleanoptionalUnpin it.
--archivebooleanoptionalMove it to the archive.
--unarchivebooleanoptionalBring it back out of the archive.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl note update c81b5d47 --pin

todowl note delete <id>

Calls delete_note

Move a note to Trash.

Also spelled: todowl note rm

Example call

todowl note delete c81b5d47

Habits

Check in, look at a month, pause one.

Written from: cli/src/commands/content.ts

todowl habit list

Calls list_habits

Habits, with when in the day each one belongs.

Also spelled: todowl habitstodowl habit

Options

NameTypeRequiredWhat it does
--allbooleanoptionalInclude what is normally hidden: archived rows, and paused habits.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl habit list --all

todowl habit create --name "Read 20 pages"

Calls create_habit

Start tracking a habit.

Also spelled: todowl habit add

Options

NameTypeRequiredWhat it does
--name <text>stringrequiredWhat the habit is called.
--when <morning|afternoon|evening|anytime>string, one of morning, afternoon, evening, anytime, default anytimeoptionalWhen in the day it belongs.
--icon <name>stringoptionalIcon name.
--color <hex>stringoptionalColour.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl habit create --name "Read 20 pages" --when evening

todowl habit update <id>

Calls update_habit

Rename a habit, restyle it, or pause it.

Also spelled: todowl habit edit

Options

NameTypeRequiredWhat it does
--name <text>stringoptionalNew name.
--icon <name>stringoptionalNew icon.
--color <hex>stringoptionalNew colour.
--when <morning|afternoon|evening|anytime>string, one of morning, afternoon, evening, anytimeoptionalWhen in the day it belongs.
--pausebooleanoptionalStop tracking it, history kept.
--resumebooleanoptionalStart tracking it again.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl habit update 5c9a11de --pause

todowl habit checkin <id>

Calls checkin_habit

Mark a habit done for a day, or record a miss.

Also spelled: todowl habit check

Options

NameTypeRequiredWhat it does
--date <YYYY-MM-DD>string, YYYY-MM-DDoptionalWhich day. Today, if left out.
--missbooleanoptionalRecord the day as not done.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl habit checkin 5c9a11de

todowl habit log <id>

Calls get_habit_checkins

The check-in history of one habit, day by day.

Options

NameTypeRequiredWhat it does
--from <YYYY-MM-DD>string, YYYY-MM-DDoptionalFirst day. The first of this month, if left out.
--to <YYYY-MM-DD>string, YYYY-MM-DDoptionalLast day. Today, if left out.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl habit log 5c9a11de --from 2026-08-01 --to 2026-08-31

todowl habit delete <id>

Calls delete_habit

Delete a habit and its history for good.

Also spelled: todowl habit rm

Example call

todowl habit delete 5c9a11de

Worth knowing

  • Habits have no Trash. Pausing with --pause is the reversible option.

Trash

Everything deleted, and the two ways out of it.

Written from: cli/src/commands/content.ts

todowl trash list

Calls list_trash

What is in Trash and can still be restored.

Also spelled: todowl trash

Options

NameTypeRequiredWhat it does
--kind <task|event|note|project|list>string, one of task, event, note, project, listoptionalOnly one sort of thing.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl trash list --kind task

todowl trash restore --kind <kind> <id>

Calls restore_from_trash

Put something back where it was.

Options

NameTypeRequiredWhat it does
--kind <task|event|note|project|list>string, one of task, event, note, project, listrequiredWhat is being restored.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl trash restore --kind task 9b2c1f04

todowl trash delete --kind <kind> <id>

Calls delete_from_trash_forever

Erase one item for good.

Also spelled: todowl trash rm

Options

NameTypeRequiredWhat it does
--kind <task|event|note|project|list>string, one of task, event, note, project, listrequiredWhat is being erased.
--jsonbooleanoptionalPrint one machine-readable object and nothing else. This is what makes the tool usable inside a script.

Example call

todowl trash delete --kind task 9b2c1f04

Worth knowing

  • This one cannot be undone.

A rule this page lives by

A new tool, endpoint or command without a block on this page counts as unfinished work. A check in the repository fails when the catalogue and the server disagree, so the page cannot quietly go stale.