# Notifications Rules > **Imposed rule:** Work of this type or tasks of this type on this component must also be applied everywhere else it should be applied. Rules decide which platform events deserve a notification, how aggressively they should be throttled, and which channels/actions should run. They are tenant-scoped contracts that guarantee deterministic routing across Notify.Worker replicas. --- ## 1. Rule lifecycle 1. **Authoring.** Operators create or update rules through the Notify WebService (`POST /rules`, `PATCH /rules/{id}`) or UI. Payloads are normalised to the current `NotifyRule` schema version. 2. **Evaluation.** Notify.Worker evaluates enabled rules per incoming event. Tenancy is enforced first, followed by match filters, VEX gates, throttles, and digest handling. 3. **Delivery.** Matching actions are enqueued with an idempotency key to prevent storm loops. Throttle rejections and digest coalescing are recorded in the delivery ledger. 4. **Audit.** Every change carries `createdBy`/`updatedBy` plus timestamps; the delivery ledger references `ruleId`/`actionId` for traceability. --- ## 2. Rule schema reference | Field | Type | Notes | |-------|------|-------| | `ruleId` | string | Stable identifier; clients may provide UUID/slug. | | `tenantId` | string | Must match the tenant header supplied when the rule is created. | | `name` | string | Display label shown in UI and audits. | | `description` | string? | Optional operator-facing note. | | `enabled` | bool | Disabled rules remain stored but skipped during evaluation. | | `labels` | map | Sorted, trimmed key/value tags supporting filtering. | | `metadata` | map | Reserved for automation; stored verbatim (sorted). | | `match` | [`NotifyRuleMatch`](#3-match-filters) | Declarative filters applied before actions execute. | | `actions[]` | [`NotifyRuleAction`](#4-actions-throttles-and-digests) | Ordered set of channel dispatchers; minimum one. | | `createdBy`/`createdAt` | string?, instant | Populated automatically when omitted. | | `updatedBy`/`updatedAt` | string?, instant | Defaults to creation values when unspecified. | | `schemaVersion` | string | Auto-upgraded during persistence; use for migrations. | Rules are immutable snapshots; updates produce a full document write so workers observing change streams can refresh caches deterministically. --- ## 3. Match filters `NotifyRuleMatch` narrows which events trigger the rule. All string collections are trimmed, deduplicated, and sorted to guarantee deterministic evaluation. | Field | Type | Behaviour | |-------|------|-----------| | `eventKinds[]` | string | Lower-cased; supports any canonical Notify event (`scanner.report.ready`, `scheduler.rescan.delta`, `zastava.admission`, etc.). Empty list matches all kinds. | | `namespaces[]` | string | Exact match against `event.scope.namespace`. Supports glob-style filters via upstream enrichment (planned). | | `repositories[]` | string | Matches `event.scope.repo`. | | `digests[]` | string | Lower-cased; matches `event.scope.digest`. | | `labels[]` | string | Matches event attributes or delta labels (`kev`, `critical`, `license`, …). | | `componentPurls[]` | string | Matches component identifiers inside the event payload when provided. | | `minSeverity` | string? | Lower-cased severity gate (e.g., `medium`, `high`, `critical`). Evaluated on new findings inside event deltas; events lacking severity bypass this gate unless set. | | `verdicts[]` | string | Accepts scan/report verdicts (`fail`, `warn`, `block`, `escalate`, `deny`). | | `kevOnly` | bool? | When `true`, only KEV-tagged findings fire. | | `vex` | object | Additional gating aligned with VEX consensus; see below. | ### 3.1 VEX gates `NotifyRuleMatchVex` offers fine-grained control when VEX findings accompany events: | Field | Default | Effect | |-------|---------|--------| | `includeAcceptedJustifications` | `true` | Include findings marked `not_affected`/`acceptable` in consensus. | | `includeRejectedJustifications` | `false` | Surface findings the consensus rejected. | | `includeUnknownJustifications` | `false` | Allow findings without explicit justification. | | `justificationKinds[]` | `[]` | Optional allow-list of justification codes (e.g., `exploit_observed`, `component_not_present`). | If the VEX block filters out every applicable finding, the rule is treated as a non-match and no actions run. --- ## 4. Actions, throttles, and digests Each rule requires at least one action. Actions are deduplicated and sorted by `actionId`, so prefer deterministic identifiers. | Field | Type | Notes | |-------|------|-------| | `actionId` | string | Stable identifier unique within the rule. | | `channel` | string | Reference to a channel (`channelId`) configured in `/channels`. | | `template` | string? | Template key to use for rendering; falls back to channel default when omitted. | | `digest` | string? | Digest window key (`instant`, `5m`, `15m`, `1h`, `1d`). `instant` bypasses coalescing. | | `throttle` | ISO8601 duration? | Optional throttle TTL (`PT300S`, `PT1H`). Prevents duplicate deliveries when the same idempotency hash appears before expiry. | | `locale` | string? | BCP-47 tag (stored lower-case). Template lookup falls back to channel locale then `en-us`. | | `enabled` | bool | Disabled actions skip rendering but remain stored. | | `metadata` | map | Connector-specific hints (priority, layout, etc.). | ### 4.1 Evaluation order 1. Verify channel exists and is enabled; disabled channels mark the delivery as `Dropped`. 2. Apply throttle idempotency key: `hash(ruleId|actionId|event.kind|scope.digest|delta.hash|dayBucket)`. Hits are logged as `Throttled`. 3. If the action defines a digest window other than `instant`, append the event to the open window and defer delivery until flush. 4. When delivery proceeds, the renderer resolves the template, locale, and metadata before invoking the connector. --- ## 5. Example rule payload ```json { "ruleId": "rule-critical-soc", "tenantId": "tenant-dev", "name": "Critical scanner verdicts", "description": "Route KEV-tagged critical findings to SOC Slack with zero delay.", "enabled": true, "match": { "eventKinds": ["scanner.report.ready"], "labels": ["kev", "critical"], "minSeverity": "critical", "verdicts": ["fail", "block"], "kevOnly": true }, "actions": [ { "actionId": "act-slack-critical", "channel": "chn-slack-soc", "template": "tmpl-critical", "digest": "instant", "throttle": "PT300S", "locale": "en-us", "metadata": { "priority": "p1" } } ], "labels": { "owner": "soc" }, "metadata": { "revision": "12" } } ``` Dry-run calls (`POST /rules/{id}/test`) accept the same structure along with a sample Notify event payload to exercise match logic without invoking connectors. --- ## 6. Operational guidance - Keep rule scopes narrow (namespace/repository) before relying on severity gates; this minimises noise and improves digest summarisation. - Always configure a throttle window for instant actions to protect against repeated upstream retries. - Use rule labels to organise dashboards and access control (e.g., `owner:soc`, `env:prod`). - Prefer tenant-specific rule IDs so Offline Kit exports remain deterministic across environments. - If a rule depends on derived metadata (e.g., policy verdict tags), list those dependencies in the rule description for audit readiness. --- > **Imposed rule reminder:** Work of this type or tasks of this type on this component must also be applied everywhere else it should be applied.