Writing alert messages people can act on

Rules for writing alert titles and bodies that tell engineers what broke, who owns it, and what to do first.

Published 14 November 2026.

The person who gets the alert may be half awake, on a phone, and may not know the service. The message has to tell them what broke, where, how bad, and what to do first, without a laptop. Most of that is decided when the alert rule is written, not when it fires. This guide covers what the alert says. It is a companion to what to alert on, which covers whether an alert should exist at all.

The title: what, where, how bad

Lock screens, SMS previews, chat notifications, and incident lists often display the title in isolation. It must communicate three facts in this specific order: the location of the issue, the symptom, and the severity.

Follow this pattern: <service> <env>: <symptom> <value> (threshold <threshold>). An example is checkout-api prod: 5xx rate 7.2% (threshold 2%). Name the symptom users feel, not the internal cause. See what to alert on.

Place important words at the start. Previews and short channels cut off the end. Avoid using only the rule's internal name like HighErrorRate. Generic phrases like "Alert triggered" or "Check failed" do not help. Status words without a subject confuse readers. Skip emoji in titles, as they may appear in SMS.

Keep the title shape identical for every fire of the same rule. Grouping and search work better. People learn to read it at a glance.

Fields to include in the body

FieldExampleWhy
Servicecheckout-apiWho owns it, which runbook
Environmentprod, eu-west-1Staging alerts should not look like production
Value and threshold7.2%, threshold 2%How far over, without opening a dashboard
Since when09:20 UTC, for 10 minutesNew, or ongoing for hours
User impact"Checkouts are failing for some users"Decides the severity
Runbook linkhttps://runbooks.example.com/...The first steps. See runbooks
Dashboard linkFiltered to this service and environmentOne tap to the graph
Ownerteam: paymentsWho to escalate to
SeveritycriticalWhich response applies. See severity levels

Write times in UTC, or always state the time zone. Links must work from a phone and without a VPN where possible.

Length limits that matter

WhereLimitSource
SMS, GSM-7160 characters in one segment, 153 per segment when splitTwilio
SMS, UCS-2 (any emoji or non-GSM character)70 in one segment, 67 per segment when splitTwilio
PagerDuty Events API v2 summary1024 charactersPagerDuty
Slack message textKeep under 4,000 characters. Longer may be split; above 40,000 it is truncatedSlack chat.postMessage
Slack attachments100 per messageSlack chat.postMessage
Slack blocks50 per messageSlack blocks
Slack header block150 charactersSlack header block
Slack section block3000 characters of text; up to 10 fields of 2000 eachSlack section block

One emoji forces the entire SMS into UCS-2 encoding. The character limit drops from 160 to 70. Some GSM-7 characters, such as { and ], count as two characters (Twilio GSM-7).

Slack uses the text field as the fallback shown in notifications when a message uses blocks. Always set it.

Tools cut silently. Alertmanager truncates the PagerDuty summary to 1024 characters and the Slack title to 1024 characters, and logs a warning (pagerduty.go, slack.go). Design the title to work at SMS length.

Before and after

The examples below are illustrations, not real alerts.

A poor message looks like this:

[FIRING:1] HighErrorRate
CRITICAL: check failed

This text fails to tell the reader which service is affected, which environment it is in, how bad the situation is, or what to do next.

A better message appears as a chat or push notification:

[FIRING] checkout-api prod: 5xx rate 7.2% (threshold 2%)
More than 2% of requests to checkout-api in prod have returned 5xx for at least 10 minutes. Checkouts are failing for some users.
Since: 09:20 UTC
Owner: payments
Runbook: https://runbooks.example.com/checkout-api/high-error-rate
Dashboard: https://grafana.example.com/d/checkout?var-env=prod

The same alert as an SMS (136 characters, GSM-7, one segment):

SEV1 checkout-api prod: 5xx rate 7.2% (threshold 2%) since 09:20 UTC. Runbook: https://runbooks.example.com/checkout-api/high-error-rate

Put the message in the alert rule

