# Organizing a project with headings

A **heading** is a named group of to-dos inside a project or a subproject: "before the move", "moving day", "after". It only orders and groups the project's list. It has no page, no dates, no notes and no trash of its own, and its to-dos still belong to the project.

On a to-do that means two fields:

| field | what it is |
|---|---|
| `parentId` | the project the to-do is in, as always |
| `headingId` | the heading it sits under in that project, or `null` |

A to-do's `headingId` is always a heading of its own `parentId`. Move the to-do to another project and its heading is cleared in the same write. Areas have no headings: an area's page already groups its to-dos by project.

## Heading or subproject?

Both group to-dos inside a project. They are not the same thing:

- A **subproject** is a project of its own. It has a page, a deadline, a progress pie, notes and attachments, and its to-dos show under its name in today and planned.
- A **heading** is a divider inside the project. Its to-dos are the project's.

If a group needs its own deadline or notes, make it a subproject. If it only orders the list, make it a heading. A heading that grows up can become a subproject in one call ([below](#make-it-a-subproject)).

## The order of a project's page

The apps draw a project's to-dos in this order:

1. the to-dos with no heading, in their own order;
2. then each heading, in heading order, with its to-dos in their order.

Headings do not change a to-do's `sortKey`: they group, they never re-key. Every list outside the project's page (today, planned, anytime, someday, all tasks, search, a tag, a filter) shows the to-do under its project as usual, without the heading.

`GET /v1/todos?parent_id=<project>&group_by=heading` returns the page order, with the project's headings alongside:

```bash
curl -s "https://api.inittasks.com/v1/todos?parent_id=$PROJECT&group_by=heading" \
  -H "Authorization: Bearer $INITTASKS_TOKEN"
```

The body is the usual page (`data`, `next_cursor`) plus `headings`, the project's headings in order. Match each to-do to its group by `headingId`. `?heading_id=` keeps only the to-dos under one heading (`null` for those under none).

Headings are project data, so `headings` is included only when your credential also holds `containers:read`. A credential with `todos:read` alone still gets the to-dos in page order, each with its `headingId`, but not the headings' names ([scopes](/scopes)). The cursor follows the heading order at the time of each request: if someone reorders the headings while you page, rows can be skipped or repeated, so start over after a reorder.

## Making headings

```bash
curl -s https://api.inittasks.com/v1/headings \
  -H "Authorization: Bearer $INITTASKS_TOKEN" -H "Content-Type: application/json" \
  -d "{\"projectId\": \"$PROJECT\", \"name\": \"moving day\"}"
```

A new heading goes last. `"after": null` puts it first, and `"after": "<heading id>"` puts it right after that heading. The name is trimmed and shown as typed.

`GET /v1/headings?project_id=<project>` lists a project's headings in order, each with `openCount`, the open to-dos under it. `PATCH /v1/headings/{id}` renames it (`name`) or moves it (`after`). A heading never changes project.

## Filing to-dos under a heading

```bash
curl -s -X PATCH "https://api.inittasks.com/v1/todos/$TODO" \
  -H "Authorization: Bearer $INITTASKS_TOKEN" -H "Content-Type: application/json" \
  -d "{\"headingId\": \"$HEADING\"}"
```

- **Same project:** `PATCH /v1/todos/{id}` with `headingId`, or `null` to take it out of its heading.
- **Another project:** `POST /v1/todos/{id}/move` with `parentId` and `headingId` (a heading of the destination). Without `headingId`, a move to another project clears it, and a reorder in the same project keeps it.
- **A new to-do:** `POST /v1/todos` with `parentId` and `headingId`.

A heading of another project, or a heading on a to-do with no project, is `validation_failed` at `/headingId`.

## Deleting a heading keeps its to-dos

`DELETE /v1/headings/{id}` is the only delete, and the to-dos always stay. Each one gets `headingId: null` and keeps its project and its place; then the heading is gone for good (headings have no trash). The answer lists the to-dos that lost it, `clearedTodoIds`.

A heading follows its project: while the project is in the trash its headings are hidden with it, and restoring the project brings them back. Deleting the project for good deletes its headings. While the project is in the trash its headings take no writes: renaming, moving, deleting, converting or completing one is `validation_failed` until you restore the project. You can still read one by id.

## Make it a subproject

`POST /v1/headings/{id}/convert` does three things in one call: it creates a subproject of the project with the heading's name, moves every to-do under the heading into it in their order, and deletes the heading. The answer is the new subproject and the to-dos it holds. It is safe to retry: unless you pass an `id`, the subproject's id is derived from the heading, so a second call finds the subproject the first one made and finishes the move.

Convert always makes a **new** subproject. It never files into one that already exists: an `id` that is already in use is `duplicate_id` (409). If the subproject a first attempt made was trashed before the retry, the retry is `conflict` (409): restore the subproject, then convert again. With your own `id`, a retry after a failure part-way is `duplicate_id`, so leave `id` out if you may retry.

Only a heading in a **project** can become a subproject. A subproject holds to-dos only, so a heading inside a subproject is refused.

## Complete all

`POST /v1/headings/{id}/complete` completes every open to-do under the heading, the way completing each one would: a repeating to-do moves to its next date and stays open. The heading stays. Send an `Idempotency-Key` if you may retry.

## Events

`heading.created`, `heading.updated` and `heading.deleted`. Like every event they are thin: `data.id` is the heading, and you read it to learn more. A to-do filed under a heading, taken out of one, or cleared when its heading was deleted is a `todo.updated`; read its `headingId`. `heading.deleted` does not list the to-dos it held, because the server cannot read them when an app deletes a heading. Each of them sends its own `todo.updated`.

## For AI assistants

An assistant asked to "organize my project into phases" can do it all with the [MCP tools](/mcp):

1. `get_project` with the project's name shows the page as the apps draw it: the loose to-dos, then each heading with its to-dos, with every id.
2. `create_heading` for each phase, in order (`after: "start"` puts one first).
3. `update_todo` with `heading` files a to-do under a heading by name. `create_todo` and `move_todo` take `heading` too. Given a heading and no project, the to-do goes to the heading's project.
4. `move_heading` reorders, `rename_heading` renames.
5. `complete_heading` finishes a phase, `convert_heading_to_subproject` promotes one that needs its own deadline, and `delete_heading` removes the label and keeps the to-dos.

Headings resolve by id or by exact name, looked up in the project you name, else in the to-do's own project. Pass `project` when two projects use the same heading name. In `move_todo`, `heading: "none"` (also `"null"` or `""`) takes the to-do out of its heading, so a heading literally named "none" has to be given by its id there.

`delete_heading` is permanent and has no undo, but it only removes the label: the to-dos stay in the project. Unlike other deletes it does not go through the trash.

A good request to give an assistant:

```text
Look at my "apartment move" project and split it into the phases
"before the move", "moving day" and "after". Put each to-do under the
phase it belongs to and leave anything unclear without a heading.
```
