Create a work item property
Define a new custom field in a project. You choose a label and a property_type, and Plane returns the property definition, including the derived name slug you can use as a key in your own storage.
Creating a property does not put it on any work item. The definition exists, but nothing renders it until you attach it to a work item type with Attach a type property.
You write display_name, not name. name is the read-side slug Plane derives, and it is not a body field.
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 define the property in. Project-level properties belong only to this project.
Body parameters
display_name:requiredstringThe human-readable label for the field, for example Severity. Maximum 255 characters. Plane derives the read-only name slug from this value and returns both.
property_type:requiredstringWhat kind of data the field holds. The rest of the property's configuration depends on it.
TEXT: free-form textDATETIME: a point in time, written as an ISO 8601 timestampDECIMAL: a number, carried as a string in the value arrayBOOLEAN: a yes/no flagOPTION: a choice from a fixed set you defineRELATION: a reference to another record. Pair it withrelation_type.URL: a linkEMAIL: an email addressFILE: an uploaded fileFORMULA: a value Plane computes, not one a person enters
The full reference, including what each type means for the value you send on a work item, is on the properties overview.
relation_type:optionalstringWhat a RELATION property points at. Only meaningful when property_type is RELATION. Leave it out otherwise.
ISSUE: a work itemUSER: a memberRELEASE: a releaseRICH_TEXT: rich text content
description:optionalstringFree-form explanation of what the field is for. People read it as helper text when deciding what to enter.
options:optionalarray of objectWrite-only. Define the choices for an OPTION property inline, in the same request that creates it, instead of a second round-trip per choice.
Each entry is a property option object. name is required, and description, is_default, external_id, and external_source are accepted. See Create a property option for the full field list.
The response does not return options in the shape you sent. It returns the resolved options array, with each choice's generated id and sort_order.
is_multi:optionalbooleanAllow more than one value on a work item, for example a multi-select list of severities, a set of reviewers, or several linked work items. Leave it off for a single-valued field.
is_required:optionalbooleanRequire a value. Give a required property a default_value so existing flows have something to fall back on.
is_active:optionalbooleanWhether the property is offered. Create it inactive if you want to define the field and its options now but roll it out later.
default_value:optionalarray of stringThe value applied when none is supplied. Always an array, even for a single-valued property: a DECIMAL field that defaults to 3 is sent as ["3"], not 3. A property with no default sends [] or omits the field.
For an OPTION property, mark the default choice with is_default on the option itself instead of repeating it here.
settings:optionalanyFree-form object holding type-specific configuration. What belongs in it depends on property_type, so there is no single schema. Send the whole object instead of assuming keys.
validation_rules:optionalanyFree-form object holding type-specific validation constraints. As with settings, the shape depends on property_type.
external_id:optionalstringYour system's identifier for this field, for sync and import correlation. Maximum 255 characters.
external_source:optionalstringThe system external_id came from, for example jira or linear. Maximum 255 characters.
Response shaping
fields:optionalstringComma-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: created_at, default_value, description, display_name, external_id, external_source, id, is_active, is_multi, is_required, logo_props, name, options, property_type, relation_type, settings, updated_at, updated_by_id, validation_rules.
See Sparse fields.
Scopes
projects.work_item_properties:write
Errors
| Status | Code | Cause |
|---|---|---|
400 | invalid_request | Missing display_name or property_type, or an enum value outside the list. |
401 | unauthorized | Missing or invalid credentials. |
402 | payment_required | The feature this endpoint belongs to isn't enabled on your plan, or is switched off. |
403 | forbidden | Your role or token scope can't create properties. |
404 | not_found | No such workspace or project, or it's outside your tenant. |
406 | not_acceptable | The Accept header asks for a representation the API can't produce. |
409 | conflict | This workspace manages work item types at the workspace level. Create the property on the workspace endpoint instead. |
413 | payload_too_large | The request body is over the size limit. |
415 | unsupported_media_type | The Content-Type isn't one this endpoint accepts. |
429 | rate_limited | Throttled. Honor the Retry-After header before retrying. |
A 409 means wrong surface, not missing permission
If the workspace manages work item types at the workspace level, this project endpoint returns 409 work_item_types_managed_at_workspace. Nothing is broken and no permission is missing. Create the property through Create a workspace work item property instead. See Work item type modes for how to detect which mode you are in.
curl -X POST \
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-item-properties/" \
-H "X-Api-Key: $PLANE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"display_name": "Severity",
"description": "How badly this affects customers",
"property_type": "OPTION",
"is_multi": false,
"is_required": true,
"is_active": true,
"options": [
{ "name": "Critical", "description": "Production is down" },
{ "name": "Major", "description": "A core workflow is broken", "is_default": true },
{ "name": "Minor", "description": "Cosmetic or low impact" }
]
}'import requests
response = requests.post(
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-item-properties/",
headers={"X-Api-Key": "your-api-key"},
json={
"display_name": "Severity",
"description": "How badly this affects customers",
"property_type": "OPTION",
"is_multi": False,
"is_required": True,
"is_active": True,
"options": [
{"name": "Critical", "description": "Production is down"},
{"name": "Major", "description": "A core workflow is broken", "is_default": True},
{"name": "Minor", "description": "Cosmetic or low impact"},
],
},
)
print(response.json())const response = await fetch(
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-item-properties/",
{
method: "POST",
headers: {
"X-Api-Key": "your-api-key",
"Content-Type": "application/json",
},
body: JSON.stringify({
display_name: "Severity",
description: "How badly this affects customers",
property_type: "OPTION",
is_multi: false,
is_required: true,
is_active: true,
options: [
{ name: "Critical", description: "Production is down" },
{ name: "Major", description: "A core workflow is broken", is_default: true },
{ name: "Minor", description: "Cosmetic or low impact" },
],
}),
},
);
const data = await response.json();{
"id": "9d2f0b74-6a51-4c8e-b3d7-2f1a8c05e964",
"name": "severity",
"display_name": "Severity",
"description": "How badly this affects customers",
"property_type": "OPTION",
"relation_type": null,
"is_multi": false,
"is_required": true,
"is_active": true,
"default_value": [],
"options": [
{
"id": "3e7a5c19-42b8-4d06-9f3e-7c1b8a0d2456",
"name": "Critical",
"description": "Production is down",
"parent_id": null,
"is_default": false,
"is_active": true,
"sort_order": 15000,
"logo_props": {},
"external_id": null,
"external_source": null,
"updated_at": "2026-01-14T09:22:41.478363Z",
"updated_by_id": null
},
{
"id": "b6c04f83-1d29-4e57-8a3b-90e2f5c7d418",
"name": "Major",
"description": "A core workflow is broken",
"parent_id": null,
"is_default": true,
"is_active": true,
"sort_order": 25000,
"logo_props": {},
"external_id": null,
"external_source": null,
"updated_at": "2026-01-14T09:22:41.478363Z",
"updated_by_id": null
},
{
"id": "7f52d3a8-0e14-4c69-b28d-a1f6e903c5b7",
"name": "Minor",
"description": "Cosmetic or low impact",
"parent_id": null,
"is_default": false,
"is_active": true,
"sort_order": 35000,
"logo_props": {},
"external_id": null,
"external_source": null,
"updated_at": "2026-01-14T09:22:41.478363Z",
"updated_by_id": null
}
],
"settings": {},
"validation_rules": {},
"logo_props": {},
"external_id": null,
"external_source": null,
"created_at": "2026-01-14T09:22:41.478363Z",
"updated_at": "2026-01-14T09:22:41.478363Z",
"updated_by_id": null
}{
"type": "conflict",
"code": "work_item_types_managed_at_workspace",
"detail": "Work item types are managed at the workspace level for this workspace."
}{
"type": "invalid_request",
"code": "invalid_request",
"detail": "The request body failed validation.",
"errors": [
{
"field": "property_type",
"code": "invalid_choice",
"message": "\"SELECT\" is not a valid choice."
}
]
}Creating a relation property
A RELATION property needs relation_type to say what it points at. This one lets a work item name several reviewers:
{
"display_name": "Reviewer",
"description": "Who signs this off",
"property_type": "RELATION",
"relation_type": "USER",
"is_multi": true
}relation_type stays null on every non-RELATION property.
After you create
- Attach it to a type with Attach a type property. That makes the field appear on work items. One property can be attached to several types.
- Add or change options later. Inline
optionssets up the initial choices, and Property options handles changes over the property's life.