Write the message where the alert is defined, so every channel receives the same text. In Prometheus, annotations hold this content. Annotation values are templates. The $labels variable holds the alert's labels. The $value variable holds the evaluated value. See Prometheus alerting rules.

Use template functions to convert raw numbers into readable formats. humanizePercentage converts ratios to percentages. humanizeDuration formats durations. See the template reference.

The common annotation names are summary, description, and runbook_url. Grafana uses the same three names. See Grafana annotations. The dashboard_url below is a custom name. Any name works.

groups:
  - name: checkout-api
    rules:
      - alert: CheckoutApiHighErrorRate
        expr: |
          sum by (service, env) (rate(http_requests_total{service="checkout-api", code=~"5.."}[5m]))
            /
          sum by (service, env) (rate(http_requests_total{service="checkout-api"}[5m]))
            > 0.02
        for: 10m
        labels:
          severity: critical
          team: payments
        annotations:
          summary: '{{ $labels.service }} {{ $labels.env }}: 5xx rate {{ $value | humanizePercentage }} (threshold 2%)'
          description: 'More than 2% of requests to {{ $labels.service }} in {{ $labels.env }} have returned 5xx for at least 10 minutes. Checkouts are failing for some users.'
          runbook_url: 'https://runbooks.example.com/checkout-api/high-error-rate'
          dashboard_url: 'https://grafana.example.com/d/checkout?var-env={{ $labels.env }}'

The rule passes promtool check rules (Prometheus v3.5.0). A promtool test rules unit test with a 7.2% error rate renders the summary as checkout-api prod: 5xx rate 7.2% (threshold 2%). The sum by (service, env) keeps those two labels on the result, so the templates can use them.

Format it in the notification template

Alertmanager groups alerts. Templates access .Status, .CommonAnnotations, .CommonLabels and .Alerts. Each alert exposes .Labels, .Annotations and .StartsAt (notification template reference). .CommonAnnotations contains only annotations shared by every alert in the group. If the group spans two environments, .CommonAnnotations.summary is empty. Group by the labels the summary uses, or range over .Alerts.

route:
  receiver: payments-slack
  group_by: [alertname, service, env]
receivers:
  - name: payments-slack
    slack_configs:
      - api_url_file: /etc/alertmanager/secrets/slack-webhook
        channel: '#payments-alerts'
        send_resolved: true
        title: '[{{ .Status | toUpper }}] {{ .CommonAnnotations.summary }}'
        title_link: '{{ .CommonAnnotations.dashboard_url }}'
        text: |-
          {{ range .Alerts }}{{ .Annotations.description }}
          Since: {{ .StartsAt.Format "15:04 MST" }}
          Owner: {{ .Labels.team }}
          Runbook: {{ .Annotations.runbook_url }}
          {{ end }}

Set send_resolved to true. The Slack receiver defaults to false (configuration). The channel then shows when the problem ends.

Match group_by to the labels the summary uses. See Alertmanager routing and alert deduplication and grouping.

Checked with amtool check-config and amtool template render (Alertmanager v0.35.0).

Resolved messages

State what recovered and how long it lasted. Use the same title shape as the firing message. This pairs the resolve with the alert in a channel or incident timeline.

Send the resolve to every channel the alert used. Otherwise someone may keep working on a fixed problem. In the PagerDuty format the resolve needs the same dedup_key as the trigger. See the PagerDuty Events API v2 format.

Checklist

  • The title follows the service, environment, symptom, value, and threshold sequence.
  • The title stays under 160 GSM-7 characters for SMS compatibility and excludes emoji.
  • The body specifies the start time, user impact, owner, runbook link, and dashboard link.
  • All times carry a time zone.
  • Links function correctly on mobile devices.
  • Annotations derive from label-based templates rather than manual typing.
  • Grouping aligns with the labels in the summary.
  • Resolved notifications remain enabled.
  • Review the last ten alerts as an on-call engineer would on a phone. Rewrite any message that lacks actionable information.

See choosing alert notification channels for where alerts go next.