Tasks
Create, update, move, and finalize tasks via the REST API.
A task is the core unit of work. Every task belongs to exactly one workspace and one stage, optionally to a project, and may have assignees, tags, subtasks, comments, attachments, and time entries.
Endpoints
| Method | Path | Notes |
|---|---|---|
GET | /api/v1/tasks | List with filters. Cursor pagination. |
POST | /api/v1/tasks | Create. Requires workspaceId. Accepts Idempotency-Key. |
GET | /api/v1/tasks/{id} | Single task with assignees, tags, type, project, stage, subtasks, custom fields. |
PATCH | /api/v1/tasks/{id} | Update fields (incl. custom fields), or move by sending workspaceId / stageId / sortOrder. |
DELETE | /api/v1/tasks/{id} | Delete the task. |
POST | /api/v1/tasks/{id}/duplicate | Create an open copy of the task. No body. Accepts Idempotency-Key. |
Subtasks have their own nested resource.
Task model
{
"id": "t1…",
"organizationId": "...",
"number": 128,
"workspaceId": "...",
"stageId": "...",
"sortOrder": 2,
"projectId": null,
"taskTypeId": null,
"subject": "Wire the OAuth login button",
"descriptionHtml": "<p>Hook the button up to Better Auth.</p>",
"priority": "high",
"expectedStartDate": null,
"expectedEndDate": null,
"estimatedHours": 4,
"finalizedAt": null,
"finalizationDate": null,
"blockedAt": null,
"blockReason": null,
"blockedById": null,
"assignees": [{ "user": { "id": "...", "name": "Alex" } }],
"tags": [{ "tag": { "id": "...", "name": "backend" } }],
"taskType": null,
"project": null,
"stage": { "id": "...", "name": "Doing", "isFinalizing": false },
"subtasks": [],
"customFields": [
{ "definitionId": "cf1…", "name": "Severity", "type": "select", "value": "opt2…", "optionLabel": "High" },
{ "definitionId": "cf2…", "name": "Estimate", "type": "number", "value": 4.5 }
]
}| Field | Type | Notes |
|---|---|---|
number | integer | Read-only. Human-readable task number, unique inside the organization and starting at 1. Assigned on creation and never changed; a deleted task's number is never reused, so gaps are expected. It is the number members see in the app. Not settable and not filterable. |
priority | enum | low · normal · high · urgent |
descriptionHtml | string | Sanitized HTML produced by the Tiptap editor. Inline image URLs must be app-hosted (no base64). |
estimatedHours | decimal | null | Hours planned; reported time is computed separately from TimeEntry rows. |
finalizedAt | ISO date | null | Set when the task is closed; auto-set when moved into a finalizing stage. |
blockedAt | ISO date | null | Set while the task is blocked (read-only). Blocking is done in the app, not via the API. |
blockReason | string | null | Why the task is blocked, shown while blockedAt is set. |
blockedById | uuid | null | The member who blocked the task. |
Listing tasks
GET /api/v1/tasks?workspaceId=...&priority=high&limit=50| Param | Type | Notes |
|---|---|---|
workspaceId | UUID | Filter to one workspace. |
stageId | UUID | Filter to one stage. |
projectId | UUID | Project filter. Combines with project visibility. |
taskTypeId | UUID | Task-type filter. |
assigneeId | UUID | Tasks assigned to a specific member. |
tagId | UUID | Tasks carrying a specific tag. |
priority | enum | One of the priority values. |
query | string | Case-insensitive search on subject. |
finalized | boolean | Omit for all tasks; false returns only open tasks, true only finalized. |
orderBy | enum | createdAt (default) or dueDate (by expectedEndDate ascending, undated last). |
cursor | UUID | From a previous response. |
limit | int 1–100 | Default 50. |
Returns { data, pagination: { nextCursor } }. By default tasks are ordered by (createdAt DESC, id DESC) so newer work surfaces first; pass orderBy=dueDate to sort by expectedEndDate ascending (tasks without a due date last).
Creating a task
POST /api/v1/tasks
Content-Type: application/json
Idempotency-Key: ...
{
"workspaceId": "1f3c…",
"subject": "Wire the OAuth login button",
"priority": "high",
"estimatedHours": 4
}Required: workspaceId, subject.
The task is placed in the workspace's leftmost stage (lowest sortOrder) — you cannot pass stageId on create. Within that stage it's inserted at the top (sortOrder: 0); any tasks already there shift down one position, keeping their order relative to one another. If the leftmost stage is the finalizing stage, the task is finalized on creation.
| Optional field | Type | Notes |
|---|---|---|
descriptionHtml | string | ≤ 50 000 chars. |
projectId | UUID | null | Project access enforced. The project must be open — a completed or cancelled project is rejected with 400. |
taskTypeId | UUID | null | Must belong to the org. |
priority | enum | Defaults to normal. |
expectedStartDate / expectedEndDate | ISO date | null | — |
estimatedHours | number ≥ 0 | ≤ 99 999.99. |
assigneeIds | UUID[] | Must be org members; for a task with a projectId, each must also have access to that project (the project's members, plus org owners/admins). Triggers in-app + email notifications. |
tagIds | UUID[] | Must belong to the org. |
customFields | array | Values to set on the new task — see Custom fields. |
Response: 201 Created with the full task (including its customFields).
Updating or moving a task
PATCH /api/v1/tasks/{id}
{ "subject": "OAuth wiring", "priority": "normal" }All fields are optional. Sending any of workspaceId, stageId, or sortOrder switches the request to move mode:
- Move within the same stage (
sortOrderonly) — reorders the task. - Move to a new stage —
stageIdmust belong to the same (or target) workspace. - Cross-workspace move — pass
workspaceId(and optionallystageId). IfstageIdis omitted, the task lands in the target workspace's leftmost stage. - Moving into a finalizing stage auto-sets
finalizedAt/finalizationDateto today (in the calling user's timezone). - Moving out of a finalizing stage clears them.
- Moving into a finalizing stage is rejected with
412while a timer is still running on the task or on any of its subtasks — a finalized task is read-only, so its timers must be stopped first. Stop them with the app's timer control, then retry the move.
A move on a project-restricted task requires project edit access; otherwise the response is 404.
Subtask estimates and due dates limit two task fields. Both return 409 Conflict:
estimatedHourscannot change while any subtask has an estimate greater than0. The task estimate is then the sum of the subtask estimates — change the subtask estimates instead.expectedEndDatecannot be earlier than the latest subtask due date, and cannot benullwhile any subtask has a due date. Finalized subtasks count.
Sending the current value again is not a change and succeeds. See Estimate and due date.
Custom fields
Tasks carry your organization's custom field values. Discover the field ids, types, and select options with GET /api/v1/custom-fields first — you need them to write values.
Reading. Every task's customFields array holds one entry per set field (fields with no value are omitted):
"customFields": [
{ "definitionId": "cf1…", "name": "Severity", "type": "select", "value": "opt2…", "optionLabel": "High" },
{ "definitionId": "cf2…", "name": "Estimate", "type": "number", "value": 4.5 },
{ "definitionId": "cf3…", "name": "Due contract", "type": "date", "value": "2026-08-01T00:00:00.000Z" }
]value is typed by type: a string for text / longText, a number for number, an ISO date-time for date, a boolean for boolean, and the selected option's id for select (with optionLabel alongside).
Writing. Send customFields on POST or PATCH. Each entry names a definitionId and a value object keyed by type. Set the type's field to store a value, or to null to clear it:
"customFields": [
{ "definitionId": "cf1…", "value": { "type": "select", "optionId": "opt2…" } },
{ "definitionId": "cf2…", "value": { "type": "number", "number": 4.5 } },
{ "definitionId": "cf3…", "value": { "type": "date", "date": "2026-08-01T00:00:00.000Z" } },
{ "definitionId": "cf4…", "value": { "type": "text", "text": null } }
]The value's field per type is text (text / longText), number, date (ISO string), boolean, or optionId (select). On PATCH, a null value removes the stored value; on POST it is simply a no-op. A definitionId may appear at most once per request.
| Failure | Code |
|---|---|
definitionId from another org (or unknown) | 404 |
value.type doesn't match the field's type | 400 |
select optionId not on that field | 400 |
Same definitionId twice in one request | 422 |
Duplicating a task
POST /api/v1/tasks/{id}/duplicateReturns 201 with { "data": <task> } — the new task, in the same shape as GET /api/v1/tasks/{id}.
- The subject gets a localized suffix:
" - copy"(or" - cópia"for a Portuguese API key owner). - Copied: fields, assignees, tags, custom field values, subtask groups, and subtasks with their assignees.
- Not copied: comments, attachments, time entries, and history.
- The copy is always open. Subtasks start unfinalized.
- The copy goes to the top of the source stage. If that stage is a finalizing stage, the copy goes to the first non-finalizing stage.
- Assignees who no longer have access to the task's project are left out.
Deleting a task
DELETE /api/v1/tasks/{id}Returns { "data": { "success": true } }. Cascade-deletes subtasks, comments, attachments, and time entries owned by the task.
Common precondition failures
| Code | Cause |
|---|---|
404 | Cross-org id, missing workspace, missing task-type, or project access denied. |
412 | Target workspace has no stages — you cannot place a task in it. |
412 | The task is finalized or blocked and therefore read-only — PATCH and DELETE are rejected until it is reopened / unblocked (both are app-only actions). |
412 | A timer is still running on the task or one of its subtasks, and the PATCH would finalize it by moving it into a finalizing stage. Stop the timer first. |
409 | estimatedHours changed while the estimate is the sum of the subtask estimates. |
409 | expectedEndDate is earlier than the latest subtask due date, or cleared while a subtask has a due date. |
400 | stageId belongs to a different workspace than the target, or an assignee is not a member of the task's project. |
400 | projectId points at a closed project (completed / cancelled). Closed projects are outside the billing cycle, so no task may be linked to one. Clearing the link (projectId: null) is always allowed. |