Create a work item
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:requiredstringThe 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:requiredstringTitle of the work item. Maximum 255 characters. The only required field.
description_html:optionalstringRich-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:optionalstringOne 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:optionalstringThe 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:optionalstringThe 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:optionalstringThe 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 stringLabel 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:optionalstringThe 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:optionalstringYour 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:optionalstringThe 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.
Scopes
projects.work_items:write
Errors
| Status | Code | Cause |
|---|---|---|
400 | validation_error | Missing 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. |
401 | unauthorized | Missing or invalid credentials. |
403 | forbidden | Your role or token scope can't create work items in this project. |
403 | workflow_transition_denied | A workflow rule forbids creating a work item in the requested state. |
404 | resource_not_found | No such workspace or project, or it's outside your tenant. |
409 | conflict | The write collides with a uniqueness or protected-resource constraint. |
429 | rate_limited | Throttled. Honor the Retry-After header before retrying. |
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"
}'import requests
response = requests.post(
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/",
headers={"X-Api-Key": "your-api-key"},
json={
"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",
},
)
print(response.json())const response = await fetch(
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/",
{
method: "POST",
headers: {
"X-Api-Key": "your-api-key",
"Content-Type": "application/json",
},
body: JSON.stringify({
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",
}),
}
);
const data = await response.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": {}
}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"
}'import requests
response = requests.post(
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/",
headers={"X-Api-Key": "your-api-key"},
json={
"name": "Fix login redirect loop",
"state": "In Progress",
"type": "Bug",
"parent": "PROJ-118",
"assignees": ["ana@example.com"],
"labels": ["regression", "auth"],
"estimate": "5",
},
)
print(response.json())const response = await fetch(
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/",
{
method: "POST",
headers: {
"X-Api-Key": "your-api-key",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Fix login redirect loop",
state: "In Progress",
type: "Bug",
parent: "PROJ-118",
assignees: ["ana@example.com"],
labels: ["regression", "auth"],
estimate: "5",
}),
}
);
const data = await response.json();{
"type": "https://api.plane.so/errors/validation_error",
"title": "Validation Error",
"status": 400,
"code": "validation_error",
"detail": "The request body failed validation.",
"errors": [{ "field": "state", "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 send | What happens |
|---|---|
state_id only | Used directly. Must belong to this project. |
state only | Resolved by name, case-insensitively, within the project. |
state and state_id | 400 — "Provide either state_id or state, not both." |
| A name matching nothing | 400 naming the field and the value you sent. |
| A name matching more than one record | 400 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.

