Incident response war room
PagerDuty fires, a dedicated Slack channel spins up, the postmortem writes its own first draft.
During an incident nobody has spare attention for logistics. PagerDuty's own Slack integration already turns a trigger into a numbered channel with the responders invited, so this workflow configures that rather than rebuilding it, and spends its custom code where nothing ships out of the box: capturing the timeline from what people actually type, and creating a Notion postmortem on resolve pre-filled with detection time, duration, the timeline and the responder list — so the retro starts from evidence instead of memory.
How it flows
- 01
Monitor triggers a PagerDuty incident
Events API v2 with a dedup_key, so a flapping monitor does not open five incidents.
- 02
Severity gate decides on a war room
Priority and service conditions on the channel rule: SEV1 and SEV2 only; SEV3 stays a thread in #ops.
- 03
Channel created and responders invited
PagerDuty's Slack integration opens inc-{{incident_number}} and pulls the responders in. None of your code runs here.
- 04
Header pinned with roles and status
The integration bookmarks the incident; your app pins one message naming IC, comms and scribe.
- 05
Timeline captured from !log messages
Responders curate as they go; nobody reconstructs it afterwards.
- 06
Resolve creates the postmortem page
Times, duration, services, responders and the timeline pre-filled in Notion.
- 07
Root cause left blank on purpose
The review owner fills it in. A machine-suggested cause anchors the discussion before anyone has looked.
Set up each app
Work through these in order — later apps usually need a token or an id from an earlier one.
PagerDuty
The trigger and the source of severity
- 01
Add an Events API v2 integration
Services → Service Directory → your service → Integrations → Add another integration → Events API V2. Copy what the UI labels the Integration Key; that is the routing_key your monitors post to. One integration per service keeps it rotatable without a global outage of alerting.
POST https://events.pagerduty.com/v2/enqueue{ "routing_key": "R02A9F4C7E1B84D3F", "event_action": "trigger", "dedup_key": "checkout-api-5xx", "payload": { "summary": "checkout-api 5xx rate 14% over 5m (threshold 2%)", "severity": "critical", "source": "checkout-api.prod.us-east-1", "component": "payments", "group": "checkout", "class": "http-error-rate" }, "links": [{ "href": "https://grafana.acme.com/d/checkout", "text": "Checkout dashboard" }] } - 02
Subscribe to incident webhooks
Integrations → Generic Webhooks (v3) → New webhook → scope it to the service → events: incident.triggered, incident.acknowledged, incident.resolved. Verify the X-PagerDuty-Signature header on every delivery.
- 03
Only spin up a room for SEV1 and SEV2
Map PagerDuty severity to your SEV scale, then express the gate as conditions on the incident-channel rule below (by priority, service or team) rather than as an if-statement in your code. A war room per warning alert leaves you with 200 dead channels and a workspace nobody can search.
Severity gatecritical -> SEV1 channel rule fires, page secondary, notify #general error -> SEV2 channel rule fires, page secondary warning -> SEV3 thread in #ops only, no channel info -> ignore
Slack
The war room itself and the timeline capture
- 01
Let PagerDuty open the war room
Do not write this part. PagerDuty → Integrations → Extensions → Slack → your workspace → Incident Channel tab → turn on "Automatically create incident channels". Set the name template (inc-{{incident.incident_number}} — Slack names are lowercase, no spaces, 80 characters max, so keep the template fixed and search still works six months later), scope it with the priority/service/team conditions from the severity gate, and let it handle the invites, the incident bookmark and the status updates. Then still post one message naming IC, comms owner and scribe and pins.add it: every incident review that goes badly starts with "who was actually running this?".
- 02
Add your own app for the timeline
The native integration owns the room; your app only needs to read and write in it. Add the scopes below, subscribe to the message.channels event, and get the app into each incident channel — a message subscription only delivers channels the app is a member of. If you cannot use the PagerDuty Slack integration, you can still create the room yourself with conversations.create, conversations.invite and conversations.setPurpose (add channels:manage), taking the channel id from the create response and never hardcoding it.
Bot Token Scopeschannels:history # read the !log messages (with the message.channels event) channels:read # resolve the incident channel id chat:write # post the roles header and the postmortem link pins:write # pin that header users:read # turn member ids into the responder list - 03
Capture the timeline with a keyword
Ask responders to prefix decisive messages with "!log". Collect those from the message events; you get a human-curated timeline for free instead of parsing 400 messages afterwards.
What lands in the postmortem timeline21:47Z !log alert fired, 5xx at 14% 21:49Z !log rolled back deploy 4f21ac 21:52Z !log error rate back under 1%, watching 22:05Z !log status page updated, incident resolved
Notion
Postmortem, pre-filled from the facts
- 01
Create the "Incidents" database
Properties: Name (Title), Incident ID (Text), Severity (Select: SEV1/SEV2/SEV3), Detected At (Date with time), Resolved At (Date with time), Duration mins (Formula), Services (Multi-select), Responders (Person), Root Cause (Text), Action Items (Text), Status (Select: Draft / In review / Complete).
- 02
Let Notion compute the duration
Add the formula below to Duration mins. Deriving it beats storing it — a corrected Resolved At then fixes the number everywhere at once.
Notion formula — Duration minsif( empty(prop("Resolved At")), toNumber(""), dateBetween(prop("Resolved At"), prop("Detected At"), "minutes") ) - 03
Create the page on resolve, not on trigger
On incident.resolved, POST /v1/pages with Notion-Version: 2026-03-11 — retrieve the Incidents database first and parent the page on its data_source_id, not the database id. Body is the timeline, Detected/Resolved come from the PagerDuty timestamps. Responders is a Person property and takes Notion user ids: Slack member ids will not resolve into it, so keep a slack-to-notion map (the same shape as the reviewer map in the PR pipeline) or fall back to a Text property rather than shipping a column that is always empty. Leave Root Cause empty on purpose — that section is the human's job, and a pre-filled guess anchors the whole review.
- 04
Archive the channel with a link
Post the postmortem URL into the war room, then archive the channel after 48 hours. The channel history stays searchable; the sidebar stays usable.