named groups of to-dos inside a project, for people and for AI assistants
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).
The order of a project's page
The apps draw a project's to-dos in this order:
- the to-dos with no heading, in their own order;
- 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:
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). 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
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
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}withheadingId, ornullto take it out of its heading. - Another project:
POST /v1/todos/{id}/movewithparentIdandheadingId(a heading of the destination). WithoutheadingId, a move to another project clears it, and a reorder in the same project keeps it. - A new to-do:
POST /v1/todoswithparentIdandheadingId.
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:
get_projectwith 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.create_headingfor each phase, in order (after: "start"puts one first).update_todowithheadingfiles a to-do under a heading by name.create_todoandmove_todotakeheadingtoo. Given a heading and no project, the to-do goes to the heading's project.move_headingreorders,rename_headingrenames.complete_headingfinishes a phase,convert_heading_to_subprojectpromotes one that needs its own deadline, anddelete_headingremoves 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:
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.