Skip to content

Bulk create comments ​

POST/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/bulk-create/

Create up to 50 comments in one request. Each row is validated and written exactly as Create a comment would, so every business rule and permission check still applies per row.

200 does not mean every row succeeded

By default this endpoint reports partial success: it answers 200 whenever the batch was processed, even if some rows failed. Read failed and the per-row results rather than branching on the status code alone.

If you want all-or-nothing instead, send all_or_none: true and treat 409 as the failure signal.

Path Parameters ​

slug:requiredstring

The workspace slug. It appears in your Plane URLs — in https://app.plane.so/my-team/projects/, the slug is my-team.

project_id:requiredstring (uuid)

The project the comments belong to. Accepts the project UUID or its bare identifier, for example ENG.

work_item_id:requiredstring (uuid)

The work item the comments hang off. Accepts the work item UUID or its PROJ-123 identifier.

Body Parameters ​

items:requiredarray of object

The comments to create — each entry takes the same body as Create a comment. Between 1 and 50 per call.

all_or_none:optionalboolean

Defaults to false. Leave it off for partial success: each row runs in its own savepoint, successful rows commit, and you read the breakdown.

Set it to true to make the batch atomic. Every row is still evaluated, but if any row fails the whole batch is discarded and the call answers 409 instead of the 200 envelope. Side effects scheduled by the rows follow the same outcome.

Scopes ​

projects.work_items.comments:write

Errors ​

StatusCodeCause
400invalid_requestThe envelope itself is malformed — items missing or empty, more than 50 rows, or a non-UUID id.
401unauthorizedMissing or invalid credentials.
403forbiddenYour role or token scope can't write comments in this project.
404not_foundNo such workspace or project, or it's outside your tenant.
406not_acceptableThe Accept header asks for a representation the API can't produce.
409conflictAn all_or_none: true batch had at least one failing row, so the whole batch was discarded.
413payload_too_largeThe request body is over the size limit.
415unsupported_media_typeThe body wasn't JSON. These routes are JSON-only — a form or multipart body can't express items.
429rate_limitedThrottled. Honor the Retry-After header before retrying.
Bulk create comments
bash
curl -X POST \
  "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/b7e42a19-3c5d-4f80-9a26-8d1c0f4e7b53/comments/bulk-create/" \
  -H "X-Api-Key: $PLANE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "items": [
    {
      "comment_html": "<p>Reproduced on staging.</p>"
    },
    {
      "comment_html": "<p>Reproduced on staging.</p> 2"
    }
  ]
}'
Response200
json
{
  "results": [
    {
      "index": 0,
      "result": "created",
      "id": "b7e42a19-3c5d-4f80-9a26-8d1c0f4e7b53"
    },
    {
      "index": 1,
      "result": "failed",
      "type": "invalid_request",
      "code": "invalid_request",
      "detail": "One or more fields failed validation.",
      "errors": [
        {
          "field": "name",
          "code": "unique",
          "message": "A comment with this name already exists."
        }
      ]
    }
  ],
  "succeeded": 1,
  "failed": 1
}
Response409
json
{
  "type": "conflict",
  "code": "conflict",
  "detail": "No rows were written because all_or_none was set and at least one row failed.",
  "errors": [
    {
      "field": "items.1.name",
      "code": "unique",
      "message": "A comment with this name already exists."
    }
  ]
}

Reading the response ​

The 200 envelope has three members:

  • results — one row per input row, in request order, each carrying its index
  • succeeded / failed — counts, so you can branch without walking the array

A succeeded row is { index, result, id } where result is created. A failed row keeps the same members as a top-level error body — type, code, detail and, on validation failures, errors[] — plus index and result: "failed". So the same error-handling code works per row as at the top level. See Errors.

Atomic batches ​

With all_or_none: true, every row is still evaluated — you get the full picture of what would have failed, not just the first problem — and then the batch is rolled back. The 409 body's errors[] entries are prefixed with the row index, as items.<index>.<field>, so you can map each complaint back to the row that caused it.

Because the rollback covers the whole batch, side effects scheduled on commit — activity feed entries, webhooks — are discarded with it.

  • 50 rows per call, and at least 1. Exceeding the cap is a 400 for the whole envelope.
  • JSON only. A form or multipart body can't express items, so these routes reject it with 415 rather than silently misreading the payload.
  • Reconciling on external ids is not bulk's job. Use Upsert a comment per row, or list and diff.
  • ?fields= does not apply here — the response is a per-row result envelope, not a comment body. See Sparse fields.