Skip to content

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.

text
/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/

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.

Learn more about work item comments

The comment object ​

Attributes ​

  • id string (uuid)

    Unique identifier for the comment.

  • work_item_id string (uuid)

    The work item this comment is attached to. Always matches the work_item_id in the request path.

  • parent_id string (uuid) or null

    The top-level comment this comment replies to, or null for a top-level comment. See Replies.

  • comment_html string

    The comment body, as HTML. This is the field you write.

  • comment_stripped string

    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.

  • access string

    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_id string (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_at string (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_at string (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_id string (uuid) or null

    The user who hid the comment, or null. Read-only.

  • hidden_reason string 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_source string 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_at string (date-time)

    When the comment was created.

  • updated_at string (date-time)

    When the comment was last updated.

  • created_by_id string (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_id string (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.

Response200
json
{
  "id": "c1f7a3d9-2b64-4f80-9c1a-3d5e8b2a6c47",
  "work_item_id": "8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13",
  "parent_id": null,
  "comment_html": "<p>Deployed the fix to staging. Please re-test.</p>",
  "comment_stripped": "Deployed the fix to staging. Please re-test.",
  "access": "INTERNAL",
  "actor_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
  "external_id": null,
  "external_source": null,
  "edited_at": null,
  "hidden_at": null,
  "hidden_by_id": null,
  "hidden_reason": null,
  "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
}

Endpoints ​

MethodPathDescription
GET/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/List comments
GET/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/{pk}/Get a comment
POST/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/Create a comment
PATCH/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/{pk}/Update a comment
DELETE/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/{pk}/Delete a comment
POST/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/upsert/Upsert a comment
POST/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/bulk-create/Bulk create comments
POST/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/bulk-update/Bulk update comments
POST/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/bulk-delete/Bulk delete comments

Comments are not deduplicated ​

The API will not collapse duplicates for you

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.

Replies ​

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 and visibility ​

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.

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 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.

Changed from v1 ​

  • The path segment is /work-items/{work_item_id}/comments/ and the parent id is reported as work_item_id instead of issue.
  • actor is now actor_id, and created_by is now created_by_id.
  • updated_by, parent, and hidden_by are now updated_by_id, parent_id, and hidden_by_id.
  • Reads no longer return deleted_at, project, workspace, comment_json, attachments, is_member, or is_hidden. A comment is hidden when hidden_at is set.
  • edited_at is stamped by Plane on every PATCH.

See Migrating from v1 for the full list.