Skip to content

Update a work item

PATCH/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{pk}/

Update a work item. Every field is optional and the update is partial — send only what changes, and everything you leave out stays exactly as it was.

Omitting a field is not the same as sending null. Omit target_date and the existing due date is preserved; send "target_date": null and it is cleared.

There is no PUT in v2. A PUT to this path returns 405 method_not_allowed.

Path Parameters

slug:requiredstring

The workspace slug. It appears in your Plane URLs — in https://app.plane.so/my-team/projects/, the slug is my-team.

project_id:requiredstring (uuid)

The project the work item belongs to.

pk:requiredstring (uuid)

The work item's UUID. This lookup is UUID-only — a PROJ-142 identifier here returns 404.

Body Parameters

name:optionalstring

New title. Maximum 255 characters.

description_html:optionalstring

Replacement rich-text body as HTML. Sanitized on the way in; content that can't be sanitized is rejected with a 400. Not part of the read shape, so it won't appear in the response.

priority:optionalstring

One of urgent, high, medium, low, none.

state_id:optionalstring (uuid)

Move the work item to another state of the same project. This is the field workflow rules police — see Workflow rules.

state:optionalstring

The target state's name instead of its id, for example Done. Matched case-insensitively within the project. Write-only. Sending both state and state_id is a 400.

type_id:optionalstring (uuid)

Change the work item type. Send null to make the work item untyped.

type:optionalstring

The type's name instead of its id, for example Bug. Write-only.

parent_id:optionalstring (uuid)

Re-parent the work item. The parent must be in the same workspace and may be in a different project. Send null to detach it and make it top-level.

parent:optionalstring

The parent's identifier instead of its id, for example PROJ-118. Write-only.

assignee_ids:optionalarray of string (uuid)

Replaces the whole assignee set — it is not additive. Send the complete list you want, [] to unassign everyone, or omit the field to leave assignees untouched.

assignees:optionalarray of string (email)

Member email addresses instead of ids. Same replace-the-set semantics. Every address must belong to an active, assignable project member. Write-only.

label_ids:optionalarray of string (uuid)

Replaces the whole label set. Send [] to strip all labels, or omit the field to leave them untouched.

labels:optionalarray of string

Label names instead of ids. Same replace-the-set semantics. A name that exists at both project and workspace level is ambiguous and returns a 400 telling you to use label_ids. Write-only.

estimate_point_id:optionalstring (uuid)

Change the estimate point, from the project's active estimate system. Send null to clear the estimate.

estimate:optionalstring

The estimate point's value instead of its id, for example 5. Write-only.

start_date:optionalstring (date)

Planned start, or null to clear it. Must not be after target_date.

target_date:optionalstring (date)

Planned due date, or null to clear it.

external_id:optionalstring

Your system's identifier for this work item. Maximum 255 characters. Filterable on List work items, but not returned on reads.

external_source:optionalstring

The system external_id came from, for example github or jira. Maximum 255 characters.

Fields you cannot patch

id, identifier, sequence_id, is_draft, archived_at, created_at, and created_by_id are read-only — sending them has no effect. In particular, archiving is not a PATCH on archived_at; use Archive a work item.

Custom property values go through a separate custom_fields object on the request body, keyed by property name — its shape follows the work item's type, so it is not declared in the OpenAPI schema. See the type's schema endpoint. On a PATCH only the properties you submit are replaced; untouched properties keep their values and are never required-checked.

Scopes

projects.work_items:write

Errors

StatusCodeCause
400validation_errorA bad priority; start_date after target_date; an unresolvable or ambiguous state/type/parent/assignees/labels/estimate; both a name and its *_id; or an id from another project or workspace.
401unauthorizedMissing or invalid credentials.
403forbiddenYour role or token scope can't edit this work item.
403workflow_transition_deniedA workflow rule forbids the requested state transition.
404resource_not_foundNo such work item, or it's outside your project or tenant.
409conflictThe write collides with a uniqueness or protected-resource constraint.
429rate_limitedThrottled. Honor the Retry-After header before retrying.
Update a work item
bash
curl -X PATCH \
  "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13/" \
  -H "X-Api-Key: $PLANE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "priority": "urgent",
  "state": "In Review",
  "assignees": ["ana@example.com", "rk@example.com"],
  "target_date": null
}'
Response200
json
{
  "id": "8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13",
  "name": "Fix login redirect loop",
  "identifier": "PROJ-142",
  "sequence_id": 142,
  "priority": "urgent",
  "state_id": "5d2a91b7-64c0-4f38-b9e2-0a3f7c6d8149",
  "type_id": "2d9d1a97-5c6f-4a1e-9d5b-8c2f7e30b6a4",
  "assignee_ids": ["16c61a3a-512a-48ac-b0be-b6b46fe6f430", "9b7f4c53-2d18-4a6e-8c05-1f3e7d9a2b64"],
  "label_ids": ["c1b8f3d6-9a44-4e12-8f7a-2b6d5c9e1a03"],
  "parent_id": null,
  "start_date": "2026-01-12",
  "target_date": null,
  "is_draft": false,
  "archived_at": null,
  "created_at": "2026-01-14T09:22:41.478363Z",
  "created_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
  "custom_fields": {
    "severity": {
      "id": "b52e7c18-4d3f-4a90-8e61-0f7a3c9d2b45",
      "value": "Sev-1",
      "value_detail": {
        "id": "7c0a5f39-2e84-4b17-9a6c-1d8e4f2b60c9",
        "name": "Sev-1",
        "logo_props": {}
      }
    }
  }
}
Response403
json
{
  "type": "https://api.plane.so/errors/workflow_transition_denied",
  "title": "Forbidden",
  "status": 403,
  "code": "workflow_transition_denied",
  "detail": "State transition is not allowed."
}

Omitted, empty, and null

These three are different, and the difference matters most on the list fields:

Request bodyResult
{} — field omittedAssignees unchanged.
{"assignee_ids": []}All assignees removed.
{"assignee_ids": ["…"]}Assignees become exactly that list.
{"parent_id": null}Parent detached.

assignee_ids and label_ids replace the set rather than adding to it, so to add one assignee you send the existing ids plus the new one. Read the work item first if you don't already hold the current list.

Workflow rules can reject a transition

If the project runs workflow rules, a state change they don't permit returns 403 with workflow_transition_denied and nothing is written. That is a different situation from forbidden, which means your role or token scope can't edit the work item at all — branch on the code, not the status.

Only a state change can trigger it. Updating a name or a due date never runs the transition check.