Update a work item
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:requiredstringThe 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:optionalstringNew title. Maximum 255 characters.
description_html:optionalstringReplacement 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:optionalstringOne 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:optionalstringThe 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:optionalstringThe 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:optionalstringThe 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 stringLabel 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:optionalstringThe 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:optionalstringYour system's identifier for this work item. Maximum 255 characters. Filterable on List work items, but not returned on reads.
external_source:optionalstringThe 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
| Status | Code | Cause |
|---|---|---|
400 | validation_error | A 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. |
401 | unauthorized | Missing or invalid credentials. |
403 | forbidden | Your role or token scope can't edit this work item. |
403 | workflow_transition_denied | A workflow rule forbids the requested state transition. |
404 | resource_not_found | No such work item, or it's outside your project or tenant. |
409 | conflict | The write collides with a uniqueness or protected-resource constraint. |
429 | rate_limited | Throttled. Honor the Retry-After header before retrying. |
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
}'import requests
response = requests.patch(
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b"
"/work-items/8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13/",
headers={"X-Api-Key": "your-api-key"},
json={
"priority": "urgent",
"state": "In Review",
"assignees": ["ana@example.com", "rk@example.com"],
"target_date": None,
},
)
print(response.json())const response = await fetch(
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b" +
"/work-items/8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13/",
{
method: "PATCH",
headers: {
"X-Api-Key": "your-api-key",
"Content-Type": "application/json",
},
body: JSON.stringify({
priority: "urgent",
state: "In Review",
assignees: ["ana@example.com", "rk@example.com"],
target_date: null,
}),
}
);
const data = await response.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": {}
}
}
}
}{
"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 body | Result |
|---|---|
{} — field omitted | Assignees 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.

