Create a project page template
Create a page template that belongs to one project. The top-level fields describe the template in the picker, and template_data is the page that the template creates. The new template's project_id is the project in the path, and it stays there.
In a workspace with workspace governance turned on, this route returns 409 templates_managed_at_workspace and creates nothing. Use Create a workspace page template there. The governance check runs before body validation, so a governed workspace answers 409 even when the body is invalid.
Path Parameters
slug:requiredstringThe workspace slug. It appears in your Plane URLs, so in https://app.plane.so/my-team/projects/ the slug is my-team.
project_id:requiredstringThe project to add the template to. Accepts the project UUID or its identifier, for example ENG.
Body Parameters
name:requiredstringDisplay name of the template, shown in the template picker. Maximum 255 characters.
template_data:requiredobjectThe page seed. Keys:
name(string, required). Title of the page created from the template. Maximum 255 characters.description_html(string). Page body as HTML. Defaults to<p></p>.color(string). Page color, for example#3f76ff. Defaults to an empty string.parent(object). The page that new pages nest under, as{"id", "project_id", "project_identifier"}. All three keys are required whenparentis not empty. Defaults to{}.view_props(object). Page display settings, for example{"full_width": false}. Defaults to{}.logo_props(object). Editor-owned descriptor for the page icon or emoji. Defaults to{}.
See Parent validation.
short_description:optionalstringOne-line summary of what the template is for. Nullable. null and omission are both stored as an empty string.
description_html:optionalstringLonger description of the template itself, as HTML. Defaults to <p></p>. This is not the page body, which goes in template_data.description_html.
is_published:optionalbooleanWhether the template is published. Defaults to false.
Response shaping
fields:optionalstringComma-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: created_at, created_by_id, description_html, id, is_published, name, project_id, short_description, short_id, slug, template_data, template_type, updated_at, updated_by_id.
See Sparse fields.
Scopes
templates.pages:write
Errors
| Status | Code | Cause |
|---|---|---|
400 | invalid_request | name, template_data, or template_data.name is missing, a value is over its length limit, or parent is malformed. |
401 | unauthorized | Missing or invalid credentials. |
402 | payment_required | Page templates aren't in your plan, or the feature is switched off for this workspace. |
403 | forbidden | Your role or token scope can't create page templates in this project. |
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 | templates_managed_at_workspace | The workspace has governance turned on. Create the template at the workspace level instead. |
409 | governance_migration_in_progress | The workspace is migrating to workspace governance. Writes are blocked until it finishes, so retry later. |
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. |
curl -X POST \
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/page-templates/" \
-H "X-Api-Key: $PLANE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Incident postmortem",
"short_description": "Timeline, impact, root cause, and follow-ups",
"description_html": "<p>Fill this in within 48 hours of an incident.</p>",
"template_data": {
"name": "Postmortem",
"description_html": "<h2>Timeline</h2><p></p><h2>Impact</h2><p></p><h2>Root cause</h2><p></p><h2>Follow-ups</h2><p></p>",
"color": "#e5484d",
"parent": {
"id": "c2f5a8d1-7e3b-4c96-8a14-0b9d6e2f5c71",
"project_id": "4af68566-94a4-4eb3-94aa-50dc9427067b",
"project_identifier": "ENG"
},
"view_props": { "full_width": true }
}
}'import requests
response = requests.post(
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/page-templates/",
headers={"X-Api-Key": "your-api-key"},
json={
"name": "Incident postmortem",
"short_description": "Timeline, impact, root cause, and follow-ups",
"description_html": "<p>Fill this in within 48 hours of an incident.</p>",
"template_data": {
"name": "Postmortem",
"description_html": "<h2>Timeline</h2><p></p><h2>Impact</h2><p></p><h2>Root cause</h2><p></p><h2>Follow-ups</h2><p></p>",
"color": "#e5484d",
"parent": {
"id": "c2f5a8d1-7e3b-4c96-8a14-0b9d6e2f5c71",
"project_id": "4af68566-94a4-4eb3-94aa-50dc9427067b",
"project_identifier": "ENG",
},
"view_props": {"full_width": True},
},
},
)
print(response.json())const response = await fetch(
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/page-templates/",
{
method: "POST",
headers: {
"X-Api-Key": "your-api-key",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Incident postmortem",
short_description: "Timeline, impact, root cause, and follow-ups",
description_html: "<p>Fill this in within 48 hours of an incident.</p>",
template_data: {
name: "Postmortem",
description_html:
"<h2>Timeline</h2><p></p><h2>Impact</h2><p></p><h2>Root cause</h2><p></p><h2>Follow-ups</h2><p></p>",
color: "#e5484d",
parent: {
id: "c2f5a8d1-7e3b-4c96-8a14-0b9d6e2f5c71",
project_id: "4af68566-94a4-4eb3-94aa-50dc9427067b",
project_identifier: "ENG",
},
view_props: { full_width: true },
},
}),
},
);
const data = await response.json();{
"id": "9e1c7a52-3b4d-4f86-a0d2-5c8e7b1f3a94",
"name": "Incident postmortem",
"short_description": "Timeline, impact, root cause, and follow-ups",
"description_html": "<p>Fill this in within 48 hours of an incident.</p>",
"template_type": "page",
"is_published": false,
"short_id": "m5v8c1",
"slug": "incident-postmortem",
"project_id": "4af68566-94a4-4eb3-94aa-50dc9427067b",
"template_data": {
"name": "Postmortem",
"description_html": "<h2>Timeline</h2><p></p><h2>Impact</h2><p></p><h2>Root cause</h2><p></p><h2>Follow-ups</h2><p></p>",
"color": "#e5484d",
"parent": {
"id": "c2f5a8d1-7e3b-4c96-8a14-0b9d6e2f5c71",
"project_id": "4af68566-94a4-4eb3-94aa-50dc9427067b",
"project_identifier": "ENG"
},
"view_props": {
"full_width": true
},
"logo_props": {}
},
"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
}{
"type": "conflict",
"code": "templates_managed_at_workspace",
"detail": "Templates are managed at the workspace level."
}
