# 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.

https://mcp.todowl.com/
https://help.todowl.com/api/

## 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.

```bash
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 |

## 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)

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:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `status` | string, one of open, completed, all, default open | optional | Which tasks to include. 'open' (the default) hides finished ones, 'all' includes both. |
| `date` | string, YYYY-MM-DD | optional | Only tasks due on exactly this date. |
| `date_from` | string, YYYY-MM-DD | optional | Only tasks due on or after this date. |
| `date_to` | string, YYYY-MM-DD | optional | Only tasks due on or before this date. |
| `no_date` | boolean | optional | True to return only undated tasks (the inbox). Ignores the date filters. |
| `project_id` | string, UUID | optional | Only tasks in this project. |
| `list_id` | string, UUID | optional | Only tasks in this list. |
| `priority` | string, one of high, medium, low, none | optional | Only tasks at this priority. |
| `include_archived` | boolean, default false | optional | Include archived tasks. Default false. |
| `limit` | integer, 1-200, default 50 | optional | Maximum rows to return. Default 50, hard maximum 200. |
| `offset` | integer | optional | Rows to skip, for paging through large results. |

Example call:

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

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `query` | string, 1-200 | required | Text to look for in the title or description. |
| `status` | string, one of open, completed, all, default open | optional | Which tasks to include. 'open' (the default) hides finished ones, 'all' includes both. |
| `include_archived` | boolean, default false | optional | Include archived tasks. Default false. |
| `limit` | integer, 1-200, default 50 | optional | Maximum rows to return. Default 50, hard maximum 200. |

Example call:

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

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `task_id` | string, UUID | required | Task UUID. |

Example call:

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

Answer:

```json
{
  "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 one task in a project. A task with no date goes to the inbox, but never with no project.

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `title` | string, 1-500 | required | Task title, the one line shown in the list. |
| `description` | string, ≤ 20000 | optional | Longer free-form notes attached to the task. Pass null to clear it. |
| `date` | string, YYYY-MM-DD | optional | Due date, YYYY-MM-DD. Omit for a task with no date (it lands in the inbox). Pass null to clear it. |
| `start_time` | string, HH:MM | optional | Start time, HH:MM, 24-hour, in the user local timezone. Pass null to clear it. |
| `end_time` | string, HH:MM | optional | End time, HH:MM, 24-hour. Pass null to clear it. |
| `all_day` | boolean | optional | True when the task occupies the whole day rather than a time slot. |
| `priority` | string, one of high, medium, low, none | optional | Priority: 'high', 'medium', 'low' or 'none'. Pass null to clear it. |
| `project_id` | string, UUID | optional | Project 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_id` | string, UUID | optional | List UUID from list_lists. Pass null to clear it. |
| `reminder` | integer, 0-10080 | optional | Reminder, in minutes before the start time. Pass null to clear it. |
| `color` | string, ≤ 32 | optional | Hex colour override, for example #6366f1. Pass null to clear it. |

Example call:

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

Answer:

```json
{
  "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)

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:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `tasks` | object[], 1-20 | required | The tasks to create, same fields as create_task. |

Fields of each item `tasks`:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `title` | string, 1-500 | required | Task title, the one line shown in the list. |
| `description` | string, ≤ 20000 | optional | Longer free-form notes attached to the task. Pass null to clear it. |
| `date` | string, YYYY-MM-DD | optional | Due date, YYYY-MM-DD. Omit for a task with no date (it lands in the inbox). Pass null to clear it. |
| `start_time` | string, HH:MM | optional | Start time, HH:MM, 24-hour, in the user local timezone. Pass null to clear it. |
| `end_time` | string, HH:MM | optional | End time, HH:MM, 24-hour. Pass null to clear it. |
| `all_day` | boolean | optional | True when the task occupies the whole day rather than a time slot. |
| `priority` | string, one of high, medium, low, none | optional | Priority: 'high', 'medium', 'low' or 'none'. Pass null to clear it. |
| `project_id` | string, UUID | optional | Project 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_id` | string, UUID | optional | List UUID from list_lists. Pass null to clear it. |
| `reminder` | integer, 0-10080 | optional | Reminder, in minutes before the start time. Pass null to clear it. |
| `color` | string, ≤ 32 | optional | Hex colour override, for example #6366f1. Pass null to clear it. |

Example call:

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

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `task_id` | string, UUID | required | Task UUID. |
| `title` | string, 1-500 | optional | Task title, the one line shown in the list. |
| `description` | string, ≤ 20000 | optional | Longer free-form notes attached to the task. Pass null to clear it. |
| `date` | string, YYYY-MM-DD | optional | Due date, YYYY-MM-DD. Omit for a task with no date (it lands in the inbox). Pass null to clear it. |
| `start_time` | string, HH:MM | optional | Start time, HH:MM, 24-hour, in the user local timezone. Pass null to clear it. |
| `end_time` | string, HH:MM | optional | End time, HH:MM, 24-hour. Pass null to clear it. |
| `all_day` | boolean | optional | True when the task occupies the whole day rather than a time slot. |
| `priority` | string, one of high, medium, low, none | optional | Priority: 'high', 'medium', 'low' or 'none'. Pass null to clear it. |
| `project_id` | string, UUID | optional | Move the task to this project. It cannot be set to null: every task belongs to a project. |
| `list_id` | string, UUID | optional | List UUID from list_lists. Pass null to clear it. |
| `reminder` | integer, 0-10080 | optional | Reminder, in minutes before the start time. Pass null to clear it. |
| `color` | string, ≤ 32 | optional | Hex colour override, for example #6366f1. Pass null to clear it. |
| `is_archived` | boolean | optional | True to archive the task (out of the lists, still searchable), false to bring it back. |

Example call:

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

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `task_id` | string, UUID | required | Task UUID. |
| `completed` | boolean, default true | optional | True to complete (default), false to reopen. |

Example call:

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

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `task_id` | string, UUID | required | Task UUID. |
| `project_id` | string, UUID | optional | Destination project UUID. |
| `list_id` | string, UUID | optional | Destination list UUID. |
| `clear_list` | boolean | optional | True to detach the task from its list, keeping it in its project. |

Example call:

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

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `task_id` | string, UUID | required | Task UUID. |

Example call:

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

Answer:

```json
{
  "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)

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:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `date_from` | string, YYYY-MM-DD | required | First day of the range, YYYY-MM-DD. |
| `date_to` | string, YYYY-MM-DD | required | Last day of the range, YYYY-MM-DD, inclusive. |
| `types` | string[], one of task, meeting, event | optional | Entry kinds to include. Defaults to all three. |
| `include_archived` | boolean, default false | optional | Include archived entries. Default false. |
| `include_completed` | boolean, default true | optional | Include entries already ticked off. Default true. |
| `limit` | integer, 1-200, default 50 | optional | Maximum rows to return. Default 50, hard maximum 200. |

Example call:

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

Answer:

```json
{
  "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)

Put a meeting or an event on the calendar.

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `title` | string, 1-500 | required | What the entry is called. |
| `date` | string, YYYY-MM-DD | required | The day it happens, YYYY-MM-DD. |
| `start_time` | string, HH:MM | optional | Start time, HH:MM, 24-hour, local to the user. |
| `end_time` | string, HH:MM | optional | End time, HH:MM, 24-hour. |
| `all_day` | boolean | optional | True for an entry that takes the whole day. |
| `type` | string, one of meeting, event, default event | optional | Kind of entry. Defaults to 'event'. |
| `description` | string, ≤ 20000 | optional | Longer free-form notes. |
| `project_id` | string, UUID | optional | Project 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. |
| `color` | string, ≤ 32 | optional | Hex colour override, for example #6366f1. |
| `reminder` | integer, 0-10080 | optional | Reminder, in minutes before the start time. |

Example call:

```json
{
  "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:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `event_id` | string, UUID | required | Entry UUID from list_events. |
| `title` | string, 1-500 | optional | New title. |
| `date` | string, YYYY-MM-DD | optional | New day, YYYY-MM-DD. |
| `start_time` | string, HH:MM | optional | New start time, HH:MM. Pass null to clear it. |
| `end_time` | string, HH:MM | optional | New end time, HH:MM. Pass null to clear it. |
| `all_day` | boolean | optional | True to make it an all-day entry. |
| `description` | string, ≤ 20000 | optional | New notes. Pass null to clear it. |
| `project_id` | string, UUID | optional | Move the entry to this project. It cannot be set to null: every entry sits on some calendar. |
| `color` | string, ≤ 32 | optional | Hex colour override. Pass null to clear it. |
| `reminder` | integer, 0-10080 | optional | Reminder, in minutes before the start. Pass null to clear it. |

Example call:

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

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `event_id` | string, UUID | required | Entry UUID from list_events. |

Example call:

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

Answer:

```json
{
  "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)

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:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `include_archived` | boolean, default false | optional | Include archived projects. Default false. |
| `limit` | integer, 1-200, default 50 | optional | Maximum rows to return. Default 50, hard maximum 200. |

Example call:

```json
{
  "include_archived": false
}
```

Answer:

```json
{
  "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. The answer carries the id to file tasks into.

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `name` | string, 1-200 | required | Project name. |
| `description` | string, ≤ 2000 | optional | Optional one-liner about the project. |
| `color` | string, ≤ 32 | optional | Hex colour, for example #6366f1. Defaults to indigo. |
| `icon` | string, ≤ 64 | optional | Optional icon name. |

Example call:

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

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `project_id` | string, UUID | required | Project UUID from list_projects. |
| `name` | string, 1-200 | optional | New project name. |
| `description` | string, ≤ 2000 | optional | One-liner about the project. |
| `color` | string, ≤ 32 | optional | Hex colour, for example #6366f1. |
| `icon` | string, ≤ 64 | optional | Icon name. |
| `is_archived` | boolean | optional | True to archive the project, false to bring it back. |

Example call:

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

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `project_id` | string, UUID | required | Project UUID from list_projects. |

Example call:

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

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `project_id` | string, UUID | optional | Only lists belonging to this project. |
| `include_archived` | boolean, default false | optional | Include archived lists. Default false. |
| `limit` | integer, 1-200, default 50 | optional | Maximum rows to return. Default 50, hard maximum 200. |

Example call:

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

Answer:

```json
{
  "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, inside a project or on its own.

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `name` | string, 1-200 | required | List name. |
| `project_id` | string, UUID | optional | Project to create the list in. |
| `icon` | string, ≤ 64 | optional | Optional icon name. |
| `color` | string, ≤ 32 | optional | Hex colour, for example #6366f1. |

Example call:

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

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `list_id` | string, UUID | required | List UUID from list_lists. |
| `name` | string, 1-200 | optional | New list name. |
| `icon` | string, ≤ 64 | optional | Icon name. |
| `color` | string, ≤ 32 | optional | Hex colour, for example #6366f1. |
| `project_id` | string, UUID | optional | Move the list into this project. |
| `is_archived` | boolean | optional | True to archive the list, false to bring it back. |

Example call:

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

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `list_id` | string, UUID | required | List UUID from list_lists. |

Example call:

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

Answer:

```json
{
  "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)

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:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `query` | string, 1-200 | optional | Case-insensitive substring to look for in the title or body. |
| `include_archived` | boolean, default false | optional | Include archived notes. Default false. |
| `limit` | integer, 1-200, default 50 | optional | Maximum rows to return. Default 50, hard maximum 200. |

Example call:

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

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `note_id` | string, UUID | required | Note UUID. |

Example call:

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

Answer:

```json
{
  "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 from plain text. Blank lines become paragraph breaks in the editor.

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `title` | string, ≤ 300 | optional | Note title. Leave empty for an untitled note. |
| `content` | string, ≤ 100000 | optional | Body as plain text. |
| `project_id` | string, UUID | optional | Project 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_pinned` | boolean | optional | Pin the note to the top of the list. |

Example call:

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

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

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `note_id` | string, UUID | required | Note UUID from list_notes. |
| `title` | string, ≤ 300 | optional | New title. |
| `content` | string, ≤ 100000 | optional | New body as plain text, replacing what is there. |
| `project_id` | string, UUID | optional | Move the note to this project. It cannot be set to null: every note lives in one. |
| `is_pinned` | boolean | optional | Pin or unpin the note. |
| `is_archived` | boolean | optional | Archive the note or bring it back. |

Example call:

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

Send on the first working day.

New: VAT line."
}
```

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `note_id` | string, UUID | required | Note UUID from list_notes. |

Example call:

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

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `include_inactive` | boolean, default false | optional | Include paused habits. Default false. |
| `limit` | integer, 1-200, default 50 | optional | Maximum rows to return. Default 50, hard maximum 200. |

Example call:

```json
{
  "include_inactive": false
}
```

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `habit_id` | string, UUID | required | Habit UUID from list_habits. |
| `date_from` | string, YYYY-MM-DD | required | First day of the range, YYYY-MM-DD. |
| `date_to` | string, YYYY-MM-DD | required | Last day of the range, YYYY-MM-DD, inclusive. |
| `limit` | integer, 1-200, default 50 | optional | Maximum rows to return. Default 50, hard maximum 200. |

Example call:

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

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `habit_id` | string, UUID | required | Habit UUID from list_habits. |
| `date` | string, YYYY-MM-DD | required | The day being checked in, YYYY-MM-DD. |
| `completed` | boolean, default true | optional | True for done (default), false to record a miss. |

Example call:

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

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `name` | string, 1-200 | required | Habit name, for example "Read 20 pages". |
| `icon` | string, ≤ 64 | optional | Optional icon name. |
| `color` | string, ≤ 32 | optional | Hex colour, for example #6366f1. |
| `time_of_day` | string, one of morning, afternoon, evening, anytime, default anytime | optional | When in the day the habit belongs. Defaults to anytime. |

Example call:

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

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `habit_id` | string, UUID | required | Habit UUID from list_habits. |
| `name` | string, 1-200 | optional | New name. |
| `icon` | string, ≤ 64 | optional | Icon name. |
| `color` | string, ≤ 32 | optional | Hex colour. |
| `time_of_day` | string, one of morning, afternoon, evening, anytime | optional | When in the day it belongs. |
| `is_active` | boolean | optional | False pauses the habit, true resumes it. |

Example call:

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

Answer:

```json
{
  "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 and its check-in history for good.

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `habit_id` | string, UUID | required | Habit UUID from list_habits. |

Example call:

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

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `kinds` | string[], one of task, event, note, project, list | optional | Kinds to include. Defaults to all of them. |
| `limit` | integer, 1-200, default 50 | optional | Maximum rows in the whole answer, across every kind. Default 50, hard maximum 200. |

Example call:

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

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `kind` | string, one of task, event, note, project, list | required | What is being restored, as reported by list_trash. |
| `id` | string, UUID | required | UUID of the item, as reported by list_trash. |

Example call:

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

Answer:

```json
{
  "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)

Erase an item that is already in Trash.

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `kind` | string, one of task, event, note, project, list | required | What is being erased, as reported by list_trash. |
| `id` | string, UUID | required | UUID of the item, as reported by list_trash. |

Example call:

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

Answer:

```json
{
  "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)

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:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `project_id` | string, UUID | required | Project UUID from list_projects. |

Example call:

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

Answer:

```json
{
  "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)

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:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `project_id` | string, UUID | required | Project UUID from list_projects. |
| `email` | string, email | required | Who to invite. |
| `role` | string, one of editor, viewer, default viewer | optional | 'viewer' (default) or 'editor'. |

Example call:

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

Answer:

```json
{
  "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.

#### `get_project_share_link` (writes)

A link to hand to somebody so they can join the project themselves. For when there is no email address to invite, or the person asks for a link.

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `project_id` | string, UUID | required | Project UUID from list_projects. |
| `role` | string, one of editor, viewer, default viewer | optional | What the link grants: 'viewer' (default) or 'editor'. |

Example call:

```json
{
  "project_id": "1f6d0b72-5c84-4a19-b3f7-8e2c9d10a4b5",
  "role": "viewer"
}
```

Answer:

```json
{
  "url": "https://app.todowl.com/join/hq7Ktpm3zRxv",
  "role": "viewer",
  "reused": true
}
```

Worth knowing:

- Anyone holding the link can join with the role it grants, so say that out loud when passing it on.
- Asking twice for the same project and role gives back the same link, marked reused: true, rather than making a second one. A link here never expires and can be used any number of times, so every extra one is another permanent key to the project that the owner never saw being made.
- A link with an expiry date or a limited number of uses, made in the app, is left alone: only the unlimited kind this tool creates is handed back.
- Same rules as an invitation: Pro, and the owner only.
- The token is twelve characters, drawn from an alphabet with no 0, O, 1, l or I in it, because a link like this gets read out loud and retyped.
- There is no tool that takes a link back. Revoking one is done in the Share panel in the app.

#### `set_project_member_role` (writes)

Move a member between viewer and editor.

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `project_id` | string, UUID | required | Project UUID. |
| `user_id` | string, UUID | required | Member user_id from list_project_members. |
| `role` | string, one of editor, viewer | required | 'editor' or 'viewer'. |

Example call:

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

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `project_id` | string, UUID | required | Project UUID. |
| `user_id` | string, UUID | required | Member user_id from list_project_members. |

Example call:

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

Answer:

```json
{
  "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)

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

Parameters:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `task_id` | string, UUID | required | Task UUID. |
| `user_id` | string, UUID | required | Member user_id from list_project_members. |
| `assigned` | boolean, default true | optional | True to assign (default), false to unassign. |

Example call:

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

Answer:

```json
{
  "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.ts`, `mcp/src/server.ts`, `mcp/src/auth.ts`

#### POST / (API token)

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

Request:

```http
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:

```json
{
  "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:

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

Answer:

```json
{
  "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.ts`, `mcp/src/tokens.ts`

#### POST /auth/token (signed-in session)

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

Body:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `name` | string, ≤ 120, default API token | optional | What to call the key, so you can tell it apart later. Longer names are cut to 120 characters. |

Request:

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

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

Answer:

```json
{
  "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:

```json
{
  "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:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `id` | string, UUID | required | Id of the key, as listed by GET /auth/tokens. |

Answer:

```json
{
  "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.ts`, `mcp/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:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `messages` | object[] | required | The conversation so far, in OpenAI message shape. At least one message. |
| `timezone` | string | optional | IANA zone from the browser, so that tomorrow means tomorrow where the reader is. |
| `locale` | string | optional | Language to answer in. |
| `approve` | boolean | optional | True when the person has just said yes to a deletion the model was holding. |
| `resume` | boolean | optional | True when the app is carrying on unfinished work rather than a person saying something new. |

Answer:

```json
{
  "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:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `messages` | object[] | required | The conversation to name. At least one message. |
| `locale` | string | optional | Language of the title. |

Answer:

```json
{
  "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.ts`, `cli/src/config.ts`, `cli/README.md`

#### `todowl --help`

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

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |
| `--version` | boolean | optional | Print the version and stop. |
| `--help` | boolean | optional | Same as running the tool with no arguments. |

Example call:

```bash
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.ts`, `cli/src/config.ts`

#### `todowl auth login`

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

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--provider <google|apple>` | string, one of google, apple, default google | optional | Which button to press in the browser. |
| `--email <address>` | string | optional | Ask for a password in the terminal instead of opening a browser. Useful over SSH. |
| `--no-browser` | boolean | optional | Print the sign-in URL instead of opening it. |
| `--name <name>` | string | optional | What to call the token that gets created. Defaults to the name of this machine. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

```bash
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:

```bash
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:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

```bash
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:

```bash
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.ts`, `cli/src/commands/tasks.ts`

#### `todowl project list`

Projects, with their ids and colours.

Also spelled: `todowl projects`, `todowl project`

Calls: `list_projects`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--all` | boolean | optional | Include what is normally hidden: archived rows, and paused habits. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

```bash
todowl project list --all
```

#### `todowl project create --name "Work"`

Create a project.

Also spelled: `todowl project add`

Calls: `create_project`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--name <name>` | string | required | Project name. |
| `--description <text>` | string | optional | One line about the project. |
| `--color <hex>` | string | optional | Colour, for example #6366f1. |
| `--icon <name>` | string | optional | Icon name. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

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

#### `todowl project update <id>`

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

Also spelled: `todowl project edit`

Calls: `update_project`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--name <name>` | string | optional | New name. |
| `--description <text>` | string | optional | New description. |
| `--color <hex>` | string | optional | New colour. |
| `--icon <name>` | string | optional | New icon. |
| `--archive` | boolean | optional | Move it to the archive. |
| `--unarchive` | boolean | optional | Bring it back out of the archive. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

```bash
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>`

Move a project to Trash.

Also spelled: `todowl project rm`

Calls: `delete_project`

Example call:

```bash
todowl project delete 1f6d0b72
```

Worth knowing:

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

#### `todowl list list`

Lists, optionally only those of one project.

Also spelled: `todowl lists`, `todowl list`

Calls: `list_lists`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--project <id>` | string | optional | Project, by id or by the first characters of it. |
| `--all` | boolean | optional | Include what is normally hidden: archived rows, and paused habits. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

```bash
todowl list list --project 1f6d0b72
```

#### `todowl list create --name "Backlog"`

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

Also spelled: `todowl list add`

Calls: `create_list`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--name <name>` | string | required | List name. |
| `--project <id>` | string | optional | Project, by id or by the first characters of it. |
| `--icon <name>` | string | optional | Icon name. |
| `--color <hex>` | string | optional | Hex colour, for example #6366f1. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

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

#### `todowl list update <id>`

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

Also spelled: `todowl list edit`

Calls: `update_list`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--name <name>` | string | optional | New name. |
| `--icon <name>` | string | optional | New icon. |
| `--color <hex>` | string | optional | Hex colour, for example #6366f1. |
| `--project <id>` | string | optional | Project, by id or by the first characters of it. |
| `--archive` | boolean | optional | Move it to the archive. |
| `--unarchive` | boolean | optional | Bring it back out of the archive. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

```bash
todowl list update 7a4e9c31 --name "Later"
```

#### `todowl list delete <id>`

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

Also spelled: `todowl list rm`

Calls: `delete_list`

Example call:

```bash
todowl list delete 7a4e9c31
```

### Tasks

The everyday half of the tool.

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

#### `todowl task list`

Tasks, filtered the same way the app filters them.

Also spelled: `todowl tasks`, `todowl task`

Calls: `list_tasks`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--status <open|completed|all>` | string, one of open, completed, all, default open | optional | Which tasks to include. |
| `--project <id>` | string | optional | Project, by id or by the first characters of it. |
| `--list <id>` | string | optional | List, by id or by the first characters of it. |
| `--date <YYYY-MM-DD>` | string, YYYY-MM-DD | optional | Only that day. |
| `--from <YYYY-MM-DD>` | string, YYYY-MM-DD | optional | From that day on. |
| `--to <YYYY-MM-DD>` | string, YYYY-MM-DD | optional | Up to and including that day. |
| `--priority <high|medium|low|none>` | string, one of high, medium, low, none | optional | Only that priority. |
| `--no-date` | boolean | optional | Only tasks with no date at all, the inbox. |
| `--limit <n>` | integer, 1-200, default 50 | optional | How many rows. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

```bash
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"`

Create a task.

Also spelled: `todowl task add`

Calls: `create_task`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--title <text>` | string | required | What the task is called. |
| `--description <text>` | string | optional | Longer notes. |
| `--project <id>` | string | optional | Project, by id or by the first characters of it. |
| `--list <id>` | string | optional | List, by id or by the first characters of it. |
| `--date <YYYY-MM-DD>` | string, YYYY-MM-DD | optional | Due date. Without it the task goes to the inbox. |
| `--time <HH:MM>` | string, HH:MM | optional | Start time. |
| `--priority <high|medium|low|none>` | string, one of high, medium, low, none | optional | Priority. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

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

#### `todowl task update <id>`

Change a task that already exists.

Also spelled: `todowl task edit`

Calls: `update_task`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--title <text>` | string | optional | New title. |
| `--description <text>` | string | optional | New notes. |
| `--date <YYYY-MM-DD>` | string, YYYY-MM-DD | optional | New date. |
| `--time <HH:MM>` | string, HH:MM | optional | New start time. |
| `--priority <high|medium|low|none>` | string, one of high, medium, low, none | optional | New priority. |
| `--archive` | boolean | optional | Move it to the archive. |
| `--unarchive` | boolean | optional | Bring it back out of the archive. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

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

#### `todowl task move <id>`

Re-file a task into another project or list.

Calls: `move_task`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--project <id>` | string | optional | Project, by id or by the first characters of it. |
| `--list <id>` | string | optional | List, by id or by the first characters of it. |
| `--no-list` | boolean | optional | Detach it from its list, keeping its project. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

```bash
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>`

Tick a task off, or reopen it.

Also spelled: `todowl task done`

Calls: `complete_task`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--reopen` | boolean | optional | Open the task again instead. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

```bash
todowl task complete a1b2c3d4
```

#### `todowl task delete <id>`

Move a task to Trash.

Also spelled: `todowl task rm`

Calls: `delete_task`

Example call:

```bash
todowl task delete a1b2c3d4
```

### Calendar

Meetings and events from a terminal.

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

#### `todowl event list`

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

Also spelled: `todowl events`, `todowl event`

Calls: `list_events`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--date <YYYY-MM-DD>` | string, YYYY-MM-DD | optional | One day. |
| `--from <YYYY-MM-DD>` | string, YYYY-MM-DD | optional | First day of the range. |
| `--to <YYYY-MM-DD>` | string, YYYY-MM-DD | optional | Last day of the range. |
| `--tasks` | boolean | optional | Include dated tasks, not only meetings and events. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

```bash
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`

Put a meeting or an event on the calendar.

Also spelled: `todowl event add`

Calls: `create_event`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--title <text>` | string | required | What the entry is called. |
| `--date <YYYY-MM-DD>` | string, YYYY-MM-DD | required | The day it happens. |
| `--time <HH:MM>` | string, HH:MM | optional | Start time. |
| `--end <HH:MM>` | string, HH:MM | optional | End time. |
| `--all-day` | boolean | optional | An entry that takes the whole day. |
| `--type <meeting|event>` | string, one of meeting, event, default event | optional | Kind of entry. |
| `--description <text>` | string | optional | Longer notes. |
| `--project <id>` | string | optional | Project, by id or by the first characters of it. |
| `--color <hex>` | string | optional | Colour override. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

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

#### `todowl event update <id>`

Reschedule or edit a calendar entry.

Also spelled: `todowl event edit`

Calls: `update_event`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--title <text>` | string | optional | New title. |
| `--date <YYYY-MM-DD>` | string, YYYY-MM-DD | optional | New day. |
| `--time <HH:MM>` | string, HH:MM | optional | New start time. |
| `--end <HH:MM>` | string, HH:MM | optional | New end time. |
| `--all-day` | boolean | optional | Make it an all-day entry. |
| `--description <text>` | string | optional | New notes. |
| `--project <id>` | string | optional | Project, by id or by the first characters of it. |
| `--color <hex>` | string | optional | Colour override. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

```bash
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>`

Move a calendar entry to Trash.

Also spelled: `todowl event rm`

Calls: `delete_event`

Example call:

```bash
todowl event delete e2f70a95
```

### Notes

Plain text in, plain text out.

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

#### `todowl note list`

Notes, pinned ones first.

Also spelled: `todowl notes`, `todowl note`

Calls: `list_notes`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--query <text>` | string | optional | Search titles and bodies. |
| `--all` | boolean | optional | Include what is normally hidden: archived rows, and paused habits. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

```bash
todowl note list --query invoice
```

#### `todowl note get <id>`

Print one note, body and all.

Also spelled: `todowl note show`

Calls: `get_note`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

```bash
todowl note get c81b5d47
```

#### `todowl note create`

Create a note from plain text.

Also spelled: `todowl note add`

Calls: `create_note`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--title <text>` | string | optional | Title. A note without one is untitled, which is allowed. |
| `--content <text>` | string | optional | Body. |
| `--project <id>` | string | optional | Project, by id or by the first characters of it. |
| `--pin` | boolean | optional | Pin it to the top of the list. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

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

#### `todowl note update <id>`

Change a note.

Also spelled: `todowl note edit`

Calls: `update_note`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--title <text>` | string | optional | New title. |
| `--content <text>` | string | optional | New body. It replaces the old one whole. |
| `--project <id>` | string | optional | Project, by id or by the first characters of it. |
| `--pin` | boolean | optional | Pin it. |
| `--unpin` | boolean | optional | Unpin it. |
| `--archive` | boolean | optional | Move it to the archive. |
| `--unarchive` | boolean | optional | Bring it back out of the archive. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

```bash
todowl note update c81b5d47 --pin
```

#### `todowl note delete <id>`

Move a note to Trash.

Also spelled: `todowl note rm`

Calls: `delete_note`

Example call:

```bash
todowl note delete c81b5d47
```

### Habits

Check in, look at a month, pause one.

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

#### `todowl habit list`

Habits, with when in the day each one belongs.

Also spelled: `todowl habits`, `todowl habit`

Calls: `list_habits`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--all` | boolean | optional | Include what is normally hidden: archived rows, and paused habits. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

```bash
todowl habit list --all
```

#### `todowl habit create --name "Read 20 pages"`

Start tracking a habit.

Also spelled: `todowl habit add`

Calls: `create_habit`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--name <text>` | string | required | What the habit is called. |
| `--when <morning|afternoon|evening|anytime>` | string, one of morning, afternoon, evening, anytime, default anytime | optional | When in the day it belongs. |
| `--icon <name>` | string | optional | Icon name. |
| `--color <hex>` | string | optional | Colour. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

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

#### `todowl habit update <id>`

Rename a habit, restyle it, or pause it.

Also spelled: `todowl habit edit`

Calls: `update_habit`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--name <text>` | string | optional | New name. |
| `--icon <name>` | string | optional | New icon. |
| `--color <hex>` | string | optional | New colour. |
| `--when <morning|afternoon|evening|anytime>` | string, one of morning, afternoon, evening, anytime | optional | When in the day it belongs. |
| `--pause` | boolean | optional | Stop tracking it, history kept. |
| `--resume` | boolean | optional | Start tracking it again. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

```bash
todowl habit update 5c9a11de --pause
```

#### `todowl habit checkin <id>`

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

Also spelled: `todowl habit check`

Calls: `checkin_habit`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--date <YYYY-MM-DD>` | string, YYYY-MM-DD | optional | Which day. Today, if left out. |
| `--miss` | boolean | optional | Record the day as not done. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

```bash
todowl habit checkin 5c9a11de
```

#### `todowl habit log <id>`

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

Calls: `get_habit_checkins`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--from <YYYY-MM-DD>` | string, YYYY-MM-DD | optional | First day. The first of this month, if left out. |
| `--to <YYYY-MM-DD>` | string, YYYY-MM-DD | optional | Last day. Today, if left out. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

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

#### `todowl habit delete <id>`

Delete a habit and its history for good.

Also spelled: `todowl habit rm`

Calls: `delete_habit`

Example call:

```bash
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`

What is in Trash and can still be restored.

Also spelled: `todowl trash`

Calls: `list_trash`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--kind <task|event|note|project|list>` | string, one of task, event, note, project, list | optional | Only one sort of thing. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

```bash
todowl trash list --kind task
```

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

Put something back where it was.

Calls: `restore_from_trash`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--kind <task|event|note|project|list>` | string, one of task, event, note, project, list | required | What is being restored. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

```bash
todowl trash restore --kind task 9b2c1f04
```

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

Erase one item for good.

Also spelled: `todowl trash rm`

Calls: `delete_from_trash_forever`

Options:

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `--kind <task|event|note|project|list>` | string, one of task, event, note, project, list | required | What is being erased. |
| `--json` | boolean | optional | Print one machine-readable object and nothing else. This is what makes the tool usable inside a script. |

Example call:

```bash
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.
