Bulk assign — multi-assignee
Bulk assign — multi-assignee
POST /api/v1/object/data/{objectSlug}/bulk-assign used to take exactly one
owner (admin_slug). It now takes the same assignee set as the per-record
endpoint, so the list/table bulk action can assign several people to many
records in one call.
Nothing breaks: admin_slug still works and still means "these records belong
to this one person".
1. The new body
POST /api/v1/object/data/leads/bulk-assign
{
"slugs": ["lead-1", "lead-2", "lead-3"], // records to change (max 500)
"assignees": ["sam-a1b2", "ann-c3d4"] // desired FULL set (max 20)
}| Body | Meaning |
|---|---|
assignees: ["sam", "ann"] | Every selected record ends up with exactly Sam + Ann |
assignees: [] | Unassign every selected record |
admin_slug: "sam" | Legacy single-owner form — identical to assignees: ["sam"] |
admin_slug: null / omitted | Unassign (unchanged behavior) |
| both keys together | 400 Send either admin_slug or assignees, not both |
Semantics are set-replace, the same rule as
PATCH /object/data/{objectSlug}/{recordSlug}/assignees: the request states the
final set, not a delta. There is no add/remove mode — to add someone to records
that already have assignees, send the union you want them to end up with.
Who ends up as primary owner
The primary owner is stable unless you replace them. One rule, identical on both endpoints:
| Record before | You send | Owner after |
|---|---|---|
| Owned by Sam, co-assignee Bob | ["ann","sam"] | Sam — still in the set, so ownership doesn't move |
| Owned by Bob | ["ann","sam"] | Ann — Bob was dropped, so your first pick takes it |
| Unassigned | ["ann","sam"] | Ann — first pick |
| Owned by Sam | [] | nobody — unassigned |
So a bulk assign over a mixed selection can leave different owners on
different records: every record that already had one of the chosen people as
owner keeps them. This is deliberate — reassigning a record's owner is a
separate intent from adding people to it. If you need "make Ann the owner of all
of these", send ["ann"] (or admin_slug: "ann"), which drops the previous
owner and therefore hands every record to Ann.
Ownership drives the kanban lane, the notification target, the export owner
column, and assignees[0] on every read.
Response is unchanged:
{ "message": "Bulk assign completed", "data": { "updatedCount": 3 } }updatedCount counts the records that were located and passed the access check
— records the caller cannot edit, or slugs that no longer exist, are skipped
silently, as before.
2. Errors the picker must handle
Validation is now shared with the per-record endpoint, so the same failures return the same shapes on both:
| Case | Status | Body |
|---|---|---|
| Slug unknown, or an admin from another workspace | 422 | { "code": "INVALID_ASSIGNEE", "reason": "unknown_or_foreign_workspace_admin" } |
| More than 20 assignees | 422 | { "code": "TOO_MANY_ASSIGNEES", "max": 20 } |
| More than 500 record slugs | 422 | class-validator array-size error |
Both admin_slug and assignees sent | 400 | Send either admin_slug or assignees, not both |
Changed: an unknown
admin_slugused to return400 Admin with slug 'x' not found. It now returns the422 INVALID_ASSIGNEEabove. Nothing else about the legacy form changed.
The whole request is rejected — never a partial set, and never a partial selection.
3. Permission
Unchanged: the endpoint needs assign.{objectSlug} (the dedicated assign
right), and every selected record is additionally row-scoped by the caller's
edit access. A user with assign on the object but only M-level records can
still bulk-assign only their own records.
4. What the UI should do
- Reuse the same multi-select picker as the record drawer's assignee field; send
its full value as
assignees. - Pre-fill the picker with the current set when the selection is homogeneous,
and leave it empty otherwise — because this is set-replace, an empty picker
submitted as
assignees: []unassigns everyone. - Warn before assigning over records that already have co-assignees: the set is replaced, not merged. (This was already true of the old single-owner call, which silently dropped co-assignees.)
- Don't promise the user "X will own these" in the confirmation copy unless the set you send is a single person — with a multi-person set, records that already belong to one of the chosen people keep their current owner.
- After the call, refetch the affected rows —
assignees[]is returned on every list/search read, so the avatar chips update without opening a record.
5. Not included
- No add/remove mode. Only replace. If "add X to the selection without
touching the rest" turns out to be the common gesture, say so and it becomes
a
modeflag on this endpoint. - No assignment notifications on bulk. The per-record
PATCH …/assigneesnotifies each newly-added assignee; bulk stays silent (unchanged), because a 500-record × 20-assignee call would fan out 10 000 notifications. The audit changelog and webhooks still fire per record. - The AI bulk-assign tool still takes one assignee.
bulk_assign_recordskeeps its singleassignee_slugargument; only the REST endpoint takes a set.