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.
description_htmlstringThe work item's description as HTML. Returned on single-object responses. List rows leave it out unless you name it in
?fields=. A work item created without a description reads<p></p>.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.project_idstring (uuid)The project the work item belongs to. Use it to group rows from List work items across a workspace.
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.
cycle_idstring (uuid)The cycle the work item is in, or
null. A work item belongs to at most one cycle.module_idsarray of stringThe modules the work item belongs to. Empty array when it is in none.
parent_idstring (uuid)The parent work item, or
nullfor a top-level item. A parent may live in another project of the same workspace.estimate_point_idstring (uuid)The estimate point set on the work item, or
null. Add?expand=estimate_pointto get the point'skey,value, anddescriptionbeside the id.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.
sort_ordernumberThe work item's manual position on boards. Plane assigns it on create, and
?order_by=sort_ordersorts by it. It is read-only through the API.completed_atstring (date-time)When the work item moved into a state in the
completedgroup, ornull. Plane sets it on that move and clears it when the work item moves to a state in any other group.archived_atstring (date-time)When the work item was archived, or
nullif it is active. See Archiving.external_idstringYour system's key for the record this work item mirrors, or
null. Set it on create or update.external_sourcestringThe system
external_idcomes from, for examplejira, ornull. The pair is what Upsert a work item matches on, and what theexternal_idandexternal_sourcelist filters match.created_atstring (date-time)When the work item was created.
updated_atstring (date-time)When the work item was last updated.
created_by_idstring (uuid)The user who created the work item.
nullfor items created by an automation with no acting user.updated_by_idstring (uuid) or nullThe user who last updated the work item.
nulluntil someone updates the work item.custom_fieldsobjectValues of the work item type's custom properties, keyed by property name. Populated only on single-item responses. List rows leave the key out. See Custom fields.
What list rows leave out
List rows carry every field above except description_html and custom_fields. Name description_html in ?fields= to get it on rows. custom_fields is never available on a list. The read shape has no workspace_id.
{
"id": "8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13",
"name": "Fix login redirect loop",
"description_html": "<p>Signing in after a session expires sends the user back to the login page in a loop.</p>",
"identifier": "PROJ-142",
"sequence_id": 142,
"priority": "high",
"project_id": "4af68566-94a4-4eb3-94aa-50dc9427067b",
"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"],
"cycle_id": null,
"module_ids": [],
"parent_id": null,
"estimate_point_id": null,
"start_date": "2026-01-12",
"target_date": "2026-01-20",
"is_draft": false,
"sort_order": 85535.0,
"completed_at": null,
"archived_at": null,
"external_id": null,
"external_source": null,
"created_at": "2026-01-14T09:22:41.478363Z",
"updated_at": "2026-01-14T09:22:41.478363Z",
"created_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
"updated_by_id": null,
"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 |
GET | /api/v2/workspaces/{slug}/work-items/ | List work items across a workspace |
GET | /api/v2/workspaces/{slug}/work-items/{identifier}/ | Get a work item by identifier |
GET | /api/v2/workspaces/{slug}/projects/{project_id}/work-items/{pk}/ | Get a work item |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/work-items/ | Create 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/upsert/ | Upsert a work item |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/work-items/bulk-create/ | Bulk create work items |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/work-items/bulk-update/ | Bulk update work items |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/work-items/bulk-delete/ | Bulk delete work items |
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 |
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 | .../projects/{project_id}/work-items/{pk}/ for reads and writes, or /api/v2/workspaces/{slug}/work-items/{identifier}/ for reads only |
The project-scoped route takes either key as {pk}, so you can read or write PROJ-142 once you know its project. The workspace identifier route is read-only, and it exists so a caller holding only PROJ-142 (a chat bot, a commit hook, an LLM agent) can fetch the work item without first finding out 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 not on list rows
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 on single-object responses: retrieve, retrieve by identifier, create, update, upsert, archive, and unarchive. List rows leave the key out, and naming it in ?fields= on a list is a 400.
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
The accepted ?expand= values are state, type, parent, estimate_point, assignees, labels, cycle, and modules, 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. To list them, call List work items with?archived=trueto get them alongside active work items, or with?archived=onlyto get just the archived ones. - 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
workspace,description_stripped,description_binary, ormodule.project,updated_by, andestimate_pointcome back asproject_id,updated_by_id, andestimate_point_id. - List rows leave out
description_htmlunless?fields=names it. Single-object responses include it by default. 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,estimate_point,assignees,labels,cycle, andmodules. v1'sprojectexpansion is 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.

