Skip to content

Bulk update projects ​

POST/api/v2/workspaces/{slug}/projects/bulk-update/

Update up to 50 projects in one request, each addressed by id. Every row goes through the same path as a single Update a project.

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.

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.

Body parameters ​

items:requiredarray of object

The changes to apply. Each entry is the same body as Update a project plus a required id naming the row to update. Between 1 and 50 per call.

As with a single PATCH, each entry is a partial update: omitted keys keep their current value.

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.

Scopes ​

projects:write

Errors ​

StatusCodeCause
400invalid_requestThe envelope is malformed: items is missing or empty, has more than 50 rows, or contains an id that isn't a UUID.
401unauthorizedMissing or invalid credentials.
403forbiddenYour role or token scope can't write projects in this workspace.
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 accept JSON only, and a form or multipart body can't express items.
429rate_limitedThrottled. Honor the Retry-After header before retrying.
Bulk update projects
bash
curl -X POST \
  "https://api.plane.so/api/v2/workspaces/my-team/projects/bulk-update/" \
  -H "X-Api-Key: $PLANE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "items": [
    {
      "id": "b7e42a19-3c5d-4f80-9a26-8d1c0f4e7b53",
      "name": "Payments Platform"
    },
    {
      "id": "9c1f0b3d-6d2e-4c9a-8b41-2f7e5a0d6c88",
      "name": "Payments Platform"
    }
  ]
}'
Response200
json
{
  "results": [
    {
      "index": 0,
      "result": "updated",
      "id": "b7e42a19-3c5d-4f80-9a26-8d1c0f4e7b53"
    },
    {
      "index": 1,
      "result": "failed",
      "type": "not_found",
      "code": "not_found",
      "detail": "No project matches the given query."
    }
  ],
  "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": "not_found",
      "message": "No project matches the given query."
    }
  ]
}

Reading the response ​

The 200 envelope has three members:

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

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

  • Each call takes between 1 and 50 rows. Sending more than 50 fails the whole request with 400.
  • These routes accept JSON only. A form or multipart body can't express items, so they reject it with 415 instead of misreading the payload.
  • Bulk endpoints don't reconcile on external ids. To do that, use Upsert a project per row, or list and diff.
  • ?fields= doesn't apply here. The response is a per-row result envelope, not a project body. See Sparse fields.