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 key off in your own storage.
Two things to know before you send this request:
- 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, notname.nameis the read-side slug Plane derives; 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 to this project alone.
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. This is the decision that shapes everything else about the property.
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_typeURL— A linkEMAIL— An email addressFILE— An uploaded fileFORMULA— A value Plane computes rather than 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. Worth filling in — it is the helper text people read when deciding what to type.
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.
You never get options back in the shape you sent it. The response carries the resolved options array, with each choice's generated id and sort_order.
is_multi:optionalbooleanAllow more than one value on a work item — a multi-select list of severities, a set of reviewers, several linked work items. Leave it off for a single-valued field.
is_required:optionalbooleanForce a value to be present. 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 rather than repeating it here.
settings:optionalanyFree-form object holding type-specific configuration. What belongs in it depends entirely on property_type, so there is no single schema — send the whole object rather than assuming keys.
validation_rules:optionalanyFree-form object holding type-specific validation constraints. Same shape caveat as settings.
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.
Scopes
projects.work_item_properties:write
Errors
| Status | Code | Cause |
|---|---|---|
400 | validation_error | Missing display_name or property_type, or an enum value outside the list. |
401 | unauthorized | Missing or invalid credentials. |
403 | forbidden | Your role or token scope can't create properties. |
404 | resource_not_found | No such workspace or project, or it's outside your tenant. |
409 | work_item_types_managed_at_workspace | This workspace manages work item types at the workspace level. Create the property on the workspace endpoint instead. |
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 — the same property is created through Create a workspace work item property. 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",
"is_default": false,
"sort_order": 15000,
"external_id": null,
"external_source": null
},
{
"id": "b6c04f83-1d29-4e57-8a3b-90e2f5c7d418",
"name": "Major",
"description": "A core workflow is broken",
"is_default": true,
"sort_order": 25000,
"external_id": null,
"external_source": null
},
{
"id": "7f52d3a8-0e14-4c69-b28d-a1f6e903c5b7",
"name": "Minor",
"description": "Cosmetic or low impact",
"is_default": false,
"sort_order": 35000,
"external_id": null,
"external_source": null
}
],
"settings": {},
"validation_rules": {},
"logo_props": {},
"external_id": null,
"external_source": null,
"created_at": "2026-01-14T09:22:41.478363Z"
}{
"type": "https://api.plane.so/errors/conflict",
"title": "Conflict",
"status": 409,
"code": "work_item_types_managed_at_workspace",
"detail": "Work item types are managed at the workspace level for this workspace."
}{
"type": "https://api.plane.so/errors/validation-error",
"title": "Validation Error",
"status": 400,
"code": "validation_error",
"detail": "The request body failed validation.",
"errors": [
{
"field": "property_type",
"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. Attach a type property is what makes the field appear on work items. One property can be attached to several types.
- Add or adjust options later. Inline
optionscovers the initial set; Property options handles changes over the property's life.

