Kodori has four bulk operations the agent can run across a collection / saved-search / "uncollected" docs in one shot:
- **bulkSetDocumentSensitivity** — set sensitivity on every doc (deny-wins on legal-held downgrades). - **bulkSetDocumentRetentionClass** — apply a retention class to every doc. - **bulkAddDocumentsToCollection** — pin every doc to a target collection (inheritance applies). - **bulkSetDocumentMetadata** — patch arbitrary metadata jsonb keys on every doc (this article).
**bulkSetDocumentMetadata** is the freeform-key path. Use it for matter numbers, client codes, parties arrays, custom keywords — any tenant-defined field stored in `document_objects.metadata` jsonb.
**Examples.** Ask the agent in plain language:
- *"Tag every doc in the Acme Corp matter with matterNumber=2024-0042 and clientCode=ACME."* - *"Set parties=[\"Acme\",\"Beta\"] on all the docs in Q1 Invoices."* - *"Add keyword 'NDA' to every uncollected doc."*
The agent picks the right source (collection / saved-search / uncollected), composes the patch, and the tool walks up to 500 docs per call (paginated for larger batches via `cursor`).
**Idempotency.** Keys whose value already matches the patch are skipped — no event emitted, no row update. Re-running with the same patch and the same source is a no-op.
**Removing a key.** Pass `null` as the value: *"Clear the matterNumber field on every doc in Drafts."* The patcher removes the key from the jsonb on each doc and emits one `document.metadata-set` event per removed key.
**What it can't do.** `sensitivityLabel` is rejected — it has its own deny-wins gate against legal-held docs that lives in `bulkSetDocumentSensitivity`. Routing sensitivity through the generic patcher would bypass that gate. Similarly, retention class binding goes through `bulkSetDocumentRetentionClass` so the held-doc deny-wins applies. The error message points to the right tool when this happens.
**Audit.** One `document.metadata-set` event per (doc, changed-key) pair. Each event payload carries `field`, `previous`, `next`, the operator-supplied `reason`, and a `bulkSource` tag indicating whether the change came from a collection / saved-search / uncollected query.
**Cap.** 500 docs per call by default, 2,000 max. The agent loops calls until `nextCursor` returns `null` for sources larger than that. Each iteration goes through the same per-doc gate, so permission-trimming and the audit-log shape are identical to the single-doc UI.
**Editing metadata on a single doc — UI path (D307).** The agent + REST + bulk surfaces are great for batch work; the inline **Metadata** panel on /doc/[id] is the fastest path for "fix this one key right now." Every top-level metadata key renders in a sortable table — values type-aware (arrays of scalars as chips, objects in collapsible `<details>`, scalars plain). Add / update via the form at the bottom: type the key + value, click "Add / update". Empty value deletes that key. The value field parses as JSON first then falls back to a literal string, so `["Smith","Jones LLC"]` lands as a real array but typing `Smith Holdings LLC` without quotes still works as a string. Per-row "Delete" button on each existing key for one-click removal. Same audit shape as every other path — one `document.metadata-set` event per changed key. Permission gate is identical to the API: creator OR tenant admin / owner.