---
title: "Alert rules API"
description: "API reference for Alpacon metric alert rules. Set direction, duration, recovery threshold, no-data window, device scope, and notification destinations."
---

> Full documentation index: https://docs.alpacax.com/llms.txt

# Alert rules

**Endpoint**: `/api/metrics/alert-rules/`

Alert rules watch a server's metrics and raise an alert when a value crosses a threshold, using the data the agent has already sent. Rules are evaluated on Alpacon's own schedule, about once a minute—see [Metric alerts](/use/servers/monitor/#metric-alerts) for how a rule decides to fire and clear.

Requires the Metrics extension to be enabled for the workspace, for every action—including reading the seeded default disk-usage rule that every server carries regardless of plan. That rule is still evaluated on a workspace with the extension off; this API just isn't how you'd read it there.

## Who can edit or delete a rule

A workspace admin can update or delete any rule. So can anyone whose role carries the workspace-wide `alert_rule:update` (for `PUT`/`PATCH`) or `alert_rule:delete` permission—the built-in `operator` role has both, `member` has neither. Creating a rule doesn't grant its creator any special access to it on its own.

Beyond those roles, an admin can give a specific person, group, or service token's Application the right to update and delete one rule, without changing anyone's workspace-wide role. That grant is an explicit rule-scoped `alert_rule:maintainer` binding, made through the RBAC role API—the console's role pickers can't scope a grant to a single rule yet. A service token reaches a rule only through a binding like this one; its Application's workspace-wide roles, even `operator`, don't reach a rule on their own. A personal API token acts as the person who issued it, so it follows their role and any binding they hold.

A rule created before Alpacon stopped granting this binding to creators automatically may still carry the binding its creator was given at the time, and that creator keeps updating and deleting the rule until Alpacon finishes cleaning up those older bindings.

### Default rules

The extension default rules—CPU usage, memory usage, and any other non-core rule marked default—can be updated by anyone whose role carries `alert_rule:update`, the same as any other rule. No default rule can be deleted while it's still marked as default, whoever asks; see [Delete alert rule](#delete-alert-rule).

### The core disk usage rule

Every workspace carries a seeded disk-usage rule (`is_default: true`, target `disk-usage`) that stays active whether or not the Metrics extension is on. Only a workspace admin may:

- Update it with `PUT` or `PATCH`
- Create, or turn an existing rule into, one with `is_default: true` and target `disk-usage`—on any device
- Create, update, or delete a [server override](/reference/api/metrics/rule-overrides/) on it, or move an override onto or off it
- Detach it from a server—through the servers API's detach-rule action, or a server update that drops it from `rules`

Anyone else—an operator, a principal with a rule-scoped binding on it, and every service token—gets 403 `metrics_alert_rule_core_admin_only`. Attaching the rule back to a server isn't restricted this way, and previewing what it would do ([recipients](#preview-recipients) or [evaluation](#preview-evaluation)) stays open to anyone who can already read the rule.

  
    ## List alert rules

    Retrieve a paginated list of alert rules.

    ### Request

    ```http
    GET /api/metrics/alert-rules/
    ```

    ### Query parameters

    | Parameter | Type | Description |
    |-----------|------|-------------|
    | `name` | string | Filter by rule name |
    | `target` | string | Filter by target metric |
    | `threshold` | number | Filter by threshold |
    | `operator` | string | Filter by direction (`gte` or `lte`) |
    | `device` | string | Filter by device |
    | `severity` | string | Filter by severity (`critical`, `warning`, `info`) |
    | `is_default` | boolean | Filter by default status |
    | `server` | UUID | Only rules attached to this server |
    | `exclude_server` | UUID | Only rules not attached to this server |
    | `search` | string | Search across name, target, and default status |
    | `ordering` | string | Sort order. Default: `-added_at` |
    | `page` | integer | Page number |
    | `page_size` | integer | Results per page (default: 15, max: 100) |

    ### Response

    ```json
    {
      "count": 2,
      "next": null,
      "previous": null,
      "results": [
        {
          "id": "3f1c9b2e-6a4d-4e8f-9b1a-2c3d4e5f6a7b",
          "name": "Core disk usage",
          "target": "disk-usage",
          "threshold": 80,
          "operator": "gte",
          "duration_s": 0,
          "recovery_threshold": 78,
          "no_data_after_s": null,
          "device": "",
          "severity": "warning",
          "notify_email": "all",
          "notify_slack_channel": true,
          "is_default": true
        },
        {
          "id": "8c4d6e2a-1b3f-4a5c-8d9e-0f1a2b3c4d5e",
          "name": "Sustained high CPU",
          "target": "cpu-usage",
          "threshold": 80,
          "operator": "gte",
          "duration_s": 300,
          "recovery_threshold": 70,
          "no_data_after_s": 600,
          "device": "",
          "severity": "critical",
          "notify_email": "admins",
          "notify_slack_channel": false,
          "is_default": false
        }
      ]
    }
    ```

    Required scope: [`alert_rule:read`](/reference/scopes/#alert_rule-read)

    ### Example

    ```bash
    curl -X GET "https://your-workspace.us1.alpacon.io/api/metrics/alert-rules/?target=cpu-usage" \
      -H "Authorization: token=\"alpat-xxxxxxxxxxxxxxxxxx\""
    ```
  

  
    ## Create alert rule

    Create a new alert rule.

    ### Request

    ```http
    POST /api/metrics/alert-rules/
    ```

    ### Request body

    ```json
    {
      "name": "Sustained high CPU",
      "target": "cpu-usage",
      "threshold": 80,
      "operator": "gte",
      "duration_s": 300,
      "recovery_threshold": 70,
      "no_data_after_s": 600,
      "severity": "critical",
      "notify_email": "admins",
      "notify_slack_channel": false
    }
    ```

    ### Parameters

    | Field | Type | Required | Description |
    |-------|------|----------|-------------|
    | `name` | string | Yes | Rule name. Must be unique in the workspace |
    | `target` | string | Yes | Metric to watch: `cpu-usage`, `memory-usage`, `disk-usage`, `peak-read-bps`, `peak-write-bps`, `avg-read-bps`, `avg-write-bps`, `peak-input-pps`, `peak-input-bps`, `peak-output-pps`, `peak-output-bps`, `avg-input-pps`, `avg-input-bps`, `avg-output-pps`, `avg-output-bps` |
    | `threshold` | number | Yes | The value that raises the alert |
    | `operator` | string | No | `gte`—raise at or above the threshold (default)—or `lte`—raise at or below it |
    | `duration_s` | integer | No | How long the condition has to hold before the alert raises, in seconds. Default `0`: the first breaching sample raises it. A positive value must be at least two collection intervals for the target—shorter is rejected with `metrics_alert_rule_duration_too_short` |
    | `recovery_threshold` | number | No | The value the metric has to reach before the alert clears. Must sit on the recovering side of `threshold` for the chosen `operator`—below it for `gte`, above it for `lte`. Leave empty and the threshold itself clears the alert |
    | `no_data_after_s` | integer | No | Raise a separate alert when no sample arrives for this many seconds. Must be at least this target's collection interval and at most 86400 (24 hours). Leave empty to turn this off |
    | `device` | string | No | Limit the rule to one disk or network interface. Only valid on a device-scoped target—`disk-usage` and every disk I/O and network target above. Rejected on `cpu-usage` and `memory-usage`, which apply to the whole server |
    | `severity` | string | No | Severity the raised alert carries: `critical`, `warning`, or `info`. Default `warning`. Severity decides which channel can fire at all: only `critical` can email, per `notify_email`; `critical` and `warning` can post to Slack, per `notify_slack_channel`; `info` posts to neither. Every severity is still recorded in the console and the bell, delivered to any webhook subscribed to the rule's events, and included in the hourly [open alert digest](/reference/notifications/#open-alert-digest) while it's open |
    | `notify_email` | string | No | Who the alert email is addressed to, within the people who can already see the server: `all` (default)—everyone who can see it; `admins`—workspace admins only; `group_members`—server group members and the owner; `none`—no individual alert email. Independent of `notify_slack_channel`, and doesn't affect the separate [open alert digest](/reference/notifications/#open-alert-digest). Only takes effect on a `critical`-severity rule; a `warning`- or `info`-severity rule sends no mail whatever this says |
    | `notify_slack_channel` | boolean | No | Whether a raise and its resolution post to the workspace's connected Slack channel. Default `true`. Independent of `notify_email`. Applies at `critical` and `warning` severity; an `info`-severity rule posts nothing whatever this says |
    | `is_default` | boolean | No | Whether this rule is automatically attached to every server registered afterward. Only one default rule per target and device |

    Only a signed-in workspace member can set `notify_email` or `notify_slack_channel`—an application or service token gets `metrics_alert_rule_destination_not_allowed` if the request touches either field, even to send the same value the rule already has.

    Creating a rule doesn't grant the creator any special access to it—see [Who can edit or delete a rule](#who-can-edit-or-delete-a-rule). A create that leaves the new rule with `is_default: true` and target `disk-usage` is refused with `metrics_alert_rule_core_admin_only` unless the caller is a workspace admin—see [the core disk usage rule](#the-core-disk-usage-rule).

    ### Response

    ```json
    {
      "id": "8c4d6e2a-1b3f-4a5c-8d9e-0f1a2b3c4d5e",
      "name": "Sustained high CPU",
      "target": "cpu-usage",
      "threshold": 80,
      "operator": "gte",
      "duration_s": 300,
      "recovery_threshold": 70,
      "no_data_after_s": 600,
      "device": "",
      "severity": "critical",
      "notify_email": "admins",
      "notify_slack_channel": false,
      "is_default": false
    }
    ```

    Required scope: [`alert_rule:create`](/reference/scopes/#alert_rule-create)

    ### Example

    ```bash
    curl -X POST "https://your-workspace.us1.alpacon.io/api/metrics/alert-rules/" \
      -H "Authorization: token=\"alpat-xxxxxxxxxxxxxxxxxx\"" \
      -H "Content-Type: application/json" \
      -d '{"name": "Sustained high CPU", "target": "cpu-usage", "threshold": 80, "duration_s": 300}'
    ```
  

  
    ## Get alert rule

    Retrieve a specific alert rule by ID.

    ### Request

    ```http
    GET /api/metrics/alert-rules/{rule_id}/
    ```

    ### Path parameters

    | Parameter | Type | Required | Description |
    |-----------|------|----------|-------------|
    | `rule_id` | UUID | Yes | Alert rule ID |

    ### Response

    ```json
    {
      "id": "8c4d6e2a-1b3f-4a5c-8d9e-0f1a2b3c4d5e",
      "name": "Sustained high CPU",
      "target": "cpu-usage",
      "threshold": 80,
      "operator": "gte",
      "duration_s": 300,
      "recovery_threshold": 70,
      "no_data_after_s": 600,
      "device": "",
      "severity": "critical",
      "notify_email": "admins",
      "notify_slack_channel": false,
      "is_default": false
    }
    ```

    Required scope: [`alert_rule:read`](/reference/scopes/#alert_rule-read)

    ### Example

    ```bash
    curl -X GET "https://your-workspace.us1.alpacon.io/api/metrics/alert-rules/8c4d6e2a-1b3f-4a5c-8d9e-0f1a2b3c4d5e/" \
      -H "Authorization: token=\"alpat-xxxxxxxxxxxxxxxxxx\""
    ```
  

  
    ## Preview recipients

    Preview who an alert rule would notify, as counts—never names—without waiting for it to actually fire.

    ### Request

    ```http
    GET /api/metrics/alert-rules/{rule_id}/recipients/
    ```

    ### Path parameters

    | Parameter | Type | Required | Description |
    |-----------|------|----------|-------------|
    | `rule_id` | UUID | Yes | Alert rule ID |

    ### Response

    ```json
    {
      "email": {
        "mode": "group_members",
        "count": 7,
        "reasons": { "admins": 0, "group_members": 6, "owner": 1 }
      },
      "slack_channel": { "enabled": true, "connected": true },
      "event_subscriptions": 2
    }
    ```

    | Field | Description |
    |-------|-------------|
    | `email.mode` | The rule's `notify_email` value, echoed back |
    | `email.count` | How many people this rule's email destination would reach, counted over the servers you can see that the rule is attached to and can actually fire on. Always `0` for `notify_email: "none"`, and always `0` for a `warning`- or `info`-severity rule, whatever `notify_email` says—only a `critical` rule mails directly |
    | `email.reasons` | How many of those people hold each ground: `admins` (workspace admins), `group_members` (members of a group one of those servers is in), `owner` (owners of those servers who still have access to the server they own; this is not the rule's creator). The grounds overlap—someone can be a workspace admin and a group member at once—so they don't have to add up to `count` |
    | `slack_channel.enabled` | The rule's `notify_slack_channel` value, echoed back |
    | `slack_channel.connected` | Whether the workspace has a Slack installation the alert path can post through. An `info`-severity rule posts nothing regardless of either value |
    | `event_subscriptions` | Live event subscriptions that would receive this rule's raise and resolve events—those targeting one of the servers counted above, plus workspace-wide subscriptions. A count only; unaffected by `notify_email` or `notify_slack_channel` |

    No user names, emails, or IDs appear anywhere in the response.

    A rule whose servers are all outside what you can see returns `404`, the same as a rule you can't reach at all. A rule that isn't attached to any server returns zeros rather than `404`—that's the state a rule is in between being created and attached, and `404` there would read as the rule having failed to save.

    Required scope: [`alert_rule:read`](/reference/scopes/#alert_rule-read)

    ### Example

    ```bash
    curl -X GET "https://your-workspace.us1.alpacon.io/api/metrics/alert-rules/8c4d6e2a-1b3f-4a5c-8d9e-0f1a2b3c4d5e/recipients/" \
      -H "Authorization: token=\"alpat-xxxxxxxxxxxxxxxxxx\""
    ```
  

  
    ## Preview evaluation

    Replay a rule against recent realtime samples to see when it would have raised and cleared, without saving anything. Every raise and resolution is decided by the same evaluator that runs the workspace's actual rules—this only replays the samples through it.

    ### Request

    ```http
    POST /api/metrics/alert-rules/preview/
    ```

    ### Request body

    ```json
    {
      "rule": {"target": "cpu-usage", "operator": "gte", "threshold": 80,
               "duration_s": 300, "recovery_threshold": 75, "no_data_after_s": null,
               "device": "", "severity": "warning",
               "notify_email": "all", "notify_slack_channel": true},
      "rule_id": "8c4d6e2a-1b3f-4a5c-8d9e-0f1a2b3c4d5e",
      "servers": ["3f0e2b1c-5d6a-4e7f-8a9b-0c1d2e3f4a5b"],
      "window_s": 3600
    }
    ```

    ### Parameters

    | Field | Type | Required | Description |
    |-------|------|----------|-------------|
    | `rule` | object | Yes | A rule body, same fields as [Create alert rule](#create-alert-rule) minus `name` and `is_default`—sending either is ignored. Always required, even with `rule_id` set: with `rule_id`, send only the fields you want to try (the rest come from that rule), or `{}` to preview the rule unchanged |
    | `rule_id` | UUID | No | An existing rule to preview against. Its [server overrides](/reference/api/metrics/rule-overrides/) apply, and its attached servers become the default for `servers` |
    | `servers` | array of UUID | No, unless `rule_id` is omitted | Servers to replay over, up to 20 |
    | `window_s` | integer | No | How far back to replay, in seconds. Default `86400` (24 hours), minimum `60`, maximum `604800` (7 days)—as long as realtime samples are kept |

    At least one of `rule_id` or `servers` is required.

    Two caps beyond the fields above: at most 20 servers, and servers × `window_s` can't exceed 5 × 604800 seconds—20 servers caps the window at a little under 1.75 days, and the full 7-day window caps servers at 5. Separately, a replay can't read or judge more than 300,000 sample rows; a server reporting many disks or interfaces can hit that before the server cap does. With `rule_id` alone, `servers` defaults to the servers that rule is attached to; if that default set is larger than 20, the 20-server cap refuses it, so `servers` must be given explicitly to pick which ones to replay.

    The same [destination restriction](#create-alert-rule) as saving a rule applies here: an application or service token gets `metrics_alert_rule_destination_not_allowed` if the body sets `notify_email` or `notify_slack_channel`.

    ### Response

    ```json
    {
      "window": {"start": "2026-09-01T11:00:00Z", "end": "2026-09-01T12:00:00Z"},
      "sample_interval_s": 60,
      "summary": {"episodes": 1, "would_interrupt": 1, "open_at_end": 0, "longest_s": 240},
      "servers": [
        {"server": "3f0e2b1c-5d6a-4e7f-8a9b-0c1d2e3f4a5b", "device": "", "samples": 60,
         "episodes": [
           {"condition": "threshold", "raised_at": "2026-09-01T11:50:00Z",
            "resolved_at": "2026-09-01T11:54:00Z", "peak": 91.2, "outlived_hold": true,
            "would_interrupt": true}
         ]}
      ],
      "recipients": {
        "email": {"mode": "all", "count": 3,
                  "reasons": {"admins": 1, "group_members": 2, "owner": 1}},
        "slack_channel": {"enabled": true, "connected": false},
        "event_subscriptions": 0
      },
      "not_reproduced": ["tick_jitter", "state_before_window"]
    }
    ```

    | Field | Description |
    |-------|-------------|
    | `window.start` / `window.end` | The replayed window. `end` is when the request ran, `start` is `window_s` seconds before it |
    | `sample_interval_s` | The tick the replay judges at, `60` |
    | `summary.episodes` | How many episodes fired, across every server and device in the response |
    | `summary.would_interrupt` | How many episodes have `would_interrupt: true`—outlived the roughly two-minute hold and would route to email or Slack under the rule's severity and destinations, the same test the live evaluator applies. `0` if the rule's severity and destinations reach neither channel, whatever the episodes did |
    | `summary.open_at_end` | Episodes still open at `window.end` |
    | `summary.longest_s` | The longest episode, in seconds; one still open at `window.end` is measured up to there |
    | `servers[]` | One row per (server, device) replayed. A host-wide target (`cpu-usage`, `memory-usage`) gets one row with `device: ""`; a rule scoped to one device gets that device; a device-scoped target left unscoped gets one row per device that sent a sample in the window. A server that's unmanaged, not commissioned, or excluded by its own override is left out entirely |
    | `servers[].episodes[].condition` | `threshold` or `no_data` |
    | `servers[].episodes[].raised_at` / `resolved_at` | When the episode opened and closed. `resolved_at` is `null` if it's still open at `window.end` |
    | `servers[].episodes[].peak` | The highest value seen (lowest, for an `lte` rule) from the oldest sample the raise judged through resolution, or through `window.end` if still open. `null` for a `no_data` episode |
    | `servers[].episodes[].outlived_hold` | Whether the episode stayed open past the roughly two-minute hold that gates a live email or Slack post |
    | `servers[].episodes[].would_interrupt` | Whether this episode outlived the hold *and* the rule's severity and destinations route it to an interrupting channel (email or Slack). Implies `outlived_hold: true`; still `false` when the episode outlived the hold but the rule's severity and destinations wouldn't send anything |
    | `recipients` | Same shape as [Preview recipients](#preview-recipients), computed over the servers actually replayed |
    | `not_reproduced` | Which differences between this replay and the live evaluator apply to this response: `tick_jitter`—a live tick can land a few seconds off the minute this replay assumes; `state_before_window`—the replay can't see state from before `window.start`, so an episode already open when the window begins is missed; `maintenance_window_no_data`—present when the rule has a `no_data_after_s`, a maintenance window's own no-data handling isn't replayed; `core_scope_current`—present for the seeded core disk-usage rule, which volume counts as "core" is resolved once as of `window.end`, not tick by tick |

    A `rule_id` that doesn't exist, or whose attached servers are all outside what you can see, returns `404`. An out-of-reach or unknown entry in `servers` returns `400` with `server_not_found` instead. Previewing someone else's rule this way isn't refused—the preview stores nothing—and it isn't recorded in the [activity log](/use/audit/governance/activity-log/).

    Required scope: [`alert_rule:read`](/reference/scopes/#alert_rule-read)

    ### Example

    ```bash
    curl -X POST "https://your-workspace.us1.alpacon.io/api/metrics/alert-rules/preview/" \
      -H "Authorization: token=\"alpat-xxxxxxxxxxxxxxxxxx\"" \
      -H "Content-Type: application/json" \
      -d '{"rule": {"target": "cpu-usage", "threshold": 80, "duration_s": 300}, "servers": ["3f0e2b1c-5d6a-4e7f-8a9b-0c1d2e3f4a5b"]}'
    ```
  

  
    ## Update alert rule

    Update an existing alert rule. Supports partial updates via `PATCH`.

    ### Request

    ```http
    PATCH /api/metrics/alert-rules/{rule_id}/
    ```

    ### Path parameters

    | Parameter | Type | Required | Description |
    |-----------|------|----------|-------------|
    | `rule_id` | UUID | Yes | Alert rule ID |

    ### Request body

    ```json
    {
      "threshold": 85,
      "recovery_threshold": 75
    }
    ```

    ### Parameters

    Same fields as [Create alert rule](#create-alert-rule), all optional. Changing `operator` or `threshold` is refused if it would leave this rule's own [recovery threshold](#create-alert-rule) or any [server override](/reference/api/metrics/rule-overrides/)'s recovery threshold on the wrong side of the new value.

    Only certain principals can reach a given rule to update it—see [Who can edit or delete a rule](#who-can-edit-or-delete-a-rule). The core disk-usage rule accepts `PUT`/`PATCH` only from a workspace admin; everyone else gets `metrics_alert_rule_core_admin_only`.

    ### Response

    Returns the full alert rule object with updated fields (same schema as [Get alert rule](#get-alert-rule)).

    Required scope: [`alert_rule:update`](/reference/scopes/#alert_rule-update)

    ### Example

    ```bash
    curl -X PATCH "https://your-workspace.us1.alpacon.io/api/metrics/alert-rules/8c4d6e2a-1b3f-4a5c-8d9e-0f1a2b3c4d5e/" \
      -H "Authorization: token=\"alpat-xxxxxxxxxxxxxxxxxx\"" \
      -H "Content-Type: application/json" \
      -d '{"threshold": 85, "recovery_threshold": 75}'
    ```
  

  
    ## Delete alert rule

    Remove an alert rule. A default rule (`is_default: true`) can't be deleted while it's still marked as default—unset `is_default` first.

    Only certain principals can reach a given rule to delete it—see [Who can edit or delete a rule](#who-can-edit-or-delete-a-rule).

    ### Request

    ```http
    DELETE /api/metrics/alert-rules/{rule_id}/
    ```

    ### Path parameters

    | Parameter | Type | Required | Description |
    |-----------|------|----------|-------------|
    | `rule_id` | UUID | Yes | Alert rule ID |

    ### Response

    ```http
    204 No Content
    ```

    Required scope: [`alert_rule:delete`](/reference/scopes/#alert_rule-delete)

    ### Example

    ```bash
    curl -X DELETE "https://your-workspace.us1.alpacon.io/api/metrics/alert-rules/8c4d6e2a-1b3f-4a5c-8d9e-0f1a2b3c4d5e/" \
      -H "Authorization: token=\"alpat-xxxxxxxxxxxxxxxxxx\""
    ```
  

## Error responses

| Status | Error code | Description |
|--------|------------|-------------|
| 400 | `metrics_alert_rule_exists` | A default rule already exists for this target and device |
| 400 | `metrics_alert_rule_recovery_threshold_invalid` | `recovery_threshold` is not on the recovering side of `threshold` |
| 400 | `metrics_alert_rule_override_recovery_threshold_invalid` | Changing `operator`, `threshold`, or `recovery_threshold` would leave a [server override](/reference/api/metrics/rule-overrides/)'s own recovery threshold on the firing side |
| 400 | `metrics_alert_rule_duration_too_short` | `duration_s` is positive but shorter than two collection intervals for the target |
| 400 | `metrics_alert_rule_override_duration_too_short` | Changing `target` would leave a server override's positive `duration_s` below the new target's floor (two collection intervals) |
| 400 | `metrics_alert_rule_no_data_window_too_short` | `no_data_after_s` is shorter than this target's collection interval |
| 400 | `metrics_alert_rule_no_data_window_too_long` | `no_data_after_s` is longer than 86400 seconds (24 hours) |
| 400 | `metrics_alert_rule_device_not_supported` | `device` was set on a host-wide target (`cpu-usage` or `memory-usage`) |
| 400 | `metrics_alert_rule_cannot_be_deleted` | Tried to delete a rule while `is_default` is still `true` |
| 400 | `metrics_alert_rule_destination_not_allowed` | `notify_email` or `notify_slack_channel` was set by an application or service token instead of a signed-in workspace member |
| 400 | `server_not_found` | [Preview evaluation](#preview-evaluation)'s `servers` names a server you can't reach or that doesn't exist |
| 400 | `metrics_alert_rule_preview_window_too_long` | [Preview evaluation](#preview-evaluation)'s `window_s` is longer than 604800 seconds (7 days) |
| 400 | `metrics_alert_rule_preview_too_many_servers` | [Preview evaluation](#preview-evaluation) named more than 20 servers, or servers × `window_s` exceeds 5 × 604800 |
| 400 | `metrics_alert_rule_preview_too_many_samples` | [Preview evaluation](#preview-evaluation) would read or judge more than 300,000 sample rows |
| 400 | `metrics_alert_rule_preview_servers_required` | [Preview evaluation](#preview-evaluation) named neither `rule_id` nor `servers` |
| 401 | `UNAUTHORIZED` | Invalid or missing authentication |
| 403 | `metrics_alert_rule_core_admin_only` | A write to the core disk-usage rule—or one that would create or retarget a rule onto it—from anyone but a workspace admin |
| 403 | `FORBIDDEN` | Insufficient permissions, or the Metrics extension isn't enabled for this workspace |
| 404 | `NOT_FOUND` | Alert rule not found |

The rule's own values are checked first and still refuse with the non-override codes above. For the two override codes, an update is refused only after every override on the rule has been checked, and `field_errors` keys each affected override `overrides.<server id>.recovery_threshold` or `overrides.<server id>.duration_s`—a server hit by both carries both paths. When the caller can't see the affected server, the response omits the `overrides.*` path and carries only `non_field_errors` with the same code. Overrides changed directly through the [rule overrides API](/reference/api/metrics/rule-overrides/) keep their own, unchanged error codes.

Source: https://docs.alpacax.com/reference/api/metrics/rules/
