Tasks domain — full reference
Entities
Task
| Field | Type | Notes |
|---|---|---|
id |
uuid | |
project_id |
uuid | |
parent_task_id |
uuid | null | When set, this task is a child of a feature parent. |
type |
task|bug|chore|spike|feature |
|
title |
text | Required. |
description_md |
text | Markdown. |
state |
todo|doing|done|wont_do |
Lifecycle. done and wont_do are both terminal. |
priority |
int | 0–100, read-only. Higher = picked sooner. Set it with priority_key. |
assignee_user_id |
uuid | null | Specific user. MCP shows it as assignee: "me" or a login. |
target_role_id |
uuid | null | Role-scoped; eligible to any holder. MCP shows role: the key. |
actual_start |
timestamp | null | Set automatically when entering doing (kept across re-enters). |
actual_end |
timestamp | null | Set automatically when entering done, cleared on revert. |
created_by_user_id |
uuid | null | Audit. |
created_by_token_id |
uuid | null | Audit. |
Dependency
A directed edge (task_id, depends_on_id). The Nottario server
rejects edges that would create a cycle. A task is considered
eligible (nottario.tasks.next) only when every depends_on is
in state = 'done'.
Commit link
(task_id, repo, sha, message). Repo must be in "owner/repo"
form. Re-linking the same (task_id, repo, sha) updates the message.
Comment
(id, task_id, author_user_id, author_token_id, body_md, created_at). Append-only.
Step
(id, position, text, done, done_at, done_by_user_id). An ordered
checklist on a task: short actions worth remembering that need no
owner, role, dependency, commits or discussion of its own. If an item
needs any of those, it is a subtask, not a step. A step is one line of
up to 200 characters; a task holds at most 50. Steps are either done
or deleted: there is no "skipped" state.
Tool surface
nottario.tasks.list
Filter parameters (all optional except project_id):
-
state— restrict to one state. -
type— restrict to one task type. -
assignee_user_id/target_role_id— restrict assignment. Pass"me"for yourself and a role by its key ("backend"); uuids work too. Responses use the same terms:assignee: "me"or a login,role: "backend", andupdated_atin UTC to the second. -
parent_task_id— list children of a specific feature. -
include_children: true— by default only top-level tasks (parent_task_id IS NULL) are returned; set this to flatten feature subtrees. -
include_closed: true— by default the response is restricted to open tasks (statetodoordoing) so closed rows don't dominate every walk in a long-lived project. Set this to also includedoneandwont_do. An explicitstatefilter (state: 'done',state: 'wont_do') always wins regardless of this flag. -
cycle_id(optional) — when omitted, all read tools (tasks.list,tasks.next,tasks.claim_next) default to the project's active cycle. Pass a specificcycle_idto inspect a closed cycle. Pass"all"for the no-filter behaviour (every cycle). To see what shipped in sprint-2 specifically:nottario.tasks.list { project_id, cycle_id: <sprint-2 id>, state: 'done' }See
domains/cycles.mdfor the full cycles surface.
Ordering: priority DESC, created_at ASC.
Pagination
Keyset pagination: limit (1..500; default the project's
mcp_page_size, 50 unless changed in Settings → MCP) and cursor
(empty for the first page, then the previous next_cursor). Each
page is {tasks, next_cursor, has_more}; loop while has_more. The
cursor encodes (priority, created_at, id), so concurrent writes do
not skip or repeat rows.
tasks.next and claim_next only look at todo: before previewing
new work, list your own doing rows (assignee_user_id: "me") and
report them first (skill.md §3).
nottario.tasks.next
Returns the next eligible task or {task: null} if nothing is
available. Eligibility:
state = 'todo'.type <> 'feature'(features are containers).- No
depends_onrow points to a task that is notdone.
Filters narrow the candidate set:
assignee_user_idset → tasks assigned to that user or unassigned tasks whosetarget_rolematches one of the user's roles in the project.role_idset → tasks whosetarget_role_idequals it or is null.
Priorities
Each project defines its own priority buckets (named labels mapped to
a numeric value). Defaults seeded on project creation: low=30,
medium=60, high=90, critical=100. Admins can rename, retune or
add buckets per project.
Priorities are set by key, never by number. Pass one of the
project's keys as priority_key to tasks.create / tasks.update;
nottario.projects.list_priorities returns them. The task tools take
no numeric priority: a number that misses every bucket would show on
the board as an unnamed p70 instead of high. An unknown key is
rejected, and the error lists the keys the project does have.
The priority number you read back on a task is the value of its
bucket. Do not try to place a task between two buckets: tasks inside
a bucket are already ordered oldest first. If the project needs a
level it does not have, ask the humans to add a bucket in the project
settings.
nottario.tasks.create
Defaults: state=todo, type=task, the project's default priority.
claim: true when the work is yours and starts now: the task is
created assigned to you and in doing. Leave it out for backlog work,
work for someone else, or later.
One task per role. Work that spans roles is a feature parent
with one child per role, chained with add_dependency in execution
order; each child is pickable by the right agent, and the parent
closes itself when they are all done:
F = create { type: "feature", title: "Sign in with Google" }
B = create { parent_task_id: F.id, target_role_id: "backend", title: "OAuth callback + session" }
Fe = create { parent_task_id: F.id, target_role_id: "frontend", title: "Sign-in button" }
add_dependency { task_id: Fe.id, depends_on_id: B.id }
Q = create { parent_task_id: F.id, target_role_id: "qa", title: "Smoke the flow" }
add_dependency { task_id: Q.id, depends_on_id: Fe.id }
New tasks land in the active cycle (no cycle_id here); move one with
tasks.update afterwards.
Send text as you would type it. Plain UTF-8: Build & deploy, not
Build & deploy; real quotes and real line breaks, never \" or
a literal \n. The transport encodes for you, and pre-escaped text is
stored and shown verbatim. Same for tasks.update.
nottario.tasks.update
Mutates the fields you pass. Notable nuances:
- Pass
assignee_user_id: ""(empty string) to unassign the user. Same fortarget_role_id. - Changing the priority is the canonical way to reorder; pass
priority_key. There is no numeric input. - Use this for description edits and renames; do not delete-and-recreate.
- Reparenting cascades
cycle_id: settingparent_task_idon a leaf task forces the task'scycle_idto match the new parent's cycle (DB trigger; anycycle_idyou pass alongside is overridden). To move a leaf to a different cycle, either detach it from its feature parent first, or move the whole feature subtree instead.
nottario.tasks.set_state
Moves a task between states and manages actual_start /
actual_end for you:
todo→ clears both, and the task goes back to having no owner.doing→ fillsactual_start(only if currently null).done→ fillsactual_endand preserves any earlieractual_start.
Moving a task out of todo makes it yours if nobody owns it. The
server assigns you as it moves, so a task can never sit in doing,
or reach done, with nobody answering for it. It never takes a task
away from an existing assignee. This is a safety net, not the
intended route: claim the work deliberately with tasks.claim,
claim_next, or create { claim: true }, so the assignment says "I
took this" rather than "the server noticed nobody had".
For done and wont_do, use nottario.tasks.close instead. It
carries the closing comment and the commit links in the same
transaction, so a precondition failure cannot leave an orphan comment
behind. set_state done works and is not going away, but on its own
it closes a task with no record of what shipped — if you use it, you
still owe the comment and the commit links.
nottario.tasks.close
The only way to finish a task: commit links, closing comment and state transition in one transaction.
nottario.tasks.close {
project_id, task_id,
state: "done", # or "wont_do"
comment: "Fix: … Repro: … Test: … abc1234.",
commits: [{ repo: "owner/repo", sha: "abc1234", message: "feat: x" }]
}
# → {task, comment_id?, linked_commit_count}
- Link every commit the work produced. The UI's Commits panel and "what shipped" queries read the links, not the comment. Only documentation or DB-recovery work has none.
- Always comment, briefly. Task / chore / spike: one sentence of
what and why, plus the sha. Bug:
Repro: … Fix: … Test: …in one or two lines, so a future agent can re-trigger it and trust the fix.wont_do: why ("superseded by", "out of scope after X"). The commit message and the diff carry the detail. Feature parents close themselves when their children are done. - Steps left undone do not block the close; the response adds
pending_steps: [{id, text}]and anotice. Tell the human which ones were left. - Never split it into
link_commit+add_comment+set_state: a failure half-way leaves a comment and links on an open task.
Preconditions. done is refused while a direct dependency is not
done; nothing is written and the response lists what is open:
{ "error": "cannot close task: 1 unresolved precondition",
"preconditions": [{ "id": "…", "title": "API endpoint", "state": "todo" }] }
Close the preconditions first (claim the ones still in todo), never
work around them. Feature parents are exempt: they roll up on their
own.
nottario.tasks.add_dependency / remove_dependency
add_dependency rejects edges that would form a cycle:
A depends_on B ✓
B depends_on A ✗ (cycle)
Cycle detection considers the entire transitive graph, not just the direct edge.
nottario.tasks.link_commit
repo is the GitHub-style "owner/repo" string. sha can be the
full 40-char hash or a shorter prefix; we store what you send.
message is optional (it's the human-readable subject for
display). Re-linking the same (task, repo, sha) updates the
message in place.
nottario.tasks.unlink_commit
The undo of link_commit: { project_id, task_id, repo, sha }.
Reach for it when a link is wrong — the commit went to the wrong task,
or you amended or rebased after linking and the old sha no longer
exists. In the second case, unlink the old sha and link the new one.
repo and sha must match the link exactly as it was stored; read
them with tasks.get { include_commits: true } rather than guessing.
A link that does not exist is an error, not a silent success: it
almost always means the sha is wrong.
nottario.tasks.add_comment
Body is markdown. The comment is attributed to the calling user and token. Comments are append-only; there is no edit or delete from the agent surface (a human can purge data directly in Postgres if needed).
Steps: add_steps, update_step, remove_step, move_step
nottario.tasks.add_steps { project_id, task_id, texts: ["Write the migration", "Regenerate sqlc"] } # → {steps: new ones, progress}
nottario.tasks.update_step { project_id, task_id, step_id, done: true } # → {id, text, done, progress}
nottario.tasks.update_step { project_id, task_id, step_id, text: "Regenerate sqlc and commit dbq" }
nottario.tasks.remove_step { project_id, task_id, step_id } # → {removed: true, progress}
nottario.tasks.move_step { project_id, task_id, step_id, position: 0 } # → {moved: true, position}
Each step comes as {id, text, done}, and progress is
{done, total}. Writes answer with what they touched, never the whole
checklist: keep the ids add_steps gives you, you need them to tick.
tasks.get lists only the open steps under steps, in order, with
the count in task.steps: {done, total}; pass
include_done_steps: true when you need the done ones too. List rows
and slim task shapes carry only the progress, steps: {done, total},
and omit it when the task has no steps.
How to use them:
- When you start a task whose work has more than one distinct action, write its steps. The list tells the human, and the next agent, what is done and what is left without reading comments.
- When new work appears mid-task, add a step instead of writing a long comment about it. If the new work needs its own owner, role or commits, file a subtask instead.
- Tick each step as you finish it, not all at the end: the progress on the board is only useful while it is current.
- Past about 15 steps, look for subtasks. A list that long usually holds separate deliverables (different commits, roles or review); split those into child tasks and keep steps for the small actions inside each. The cap stays at 50.
- Keep steps short and imperative ("Add the index", not "I think we should probably add an index at some point").
- Delete a step that turned out to be unneeded rather than leaving it pending; a pending step reads as unfinished work.
- Before closing, check the steps. Closing with steps undone is
allowed:
tasks.closeandset_state donestill close the task, and their response addspending_stepsand anotice. When you see them, tell the human which steps were left undone, in your reply, not only in the closing comment.
When to file a task before doing the work
Two shapes of "new work" both go through nottario.tasks.create
before you write any code, open any editor, or run any command:
- Side-channel requests / bugs spotted in passing. "Ah, and we should also…", "this is broken: …", "I noticed X". File the row, decide whether to pivot or stay on the current task. Verbatim quotes from the user (the bug repro, the half-formed idea) belong in the description — future-you will not remember them.
- Substantive new work the user explicitly asks you to do.
"Let's add Biome", "do the design review of the Kanban", "rename
content_mdtocontent". Even when the user is telling you to act, the act starts withtasks.create { claim: true }→ work. Skipping the row because "the request is obviously the task" leaves the backlog blind: the work has no handle for tracking, no audit trail, no link to the resulting commits. The exception is conversational tweaks that fit in a single small commit and need no follow-up (a typo fix in a doc, a one-line CSS adjustment) — those can land directly.
File the first shape plain — it goes to the backlog — and the second
with claim: true when you start on it right away.
Both shapes need the right target_role, an honest description,
dependencies linked if relevant, and a split into role children when
multi-role. The bar is: if I had to leave the session right now,
would someone else be able to pick this up? If not, file more
context.
Idiomatic patterns
"Carry on" — the loop
The loop itself is in skill.md §3. Details:
claim_nextisSELECT … FOR UPDATE SKIP LOCKED: parallel agents get different tasks. Passassignee_user_id: "me"to include todos targeted at your roles, orrole_id: "backend"to stay in one lane.- A topic or an id from the human: find candidates with
tasks.list { state: "todo" }(already ordered priority desc, then oldest first) andtasks.claimthem one by one until one succeeds. A failed claim returns{error, reason, current_state, current_assignee, preconditions, pending_children_count}: someone took it, a precondition is open, or a feature still has children. Surface that to the human rather than retrying blindly. - Never
next+update+set_state. Another agent can take the task between any two calls. - A task someone else holds is theirs: comment or ask the human, never reassign it silently.
"I found a bug while doing my task"
Create it; do not silently fix it without filing:
nottario.tasks.create {
project_id, title, type: "bug", priority_key: "high",
description: "context, repro, suspected fix",
}
Then continue with your current task. The general rule (verbatim
quote, file path, proposed direction, role split) lives in skill.md
§5.
"The user just mentioned a different task / bug / feature"
Same rule, broader scope: any side-comment from the human about work
that is NOT what you're currently executing → FIRST action is
nottario.tasks.create, only THEN decide whether to pivot or keep
going. If the item lives only in conversation, multi-agent and
multi-session work lose the single source of truth.
When to pivot vs. keep going: cheap context-switch (≤5 min, no architectural decision required) → pivot, file, fix, return. Otherwise: file with enough context that someone else can pick it up, and resume your current task.
"Block this until X is done"
After you create both tasks, declare the order:
nottario.tasks.add_dependency {
project_id, task_id: B, depends_on_id: A,
}
tasks.next will then skip B until A is done.
Token discipline
Everything a tool returns stays in your context for the rest of the session. Ask for the minimum.
- Responses are slim by default. Mutations and
tasks.listreturn{id, type, title, state, priority, role, assignee, updated_at, steps}, never the description or comment you just sent.tasks.getadds the description,relatedand the open steps.verbose: truebrings the full row; you almost never need it. - Opt in deliberately:
include_commits,include_comments,include_done_stepsontasks.get;include_closedor a closedstateontasks.list(closed rows pile up forever). - Don't re-fetch. Reuse the id and fields a previous call gave you;
whoamionce per session; never re-read a page of this bundle you already read this session. - No pickup comments ("starting this", "on it"):
doingand the assignee already say it. Comment when a future reader needs it: a decision, a blocker, the close. - Closing comments are a line, not an essay (§
tasks.close). - Step writes answer small (the touched step and the progress);
keep the ids
add_stepsreturns instead of re-reading the task. nottario.searchis capped at 20 hits by default (max 100) and slim; raise the limit only for a genuinely wide sweep.
Things you cannot do (today)
- Delete a comment via MCP (the web UI can; agents cannot).
- Edit historic actual_start/actual_end.
- Modify the project, role catalogue or memberships (admin-only, done via the web UI).
- Subscribe to live updates from the MCP — agents poll. The web UI uses SSE; agents do not need real-time and we keep the protocol simple.