Create an artifact
Create an artifact together with its first HTML version. The new artifact starts at current_version: 1 and is unpublished until you call Publish an artifact.
Requires the Applets feature and workspace admin or owner — see Artifacts overview.
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.
Body Parameters
html:requiredstringThe rendered HTML for version 1. This is the only genuinely required field — an empty or missing value is a 400.
name:optionalstringDisplay name. Truncated to 255 characters. Falls back to Untitled dashboard when omitted, empty, or whitespace — it never rejects a missing name.
description:optionalstringFree-form description. Truncated to 2000 characters rather than rejected.
prompt:optionalstringThe prompt that produced this HTML, stored against the version for provenance. Not returned by any read endpoint.
project:optionalstring (uuid)Optionally scope the artifact to a project. Note the field is project, not project_id — the *_id convention used elsewhere in v2 does not apply on this surface.
data_mode:optionalstringHow the artifact's data is sourced. One of snapshot (frozen at generation time) or live (re-read on view). Defaults to snapshot. Any other value is a 400.
A project id from another workspace is silently dropped
project is validated for membership in the calling workspace, and a value that fails that check is set to null rather than rejected — the artifact is created workspace-scoped instead. If project scoping matters to you, read the project back rather than assuming it stuck.
Scopes
workspaces.artifacts:write
Errors
| Status | Code | Cause |
|---|---|---|
400 | — | html is missing or empty, or data_mode is outside the enum. See the note below. |
401 | unauthorized | Missing or invalid credentials. |
402 | payment_required | The Applets feature isn't enabled on your plan. |
403 | forbidden | You are not a workspace admin or owner, or your token lacks the scope. |
404 | not_found | No such workspace, or it's outside your tenant. |
406 | not_acceptable | The Accept header asks for a representation the API can't produce. |
409 | conflict | Declared by the schema; no current condition produces it on this route. |
413 | payload_too_large | The request body is over the size limit. Large HTML can hit this. |
415 | unsupported_media_type | The Content-Type isn't one this endpoint accepts. |
429 | rate_limited | Throttled. Honor the Retry-After header before retrying. |
The 400 here is not problem+json
Unlike the rest of v2, the two validation failures on this route return a bare {"detail": "…"} body with no type or code member. Code that branches on problem.code needs a fallback for this shape. The schema declares the standard ValidationProblemDetail for 400, so this is a known inconsistency rather than intended behavior.
curl -X POST \
"https://api.plane.so/api/v2/workspaces/my-team/artifacts/" \
-H "X-Api-Key: $PLANE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Q1 velocity by squad",
"description": "Throughput and cycle time, split by squad.",
"html": "<section><h1>Q1 velocity by squad</h1></section>",
"data_mode": "snapshot"
}'import requests
response = requests.post(
"https://api.plane.so/api/v2/workspaces/my-team/artifacts/",
headers={"X-Api-Key": "your-api-key"},
json={
"name": "Q1 velocity by squad",
"description": "Throughput and cycle time, split by squad.",
"html": "<section><h1>Q1 velocity by squad</h1></section>",
"data_mode": "snapshot",
},
)
print(response.json())const response = await fetch("https://api.plane.so/api/v2/workspaces/my-team/artifacts/", {
method: "POST",
headers: {
"X-Api-Key": "your-api-key",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Q1 velocity by squad",
description: "Throughput and cycle time, split by squad.",
html: "<section><h1>Q1 velocity by squad</h1></section>",
data_mode: "snapshot",
}),
});
const data = await response.json();{
"id": "b7e42a19-3c5d-4f80-9a26-8d1c0f4e7b53",
"name": "Q1 velocity by squad",
"current_version": 1,
"is_published": false,
"anchor": null,
"data_mode": "snapshot"
}{
"detail": "`html` is required."
}The response status is 201, not 200
A successful create answers 201 Created. The OpenAPI document declares 200 for this operation — a schema annotation gap, not a behavior you should code against. Accept 2xx rather than matching 200 exactly.
The body is deliberately compact: it echoes the artifact's identity and publish state, not its HTML. Read the HTML back with Get an artifact if you need it.
Next steps
- Publish an artifact — get a shareable anchor
- Append a new version — when the content is regenerated

