/bulk-ops applies one of three operations across every readable doc in a source — a collection or a saved search. The pattern matches D125 (bulkAddDocumentsToLegalHold): permission-trimmed via userCanReadDocument, idempotent on already-applied docs, one audit event per affected doc.
**Three operations:**
- **Add to collection** — pin every readable doc in the source into a target collection. `bulkAddDocumentsToCollection` MCP tool (#64). Idempotent — already-member docs counted in alreadyMember. - **Set retention class** — apply a retention class to every readable doc in the source. `bulkSetDocumentRetentionClass` MCP tool (#65). Refuses if the retention class is archived. - **Set sensitivity** — change sensitivity tier (public / internal / confidential / restricted / regulated) across the source. `bulkSetDocumentSensitivity` MCP tool (#66). Requires a free-text reason for the audit log. Refuses to LOWER sensitivity on docs under active legal hold (deny-wins, mirrors per-doc setDocumentSensitivity behavior — refusedHeldDowngrade counts surface in the result).
**Source flexibility.** Three source kinds accept the same downstream pipeline:
- **Collection** — pinned members. - **Saved search** — re-runs the search via runSavedSearchTool. Accepts UUID OR per-user name in `savedSearchIdOrName` (per-user names cannot collide). - **Uncollected** — every live tenant doc with no collection membership. Carries no other fields — `kind: "uncollected"` IS the whole query. Pinned-only definition: rule-derived collection membership doesn't qualify (recomputed at read time; pinning is the explicit signal). Use when the operator says "pin every uncollected doc to <collection>" or "the inbox" or "loose docs". Same variant works on retention / sensitivity / legal-hold variants.
**Cap.** 500 docs per call by default; up to 2000 via the MCP tool directly. The 2,000-cap is a safety floor that hasn't moved — it prevents accidental holds across millions of docs.
**Cursor pagination (D283).** When a source set is larger than the per-call cap, the bulk tools return a `nextCursor` opaque string. The caller passes it back on the next call to fetch the next page. Loop until `nextCursor === null` to drain the source. Cursor pagination applies to `collection` and `uncollected` sources only — `saved-search` sources are naturally bounded by runSavedSearch's 50-hit internal cap (cursor is ignored there; nextCursor is always null so the loop terminates after one call). Source queries are ordered by documentId for stable pagination. Per-doc audit semantics are unchanged — the existing one-event-per-changed-doc pattern naturally batches across calls.
**Result counts surface inline.** Each run surfaces: - *added / changed* — newly applied (counted toward audit events) - *alreadyMember / alreadyClassified / alreadyAtLabel* — counted but not re-emitted - *refusedHeldDowngrade* (sensitivity only) — held-doc downgrade refusal count - *skippedNoPermission* — docs you can't read - *skippedNotFound* — non-live or missing docs - *totalCandidates* — what the source resolved to before filtering
**Automation-callable.** All three tools are registered in the MCP catalog. Write event-triggered rules like "when a doc is filed in the Smith Matter collection, add it to the Smith Production saved-folder" via /automations mcp-tool-call action.
**Sibling to /search bulk ops.** /search has its own per-doc-id selection bulk ops (operator picks N specific hits, applies tombstone / collection / retention via the per-doc MCP tools iteratively). /bulk-ops is the source-driven counterpart — apply to EVERY doc in a source rather than to a hand-picked subset.