A comment is a note posted on a single work item. Comments are a sub-resource of a work item, not a project-level collection: every route is mounted under the work item, so a comment can only be read or written through the work item it belongs to.
That means the collection routes carry three path parameters (slug, project_id, work_item_id) and the detail routes carry four (plus pk). There is no workspace-wide or project-wide comment feed — to gather comments across work items, list them per work item.
The work item this comment is attached to. Always matches the work_item_id in the request path.
parent_idstring (uuid) or null
The top-level comment this comment replies to, or null for a top-level comment. See Replies.
comment_htmlstring
The comment body, as HTML. This is the field you write.
comment_strippedstring
The plain-text version of comment_html, derived server-side. It is what ?search= matches, so markup never affects a search hit. Read-only — you never send it.
accessstring
Visibility of the comment. One of INTERNAL or EXTERNAL. INTERNAL keeps the comment inside the project team; EXTERNAL marks it as visible outside the team, for example on a published project.
actor_idstring (uuid) or null
The author of the comment — the principal that posted it. Set server-side from the authenticated caller, so you cannot post a comment on someone else's behalf.
edited_atstring (date-time) or null
When the comment was last updated through a PATCH. null on a comment that has never been edited, which is how clients render an "edited" marker.
hidden_atstring (date-time) or null
When the comment was hidden from the discussion, or null if it is visible. Read-only. The API returns hidden comments like any other, so check this field before you display a body.
hidden_by_idstring (uuid) or null
The user who hid the comment, or null. Read-only.
hidden_reasonstring or null
Why the comment was hidden. One of abuse, spam, off_topic, sensitive_info, or other, or null when the comment is not hidden. Read-only.
external_id , external_sourcestring or null
Correlation fields for sync and import. Together they let you map a comment to a record in another system and find it again with ?external_id= and ?external_source=.
created_atstring (date-time)
When the comment was created.
updated_atstring (date-time)
When the comment was last updated.
created_by_idstring (uuid) or null
The user record that created the comment. For comments posted by a person this is the same user as actor_id; actor_id is the domain field that Plane renders as the comment author.
updated_by_idstring (uuid) or null
The user who last updated the comment. null until someone updates it.
Write comment_html, read comment_stripped
comment_html is the only body field you can set. Plane derives comment_stripped from it on every write, so the two never drift apart.
Nothing about a comment's body or external_id identifies it uniquely, so posting the same comment_html twice creates two comments. If your integration must post a comment at most once, send an external_id and look for it first with GET …/comments/?external_id=…&external_source=… before creating.
Writes can still fail with 409 conflict, so branch on the code in the problem response instead of assuming a create always succeeds.
A comment with a parent_id is a reply. Send parent_id on create, update, upsert, or a bulk row to make one.
Replies are one level deep. The parent must be a top-level comment, so a reply to a reply is a 400.
Plane doesn't stop you from moving a top-level comment that has replies under another comment. Its replies stay attached to it, so the thread ends up two levels deep. Check for replies before you set parent_id on a top-level comment.
The parent must be on the same work item. A parent on another work item is a 400 on parent_id, and so is a comment that points at itself.
A reply takes its parent's access. The parent's access replaces whatever access you send with a reply.
PATCH a reply with "parent_id": null to make it a top-level comment.
Lists are flat, and a reply is a row of its own. Group rows by parent_id to rebuild threads.
access is set per comment and can be changed later with a PATCH. Filter a list down to one visibility with ?access=INTERNAL or ?access=EXTERNAL when you need only the comments a particular audience can see.
Changing access on a top-level comment also changes it on every reply to that comment.
Comment visibility is inherited from the work item in the path — there is no per-comment read permission. A pk that belongs to a different work item is not visible on this route and returns 404 not_found, the same as a comment id that does not exist. Existence is never leaked.
Work item comments overview
A comment is a note posted on a single work item. Comments are a sub-resource of a work item, not a project-level collection: every route is mounted under the work item, so a comment can only be read or written through the work item it belongs to.
That means the collection routes carry three path parameters (
slug,project_id,work_item_id) and the detail routes carry four (pluspk). There is no workspace-wide or project-wide comment feed — to gather comments across work items, list them per work item.Learn more about work item comments
The comment object
Attributes
idstring (uuid)Unique identifier for the comment.
work_item_idstring (uuid)The work item this comment is attached to. Always matches the
work_item_idin the request path.parent_idstring (uuid) or nullThe top-level comment this comment replies to, or
nullfor a top-level comment. See Replies.comment_htmlstringThe comment body, as HTML. This is the field you write.
comment_strippedstringThe plain-text version of
comment_html, derived server-side. It is what?search=matches, so markup never affects a search hit. Read-only — you never send it.accessstringVisibility of the comment. One of
INTERNALorEXTERNAL.INTERNALkeeps the comment inside the project team;EXTERNALmarks it as visible outside the team, for example on a published project.actor_idstring (uuid) or nullThe author of the comment — the principal that posted it. Set server-side from the authenticated caller, so you cannot post a comment on someone else's behalf.
edited_atstring (date-time) or nullWhen the comment was last updated through a
PATCH.nullon a comment that has never been edited, which is how clients render an "edited" marker.hidden_atstring (date-time) or nullWhen the comment was hidden from the discussion, or
nullif it is visible. Read-only. The API returns hidden comments like any other, so check this field before you display a body.hidden_by_idstring (uuid) or nullThe user who hid the comment, or
null. Read-only.hidden_reasonstring or nullWhy the comment was hidden. One of
abuse,spam,off_topic,sensitive_info, orother, ornullwhen the comment is not hidden. Read-only.external_id,external_sourcestring or nullCorrelation fields for sync and import. Together they let you map a comment to a record in another system and find it again with
?external_id=and?external_source=.created_atstring (date-time)When the comment was created.
updated_atstring (date-time)When the comment was last updated.
created_by_idstring (uuid) or nullThe user record that created the comment. For comments posted by a person this is the same user as
actor_id;actor_idis the domain field that Plane renders as the comment author.updated_by_idstring (uuid) or nullThe user who last updated the comment.
nulluntil someone updates it.Write
comment_html, readcomment_strippedcomment_htmlis the only body field you can set. Plane derivescomment_strippedfrom it on every write, so the two never drift apart.Endpoints
GET/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/GET/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/{pk}/POST/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/PATCH/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/{pk}/DELETE/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/{pk}/POST/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/upsert/POST/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/bulk-create/POST/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/bulk-update/POST/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/bulk-delete/Comments are not deduplicated
The API will not collapse duplicates for you
Nothing about a comment's body or
external_ididentifies it uniquely, so posting the samecomment_htmltwice creates two comments. If your integration must post a comment at most once, send anexternal_idand look for it first withGET …/comments/?external_id=…&external_source=…before creating.Writes can still fail with
409 conflict, so branch on thecodein the problem response instead of assuming a create always succeeds.Replies
A comment with a
parent_idis a reply. Sendparent_idon create, update, upsert, or a bulk row to make one.400.parent_idon a top-level comment.400onparent_id, and so is a comment that points at itself.access. The parent'saccessreplaces whateveraccessyou send with a reply.PATCHa reply with"parent_id": nullto make it a top-level comment.Lists are flat, and a reply is a row of its own. Group rows by
parent_idto rebuild threads.Access and visibility
accessis set per comment and can be changed later with aPATCH. Filter a list down to one visibility with?access=INTERNALor?access=EXTERNALwhen you need only the comments a particular audience can see.Changing
accesson a top-level comment also changes it on every reply to that comment.Reads follow the parent work item
Comment visibility is inherited from the work item in the path — there is no per-comment read permission. A
pkthat belongs to a different work item is not visible on this route and returns404 not_found, the same as a comment id that does not exist. Existence is never leaked.Changed from v1
/work-items/{work_item_id}/comments/and the parent id is reported aswork_item_idinstead ofissue.actoris nowactor_id, andcreated_byis nowcreated_by_id.updated_by,parent, andhidden_byare nowupdated_by_id,parent_id, andhidden_by_id.deleted_at,project,workspace,comment_json,attachments,is_member, oris_hidden. A comment is hidden whenhidden_atis set.edited_atis stamped by Plane on everyPATCH.See Migrating from v1 for the full list.