Skip to content

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.

Learn more about work items

The work item object ​

Attributes ​

  • id string (uuid)

    Unique identifier for the work item. This is the {pk} on every project-scoped detail route.

  • name string

    Title of the work item. Maximum 255 characters.

  • description_html string

    The 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>.

  • identifier string

    The human key, for example PROJ-142. It is the project's identifier joined to sequence_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_id integer

    The work item's number within its project. Assigned by Plane and never reused.

  • priority string

    One of urgent, high, medium, low, or none. Never null — an unprioritized work item reads none.

  • project_id string (uuid)

    The project the work item belongs to. Use it to group rows from List work items across a workspace.

  • state_id string (uuid)

    The workflow state the work item is currently in.

  • type_id string (uuid)

    The work item type. null when the project has no types enabled or the item is untyped.

  • assignee_ids array of string

    User ids assigned to the work item. Empty array when unassigned.

  • label_ids array of string

    Label ids applied to the work item. Empty array when unlabeled.

  • cycle_id string (uuid)

    The cycle the work item is in, or null. A work item belongs to at most one cycle.

  • module_ids array of string

    The modules the work item belongs to. Empty array when it is in none.

  • parent_id string (uuid)

    The parent work item, or null for a top-level item. A parent may live in another project of the same workspace.

  • estimate_point_id string (uuid)

    The estimate point set on the work item, or null. Add ?expand=estimate_point to get the point's key, value, and description beside the id.

  • start_date string (date)

    Planned start, or null.

  • target_date string (date)

    Planned due date, or null.

  • is_draft boolean

    Whether the work item is still a draft. Drafts are created in the Plane app and are excluded from most boards.

  • sort_order number

    The work item's manual position on boards. Plane assigns it on create, and ?order_by=sort_order sorts by it. It is read-only through the API.

  • completed_at string (date-time)

    When the work item moved into a state in the completed group, or null. Plane sets it on that move and clears it when the work item moves to a state in any other group.

  • archived_at string (date-time)

    When the work item was archived, or null if it is active. See Archiving.

  • external_id string

    Your system's key for the record this work item mirrors, or null. Set it on create or update.

  • external_source string

    The system external_id comes from, for example jira, or null. The pair is what Upsert a work item matches on, and what the external_id and external_source list filters match.

  • created_at string (date-time)

    When the work item was created.

  • updated_at string (date-time)

    When the work item was last updated.

  • created_by_id string (uuid)

    The user who created the work item. null for items created by an automation with no acting user.

  • updated_by_id string (uuid) or null

    The user who last updated the work item. null until someone updates the work item.

  • custom_fields object

    Values 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.

Response200
json
{
  "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 ​

MethodPathDescription
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 ​

KeyLooks likeRoute
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.

RelationId inputHuman inputHuman input accepts
Statestate_idstateThe state's name, for example In Progress
Typetype_idtypeThe type's name, for example Bug
Parentparent_idparentThe parent's identifier, for example PROJ-118
Assigneesassignee_idsassigneesMember email addresses
Labelslabel_idslabelsLabel names
Estimateestimate_point_idestimateThe 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 state and state_id) is a 400. Pick one.
  • The human inputs are write-only. Responses always come back in the sparse id shape, so state never appears in a response body — state_id does.

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.

bash
GET .../work-items/?expand=state,assignees

An 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=true to get them alongside active work items, or with ?archived=only to 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, or module. project, updated_by, and estimate_point come back as project_id, updated_by_id, and estimate_point_id.
  • List rows leave out description_html unless ?fields= names it. Single-object responses include it by default.
  • identifier (PROJ-142) and custom_fields are 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 are state, type, parent, estimate_point, assignees, labels, cycle, and modules. v1's project expansion is gone.
  • Updates are PATCH only. PUT returns 405.
  • 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.