Tasks

A project task is a durable unit of work on a project backlog. It is not a chat turn, not a memory, and not an agent run.

  • A memory is something that happened or was said, owned by an identity, recalled into hydration.
  • An agent run is a budgeted execution loop (iterations, tokens, wall-clock, tool profile, events). A run may link to a project task via taskId; it does not replace the board.
  • An agent task (AgentTask under a run) is run-local work scaffolding. It is a different table from ProjectTask.

The board answers operational questions: what is open, what kind of work it is, which subsystem it touches, whether it gates launch, what blocks what. Priority alone cannot carry that — it was tried and the High column stopped meaning anything (2026-07-28 rebalance). Classification is a separate axis, enforced at create (#273).

All endpoints require authentication. Responses are wrapped in ApiEnvelope<T>. Canonical API host: https://api.atamaia.ai.

Permissions: TaskView, TaskCreate, TaskEdit, TaskDelete, TaskManageDependencies.


Classification — why priority was not enough

Three fields, shipped 2026-08-13:

Field Type Role
kind TaskKind? What the work is
subsystem TaskSubsystem? Where it primarily lands
isLaunchBlocker bool Whether it plainly gates public launch / first external users

Kind

Value Meaning
Bug Broken vs intended behaviour — including security defects and silent failures
Feature A capability that does not exist yet
Chore Maintenance: upgrades, cleanup, refactors, infra, docs, deploys
Investigate Output is a decision or document, not code

Closed vocabulary on purpose. Free-text tags were measured against the live MemoryTag store before this shipped: 1,364 distinct tags, 61% used once. That shape recreates the unreliable [Bug] title-prefix problem this field exists to end.

Subsystem

One per task — the area whose code changes most:

Agent · CodeGraph · Mcp · Spa · Docs · Infra · Memory · Security · AiRouter · Api · Auth · BuildDeploy · Marketing

Launch blocker

Boolean, default false. Conservative: only mark when the work plainly gates launch. Independent of priority — a Normal-priority docs purge can still be a launch blocker.

Null means untriaged

Columns are nullable with no default. A task with null kind/subsystem is honestly unclassified. On create, null is rejected. Existing rows (especially historical Done) may still be null; the board can select them.

Why enforce in the service, not the controller: REST, MCP, agent tools, and skills all call TaskService.CreateAsync. A controller attribute would be one entry point remembering; the service is every entry point.

OpenAPI still shows kind/subsystem as optional on CreateTaskRequest. The running service rejects null. Prefer runtime over schema until the schema is fixed.


Status and priority

Status (TaskStatus)

Status Notes
Todo Default on create
InProgress Active work
Blocked Human-set status value
Done Complete
Cancelled Abandoned

There is no hard transition matrix in the service: any status may be written via the status endpoint. Side effects on change are separate (notes, unblock signals) — see below.

isBlocked is not the same as status Blocked.
isBlocked is a computed flag: true when any dependency’s target is still not Done/Cancelled. Task #27 is status Todo with isBlocked=true because it depends on #26.

Priority (TaskPriority)

Low · Normal (default) · High · Critical

Urgency only. Do not overload it with kind or launch semantics.


Create

POST /api/projects/{projectId}/tasks

Auth: TaskCreate.

Field Type Required Notes
title string yes (practically) Max length 500; trimmed
kind TaskKind yes at runtime 400 if missing
subsystem TaskSubsystem yes at runtime 400 if missing
description string no
parentTaskId long no Makes this a subtask
assignedToId long no Defaults to caller’s identity when omitted (if the caller has one)
specDocId long no Link a project doc
priority TaskPriority no Default Normal
isLaunchBlocker bool no Default false

Response: TaskDto summary (counts, not nested trees). Mutations deliberately avoid returning full TaskDetailDto so a one-field edit does not ship every note over MCP.

Rejection: missing classification → HTTP 400, error code INVALID_ARGUMENT, message lists valid enum names. Mapped by ExceptionMiddleware from ArgumentException (not a 500).

Initial status is always Todo.


Read

List (board)

GET /api/projects/{projectId}/tasks

Auth: TaskView.

Parameter Type Default Notes
status TaskStatus Exact status
includeCompleted bool false When no status, hide Done and Cancelled
priority TaskPriority Named filter
assignedToId long 0 means unassigned
page / pageSize int 1 / 50 Page size clamped to 200
sort string sortOrder Whitelist below
dir string asc (for default) asc / desc
filter bag See Filtering

Scope: top-level only (parentTaskId == null). Subtasks are not board rows; fetch them separately or via detail.

Live probe (project 1, defaults): active top-level totalCount 210 (2026-08-13).

Get detail

GET /api/tasks/{id}

Returns TaskDetailDto: all list fields plus:

Field Content
subTasks Ordered by sortOrder, then id
notes Newest first
blockedBy Tasks this one depends on (id, title, status)
blocks Tasks that depend on this one

Subtasks

GET /api/tasks/{parentTaskId}/subtasks

Paged TaskDto list. Same sort/filter whitelist as the board list (minus the parent scope, which is the path).


Update metadata

PUT /api/tasks/{id}

Auth: TaskEdit. Partial: omit a field to leave it unchanged.

Field Notes
title, description
assignedToId Sentinel 0 clears assignee (null cannot mean “clear” under PATCH-style semantics)
specDocId
priority, sortOrder
kind, subsystem, isLaunchBlocker Re-triage allowed. Cannot clear kind/subsystem back to null via this request — null means leave unchanged

Status is not on this body. Use the status endpoint.

Response: TaskDto summary.


Status transitions

PUT /api/tasks/{id}/status

Auth: TaskEdit.

Field Type Required
status TaskStatus yes
note string no — stored as [{Status}] {note}

Observed side effects (TaskService.UpdateStatusAsync):

  1. Start while dependency-blocked: moving to InProgress when incomplete dependencies exist is allowed. A system note is added: [Warning] Started while blocked by: ….
  2. Optional note on any status change when note is supplied.
  3. On Done:
    • Downstream tasks that are now fully unblocked get a note: [Unblocked] All dependencies resolved… (status of those tasks is not auto-changed).
    • If this task is a subtask and all siblings are Done/Cancelled, the parent gets [Subtasks Complete]… — parent is not auto-completed.
  4. Publishes TaskStatusChangedEvent.

Moving to Done while still dependency-blocked does not add the same warning note as InProgress (only the InProgress branch writes that warning).


Notes

POST /api/tasks/{taskId}/notes
GET  /api/tasks/{taskId}/notes

Auth: edit for POST, view for GET.

Notes are the activity log — status changes, unblock signals, human commentary, client-report recurrences. Search indexes note bodies (#426).

GET supports paging, sort (createdAtUtc default desc, id, createdById), and filters (createdById, content contains).


Dependencies

POST   /api/tasks/{taskId}/dependencies
DELETE /api/tasks/{taskId}/dependencies

Auth: TaskManageDependencies.

Body (both):

{ "dependsOnTaskId": 26 }

Semantics: taskId depends on dependsOnTaskId — taskId is blocked until that dependency is Done or Cancelled.

Rejected (POST → 400, message names the cases):

  • Self-dependency
  • Duplicate edge
  • Cycle — BFS from dependsOnTaskId walking upstream edges; if taskId is reachable, the add is refused

DELETE returns 404 if the edge is absent.

There is no separate “list dependencies” route; they appear on GET /api/tasks/{id} as blockedBy / blocks.


Search

GET /api/projects/{projectId}/tasks/search

Auth: TaskView.

Parameter Notes
query or q Required; empty/whitespace → 400
includeCompleted Default false. Use true when checking duplicates before filing
page / pageSize Same paging caps

Matches title, description, and note content via case-insensitive ILIKE. Title hits rank above body hits; then updatedAtUtc desc.

Unlike list, search is not limited to top-level tasks — subtasks can appear.

Why it exists: paging task_list at pageSize 200 cannot prove absence. “Search before filing” is only enforceable if the API can look at the whole board including notes (where intent often lives).

Live probe: query=classification&includeCompleted=true returns task #273 among others.


Filtering, sorting, untriaged

Sort whitelist (sort + dir)

sortOrder (service default) · title · status · priority · kind · subsystem · createdAtUtc · updatedAtUtc · noteCount

Unknown sort keys fall back to the map default. Tie-break: id.

(The SPA UI descriptor defaults the board view to updatedAtUtc desc; the API default without sort remains sortOrder.)

Filter bag

Passed as filter[field]=value (URL-encode brackets).

Key Values Behaviour
status enum name
priority enum name
assignedToId long Also available as named query param
kind enum name or none nonekind IS NULL (untriaged)
subsystem enum name or none nonesubsystem IS NULL
isLaunchBlocker true / false

AddEnumOrNone is required for untriaged: a plain enum filter that fails to parse drops the predicate and would return the whole board as if it were “untriaged” — a wrong answer wearing a right answer’s clothes. Literal none is handled explicitly.

Live probes (project 1, 2026-08-13):

Query totalCount
filter[kind]=none&includeCompleted=true 195 (mostly old Done rows never backfilled)
filter[kind]=Bug (active default) 72
filter[isLaunchBlocker]=true (active) 11

includeCompleted is a scope switch, not a filter field — it does not appear in the filter map.

Named query params status, priority, assignedToId still work alongside the bag.


Delete

DELETE /api/tasks/{id}

Auth: TaskDelete.

Soft delete only (IsDeleted = true). Global query filters hide deleted rows. No hard delete path on this surface.


Client reports (SPA → board)

POST /api/projects/{projectId}/client-reports

Auth: TaskCreate.

The SPA files bugs, feature requests, and crashes into the same task table. It does not invent a second issue tracker.

Request (CreateClientReportRequest)

Field Type Notes
kind ClientReportKind Bug, Feature, Crash
summary string Required (trimmed non-empty)
detail string Repro steps or component stack
route string SPA path, e.g. /ai/models
stack string Truncated server-side (~4000)
userAgent string SPA always sends navigator.userAgent
priority TaskPriority? Honoured for Bug/Feature only

What the server derives (not required from the client)

Derived Rule
Task kind Feature → Feature; Bug or Crash → Bug
Task subsystem Always Spa (honest coarse default; triager can move it)
Task priority Crash → always High; else request priority or Normal
Task title [{prefix}] {route} — {summary head} where prefix is [Feature] / [UI Crash] / [UI Bug], route defaults to app, summary head truncated to 90 chars
Task description Markdown body: route, summary, optional Detail/Stack/UA sections

Deduplication

Signature = full title. Open tasks (Todo or InProgress) matching that title prefix collapse:

  • Returns existing taskId with deduplicated: true
  • Appends a recurrence note Recurrence at {utc} — {route} only if it differs from the last note (prevents crash-loop note floods)

Response (ClientReportResult)

{ "taskId": 123, "title": "[UI Crash] /x — …", "deduplicated": false }

SPA behaviour (Atamaia.Web report.ts / ErrorBoundary)

  • Hard-coded REPORT_PROJECT_ID = 1 (Atamaia board).
  • Crash: error boundary → kind: Crash, in-session signature set so a render loop does not POST every frame.
  • API errors: auto-file when the SPA composed the bad request (5xx/network; failed GET/DELETE; non-field 4xx). Skip 401/403/404/409 and field-validation failures on writes. Never report failures of /client-reports itself.
  • Failures are swallowed on purpose — a failed report must not replace the crash screen. That is why classification is derived on the server rather than required from the client: a 400 here would be invisible and bug filing would silently stop.

MCP tools vs REST

MCP wraps the same ITaskService (API-first). Tools in TaskTools.cs:

MCP tool REST analogue Differences
task_list GET …/tasks No sort/dir/priority/assignedToId/filter bag — status + includeCompleted + paging only
task_search GET …/tasks/search Same core behaviour; query required as tool arg
task_get GET /api/tasks/{id} Full detail
task_create POST …/tasks kind + subsystem positional non-nullable in schema; no specDocId on the tool
task_update PUT /api/tasks/{id} Includes kind/subsystem/isLaunchBlocker/specDocId
task_update_status PUT …/status Same
task_add_note POST …/notes Same
task_add_dependency POST …/dependencies Returns bool; no remove tool

REST-only (no MCP tool in TaskTools):

  • DELETE /api/tasks/{id}
  • DELETE …/dependencies
  • GET …/notes (paged)
  • GET …/subtasks
  • POST …/client-reports
  • List filter bag / sort / priority / assignee filters

MCP returns service DTOs directly (not necessarily the outer ApiEnvelope shape the HTTP client sees). Create/update return summary TaskDto, same as REST mutations.

Agent-executor tool TaskCreateTool also requires kind/subsystem in JSON schema and validates in ExecuteAsync (local models omit required fields routinely).


Endpoint catalog

GET    /api/projects/{projectId}/tasks
POST   /api/projects/{projectId}/tasks
GET    /api/projects/{projectId}/tasks/search
POST   /api/projects/{projectId}/client-reports
GET    /api/tasks/{id}
PUT    /api/tasks/{id}
DELETE /api/tasks/{id}
PUT    /api/tasks/{id}/status
GET    /api/tasks/{parentTaskId}/subtasks
GET    /api/tasks/{taskId}/notes
POST   /api/tasks/{taskId}/notes
POST   /api/tasks/{taskId}/dependencies
DELETE /api/tasks/{taskId}/dependencies

Related but not this board: GET/POST /api/agent/runs/{runId}/tasks, PATCH /api/agent/tasks/{id} — agent-run work items.


TaskDto fields (list / mutation response)

Field Notes
id, guid D3: long + guid
projectId, projectKey
parentTaskId null = top-level
assignedToId, assignedToName
specDocId, specDocPath
title, description
status, priority enums as names on the wire
kind, subsystem nullable; omitted when null in JSON
isLaunchBlocker
sortOrder
subTaskCount, noteCount
isBlocked computed from open dependencies
createdAtUtc, updatedAtUtc