Create an alert rule
const url = 'http://localhost:3000/api/v1/alert-rules';const options = { method: 'POST', headers: {'X-API-Key': '<X-API-Key>', 'Content-Type': 'application/json'}, body: '{"name":"CPU above 80% on edge nodes","ruleType":"DEVICE_OFFLINE","scope":"GLOBAL","targetId":"2489E9AD-2EE2-8E00-8EC9-32D5F69181C0","targetLabels":{"site":"berlin","tier":"edge"},"expression":"cpu > 80 AND device.label.tier = \"edge\" for 5m","thresholdMinutes":5,"severity":"INFO","enabled":true,"notificationTargetIds":["2489E9AD-2EE2-8E00-8EC9-32D5F69181C0"],"suppression":{"enabled":true,"trigger":"AFTER_FIRST","maxNotifications":1,"intervalSeconds":1,"liftSchedule":{"type":"dailyAt","time":"23:59"},"liftTimezone":"UTC","liftMode":"DIGEST"}}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url http://localhost:3000/api/v1/alert-rules \ --header 'Content-Type: application/json' \ --header 'X-API-Key: <X-API-Key>' \ --data '{ "name": "CPU above 80% on edge nodes", "ruleType": "DEVICE_OFFLINE", "scope": "GLOBAL", "targetId": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "targetLabels": { "site": "berlin", "tier": "edge" }, "expression": "cpu > 80 AND device.label.tier = \"edge\" for 5m", "thresholdMinutes": 5, "severity": "INFO", "enabled": true, "notificationTargetIds": [ "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0" ], "suppression": { "enabled": true, "trigger": "AFTER_FIRST", "maxNotifications": 1, "intervalSeconds": 1, "liftSchedule": { "type": "dailyAt", "time": "23:59" }, "liftTimezone": "UTC", "liftMode": "DIGEST" } }'The expression is validated against the alert DSL at write time — a malformed rule is rejected here rather than silently never firing.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Header Parameters
Section titled “Header Parameters”UUID of the organization (tenant) the request acts in. It selects the tenant and grants nothing: the server checks membership or an open tenant switch. Not needed for global operations. A malformed value returns 400. API keys are fixed to their own organization; another organization returns 403. The x-organization-source header grants nothing.
Request Bodyrequired
Section titled “Request Bodyrequired”object
Human-readable rule name
Example
CPU above 80% on edge nodesCondition family the rule evaluates
Example
METRIC_THRESHOLDWhich devices the rule applies to. GLOBAL covers every device in the organization; LABEL matches the selector in targetLabels; DEVICE matches the single device in targetId. ROLLOUT_HALTED rules take GLOBAL (every deployment of the organization) or DEPLOYMENT (the single deployment in targetId); the device scopes are rejected for them, and DEPLOYMENT is rejected for every other rule type.
Device id when scope is DEVICE, deployment id when scope is DEPLOYMENT (it must be a deployment of the caller’s organization); required for both, ignored otherwise. On PATCH, null clears it (rejected while the resulting scope needs it).
Label selector. Required when scope is LABEL, ignored otherwise. A device matches when every key/value pair is present on its labels.
object
Example
{ "site": "berlin", "tier": "edge"}Condition expression. Required for METRIC_THRESHOLD and WORKLOAD_CRASH; rejected for DEVICE_OFFLINE (which uses thresholdMinutes) and ROLLOUT_HALTED (which has no condition). Syntax: field op value [AND field op value ...] [for <duration>], where duration is e.g. 30s, 5m, 2h. The field vocabulary (names, units, admitted operators, allowed values) is served by GET /api/v1/alert-rules/metric-catalog; an expression naming any other field is rejected with 400 at write time.
Example
cpu > 80 AND device.label.tier = "edge" for 5mDEVICE_OFFLINE only: minutes without a heartbeat before the rule fires. Defaults to 5 per FR-FM-021.
Operator-assigned severity, carried verbatim into every notification the rule produces (email subject and body, webhook payload). Defaults to WARNING.
Whether the rule is evaluated
Notification targets to deliver this rule’s alerts to. Ids belonging to another organization are rejected.
Notification suppression for this rule (FM-1269). Omit entirely for no suppression — today’s behaviour, unchanged: every occurrence notifies.
object
Whether suppression is active for this rule. Defaults to true.
What opens the suppression window. AFTER_FIRST opens as soon as the first notification for the scope selection has pushed; RATE opens once maxNotifications have pushed within intervalSeconds. Defaults to AFTER_FIRST.
RATE trigger only: how many notifications pass per intervalSeconds before the window opens. Required together with intervalSeconds when trigger is RATE; rejected when trigger is AFTER_FIRST.
RATE trigger only: the interval, in seconds, that the maxNotifications budget applies to. 60 seconds to 604800 seconds (one week).
Structured lift schedule for when the suppression window opens — NEVER a cron expression (ADR-0018-1 §2.4). Exactly one of: {“type”:“everyHours”,“hours”:1..168}, {“type”:“dailyAt”,“time”:“HH:MM”} (24-hour, zero-padded), {“type”:“weeklyAt”,“days”:[“MON”..“SUN”],“time”:“HH:MM”}. Validated against this exact shape server-side; an unknown type, a missing or extra field, or a malformed value is rejected with a 400 naming the field.
object
Example
{ "type": "dailyAt", "time": "23:59"}IANA time zone the lift schedule is read in, e.g. Europe/Berlin or UTC. Defaults to UTC.
Example
Europe/BerlinWhat the scheduled lift produces. DIGEST sends one summary notification naming every occurrence suppressed during the window (the default). REPEAT re-notifies every occurrence still open at lift time. SILENT sends nothing — the delivery history is the only record.
Responses
Section titled “Responses”object
object
object
object
object
object
Notification suppression for this rule (FM-1269), or null when none is set.
object
RATE trigger only. Null for AFTER_FIRST.
object
RATE trigger only. Null for AFTER_FIRST.
object
The structured lift schedule as stored — never a cron expression. See AlertSuppressionDto.liftSchedule for the three admitted shapes.
object
Example
{ "name": "CPU above 80% on edge nodes", "ruleType": "DEVICE_OFFLINE", "scope": "GLOBAL", "expression": "cpu > 80 for 5m", "severity": "INFO", "notificationTargets": [ { "name": "Ops on-call email", "channel": "EMAIL" } ], "suppression": { "trigger": "AFTER_FIRST", "liftSchedule": { "type": "dailyAt", "time": "23:59" }, "liftTimezone": "Europe/Berlin", "liftMode": "DIGEST" }}Malformed expression, or scope/target mismatch