Update a state
Change a state's name, color, description, group, position, or default flag. Work items already in the state stay in it. This request edits the state and doesn't move any work items.
PATCH is partial. Send only the fields you want to change; anything you omit keeps its current value. Omitting a field is not the same as sending null.
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 state belongs to.
pk:requiredstring (uuid)The id of the state to update.
Body parameters
Every field is optional. Send only the fields you are changing.
name:optionalstringNew display name, unique within the project. Maximum 255 characters. Renaming is safe for reporting, because boards and charts use group, not name.
color:optionalstringHex color used wherever the state is rendered, for example #3f76ff. Maximum 255 characters.
description:optionalstringFree-form description of what the state means in this workflow.
group:optionalstringMove the state to a different workflow group. This changes how every work item in the state is counted by boards, charts, and cycle progress, so switching a state from started to completed retroactively changes how those work items are reported.
backlog: not yet scheduledunstarted: scheduled but not begunstarted: actively in progresscompleted: finished successfullycancelled: closed without completiontriage: awaiting intake review
sequence:optionalnumberOrdering weight within the project. Lower values sort first. Set it to reposition the state in the workflow.
is_default:optionalbooleanMake this the project's default state, the one work items land in when no state_id is supplied. A project has exactly one default, so setting this to true clears the flag on the state that held it.
external_id:optionalstringYour system's identifier for this state, for sync and import correlation. Maximum 255 characters.
external_source:optionalstringThe system external_id came from, for example github or jira. Maximum 255 characters.
Response shaping
fields:optionalstringComma-separated list of fields to return. Fields you don't request are left out of the response instead of coming back as null, so a missing key means you didn't ask for it and null means the value is null. id is always included, whether or not you name it.
Pass all to get every requestable field. An unknown field name returns 400 with the list of valid names and the closest match, so a typo is caught instead of being silently ignored.
Requestable here: color, created_at, created_by_id, description, external_id, external_source, group, id, is_default, is_triage, name, sequence, updated_at, updated_by_id.
See Sparse fields.
Scopes
projects.states:write
Errors
| Status | Code | Cause |
|---|---|---|
400 | invalid_request | A group outside the enum, or a field over its length limit. |
401 | unauthorized | Missing or invalid credentials. |
402 | payment_required | The feature this endpoint belongs to isn't enabled on your plan, or is switched off. |
403 | forbidden | Your role or token scope can't update states. |
404 | not_found | No such state, project, or workspace, or it's outside your tenant. |
406 | not_acceptable | The Accept header asks for a representation the API can't produce. |
409 | conflict | Another state in the project already uses this name. |
409 | states_managed_at_workspace | The workspace has governance turned on, so project states are read-only. See Workspace governance. |
413 | payload_too_large | The request body is over the size limit. |
415 | unsupported_media_type | The Content-Type isn't one this endpoint accepts. |
429 | rate_limited | Throttled. Honor the Retry-After header before retrying. |
Setting is_default moves the flag
A project has exactly one default state, so promoting a state with "is_default": true demotes whichever state held the flag before. To change which state is the default, promote the new one. That single request is all you need.
is_triage is not writable
is_triage is read-only. Plane creates and manages the triage state used by intake, so it isn't accepted in the request body. You can still assign the triage group to a state you own.
curl -X PATCH \
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/states/f960d3c2-8524-4a41-b8eb-055ce4be2a7f/" \
-H "X-Api-Key: $PLANE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Building",
"color": "#f5a623"
}'import requests
response = requests.patch(
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/states/f960d3c2-8524-4a41-b8eb-055ce4be2a7f/",
headers={"X-Api-Key": "your-api-key"},
json={
"name": "Building",
"color": "#f5a623",
},
)
print(response.json())const response = await fetch(
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/states/f960d3c2-8524-4a41-b8eb-055ce4be2a7f/",
{
method: "PATCH",
headers: {
"X-Api-Key": "your-api-key",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Building",
color: "#f5a623",
}),
},
);
const data = await response.json();{
"id": "f960d3c2-8524-4a41-b8eb-055ce4be2a7f",
"name": "Building",
"description": "Actively being worked on",
"color": "#f5a623",
"group": "started",
"sequence": 25000,
"is_default": false,
"is_triage": false,
"external_id": null,
"external_source": null,
"created_at": "2026-01-14T09:22:41.478363Z",
"updated_at": "2026-02-03T16:45:12.904117Z",
"created_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
"updated_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430"
}{
"type": "invalid_request",
"code": "invalid_request",
"detail": "The request body failed validation.",
"errors": [
{
"field": "group",
"code": "invalid_choice",
"message": "\"in_review\" is not a valid choice."
}
]
}
