Notifications
Slack
Send AlertINT findings to Slack channels.
AlertINT posts structured Block Kit messages to Slack after every completed incident analysis. When alerts recover, the original message is updated in-place and a thread reply is posted — one message per incident, no channel noise.
Synthetic incidents fired by alertint drill are unmistakable in a shared
channel: every surface of their card — headline, thread details, and the
plain-text fallback — carries a 🧪 DRILL banner, so a drill never
reads as a real incident to a teammate scrolling past.
Setup — Slack app with bot token#
Bot tokens let AlertINT track the message it posted. When an incident fires, AlertINT posts a rich Block Kit message and records its position in the channel. When all alerts recover, it updates that message in-place (🔴 → ✅, a duration field appears) and posts a short resolution note in the thread.
-
Create a Slack app. Go to https://api.slack.com/apps and click Create New App → From scratch. Name it AlertINT and select your workspace.
-
Add the
chat:writescope. In the left sidebar, click OAuth & Permissions, scroll to Bot Token Scopes, and addchat:write. That is the only permission AlertINT needs. -
Install to your workspace. Scroll to the top of OAuth & Permissions and click Install to Workspace → Allow. Slack displays a Bot User OAuth Token starting with
xoxb-. Copy it. -
Invite the bot to your channel. In Slack, open the channel where alerts should appear (create
#alertsif needed) and type/invite @AlertINT. The bot must be a channel member to post there. -
Add the token to your
.envfile — the same file that holds your other secrets. Never put the token value directly inconfig.yaml:# .env (gitignored — never commit this file) SLACK_BOT_TOKEN=xoxb-... -
Add Slack to your
config.yamlundernotify. Use the env var name — not the token value itself:notify: slack: enabled: true bot_token_env: SLACK_BOT_TOKEN # name of the env var — not the token value channel: "#alerts" # channel name or ID where alerts should post
What happens at runtime:
- Firing — posts a brief main-channel message (name + root cause) and immediately posts the full analysis — severity, confidence, correlation findings, MCP hint — as a thread reply.
- Resolved — updates the original main-channel message in-place (header changes 🔴 → ✅, duration appears) and posts full resolution details — duration, alert count, resolved time — as a thread reply.
Message structure#
Every notification uses Slack Block Kit. The same blocks appear for firing and resolved — only the header and fields change on resolution.
| Block | Description |
|---|---|
| Main — header | 🔴 INCIDENT DETECTED when firing, updated to ✅ INCIDENT RESOLVED in-place when resolved. |
| Main — root cause | One-sentence root cause hypothesis, preserved when the message is updated on resolution. |
| Main — footer | Incident ID, alert count, severity, confidence, group key, and start time. Replaced by resolved time and duration on resolution. |
| Main — agent handoff | investigate incident <id> using alertint — the MCP call to action, with the full incident ID. Dropped when the incident resolves. |
| Main — steering ruling | On failure groups governed by an operator correction: one line stating whether live evidence supported it, contradicted it, or could not test it. Channel-visible on purpose — it is the triage outcome. |
| Thread — analysis | Posted immediately after the main message: severity, confidence, alert count, and group key in a fields grid. |
| Thread — operator history | The failure group's operator context: first-occurrence / seen-before state or the governing verdict's note, followed by up to three operator notes with ages. |
| Thread — evidence | One line: per-source counts (Prometheus/Loki/Changes/Sentry) that fed the triage, e.g. Prometheus 21 metrics · Loki 0 lines. A connector that could not be reached shows unreachable instead of a count; a known-issue short-circuit shows skipped (known issue); a zero-connector install shows no sources configured. Always present. |
| Thread — findings | Bullet list of correlation findings. Only shown when the LLM identified more than one contributing factor. |
| Thread — agent handoff | The same handoff block, so the call to action reads identically on every firing surface. |
| Thread — resolved | Posted when all alerts recover: duration, alert count, and resolved timestamp in a fields grid. |
Example — firing#
Two messages are posted: a brief main-channel message and an immediate thread reply with the full analysis.
Main channel:
🔴 INCIDENT DETECTED — API Tier Degraded: CPU Saturation + Error Spike
Root cause: CPU saturation on api-2 is causing request queuing, elevating
error rates and response latency across the cluster.
Incident a1b2c3d4 · 3 alerts · high · 91% · group cluster=prod · started 14:37 UTC
🤖 Investigate in your AI agent: investigate incident a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d using alertint
Thread reply (posted immediately):
Analysis details
Severity: HIGH Confidence: 91%
Alerts: 3 Group: cluster=prod
Evidence: Prometheus 14 metrics · Loki 6 lines · Changes 1 · Sentry 2 issues
Correlation findings
• HighCPU (api-2) fired 15 s before HighErrorRate — causal ordering confirmed.
• HighLatency shares the same instance label, indicating single-host origin.
👀 seen ×2 in the last 90d (since 2026-07-01) — no operator verdict yet
Operator notes
📝 observation (2h ago): api-2 is the canary host; rollout paused.
🤖 Investigate in your AI agent: investigate incident a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d using alertint
Example — resolved#
The original main-channel message is updated in-place and a resolution note is posted in the thread.
Main channel (updated in-place):
✅ INCIDENT RESOLVED — API Tier Degraded: CPU Saturation + Error Spike
Root cause: CPU saturation on api-2 is causing request queuing, elevating
error rates and response latency across the cluster.
Incident a1b2c3d4 · resolved after 15m · 14:52 UTC
Thread reply:
✅ All clear — all alerts have recovered.
Duration: 15m Alerts: 3 recovered Resolved: 14:52 UTC
Incident a1b2c3d4 · duration 15m
The MCP hint in the message footer is a pre-filled tool call. Paste it directly into Claude Code, Cursor, or Windsurf to open the full evidence pack for that incident — see MCP clients.
Recurrence resurfacing#
When an already-analyzed incident re-fires inside the collapse window, it doesn't get a new card — it attaches as another occurrence on the same incident, and the card that's already in the channel is what carries the update. Recurrence never adds channel messages: everything below happens on the existing card or inside its thread. What lands depends on what changed:
- A plain re-fire — same symptom, same severity, steady cadence — just
bumps the occurrence count on the existing card in place
(
🔁 recurred ×N · last HH:MM). No new message anywhere. - A real-world change — severity escalated, a new symptom (alertname)
joined, or the cadence sped up markedly — posts a thread reply naming
exactly why (
why: severity/why: new_alertname/why: cadence), and the incident is re-analyzed with the fresh finding edited into the same card — never a new one. - A steady flapper that never trips one of those changes still gets a thread reply at milestone counts — ×5, ×10, ×25, ×50, ×100, then every ×100 — so a long-running recurring incident keeps a visible trail in its thread even without a qualifying change.
Every recurrence reply states the reason, e.g.:
Incident a1b2c3d4 · recurred ×9 · last 14:52 UTC · why: cadence
System messages#
The configured LLM is an installation dependency, not a property of any
Incident. When it has been continuously unhealthy for
health.broadcast_after_seconds (default 5 minutes), AlertINT posts one
plain-text AlertINT system root message — on whenever Slack is enabled, no
second switch:
⚠️ AlertINT system · LLM unavailable for 5m. New Incident triage is retrying; correlation may be delayed.
⚠️ AlertINT system · LLM degraded for 5m. Verification re-judgment is failing; draft Findings continue with reduced confidence.
If the state changes again inside the same outage (degraded ↔ unavailable),
the same message is edited in place — never a second post. Once the LLM
recovers, one more edit closes it out:
✅ AlertINT system · LLM recovered after 12m. Pending triage retries continue automatically.
One root per sustained episode, edited in place, never per-Incident and never
threaded. If the LLM recovers before the root was ever successfully
delivered, the stale outage/recovery pair is suppressed — it survives only in
state, audit, and logs. A post Slack definitely rejected is retried on the
next minute; a post whose outcome is unknown (the request may have been
accepted before the connection failed, or the process restarted mid-post) is
never retried, because a second root is worse than a missing one — the
outage still shows in /health, audit, and logs. A root that turns out to
have landed after its episode already recovered is edited to that episode's
recovery — however many episodes have come and gone since, and even across a
restart before the edit lands — never left standing as a stale outage, and
never reused by the next episode, which earns its own root on its own clock.
Each Slack request is bounded by its own timeout: a request Slack accepts but
never answers counts as an unknown outcome (a post) or is retried on the
next minute (an edit), and never delays the idle probe that would report the
LLM back. During an outage, new Incident triage retries with
backoff and correlation may be delayed; this copy never claims Alert intake
itself is unaffected. See Integration health
for the /health shape behind these messages.
Two backstop triggers — a hard occurrence cap and a periodic re-analysis ceiling — force a fresh re-analysis without representing a genuine escalation, so they edit the card but never post a reply; they stay silent by design.
Control this with notify.slack.recurrence_mode:
notify:
slack:
recurrence_mode: change-gated # change-gated (default) | off
change-gated(default) — post a thread reply on a real-world change or a milestone, as described above.off— recurrence never posts replies; the card's occurrence count still updates in place, silently.
Drill incidents (alertint drill) keep their 🧪 DRILL banner on every
recurrence surface — card edit, thread reply, and fallback text — same as
every other rendered surface.