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. Every other field falls back to the project's defaults, so the smallest valid create sends one field.

Each relation accepts either its id field or a write-only human-readable equivalent: 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. With the human-readable fields, an importer or an agent can 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>. Plane sanitizes the HTML when it's written, and content that can't be sanitized is rejected with 400.

The response returns the stored description_html. Omit it and the work item reads <p></p>.

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. If you omit it, the work item is created 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.

An unknown name, or a name that matches more than one state, returns 400. So does sending both state and state_id.

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 response. 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. Reads return it as estimate_point_id whichever of the two inputs you send.

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.

Returned on reads and filterable on List work items.

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. Custom properties must be enabled for the workspace, or sending the object returns 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. 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: archived_at, assignee_ids, completed_at, created_at, created_by_id, custom_fields, cycle_id, description_html, estimate_point_id, external_id, external_source, id, identifier, is_draft, label_ids, module_ids, name, parent_id, priority, project_id, sequence_id, sort_order, start_date, state_id, target_date, type_id, updated_at, updated_by_id.

See Sparse fields.

expand:optionalstring

Comma-separated relations to embed alongside the ids: assignees (the assigned users), cycle (the cycle it belongs to), estimate_point (its estimate point), 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).

Expanded objects get their own 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 returns 400.

?fields= and ?expand= take separate sets of names. Relation names aren't valid in ?fields=, field names aren't valid in ?expand=, and field filtering never removes an expanded object: ?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",
  "description_html": "<p></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": null,
  "assignee_ids": ["16c61a3a-512a-48ac-b0be-b6b46fe6f430"],
  "label_ids": [],
  "cycle_id": null,
  "module_ids": [],
  "parent_id": null,
  "estimate_point_id": null,
  "start_date": null,
  "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": {}
}
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 with "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.

Use the id fields in code you control and reuse across runs. Use the names when you're bridging a system that already refers to things by name, such as a Jira status, a Slack email, or a label the user typed.

Workflow rules can reject a create ​

If the project uses workflow rules, creating a work item directly in a restricted state returns 403 with workflow_transition_denied, which is different from a plain permission forbidden. Branch on the code: forbidden means you need access, and workflow_transition_denied means you should create the work item in an allowed state and move it from there.