Work items overview
A work item is the unit of work in Plane — the thing that gets assigned, scheduled, moved through a workflow, and closed. Every work item belongs to exactly one project and sits in exactly one state of that project's workflow.
Work items are the most heavily used resource in the API, so the v2 shape is deliberately narrow: reads return ids, not nested objects, and every relation is a *_id or *_ids field you can resolve on your own schedule or pull in with ?expand=. Writes go the other way — they accept ids and the human-readable names you already have, so you rarely need a lookup round trip before creating something.
The work item object
Attributes
idstring (uuid)Unique identifier for the work item. This is the
{pk}on every project-scoped detail route.namestringTitle of the work item. Maximum 255 characters.
identifierstringThe human key, for example
PROJ-142. It is the project's identifier joined tosequence_id, and it is what people paste into chat and commit messages. Use it with Get a work item by identifier when you don't have the project UUID.sequence_idintegerThe work item's number within its project. Assigned by Plane and never reused.
prioritystringOne of
urgent,high,medium,low, ornone. Never null — an unprioritized work item readsnone.state_idstring (uuid)The workflow state the work item is currently in.
type_idstring (uuid)The work item type.
nullwhen the project has no types enabled or the item is untyped.assignee_idsarray of stringUser ids assigned to the work item. Empty array when unassigned.
label_idsarray of stringLabel ids applied to the work item. Empty array when unlabeled.
parent_idstring (uuid)The parent work item, or
nullfor a top-level item. A parent may live in another project of the same workspace.start_datestring (date)Planned start, or
null.target_datestring (date)Planned due date, or
null.is_draftbooleanWhether the work item is still a draft. Drafts are created in the Plane app and are excluded from most boards.
archived_atstring (date-time)When the work item was archived, or
nullif it is active. See Archiving.created_atstring (date-time)When the work item was created.
created_by_idstring (uuid)The user who created the work item.
nullfor items created by an automation with no acting user.custom_fieldsobjectValues of the work item type's custom properties, keyed by property name. Populated only on single-item responses — it is always
nullon the list endpoint. See Custom fields.
The read shape is sparse and fixed
description_html, external_id, external_source, and estimate_point_id are accepted on writes but are not part of the read shape — they will not come back on any response. There is also no updated_at, no project_id, and no workspace_id: you already know the project from the path.
{
"id": "8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13",
"name": "Fix login redirect loop",
"identifier": "PROJ-142",
"sequence_id": 142,
"priority": "high",
"state_id": "f960d3c2-8524-4a41-b8eb-055ce4be2a7f",
"type_id": "2d9d1a97-5c6f-4a1e-9d5b-8c2f7e30b6a4",
"assignee_ids": ["16c61a3a-512a-48ac-b0be-b6b46fe6f430"],
"label_ids": ["c1b8f3d6-9a44-4e12-8f7a-2b6d5c9e1a03"],
"parent_id": null,
"start_date": "2026-01-12",
"target_date": "2026-01-20",
"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": {}
}
},
"root_cause": {
"id": "9e3b6c21-7f45-4d80-a1e9-5c8b2d7f4a36",
"value": "Stale session cookie",
"value_detail": null
}
}
}Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/v2/workspaces/{slug}/projects/{project_id}/work-items/ | List work items |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/work-items/ | Create a work item |
GET | /api/v2/workspaces/{slug}/projects/{project_id}/work-items/{pk}/ | Get a work item |
PATCH | /api/v2/workspaces/{slug}/projects/{project_id}/work-items/{pk}/ | Update a work item |
DELETE | /api/v2/workspaces/{slug}/projects/{project_id}/work-items/{pk}/ | Delete a work item |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/work-items/{pk}/archive/ | Archive a work item |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/work-items/{pk}/unarchive/ | Unarchive a work item |
GET | /api/v2/workspaces/{slug}/work-items/{identifier}/ | Get by identifier |
Every project-scoped route needs projects.work_items:read for GET and projects.work_items:write for POST, PATCH, and DELETE.
Two ways to identify a work item
| Key | Looks like | Route |
|---|---|---|
UUID (id) | 8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13 | .../projects/{project_id}/work-items/{pk}/ — reads and writes |
Human (identifier) | PROJ-142 | /api/v2/workspaces/{slug}/work-items/{identifier}/ — reads only |
The two lookups are separate routes on purpose. The UUID route is the canonical surface for everything, including writes. The identifier route is workspace-scoped and read-only, and it exists precisely so a caller holding only PROJ-142 — a chat bot, a commit-hook, an LLM agent — can fetch the work item without first discovering which project it belongs to.
Custom fields
custom_fields is the work item's custom property values from its work item type, keyed by the property's name. Each entry carries the property definition id, a human-useful value, and a value_detail object that keeps the underlying record for option and relation properties (null for plain scalars). Properties the work item has no value for are omitted rather than emitted as null.
custom_fields is null on the list endpoint
Resolving custom properties costs one lookup pass per work item, so the list endpoint deliberately skips it to avoid an N+1 across a full page of results. custom_fields is populated only on retrieve, retrieve by identifier, create, and update. It is null on list, and null on the archive and unarchive responses too — those return the work item, not its property values.
If you are exporting custom property values for many work items, list to get the ids, then fetch the ones you need individually. Do not expect custom_fields to arrive from a list call.
The object is empty ({}) when the work item is untyped or custom properties are not enabled for the workspace. See work item properties for the property definitions behind these values.
Writing: send ids, or send names
Every relation on a write has two accepted inputs — the canonical id field, and a write-only human-readable parallel. You may send either one, never both.
| Relation | Id input | Human input | Human input accepts |
|---|---|---|---|
| State | state_id | state | The state's name, for example In Progress |
| Type | type_id | type | The type's name, for example Bug |
| Parent | parent_id | parent | The parent's identifier, for example PROJ-118 |
| Assignees | assignee_ids | assignees | Member email addresses |
| Labels | label_ids | labels | Label names |
| Estimate | estimate_point_id | estimate | The estimate point's value, for example 5 |
This exists because the values a caller already holds are almost never UUIDs. An importer has a Jira status string. A Slack command has an email address. An LLM agent has the word "Bug". Without the parallel inputs each of those would need a list call per field before it could write anything — six lookups to create one work item. With them, the create is a single request.
The id fields remain the precise option, and you should prefer them when you already have ids or when a name might be ambiguous:
- Names resolve case-insensitively and must match exactly one record. A name that matches nothing is a
400, and so is a name that matches more than one — the error tells you to use the id field instead. - Sending both a name and its id (for example
stateandstate_id) is a400. Pick one. - The human inputs are write-only. Responses always come back in the sparse id shape, so
statenever appears in a response body —state_iddoes.
Expanding relations
Work items are one of only two resources that support ?expand=. The accepted values are state, type, parent, assignees, and labels, comma-separated.
Expansion is separate-key: the *_id field is always present, and the expanded object is added beside it under the bare name. ?expand=state gives you both state_id and a state object; it never swaps one for the other, so a client that reads ids keeps working when someone adds an expand to the request.
GET .../work-items/?expand=state,assigneesAn unknown expand value is a 400. Expansion works on the list, both retrieve routes, and the create/update responses. See Expanding relations.
Archiving and deleting
Archiving and deleting are different operations with different consequences.
- Archive (archive / unarchive) sets or clears
archived_at. It is reversible, and it removes the work item from the default query set — archived items stop appearing in list results and in plain detail reads. - Delete (delete) is a soft delete that returns
204. It is not reversible through the API.
Neither one is PATCH-able: archived_at is read-only, so you cannot archive a work item by patching a timestamp onto it.
Changed from v1
- Relations are ids with explicit names:
state→state_id,parent→parent_id,type→type_id,assignees→assignee_ids,labels→label_ids,created_by→created_by_id. - Reads no longer return
updated_at,updated_by,project,workspace,description_html,description_stripped,description_binary,sort_order,completed_at,estimate_point, ormodule. identifier(PROJ-142) andcustom_fieldsare new on the read shape.- Writes accept human-readable parallels (
state,type,parent,assignees,labels,estimate) alongside the id fields. ?expand=is now separate-key — it adds an object beside the id instead of replacing the id. The allowed values arestate,type,parent,assignees, andlabels; v1'sprojectandmoduleexpansions are gone.- Updates are
PATCHonly.PUTreturns405. - Lists return a pagination envelope instead of a bare array, and errors are RFC 9457
application/problem+json.
See Migrating from v1 for the full list.

