Skip to content

Create a work item ​

POST/api/v2/workspaces/{slug}/projects/{project_id}/work-items/

Create a work item in a project. Only name is required — everything else falls back to the project's defaults, so the smallest useful create is one field.

Each relation accepts either its id field or a write-only human-readable parallel: state instead of state_id, assignees (emails) instead of assignee_ids, labels (names) instead of label_ids, and so on. Send one or the other, never both. This is what lets an importer or an agent write a fully-populated work item without first resolving six UUIDs.

The response is the standard sparse read shape, with custom_fields populated.

Path Parameters ​

slug:requiredstring

The 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 to create the work item in.

Body Parameters ​

name:requiredstring

Title of the work item. Maximum 255 characters. The only required field.

description_html:optionalstring

Rich-text body as HTML, for example <p>Steps to reproduce…</p>. The HTML is sanitized on the way in; content that can't be sanitized is rejected with a 400.

description_html is not part of the read shape, so it will not appear in the response.

priority:optionalstring

One of urgent, high, medium, low, none. Defaults to none.

state_id:optionalstring (uuid)

The workflow state to create the work item in. Must be a state of this project. Omit it and the work item lands in the project's default state.

state:optionalstring

The state's name instead of its id, for example In Progress. Matched case-insensitively within the project. Write-only.

Unknown name or a name that matches more than one state is a 400. Sending both state and state_id is a 400.

type_id:optionalstring (uuid)

The work item type. Accepts a type owned by the project and a workspace-level type imported into it. Requires work item types to be enabled.

type:optionalstring

The type's name instead of its id, for example Bug. Matched case-insensitively among the project's non-epic types. Write-only.

parent_id:optionalstring (uuid)

The parent work item. The parent must be in the same workspace, but it may live in a different project.

parent:optionalstring

The parent's identifier instead of its id, for example PROJ-118. Resolved within the workspace. Write-only.

assignee_ids:optionalarray of string (uuid)

User ids to assign. Each must be an active, assignable member of the project.

Leave it out and the project's default assignee is applied, if one is configured. Send [] to create the work item with no assignee at all.

assignees:optionalarray of string (email)

Member email addresses instead of ids, for example ["ana@example.com"]. Every address must belong to an active, assignable project member — any that don't are named in the 400. Write-only.

label_ids:optionalarray of string (uuid)

Label ids to apply. Each must be a label of this project, or a workspace-level label available to it.

labels:optionalarray of string

Label names instead of ids, for example ["regression", "auth"]. Matched case-insensitively against the project's labels and the workspace-level labels available to it. A name that exists at both levels is ambiguous and returns a 400 telling you to use label_ids. Write-only.

estimate_point_id:optionalstring (uuid)

The estimate point to assign, from the project's active estimate system.

estimate:optionalstring

The estimate point's value instead of its id, for example 5 or L. Resolved against the project's active estimate. Write-only.

start_date:optionalstring (date)

Planned start, for example 2026-01-12. Must not be after target_date.

target_date:optionalstring (date)

Planned due date, for example 2026-01-20.

external_id:optionalstring

Your system's identifier for this work item, for sync and import correlation. Maximum 255 characters.

Stored and filterable on List work items, but not returned on reads.

external_source:optionalstring

The system external_id came from, for example github or jira. Maximum 255 characters.

Setting custom property values

Work item type custom properties are written through a separate custom_fields object on the request body, keyed by property name, and are validated against the type you selected. Its shape depends on the type you chose, so it is not declared in the OpenAPI schema — read it from the type's schema endpoint. It requires custom properties to be enabled for the workspace — sending it otherwise is a 400. Properties you omit are filled from the type's defaults. See work item properties.

Response shaping ​

fields:optionalstring

Comma-separated list of fields to return. Unrequested keys are omitted from the response, not returned as null, so absent means "not requested" and null means "actually null". id always comes back whether or not you name it.

Pass all for every requestable field. An unknown name is a 400 that lists the valid set and suggests the closest match, so a typo can't silently cost you the saving.

Requestable here: archived_at, assignee_ids, created_at, created_by_id, custom_fields, cycle_id, id, identifier, is_draft, label_ids, module_ids, name, parent_id, priority, project_id, sequence_id, start_date, state_id, target_date, type_id.

See Sparse fields.

expand:optionalstring

Comma-separated relations to embed alongside the ids: assignees (the assigned users), cycle (the cycle it belongs to), labels (the applied labels), modules (the modules it belongs to), parent (its parent work item), state (the work item's state object), type (its work item type).

Expansion is separate-key: ?expand=state keeps state_id and adds a state object next to it, so an id is never replaced by an object. An unknown value is a 400.

?fields= and ?expand= are independent namespaces. Relation names are not valid ?fields= tokens (and vice versa), and an expanded object survives field filtering — ?fields=id,name&expand=state returns id, name and state. See Expanding relations.

Scopes ​

projects.work_items:write

Errors ​

StatusCodeCause
400invalid_requestMissing name; 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.
401unauthorizedMissing or invalid credentials.
402payment_requiredThe feature this endpoint belongs to isn't enabled on your plan, or is switched off.
403forbiddenA workflow rule forbids creating a work item in the requested state.
404not_foundNo such workspace or project, or it's outside your tenant.
406not_acceptableThe Accept header asks for a representation the API can't produce.
409conflictThe write collides with a uniqueness or protected-resource constraint.
413payload_too_largeThe request body is over the size limit.
415unsupported_media_typeThe Content-Type isn't one this endpoint accepts.
429rate_limitedThrottled. Honor the Retry-After header before retrying.
Create a work item
bash
curl -X POST \
  "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/" \
  -H "X-Api-Key: $PLANE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Fix login redirect loop",
  "priority": "high",
  "state_id": "f960d3c2-8524-4a41-b8eb-055ce4be2a7f",
  "assignee_ids": ["16c61a3a-512a-48ac-b0be-b6b46fe6f430"],
  "target_date": "2026-01-20"
}'
Response201
json
{
  "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": null,
  "assignee_ids": ["16c61a3a-512a-48ac-b0be-b6b46fe6f430"],
  "label_ids": [],
  "parent_id": null,
  "start_date": null,
  "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": {}
}
Create with names instead of ids
bash
curl -X POST \
  "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/" \
  -H "X-Api-Key: $PLANE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Fix login redirect loop",
  "state": "In Progress",
  "type": "Bug",
  "parent": "PROJ-118",
  "assignees": ["ana@example.com"],
  "labels": ["regression", "auth"],
  "estimate": "5"
}'
Response400
json
{
  "type": "invalid_request",
  "code": "invalid_request",
  "detail": "The request body failed validation.",
  "errors": [
    {
      "field": "state",
      "code": "invalid",
      "message": "No state named 'In Reviewing' in this project."
    }
  ]
}

Names or ids, not both ​

The human-readable inputs are resolved to their id equivalents before anything is written, and the rules are the same for all six:

You sendWhat happens
state_id onlyUsed directly. Must belong to this project.
state onlyResolved by name, case-insensitively, within the project.
state and state_id400 — "Provide either state_id or state, not both."
A name matching nothing400 naming the field and the value you sent.
A name matching more than one record400 telling you to use the id field instead.

Prefer the id fields in code you control and reuse across runs; prefer the names when you're bridging a system whose vocabulary is already words — a Jira status, a Slack email, a label the user typed.

Workflow rules can reject a create ​

If the project runs workflow rules, creating a work item directly into a restricted state returns 403 with workflow_transition_denied — distinct from a plain permission forbidden. Branch on the code: forbidden means ask for access, workflow_transition_denied means create it in an allowed state and move it from there.