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 returns 200 whenever it processed the batch, even if some rows failed. Check failed and the per-row results instead of relying on the status code alone.
For all-or-nothing behavior, send all_or_none: true and treat 409 as failure.
The comments to create. Each entry takes the same body as Create a comment. Between 1 and 50 per call.
To make a row a reply, set its parent_id to an existing top-level comment on the work item. A row can't reply to another row in the same batch, because the new comments have no ids until the call returns.
all_or_none:optionalboolean
Defaults to false, which gives partial success: each row runs in its own savepoint, successful rows are committed, and the response reports the outcome of each row.
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 returns 409 instead of the 200 envelope. Side effects that the rows schedule are kept or discarded along with the batch.
{ "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." } ]}
results: one entry per input row, in request order, each with its index
succeeded and failed: counts, so you can branch without walking the array
A successful row is { index, result, id }, where result is created. A failed row has the same members as a top-level error body (type, code, detail, and errors[] on validation failures) plus index and result: "failed", so the code that handles top-level errors also works per row. See Errors.
With all_or_none: true, every row is still evaluated, so you see every row that would have failed, not only the first, before the batch is rolled back. Each entry in the 409 body's errors[] is prefixed with the row index, as items.<index>.<field>, so you can trace each error back to its row.
Because the rollback covers the whole batch, side effects scheduled on commit, such as activity feed entries and webhooks, are discarded with it.
Bulk create comments
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.
200does not mean every row succeededBy default this endpoint reports partial success: it returns
200whenever it processed the batch, even if some rows failed. Checkfailedand the per-rowresultsinstead of relying on the status code alone.For all-or-nothing behavior, send
all_or_none: trueand treat409as failure.Path parameters
slug:requiredstringThe workspace slug. It appears in your Plane URLs. In
https://app.plane.so/my-team/projects/, the slug ismy-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 belong to. Accepts the work item UUID or its
PROJ-123identifier.Body parameters
items:requiredarray of objectThe comments to create. Each entry takes the same body as Create a comment. Between 1 and 50 per call.
To make a row a reply, set its
parent_idto an existing top-level comment on the work item. A row can't reply to another row in the same batch, because the new comments have no ids until the call returns.all_or_none:optionalbooleanDefaults to
false, which gives partial success: each row runs in its own savepoint, successful rows are committed, and the response reports the outcome of each row.Set it to
trueto make the batch atomic. Every row is still evaluated, but if any row fails, the whole batch is discarded and the call returns409instead of the200envelope. Side effects that the rows schedule are kept or discarded along with the batch.Scopes
projects.work_items.comments:writeErrors
400invalid_requestitemsis missing or empty, has more than 50 rows, or contains an id that isn't a UUID.401unauthorized403forbidden404not_found406not_acceptableAcceptheader asks for a representation the API can't produce.409conflictall_or_none: truebatch had at least one failing row, so the whole batch was discarded.413payload_too_large415unsupported_media_typeitems.429rate_limitedRetry-Afterheader before retrying.Reading the response
The
200envelope has three members:results: one entry per input row, in request order, each with itsindexsucceededandfailed: counts, so you can branch without walking the arrayA successful row is
{ index, result, id }, whereresultiscreated. A failed row has the same members as a top-level error body (type,code,detail, anderrors[]on validation failures) plusindexandresult: "failed", so the code that handles top-level errors also works per row. See Errors.Atomic batches
With
all_or_none: true, every row is still evaluated, so you see every row that would have failed, not only the first, before the batch is rolled back. Each entry in the409body'serrors[]is prefixed with the row index, asitems.<index>.<field>, so you can trace each error back to its row.Because the rollback covers the whole batch, side effects scheduled on commit, such as activity feed entries and webhooks, are discarded with it.
Limits and related routes
400.items, so they reject it with415instead of misreading the payload.?fields=doesn't apply here. The response is a per-row result envelope, not a comment body. See Sparse fields.