---
title: "Alerts"
description: "Route SLO burn, downed dependencies, security detections and your own alerts to email, Slack, PagerDuty, Opsgenie or a webhook, once."
section: "interfaces"
status: "Building for 0.4 · ships as a beta API"
spec: "alerts"
canonical: "https://mainsdk.dev/docs/interfaces/alerts"
updated: "2026-10-28"
---

# Alerts

> Route SLO burn, downed dependencies, security detections and your own alerts to email, Slack, PagerDuty, Opsgenie or a webhook, once.

Status: Building for 0.4 · ships as a beta API · Canonical: https://mainsdk.dev/docs/interfaces/alerts · Updated: 2026-10-28

Route SLO burn, downed dependencies, security detections and your own alerts to email, Slack, PagerDuty, Opsgenie or a webhook, once.

## At a glance

- Status: Building for 0.4 · ships as a beta API
- Phase: 1 (Core, public beta)
- Milestone: 0.4 Hardening
- Plan: free in your app (Apache-2.0)
- Spec: https://github.com/hubxero/mainsdk/blob/develop/docs/specs/alerts.md
- Category: Reliability and operations

Calls:

- `alerts.raise({ key, severity, title, attrs?, runbook? })`
- `alerts.resolve(key)`
- `alerts.ack(id)`
- `alerts.get(id)`
- `alerts.list({ state?, source?, cursor?, limit? })`

In main.yaml:

```yaml
alerts:
  min: warning
  recipients:
    oncall:
      - { driver: pagerduty, key: secret:PAGERDUTY_ROUTING_KEY }
      - { driver: slack, webhook: secret:SLACK_ALERTS_WEBHOOK }
  routes:
    - { match: { severity: critical }, to: [oncall], escalate: [{ after: 15m, to: [oncall] }] }
    - { to: [oncall] }
  quiet: { from: "22:00", to: "08:00", tz: Asia/Kolkata }
```

Guarantees, whatever the driver:

- One message per open alert, however many instances raise it
- Critical alerts go at once; others wait out quiet hours as a digest
- Never on the request path; sends run as jobs with retries
- No personal data in an alert; provider dedupe keys are keyed hashes


## Drivers

Pick one in main.yaml with `alerts.driver`.

| Driver | Id in main.yaml | Default | Note |
| --- | --- | --- | --- |
| Built in (sends through notify and email drivers) | `builtin` | yes |  |

## Codes

| Code | Where | Meaning |
| --- | --- | --- |
| `MAIN-ALERT-0001` | check | An `alerts` value is invalid: an unknown driver, source or severity, a route to an undeclared recipient, a quiet-hours time or time zone that doesn't parse, or an escalation `after` shorter than `group.wait` |
| `MAIN-ALERT-0002` | check | Production has an `alerts` section, but no route sends `critical` alerts to a recipient |
| `MAIN-ALERT-0003` | check | A `webhook` recipient's host isn't in `network.allow`, or a recipient key or webhook URL is written as a literal |
| `MAIN-ALERT-0010` | runtime | An alert was raised or resolved; the log record carries the alert |
| `MAIN-ALERT-0011` | runtime | A recipient failed after its last retry; the others were still sent to |
| `MAIN-ALERT-0012` | runtime | The runtime store didn't answer; dedupe and escalation fell back to this instance |
| `MAIN-ALERT-0013` | runtime | `alerts.raise` refused or trimmed an input: a key over 200 characters, more than 20 attrs, or an attr outside `telemetry@1` |
| `MAIN-ALERT-0014` | runtime | `alerts.ack` refused: the caller lacks `alerts.manage` (403) |
| `MAIN-ALERT-0015` | runtime | A recipient passed `max-per-hour`; the rest of the hour arrives as one summary |

## Features

| Feature | Phase | Milestone | Spec ticket |
| --- | --- | --- | --- |
| alerts section in main.yaml with its checks | 1 | 0.4 | `alerts#contract` |
| raise, resolve, ack and list with dedupe by key | 1 | 0.4 | `alerts#core` |
| Routes, grouping, repeat and storm limits | 1 | 0.4 | `alerts#routing` |
| Quiet hours and escalation | 1 | 0.4 | `alerts#quiet-escalation` |
| Slack, PagerDuty, Opsgenie, webhook and email recipients | 1 | 0.4 | `alerts#drivers` |
| Delivery as jobs off the request path | 1 | 0.4 | `alerts#delivery` |
| Health, security, resources, jobs and error sources (the backup source is planned) | 1 | 0.4 | `alerts#sources` |
| Routes in mainsdk info and mainsdk alerts test | 1 | 0.4 | `alerts#cli` |

## MainSDK Cloud

Everything on this page is free in your app.
Cloud adds:

| Plan | What it adds |
| --- | --- |
| fleet | Alert routing to owners across every app, with agent fix pull requests (spec fleet-observe) |


## Quick start

```yaml title="main.yaml"
alerts:
  min: warning
  recipients:
    oncall:
      - { driver: pagerduty, key: secret:PAGERDUTY_ROUTING_KEY }
      - { driver: slack, webhook: secret:SLACK_ALERTS_WEBHOOK }
  routes:
    - { match: { severity: critical }, to: [oncall], escalate: [{ after: 15m, to: [oncall] }] }
    - { to: [oncall] }
  quiet: { from: "22:00", to: "08:00", tz: Asia/Kolkata }
```

```ts title="app/example.ts"
import { alerts } from "mainsdk";

// one message per open key, however often it is raised; routes in main.yaml pick who hears it
await alerts.raise({ key: "import-failed", severity: "warning", title: "Nightly import failed", runbook: "https://acme.dev/runbooks/import" });

await alerts.resolve("import-failed");
```

