Tasks API
Task Workspace
Create tasks, filter work, and manage reusable task labels
These routes back the PrimeCal task workspace. They are all scoped to the authenticated user and include task CRUD, label management, the Focus view, auto-scheduling, delegation, and dependencies.
Authentication and Permissions
- All routes on this page require authentication.
- Task and label ownership is scoped to the current user.
- Task label routes are available under both
/api/tasks/labelsand legacy/api/task-labels.
Endpoint Reference
Tasks
| Method | Path | Purpose | Request or query | Auth | Source |
|---|---|---|---|---|---|
POST | /api/tasks | Create a task. | Body: task fields | JWT or user API key | tasks/tasks.controller.ts |
GET | /api/tasks | List tasks with filters. | Query: status,priority,search,dueFrom,dueTo,labelIds,sortBy,sortDirection,page,limit | JWT or user API key | tasks/tasks.controller.ts |
GET | /api/tasks/focus | Today's routine-instantiated and due tasks, schedule-ordered, excluding tasks blocked by an incomplete dependency. | Query: date (optional YYYY-MM-DD, defaults to today UTC) | JWT or user API key | tasks/tasks.controller.ts |
GET | /api/tasks/:id | Get one task. | Path: id | JWT or user API key | tasks/tasks.controller.ts |
PATCH | /api/tasks/:id | Update one task. | Path: id, body: partial task fields | JWT or user API key | tasks/tasks.controller.ts |
DELETE | /api/tasks/:id | Delete one task. | Path: id | JWT or user API key | tasks/tasks.controller.ts |
POST | /api/tasks/:id/auto-schedule | Find the task's next open calendar slot (using duration, priority, context, and preferred window) and set its due date/time to that slot. | Path: id | JWT or user API key | tasks/tasks.controller.ts |
PATCH | /api/tasks/:id/auto-schedule/enable | Turn on auto-scheduling for a task. | Path: id | JWT or user API key | tasks/tasks.controller.ts |
PATCH | /api/tasks/:id/auto-schedule/disable | Turn off auto-scheduling for a task. | Path: id | JWT or user API key | tasks/tasks.controller.ts |
POST | /api/tasks/:id/labels | Replace or extend task labels. | Path: id, body: labelIds,inlineLabels | JWT or user API key | tasks/tasks.controller.ts |
DELETE | /api/tasks/:id/labels/:labelId | Remove one label from a task. | Path: id,labelId | JWT or user API key | tasks/tasks.controller.ts |
POST | /api/tasks/:id/accept-assignment | Accept a task delegated to you. Only the current assignee may accept. | Path: id | JWT or user API key | tasks/tasks.controller.ts |
POST | /api/tasks/:id/bounce-assignment | Bounce a delegated task back to unassigned. Only the current assignee may bounce it. | Path: id, body: reason (optional) | JWT or user API key | tasks/tasks.controller.ts |
POST | /api/tasks/:id/dependencies | Mark this task as depending on another owned task. | Path: id, body: dependsOnTaskId | JWT or user API key | tasks/tasks.controller.ts |
DELETE | /api/tasks/:id/dependencies/:dependsOnTaskId | Remove a dependency link. | Path: id,dependsOnTaskId | JWT or user API key | tasks/tasks.controller.ts |
Task Checklist Items
Sub-steps inside a single task — distinct from task dependencies (which link two separate tasks) and from routine templates (which define recurring tasks). Use this for a one-off task with its own steps, e.g. "Plan birthday party" → "book venue", "order cake", "send invites".
| Method | Path | Purpose | Request or query | Auth | Source |
|---|---|---|---|---|---|
GET | /api/tasks/:id/checklist-items | List a task's checklist items, ordered. | Path: id | JWT or user API key | tasks/tasks.controller.ts |
POST | /api/tasks/:id/checklist-items | Add a checklist item. Returns the full updated checklist. | Path: id, body: title | JWT or user API key | tasks/tasks.controller.ts |
PATCH | /api/tasks/:id/checklist-items/:itemId | Rename, reorder, or check off an item. Returns the full updated checklist. | Path: id,itemId, body: partial title,isDone,order | JWT or user API key | tasks/tasks.controller.ts |
DELETE | /api/tasks/:id/checklist-items/:itemId | Delete a checklist item. Returns the full updated checklist. | Path: id,itemId | JWT or user API key | tasks/tasks.controller.ts |
Focus Sessions
A Focus Session is an optional Pomodoro-style timer against a task — a session's accumulatedSeconds is banked server-side on every pause/resume/complete/abandon, so elapsed time is never trusted from the client. Only one session may be active/paused per user at a time.
| Method | Path | Purpose | Request or query | Auth | Source |
|---|---|---|---|---|---|
POST | /api/focus-sessions | Start a session against a task the caller owns or is assigned to. 409 if the caller already has an active/paused session; 404 if the task doesn't exist or isn't accessible to the caller. | Body: taskId,plannedDurationMinutes? | JWT or user API key | tasks/focus-sessions.controller.ts |
GET | /api/focus-sessions/active | The caller's current active/paused session, with task loaded, or null. | None | JWT or user API key | tasks/focus-sessions.controller.ts |
GET | /api/focus-sessions/history | Paginated completed/abandoned sessions, newest endedAt first, with task loaded. | Query: limit (default 20, max 100), offset (default 0) | JWT or user API key | tasks/focus-sessions.controller.ts |
POST | /api/focus-sessions/:id/pause | Bank elapsed active time and pause. 409 if the session isn't active. | Path: id | JWT or user API key | tasks/focus-sessions.controller.ts |
POST | /api/focus-sessions/:id/resume | Resume a paused session. 409 if the session isn't paused. | Path: id | JWT or user API key | tasks/focus-sessions.controller.ts |
POST | /api/focus-sessions/:id/complete | Bank any remaining active time, mark completed, and optionally mark the linked task done. | Path: id, body: completeTask? | JWT or user API key | tasks/focus-sessions.controller.ts |
POST | /api/focus-sessions/:id/abandon | Bank any remaining active time and mark abandoned. The linked task is left untouched. | Path: id | JWT or user API key | tasks/focus-sessions.controller.ts |
All :id routes resolve ownership via a WHERE id = :id AND userId = :userId lookup and return 404 on any mismatch — never 403 — so a non-owner can't distinguish "not yours" from "doesn't exist."
Task Labels
| Method | Path | Purpose | Request or query | Auth | Source |
|---|---|---|---|---|---|
GET | /api/tasks/labels | List task labels. | None | JWT or user API key | tasks/task-labels.controller.ts |
POST | /api/tasks/labels | Create a task label. | Body: name,color | JWT or user API key | tasks/task-labels.controller.ts |
PATCH | /api/tasks/labels/:id | Update a task label. | Path: id, body: partial label fields | JWT or user API key | tasks/task-labels.controller.ts |
DELETE | /api/tasks/labels/:id | Delete a task label. | Path: id | JWT or user API key | tasks/task-labels.controller.ts |
GET | /api/task-labels | Legacy alias for label listing. | None | JWT or user API key | tasks/task-labels.controller.ts |
POST | /api/task-labels | Legacy alias for label creation. | Body: name,color | JWT or user API key | tasks/task-labels.controller.ts |
PATCH | /api/task-labels/:id | Legacy alias for label update. | Path: id | JWT or user API key | tasks/task-labels.controller.ts |
DELETE | /api/task-labels/:id | Legacy alias for label deletion. | Path: id | JWT or user API key | tasks/task-labels.controller.ts |
Request Shapes
Task payload
CreateTaskDto
title: required, max 240 charsbody: optional, max 8000 charsbodyFormat: optional, currently onlymarkdowncolor: optional 6-digit hex colorpriority: optional enumhigh|medium|lowstatus: optional enumtodo|in_progress|doneplace: optional, max 255 charsdueDate: optional ISO date stringdueEnd: optional ISO date stringdueTimezone: optional, max 100 charsassigneeId: optional integerdurationMinutes: optional integer,1..1440— required on a task before it can be auto-scheduledautoScheduled: optional booleanpreferredWindowStartHour/preferredWindowEndHour: optional integer,0..23— overrides the default 9–18 auto-scheduling search window for this taskcontext: optional enumdeep_work|errand|admin|kid_safe|low_energy— used by auto-scheduling (deep_workprefers a roomier gap)labelIds: optional unique integer array, max 12 items
Defaults:
bodyFormat:markdowncolor:#eab308priority:mediumstatus:todo
Delegation and dependency payloads
BounceTaskAssignmentDto.reason: optional string, max 500 chars — logged, not persisted to a dedicated audit tableAddTaskDependencyDto.dependsOnTaskId: required positive integer
Checklist item payloads
CreateTaskChecklistItemDto
title: required, max 240 chars
UpdateTaskChecklistItemDto (all optional)
title: max 240 charsisDone: booleanorder: integer — no reorder endpoint; setorderdirectly on the item(s) you want to move
A task's checklistItems are only included on GET /api/tasks/:id (the single-task read), not on the paginated GET /api/tasks list or GET /api/tasks/focus — fetch them via the dedicated endpoints above when you need them for a list view.
Focus query
date: optional query string onGET /api/tasks/focus,YYYY-MM-DD, defaults to today (UTC)
Focus Session payloads
StartFocusSessionDto
taskId: required positive integerplannedDurationMinutes: optional integer,1..180— omit for an open-ended (count-up) session
CompleteFocusSessionDto
completeTask: optional boolean — whentrue, also calls task update to setstatus: doneon the linked task (best-effort: a failure here is logged but never prevents the session itself from completing)
GetFocusSessionHistoryQueryDto (query params on GET /api/focus-sessions/history)
limit: optional integer,1..100, default20offset: optional integer,>= 0, default0
FocusSession response shape
id, userId, taskId, status(active|paused|completed|abandoned)plannedDurationMinutes(nullable),startedAt,pausedAt(nullable),accumulatedSeconds,endedAt(nullable)task: populated only onGET /activeandGET /history(not on the mutating pause/resume/complete/abandon/start responses)
Query filters
QueryTasksDto
status: optional enumtodo|in_progress|donepriority: optional enumhigh|medium|lowsearch: optional string, max 120 charsdueFrom: optional ISO date stringdueTo: optional ISO date stringlabelIds: optional unique integer array, max 10 itemssortBy:updatedAt|createdAt|dueDatesortDirection:asc|descpage: int>= 1, default1limit: int1..100, default25
Label payloads
CreateTaskLabelDto.name: required, max 64 charsCreateTaskLabelDto.color: optional 6-digit hex colorUpdateTaskLabelsDto.labelIds: optional ids of existing labelsUpdateTaskLabelsDto.inlineLabels: optional new labels to create and attach in one call
Example Calls
Create a task
curl -X POST "$PRIMECAL_API/api/tasks" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Pack school bags",
"priority": "high",
"dueDate": "2026-03-30T18:00:00.000Z",
"dueTimezone": "Europe/Budapest",
"labelIds": [3, 7]
}'
Filter tasks
curl "$PRIMECAL_API/api/tasks?status=todo&sortBy=updatedAt&sortDirection=desc&limit=25" \
-H "Authorization: Bearer $TOKEN"
Create a label
curl -X POST "$PRIMECAL_API/api/tasks/labels" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "School",
"color": "#14b8a6"
}'
Get today's Focus list
curl "$PRIMECAL_API/api/tasks/focus?date=2026-07-05" \
-H "Authorization: Bearer $TOKEN"
Create a task with a duration and context, then auto-schedule it
curl -X POST "$PRIMECAL_API/api/tasks" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Write school newsletter",
"durationMinutes": 90,
"context": "deep_work"
}'
curl -X POST "$PRIMECAL_API/api/tasks/57/auto-schedule" \
-H "Authorization: Bearer $TOKEN"
Bounce a delegated task
curl -X POST "$PRIMECAL_API/api/tasks/58/bounce-assignment" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"reason": "Forgot which bin is recycling this week"
}'
Start, pause, and complete a Focus Session
curl -X POST "$PRIMECAL_API/api/focus-sessions" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"taskId": 57,
"plannedDurationMinutes": 25
}'
curl -X POST "$PRIMECAL_API/api/focus-sessions/9/pause" \
-H "Authorization: Bearer $TOKEN"
curl -X POST "$PRIMECAL_API/api/focus-sessions/9/resume" \
-H "Authorization: Bearer $TOKEN"
curl -X POST "$PRIMECAL_API/api/focus-sessions/9/complete" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"completeTask": true
}'
Check for an in-progress session and view history
curl "$PRIMECAL_API/api/focus-sessions/active" \
-H "Authorization: Bearer $TOKEN"
curl "$PRIMECAL_API/api/focus-sessions/history?limit=10" \
-H "Authorization: Bearer $TOKEN"
Add a dependency
curl -X POST "$PRIMECAL_API/api/tasks/60/dependencies" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"dependsOnTaskId": 59
}'
Add and complete a checklist item
curl -X POST "$PRIMECAL_API/api/tasks/60/checklist-items" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Book the venue"
}'
curl -X PATCH "$PRIMECAL_API/api/tasks/60/checklist-items/12" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"isDone": true
}'
Response and Behavior Notes
- Tasks can be linked to mirrored calendar events through the task-calendar bridge, but that linkage is not directly configured in these DTOs.
POST /api/tasks/:id/labelssupports both existing labels and inline label creation.- Task label routes are intentionally duplicated under the legacy
/api/task-labelspath for compatibility. GET /api/tasks/focusexcludesdonetasks and any task with an incomplete dependency, ordered by due date (nulls last), then routine item order, then creation time.POST /api/tasks/:id/auto-schedulerequiresdurationMinutesto be set on the task; it returns400if it isn't. If no open slot is found within the priority-based search horizon, the task's scheduling state is set tounscheduledrather than erroring.POST /api/tasks/:id/accept-assignmentandPOST /api/tasks/:id/bounce-assignmentboth return403if the caller is not the task's current assignee.POST /api/tasks/:id/dependenciesrejects self-references and the direct two-task cycle (Bdepending onAwhenAalready depends onB) with400. It does not check longer dependency chains.- Changing a task's
statusfires thetask.status_changedautomation trigger for any rule that listens for it. - All checklist-item endpoints return the task's entire updated checklist (not just the affected item), ordered by
orderthenid— simplest for a client to just replace its local list with the response. - Checklist items have no owner column of their own; every checklist endpoint first verifies the caller owns the parent task, then acts on the item.
- Focus Session elapsed time is always computed server-side from
startedAt/nowand banked intoaccumulatedSecondson every pause/complete/abandon — the client's live countdown/count-up display is a UI-only computation and is never sent back to the server. POST /api/focus-sessionsreturns409if the caller already has anactiveorpausedsession; sessions are otherwise never auto-abandoned by the server, so a session left open simply waits to be resumed.POST /api/focus-sessions/:id/pauseand/resumereturn409if the session isn't currently in the expected state (activefor pause,pausedfor resume) rather than silently no-op'ing.
Best Practices
- Use
sortBy=updatedAtand a smalllimitfor interactive task lists. - Prefer
labelIdswhen attaching known labels andinlineLabelsonly when the label truly does not exist yet. - Keep
dueTimezoneexplicit for tasks that may be mirrored or interpreted across time zones. - Set
durationMinutesand, if relevant,contextbefore callingauto-schedule— auto-scheduling only ever movesdueDate/dueEnd, it never creates the task. - Treat
bounce-assignment'sreasonas advisory context for the person reassigning the task, not a persisted audit trail. - Treat
/api/tasks/labelsas the canonical label path and/api/task-labelsas a compatibility route.