---
title: "Update a task"
method: PUT
path: "/tasks/{id}"
tags: ["tasks"]
---

# Update a task

`PUT /tasks/{id}`

## Request

- Base URL: `https://api.onplana.com/api`
- URL: `https://api.onplana.com/api/tasks/{id}`
- Auth: one of:
  - HTTP bearer
  - API key in header `Authorization`

Needs task.edit.any, or task.edit.own on a task the caller holds or collaborates on. A caller whose only right is collaborating cannot change assigneeId (403 COLLABORATOR_FIELD_REFUSED); collaborators themselves change through PUT /tasks/{id}/collaborators. Assigning the task to one of its collaborators takes them off the collaborator list.

## Path parameters

- `id` string, required

## Headers

- `X-Organization-Id` string, required

## Request body

- object: Any subset of task fields to update
  - `title` string
  - `description` string
  - `status` 'TODO' | 'IN_PROGRESS' | 'REVIEW' | 'DONE' | 'BLOCKED'
  - `priority` 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'
  - `assigneeId` string, nullable
  - `dueDate` string, date-time, nullable
  - `estimatedHours` number, nullable
  - `progress` integer
  - `originalDurationMinutes` integer, nullable: Planned duration in working minutes. Setting it on a task with a startDate moves the dueDate; sending dates too lets the dates win
  - `remainingDurationMinutes` integer, nullable: Working minutes left; defaults to original x (1 - progress); DONE forces 0
  - `actualStart` string, date, nullable: Stamped from startDate the first time the task leaves TODO or gains progress; settable
  - `optimisticDurationMinutes` integer, nullable: Best case in working minutes, null to clear. Checked only when sent: at most the duration the task has after this write (the remaining duration once started)
  - `pessimisticDurationMinutes` integer, nullable: Worst case in working minutes, null to clear. Checked only when sent: at least the duration after this write, and at least the optimistic value
  - `sprintId` string, nullable
  - `epicId` string, nullable

## Response `200`

Updated task

- Task
  - `id` string, uuid, required
  - `title` string, required
  - `status` 'TODO' | 'IN_PROGRESS' | 'REVIEW' | 'DONE' | 'BLOCKED', required
  - `priority` 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL', required
  - `projectId` string, uuid, required
  - `assigneeId` string, uuid, nullable
  - `dueDate` string, date-time, nullable
  - `estimatedHours` number, nullable
  - `progress` integer, required
  - `originalDurationMinutes` integer, nullable: Planned duration in working minutes; a day is the project calendar hoursPerDay x 60 (default 480)
  - `remainingDurationMinutes` integer, nullable: Working minutes left; 0 when DONE
  - `actualStart` string, date, nullable: Day work began (YYYY-MM-DD)
  - `isManual` boolean, nullable: For a task with subtasks: true keeps its own dates, false takes its start and due dates from its subtasks, null follows the project's parentDatesFromSubtasks. Read only while the server switch parent_date_rollup_enabled is on.
  - `datesFromSubtasks` boolean: True when this task's start and due dates come from its subtasks right now (it has subtasks, is not a milestone or held by a live Planner sync, and its isManual or its project's default says so); a change to its dates, durations or constraint is then refused with PARENT_DATES_FROM_SUBTASKS. False whenever the server switch parent_date_rollup_enabled is off. On GET /tasks, /tasks/mine, /tasks/delegated-to-agents and /projects/{id}/tasks; the same flag the MCP task tools return.
  - `optimisticDurationMinutes` integer, nullable: Three-point estimate, best case, in working minutes. At most the task's duration (the remaining duration once started) when written; used by schedule risk runs. Null: the run's default range applies
  - `pessimisticDurationMinutes` integer, nullable: Three-point estimate, worst case, in working minutes. At least the task's duration (the remaining duration once started) when written
  - `collaborators` TaskCollaborator[]: People working on the task besides its one assignee, oldest first (at most 20). Present on task reads that load them; change them with PUT /tasks/{id}/collaborators.
    - `id` string, required: User id
    - `name` string, nullable, required
    - `avatar` string, nullable, required
    - `isAgentPersona` boolean, required: An AI agent member of the organization
  - `myRole` 'collaborator': GET /tasks/mine only: present on a row the caller collaborates on but does not hold. Such a row also carries `assignee`; a row the caller holds carries neither.
  - `assignee` object, nullable: GET /tasks/mine only, beside myRole: who holds the task
    - `id` string
    - `name` string, nullable

## Other responses

- `400`: Validation error
- `401`: Unauthorized - missing or invalid JWT
- `403`: Forbidden - insufficient role

---

[API](https://skmtc.dev/onplana/apis/onplana-api.md) · [All operations](https://skmtc.dev/onplana/apis/onplana-api/llms.txt) · [OpenAPI document](https://skmtc.dev/onplana/apis/onplana-api/revisions/c3d6c89e9dc8?raw)