## Configuration

See the `alerts.*` rows in [main.yaml reference](https://mainsdk.dev/docs/reference/main-yaml). `mainsdk check` reports an invalid value as `MAIN-ALERT-0001`, a production app with no route for `critical` alerts as `MAIN-ALERT-0002`, and a literal key or a webhook host outside `network.allow` as `MAIN-ALERT-0003`.

## What you get

- One message per open alert, however many instances raise it
- Critical alerts go at once; others wait out quiet hours as a digest
- Never on the request path; sends run as jobs with retries
- No personal data in an alert; provider dedupe keys are keyed hashes

### Calls

| Call | What it does |
| --- | --- |
| `alerts.raise({ key, severity, title, attrs?, runbook? })` | Opens an alert, or updates the open one with the same key, and routes it once. |
| `alerts.resolve(key)` | Closes the open alert with that key. |
| `alerts.ack(id)` | Acknowledges an alert and stops its escalation; needs the `alerts.manage` permission. |
| `alerts.get(id)` | Returns one alert. |
| `alerts.list({ state?, source?, cursor?, limit? })` | Pages through alerts, filtered by state or source. |

### Sources

The SDK raises alerts from these sources. Limit them with `alerts.from`.

| Source | Raises | Key |
| --- | --- | --- |
| `health` | A dependency in `health.critical` is down (critical); SLO burn, fast (critical) or slow (warning) | `dependency:<name>`, `slo:<slo>:<rule>` |
| `resources` | The instance enters `shed` (warning) | `resources:shed` |
| `security` | Security events at `high` (warning) and `critical` (critical) | `security:<type>` |
| `jobs` | A job moves to the dead-letter queue (`MAIN-JOB-0002`, warning) | `job:<name>` |
| `errors` | A new error group or an error spike, from the error-reporting port | `error:<fingerprint>`, `error-spike:<fingerprint>` |
| `app` | `alerts.raise` | the caller's key |

The `backup` source (a failed or overdue restore drill) is planned. `alerts.from` accepts the name, but nothing raises from it yet.

Health and resource alerts resolve when the cause clears. Security, job and new-error alerts never resolve themselves; a person acknowledges them.

### Recipients and destinations

A recipient is a named group of destinations in `alerts.recipients`. Each destination uses one of five drivers: `email`, `slack`, `pagerduty`, `opsgenie` or `webhook`. Keys and webhook URLs are written as `secret:NAME`, never as literals (`MAIN-ALERT-0003`). A route sends an alert to recipients; an escalation sends an open, unacknowledged alert to another recipient after a set time. Quiet hours hold warnings and info as one digest; critical alerts always go at once.

### Drivers

| Driver | Provider | |
| --- | --- | --- |
| `builtin` | Built in (sends through notify and email drivers) | default |

### CLI

```text
mainsdk alerts test oncall --env prod
mainsdk info
```

`alerts test` sends a test alert, marked as a test, through every driver of the recipient and prints each result. `mainsdk info` shows the recipients by name and driver (never their keys), the routes and the open alerts.

## Errors and checks

`alerts.raise` and `alerts.resolve` never wait on a provider. Sends run as jobs with retries. If a recipient fails after its last retry, the others are still sent to (`MAIN-ALERT-0011`). If the runtime store does not answer, dedupe and escalation fall back to the instance (`MAIN-ALERT-0012`) and the app keeps serving.

- `MAIN-ALERT-0001`
- `MAIN-ALERT-0002`
- `MAIN-ALERT-0003`
- `MAIN-ALERT-0010`
- `MAIN-ALERT-0011`
- `MAIN-ALERT-0012`
- `MAIN-ALERT-0013`
- `MAIN-ALERT-0014`
- `MAIN-ALERT-0015`

Each code's cause and fix is in [check codes](https://mainsdk.dev/docs/reference/check-codes).

## Swapping drivers

`alerts` has one driver, `builtin`, which sends through your `notify` and email drivers. To change where an alert goes, change the destination under a recipient (`email`, `slack`, `pagerduty`, `opsgenie` or `webhook`) and keep the calls as they are.

```yaml title="main.yaml"
alerts:
  recipients:
    oncall:
      - { driver: opsgenie, key: secret:OPSGENIE_KEY }   # was pagerduty
```

See [Swap a driver](https://mainsdk.dev/docs/guides/swap-a-driver).

## Testing

`createTestApp` runs `alerts` on the fake clock and captures outbound requests, so no provider is called. A raise with the key of an open alert folds into it.

```ts title="lib/alerts.test.ts"
import assert from "node:assert/strict";
import { createTestApp } from "@mainsdk/testing";

const app = createTestApp({
  yaml: `schema: 1
project: prj_alerts
alerts:
  recipients:
    oncall:
      - { driver: slack, webhook: secret:SLACK }
  routes:
    - { to: [oncall] }
`,
  secrets: { SLACK: "https://hooks.slack.com/services/T0/B0/xyz" },
});

await app.alerts.raise({ key: "import-failed", severity: "warning", title: "Import failed" });
await app.alerts.raise({ key: "import-failed", severity: "warning", title: "Import failed" });

const open = await app.alerts.list({ state: "open" });
assert.equal(open.items.length, 1);   // the second raise folded into the first
assert.equal(open.items[0]?.count, 2);
```

## Next

- [Raise and route alerts](https://mainsdk.dev/docs/guides/raise-and-route-alerts)
- [Alerts and health](https://mainsdk.dev/docs/guides/alerts-and-health)
- [Notifications](https://mainsdk.dev/docs/interfaces/notify)
