/automations (owner / admin only) is Kodori's programmable agent surface. Type a one-line rule in plain English; Claude Opus compiles it into a typed trigger + action; matching rules fire either on a cron tick (scheduled triggers) or on the audit log (event triggers).
**Examples to start with:**
- *"Every Monday at 8am UTC, run my 'unfilled retention' saved search and email me the new hits."* → email-saved-search action, daily-utc scheduled trigger with weekday=1 (Monday). - *"Daily at 9am, ask the agent which AP invoices have price variance and email me the answer."* → email-agent-query action, daily-utc scheduled trigger. - *"Every 4 hours, ask the agent which RFIs are overdue and email me + the project manager at pm@firm.com."* → email-agent-query action, every-240-minutes scheduled trigger. - *"When a legal hold is applied, ask the agent to summarize the matter scope and email me + counsel@firm.com."* → email-agent-query action, event trigger on `legal-hold.applied`. - *"When DLP flags a doc, ask the agent what obligations apply and email me a recommended action."* → email-agent-query action, event trigger on `document.dlp-flagged`. - *"When a legal hold is applied, POST to my Slack webhook URL https://hooks.slack.com/services/... with the message 'new hold filed.'"* → webhook action, event trigger on `legal-hold.applied`. - *"When DLP flags a doc, set its sensitivity to confidential."* → mcp-tool-call action invoking `setDocumentSensitivity` with `{documentId, sensitivityLabel: "confidential"}`, event trigger on `document.dlp-flagged`. - *"When a doc is filed, annotate it with the project number 24-1234."* → mcp-tool-call action invoking `createAnnotation` with `{documentId: "${event.payload.documentId}", body: "Project 24-1234"}`, event trigger on `document.created`.
**Filter expressions on event triggers.** Event-kinded rules can attach an optional filter — a list of NODES ANDed together — that further narrows when the rule fires. Each node is one of three shapes: leaf condition, OR-group (`{ any: [leaf, leaf, ...] }`), or NOT-clause (`{ not: leaf }`). Format:
``` { kind: "event", eventType: "anomaly.detected", filter: [ { path: "payload.severity", op: "in", value: ["high", "critical"] } ]} ```
- *Recognized paths*: `payload.<field>` (e.g. `payload.severity`, `payload.documentId`, `payload.sensitivityAfter`); top-level row columns `eventType`, `actorKind` ("user" / "agent" / "system"), `actorId`, `streamId`, `streamVersion`, `tenantId`. - *Recognized ops*: `eq`, `neq`, `in`, `nin`, `gt`, `gte`, `lt`, `lte`, `contains`. - *Top-level array is AND* across nodes; each node is leaf / any-group / not-clause. - *Cross-field OR* via `{ any: [...] }`: `[{ any: [{ path: "payload.severity", op: "eq", value: "high" }, { path: "payload.sensitivity", op: "eq", value: "restricted" }] }]` matches when severity is high OR sensitivity is restricted. - *Negation* via `{ not: leaf }`: `[{ not: { path: "actorKind", op: "eq", value: "system" } }]` matches every event NOT done by the system actor. - *Single-field "one of"* — use `in` / `nin` instead of `any`. `[{ path: "payload.severity", op: "in", value: ["high", "critical"] }]` is cleaner than the equivalent `any` group. - *Filters are always optional*. Without a filter, the rule fires on every event of the matching type.
Example translations: "when an anomaly is detected with severity above medium" → `anomaly.detected` + filter `[{ path: "payload.severity", op: "in", value: ["high", "critical"] }]`. "when severity is high OR sensitivity is restricted" → `[{ any: [{ path: "payload.severity", op: "eq", value: "high" }, { path: "payload.sensitivity", op: "eq", value: "restricted" }] }]`. "when severity is high but NOT a system action" → `[{ path: "payload.severity", op: "eq", value: "high" }, { not: { path: "actorKind", op: "eq", value: "system" } }]`.
**Variable substitution.** Action config strings (mcp-tool-call args, webhook URL + message, email-agent-query prompt) can include `${path.to.field}` placeholders that resolve at fire time. Recognized paths:
- `${event.payload.<field>}` — any field from the triggering event's payload (e.g. `${event.payload.documentId}`, `${event.payload.collectionId}`) - `${event.eventType}`, `${event.eventId}`, `${event.actorId}`, `${event.firedAt}` — other event fields - `${automation.id}`, `${automation.name}` — the rule's metadata - `${trigger.source}` (cron / manual / event), `${trigger.firedAt}` — firing context
Whole-string placeholders preserve type — `"${event.payload.amountCents}"` resolves to the underlying number, not a stringified version. Mid-string placeholders concatenate as strings.
Scheduled (cron) rules can use `${automation.*}` + `${trigger.*}` but NOT `${event.*}` — there's no event driving a scheduled fire, so those render as `[unresolved: path]` which Zod-rejects with a clear error.
**Four action types:**
- **email-saved-search** — runs an existing saved search (create them on /search), formats hits as a markdown email digest, sends to the recipients you specify. Closes the long-deferred saved-search-digest commitment. - **email-agent-query** — runs your free-form question through the Claude agent (with hybrid search of your workspace as context), emails the response. The wow-factor: programmable agent assistant without writing code. - **webhook** — POSTs a structured JSON payload to an HTTPS URL when the trigger fires. Useful for Slack incoming webhooks (auto-rewritten to Slack's `{text}` shape when the URL host is `hooks.slack.com`), Discord webhooks, n8n / Make.com / Zapier triggers, or your own Lambda / Cloud Run endpoint. No signing — these are URL-as-secret targets. For HMAC-signed delivery on every matching event, use /webhooks subscriptions instead. - **mcp-tool-call** — invokes any tool from Kodori's MCP catalog (165+ typed tools — `createAnnotation`, `addDocumentToCollection`, `setDocumentSensitivity`, `addDocumentToLegalHold`, `setDocumentRetentionClass`, `grantPermission`, `renameDocument`, etc.) with a structured args object. The compiler validates that the picked tool exists AND that the compiled args parse against the tool's Zod input schema BEFORE saving — so misconfigured rules surface in the preview, not on the next fire. Combined with event triggers this is "Zapier inside the DMS": every typed tool the agent can call is now also a programmable action.
**Two trigger families:**
*Scheduled:* fires on a cron tick that runs every 5 minutes.
- **every-N-minutes** — fixed interval (60 = hourly, 1440 = daily, 10080 = weekly). - **daily-utc** — once per day at hour:minute UTC, optionally constrained to a weekday (1=Monday).
*Event-based:* fires when a matching event lands on the audit log. Typical latency is seconds, not minutes. Recognized event types include:
- **Document lifecycle:** `document.created`, `document.version-committed`, `document.metadata-set`, `document.tombstoned`, `document.dlp-flagged`, `document.checked-out`, `document.checked-in`. - **Collections + permissions:** `collection.created`, `permission.granted`, `permission.revoked`. - **Legal hold:** `legal-hold.applied`, `legal-hold.released`. - **AP review:** `ap-invoice.approved`, `ap-invoice.rejected`. - **Anomalies:** `anomaly.detected`, `anomaly.acknowledged`, `anomaly.dismissed`, `anomaly.auto-paused`. - **Audit chain:** `audit.verification.completed`.
Event automations subscribe through the same internal channel that drives webhook fan-out — they run only when an event matches, so they cost nothing on idle workspaces.
**The compile flow:**
1. Type a rule. Click **Compile rule**. 2. Claude Opus returns a structured config (name, trigger, action) + a 1-2 sentence rationale. 3. Review the compiled output. If it picked the wrong saved search, the wrong schedule, or the wrong event type, edit your description and re-compile. 4. Click **Save automation**. Scheduled rules pick up on the next 5-minute boundary; event rules become active immediately.
Compilation counts as one agent question against your workspace quota. Uses Opus because compiling a recurring rule is the most consequential decision.
**Trust-builders:**
- **Run now** on every automation row executes the same code path the cron tick / event dispatcher uses, with the result surfaced inline. Verify a config change without waiting for the next tick or a real event. - **Disable** instead of delete to pause a rule temporarily. - **Permission-trimmed.** Automations run as the creator — a creator who lost access to a doc stops seeing it in their digests.
**Coming next:** threshold-based event filters (fire only when an anomaly's severity is above X, only when a doc's sensitivity is restricted+, etc.); per-tenant rate limiting on outbound webhook + tool-call actions; an mcp-tool-call audit panel that surfaces "what did your automations do this week?" without filtering the whole audit log.