Alert rule schema
An alert rule matches incoming events and emits notifications. To author one, see Create an alert rule. To understand the event taxonomy, see Universal ONVIF events.
Top-level fields
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
id | uuid | server-set | gen_random_uuid() | Primary key. Composite with org_id. |
name | string | yes | - | Rule display name. Unique per org (case-insensitive). |
enabled | boolean | no | true | When false the rule never fires. Takes effect immediately on change. |
rule_kind | enum | yes | - | D95 discriminator. One of camera_event, system_event. |
event_category | enum | conditional | - | D95. One of the 10 categories below. Allowed only when rule_kind=camera_event. NULL = match any category. |
trigger_topic_pattern | string | conditional | - | D95. Case-insensitive SQL LIKE-style pattern matched against normalized events.trigger_topic (e.g. %LineDetector%). Allowed only when rule_kind=camera_event. NULL or empty = no topic filter at evaluation. See Topic pattern syntax. |
source_filter | string[] | no | - | D95. Subset of {onvif, software, manual, scheduled}. Allowed only when rule_kind=camera_event. NULL = match any source. |
system_trigger | enum | conditional | - | D95. Required when rule_kind=system_event. One of camera_offline, gateway_offline. MUST be NULL on camera_event rules. gateway_reconnect is rejected because no live evaluator handles it. |
trigger_type | — | — | — | Removed by migration 000060. It is not decoded into the current request model or emitted in responses. Because the decoder permits unknown properties, an extra trigger_type is silently ignored; it cannot replace required rule_kind + camera predicates or system_trigger. |
custom_tags | string[] | no | - | Legacy CRUD field. Accepted and persisted, but runtime-inert: the immediate evaluator skips rules that carry custom tags. Use the compiled wizard for new AI rules. |
min_confidence | float | no | - | Legacy CRUD field. Accepted and persisted in the range 0.0-1.0, but runtime-inert: the immediate evaluator skips rules that carry it. Use the compiled wizard for new AI rules. |
cooldown_sec | integer | no | 300 | Range 60 - 3600. The immediate and supported system-event legacy lanes enforce this window in PostgreSQL for the same rule and camera. Compiled NBD uses a separate configuration. |
aggregation_window_seconds | integer | no | 0 | Legacy API accepts only 0; non-zero values are rejected as unsupported. |
delay_seconds | integer | no | 0 | Legacy API accepts only 0; non-zero values are rejected as unsupported. |
conditions_json | object | no | - | Must be empty or NULL. Non-empty legacy condition trees are rejected as unsupported. |
scope_type | enum | yes | all_cameras | One of all_cameras, site, cameras. The live legacy evaluators fire only all_cameras and cameras; accepted site rows are skipped. |
site_ids | uuid[] | conditional | - | Required when scope_type=site; validated and persisted, but that legacy scope does not fire today. |
camera_ids | uuid[] | conditional | - | Required when scope_type=cameras. |
zones_filter | string[] | no | - | Legacy CRUD field. The live legacy evaluators do not apply a zone gate; compiled rules own working polygon/zone evaluation. |
schedule_type | enum | no | always | Legacy-rule field. The API accepts always and business_hours; only always is evaluated. business_hours is persisted and shown in the legacy UI but skipped by both live legacy evaluators. after_hours and custom are rejected. |
recipients | object[] | no | [] | Up to 20 entries. The API does not enforce a lower bound. See Recipient object. |
created_by | uuid | server-set | - | User who created the rule. NULL after the user is deleted. |
created_at | timestamptz | server-set | now() | |
updated_at | timestamptz | server-set | now() |
Rule-kind discriminator
D95 split rules into two kinds. The DB CHECK constraint alert_rules_kind_consistency enforces the split:
rule_kind=camera_event— legacy camera-event rules. MUST set at least one ofevent_category,trigger_topic_pattern, orcustom_tags. MUST NOT setsystem_trigger. Acustom_tags-only row satisfies CRUD validation but is skipped by the live legacy evaluator.rule_kind=system_event— camera and gateway lifecycle. MUST setsystem_trigger. MUST NOT setevent_category,trigger_topic_pattern, orsource_filter.
event_category values
The 10 normalized categories defined in migration 000053 and the gateway classifier (D96).
| Value | Matches |
|---|---|
motion | Generic motion alarms (VideoSource/MotionAlarm, vendor MotionDetector rules). |
intrusion | Field-detector / intrusion / region-entered events. |
line_crossing | Line-crossing rules (LineDetector/Crossed and vendor variants). |
tamper | Camera tampering — cover, defocus, redirection. |
tampering_scene | Scene-change tampering (Profile T §7.17): ImageTooBlurry, ImageTooDark, ImageTooBright, GlobalSceneChange. |
analytics_object | Normalized object-classification analytics (person, vehicle, animal). Legacy AI predicate fields remain non-firing; use a compiled rule for AI evidence predicates. |
io | Digital input / output (e.g. Device/Trigger/DigitalInput). |
audio | Audio detection — gunshot, glass break, scream. |
schedule | Scheduled-window events. |
other | Any normalized topic with no category mapping. The namespace-stripped value is stored in events.trigger_topic; the verbatim camera value is preserved in raw_topic. Match by trigger_topic_pattern rather than event_category=other. |
Topic pattern syntax
trigger_topic_pattern is matched against events.trigger_topic, the gateway’s namespace-stripped Normalized topic. It is not matched against raw_topic, which preserves the camera’s verbatim namespace prefix.
The live sqlLikeMatches evaluator uses this contract:
| Pattern element | Meaning |
|---|---|
% | Zero or more characters. |
_ | Exactly one character. |
| Any other character | Literal text. Regular-expression characters have no special meaning. |
Matching is case-insensitive and covers the whole normalized topic. Surrounding whitespace in the pattern is trimmed. A null or empty pattern means no topic filter at evaluation.
Examples:
%SomeNewDetector%matches a normalized topic containingSomeNewDetector.RuleEngine/SomeNewDetector%matches a normalized topic with that prefix.%/Crossedmatches a normalized topic ending in/Crossed.
source_filter values
events.trigger_source values that can appear in source_filter. The set is closed.
onvif- ONVIF event from a camerasoftware- Software-side detection (reserved; not produced in v1 per D12-REV)manual- Operator-triggered Record Clipscheduled- Time-of-day scheduled event
system_trigger values
Allowed only when rule_kind=system_event.
camera_offline- Fires when a camera has been offline for 5 minutes continuouslygateway_offline- Fires when a gateway misses heartbeats past its grace windowgateway_reconnectis not supported. Current validation rejects it because the system-event evaluator handles offline transitions only.
Schedule semantics
This section describes legacy alert_rules.schedule_type, not compiled-rule schedules.
schedule_type | API/legacy UI | Live legacy evaluator behavior |
|---|---|---|
always | Accepted and persisted. | Evaluated 24/7 when the other conditions match. |
business_hours | Accepted, persisted, and shown in the legacy UI. | Skipped by both the immediate camera-event evaluator and the system-event evaluator; the rule does not fire. |
after_hours, custom | Rejected with invalid schedule_type. | Not evaluated. |
For working recurring schedules and time windows, create a compiled rule through the current /alerts/rules/new-v5 wizard. Compiled rules store schedule/time-window data in their compiled definition, and alerts/evaluator.go enforces it before the remaining predicates. See Create an alert rule.
Recipient object
Each entry in recipients is a notification destination.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
id | uuid | server-set | gen_random_uuid() | Primary key. |
user_id | uuid | yes | - | Active user in the same org. |
channels | string[] | yes | ["in_app"] | Subset of {in_app, email, webhook} (alertrule_service.go:184-186 defaults to in_app). |
A rule may carry up to 20 recipients (alertrule_service.go:147-149). The ”≥ 1 recipient required” lower bound is documented but not enforced server-side (audit GAP-7-21).
Examples
Motion alert on a specific topic
{ "name": "Front entrance motion", "rule_kind": "camera_event", "event_category": "motion", "trigger_topic_pattern": "VideoSource/MotionAlarm", "source_filter": ["onvif"], "scope_type": "cameras", "camera_ids": ["5e3c7f..."], "schedule_type": "always", "cooldown_sec": 300, "recipients": [ {"user_id": "a1b2c3...", "channels": ["in_app", "email"]} ]}All line-crossing events
{ "name": "Loading-dock line crossings", "rule_kind": "camera_event", "event_category": "line_crossing", "scope_type": "all_cameras", "schedule_type": "always", "cooldown_sec": 60, "recipients": [ {"user_id": "a1b2c3...", "channels": ["in_app"]} ]}Camera-offline alert
{ "name": "Front entrance camera offline", "rule_kind": "system_event", "system_trigger": "camera_offline", "scope_type": "cameras", "camera_ids": ["5e3c7f..."], "schedule_type": "always", "cooldown_sec": 1800, "recipients": [ {"user_id": "a1b2c3...", "channels": ["email"]} ]}Legacy custom_tags and min_confidence payloads are accepted for CRUD
round-trips but do not fire. Create new AI or time-window rules with New Alert
(NL wizard) at /alerts/rules/new-v5, which stores an active compiled version
for the compiled evaluator.
Validation rules
The API rejects rules that violate any of these. Rejections return HTTP 400 with the error message in the response body.
| Rule | Why |
|---|---|
rule_kind is required | D95 discriminator must be set. |
rule_kind must be camera_event or system_event | Closed enum. |
camera_event must set at least one of event_category, trigger_topic_pattern, or custom_tags | Without a match axis the rule fires on every event for the org. |
event_category not allowed for system_event | DB CHECK constraint. |
trigger_topic_pattern not allowed for system_event | DB CHECK constraint. |
source_filter not allowed for system_event | DB CHECK constraint. |
system_trigger required for system_event | Without it the rule never fires. |
system_trigger not allowed for camera_event | DB CHECK constraint. |
event_category value must be one of the 10 categories | Closed enum (mirrors event_category ENUM). |
system_trigger value must be camera_offline or gateway_offline | Current closed runtime-supported set; gateway_reconnect is rejected. |
min_confidence must be in 0.0 - 1.0 | Probability range (alertrule_service.go:79-81). |
cooldown_sec must be in 60 - 3600 | API range; enforced by the live legacy history inserts. |
aggregation_window_seconds must be 0 | Non-zero legacy aggregation is rejected as unsupported. |
delay_seconds must be 0 | Non-zero legacy delay is rejected as unsupported. |
conditions_json must be empty or NULL | Non-empty legacy condition trees are rejected as unsupported. |
custom_tags max 20 entries | Wizard-only cap in v1; the API does NOT enforce a 20-entry maximum server-side (audit GAP-7-18). |
recipients ≤ 20 entries | API enforces only the upper bound. The “≥1 recipient” lower bound is NOT enforced server-side (audit GAP-7-21). |
(rule_id, user_id) pair must be unique within recipients | Documented intent; server-side enforcement depends on the DB unique constraint (audit GAP-7-22 — needs verification against migration set). |
site_ids required when scope_type=site | Accepted and persisted, but skipped by the live legacy evaluators. |
camera_ids required when scope_type=cameras | Executable camera-list scope. |
Source: cloud/internal/alerts/alertrule_handler.go.
See also
- Universal ONVIF events - the event taxonomy this schema matches against
- Event endpoints - the events you’ll match
- Alert endpoints - managing rules via the REST API
- Create an alert rule - the how-to walkthrough
- Audit actions -
alert_rule.*entries logged on every change - Roles and permissions - who can create alert rules