Module 8: Project — Scheduled Alerting System
Step 4: Escalation with HITL
Capsule description
Critical alerts require human acknowledgement (ack). If nobody acks within 15 minutes, escalate to the manager. This requires 2 additional workflows: [ALERTS-ACK] (receives acks) and [ALERTS-ESCALATE] (checks pending ones).
Workflow [ALERTS-ACK]
Trigger
Webhook that receives the oncall's ack (from the Slack button or email link).
Webhook:
- Method: POST
- Path: /alerts-ack
- Auth: Header X-Ack-Secret
Processing
[Webhook]
│
[Set: extract alert_id from the payload]
│
[Sheet: update 'alerts_pending_ack']
- Find row with alert_id
- Set ack_at = $now
- Set ack_by = $json.body.user (if present)
│
[Slack: confirm to oncall: "Alert {alert_id} acknowledged"]
Link in the original notification
In the original Slack message to the #alerts-critical channel (capsule 05), include a Block Kit button:
{
"type": "button",
"text": { "type": "plain_text", "text": "✅ Acknowledge" },
"url": "https://your-n8n/webhook/alerts-ack?alert_id={{ alert_id }}"
}
Click → GET with alert_id → workflow [ALERTS-ACK] receives it → marks it as acknowledged.
For Slack interactive it really requires a webhook POST with a payload, not a GET. Simplified implementation here; in production use Slack interactivity with a Request URL.
Workflow [ALERTS-ESCALATE]
Trigger
Schedule every 5 minutes
Cron: */5 * * * *
TZ: America/Mexico_City
Processing
[Schedule]
│
[Sheet: read 'alerts_pending_ack']
- Filter: ack_at is empty (no ack) AND created_at > 15 min ago
│
[IF: any alerts with no ack > 15 min?]
└─ TRUE → escalate
Escalation
[SplitInBatches batch=1]
│
[Slack: to #manager-alerts]
"🚨 ALERT WITHOUT ACK - Manager attention required
Alert ID: {alert_id}
Metric: {metric}
Time without ack: 15+ minutes
Details: ..."
│
[Sheet: update alerts_pending_ack]
- Set escalated_at = $now
│
└─→ loop back
Ensure you don't escalate twice
If escalation runs every 5 min, it could re-escalate the same alert. A filter to avoid this:
[Sheet: filter 'alerts_pending_ack']
- ack_at is empty
- AND created_at > 15 min ago
- AND escalated_at is empty ← key
Only escalate the ones that haven't been escalated yet.
Workflow [ALERTS-MAIN] updated
In the "critical" branch (capsule 05), after notifying:
[Slack #alerts-critical, Email, etc. in parallel]
│
[Sheet append 'alerts_pending_ack']
- alert_id: {{ $now.toMillis() }} + random
- metric
- severity: 'critical'
- created_at: $now
- ack_at: empty
- escalated_at: empty
Each critical is tracked for possible escalation.
Lifecycle of a critical alert
T+0: Alert detected
→ Slack notification
→ Email to oncall
→ Sheet 'alerts_pending_ack' append
T+15: [ALERTS-ESCALATE] checks
→ If ack_at empty → escalate to manager
→ Sheet update escalated_at
T+30: Manager should respond
(next escalation if you configure it: to director, etc.)
Workflow for "alert resolved"
Optional but useful: workflow [ALERTS-RESOLVED] to close alerts when the problem is resolved.
[Webhook /alerts-resolved]
│
[Sheet update: resolved_at = $now]
│
[Slack: "Alert X resolved"]
Capsule test
Test 1: Critical with fast ack
- Trigger a critical alert
- Verify Slack arrives
- Do a POST to the ack webhook immediately
- Verify the 'alerts_pending_ack' Sheet has ack_at
- 15 min later: escalation should NOT fire
Test 2: Critical with no ack
- Trigger a critical alert
- Do NOT ack
- Wait 15-20 min
[ALERTS-ESCALATE]should:- Detect the alert with no ack
- Send Slack to the manager
- Mark escalated_at
Test 3: Multiple criticals
- Generate 3 criticals
- Ack 1, leave 2 without ack
- Escalation should escalate the 2 without ack
Validation
-
[ALERTS-ACK]receives the webhook and updates the Sheet -
[ALERTS-ESCALATE]runs every 5 min and escalates correctly - Doesn't escalate twice (escalated_at filter)
- Main Slack message has a functional ack button
- Complete audit trail in the Sheet
What's next: Step 5 — complete system resilience.
Created: May 11, 2026 Version: 1.0