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. It is read-only, so 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, meaning 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. Clients use it to decide whether to show 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 returns 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 returns 400 on parent_id, and so does 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, so the response never reveals whether the comment exists.
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. It is read-only, so 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, meaning 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. Clients use it to decide whether to show 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 does 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, so the response never reveals whether the comment exists.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.