WorkflowsMCP

On-call handoff briefing: PagerDuty → Notion → Slack

At shift change: open incidents, the runbook, and the last three postmortems, handed over.

@workflowsmcpVerified on-callshift-handoffrunbookreliabilityautomation

On-call handoffs lose context by default: PagerDuty's handoff notification is a bare "you're on/off call now" push with no incident list, runbook link, or history, and no webhook exists for a schedule handoff — only for incidents — so this polls /oncalls instead. It catches the shift change, pulls the outgoing engineer's open incidents, and looks up the runbook and last three postmortems from a shared, read-only Notion "Incidents" database — sent in one Slack message at the shift boundary, not in reaction to an incident.

How it flows

  1. 01

    Poller checks /oncalls every 10 minutes

    Compares the current on-call user per schedule against the last poll. PagerDuty has no webhook for a schedule handoff, only for incidents, so polling is the only way to catch the moment a shift changes.

  2. 02

    A changed on-call user is a shift change

    The previous on-call user becomes "outgoing", the new one "incoming".

  3. 03

    Outgoing engineer's open incidents are pulled

    Escalation policy → every service it covers → /incidents filtered to triggered and acknowledged.

  4. 04

    Runbook and recent postmortems looked up in Notion

    Runbook by exact service name; the three most recently detected Complete rows from the shared "Incidents" database.

  5. 05

    PagerDuty user ids resolved to emails, then to Slack user ids

    GET /users/{id} on the outgoing and incoming PagerDuty user ids (Oncall only carries a reference, not an email), then users.lookupByEmail on each, assuming a shared SSO email domain.

  6. 06

    Briefing posted to the channel and/or DMed to incoming

    Open incidents, runbook link, last three postmortems — one message, not a wall of separate pings.

Set up each app

Work through these in order — later apps usually need a token or an id from an earlier one.

PagerDuty

Signals the shift change and the still-open incidents

  1. 01

    Find the schedules and escalation policies to watch

    People → Schedules lists every rotation; opening one and reading the id off the end of the URL works (format PI7DH85, same shape the API returns), but for more than one or two schedules it is faster to grab them all in one call with include[] and let the API resolve the escalation-policy and user names for you instead of clicking through each one.

    GET /oncalls
    curl -s -G "https://api.pagerduty.com/oncalls" \
      -H "Authorization: Token token=$PD_API_KEY" \
      -H "Accept: application/vnd.pagerduty+json;version=2" \
      -d "schedule_ids[]=PI7DH85" \
      -d "include[]=escalation_policies" \
      -d "include[]=users"
  2. 02

    Mint a read-only credential

    Two ways to get a key: an account-level REST API key (Integrations → API Access Keys) is the fastest path but is scoped to the whole account with no way to narrow it; a Scoped OAuth App (turn on Developer Mode first) lets you pick only the scopes below. This workflow only reads PagerDuty and never writes to it, so give it read scopes and nothing else.

    Minimum Scoped OAuth App scopes
    oncalls.read              # GET /oncalls
    incidents.read            # GET /incidents
    escalation_policies.read  # GET /escalation_policies/{id}
    users.read                # GET /users — email lookup for the Slack step
    schedules.read            # optional, only needed to resolve a schedule's display name
  3. 03

    Resolve each policy's services, then pull the unresolved incidents

    The Oncall object from the previous call carries escalation_policy.id, not the services under it — fetch GET /escalation_policies/{id} once per policy and read services[] off the response; that id is the only link between "who is on call" and "which incidents are theirs". Pass every one of those service ids into the incidents query below, not just the first: one escalation policy commonly covers more than one service, and dropping the rest silently shrinks the briefing rather than erroring. statuses[] has to be repeated as separate query parameters — a comma-joined value is read as one invalid status, not two valid ones.

    GET /incidents
    curl -s -G "https://api.pagerduty.com/incidents" \
      -H "Authorization: Token token=$PD_API_KEY" \
      -H "Accept: application/vnd.pagerduty+json;version=2" \
      -d "service_ids[]=PXXXXX1" \
      -d "service_ids[]=PXXXXX2" \
      -d "statuses[]=triggered" \
      -d "statuses[]=acknowledged" \
      -d "sort_by=created_at:desc"
  4. 04

    Resolve the outgoing and incoming engineers' emails

    The Oncall object only carries a UserReference for user — id and summary, no email. Call GET /users/{id} for the outgoing and incoming user ids (both come straight off the /oncalls poll) to get the address the Slack step's users.lookupByEmail needs.

    GET /users/{id}
    curl -s "https://api.pagerduty.com/users/PXXXXX1" \
      -H "Authorization: Token token=$PD_API_KEY" \
      -H "Accept: application/vnd.pagerduty+json;version=2"

Notion

Runbook links and the shared postmortem history

  1. 01

    Create the "Runbooks" database

    One row per PagerDuty service. Add exactly two properties: Service (Title), holding the PagerDuty service name character-for-character, and Runbook (URL). A rename on either side makes the lookup return an empty result rather than an error, so treat the two names as the one pair that has to change together.

  2. 02

    Point postmortems at a shared "Incidents" database

    If this workspace already keeps a Notion "Incidents" database for postmortems from another workflow, reuse it read-only here instead of standing up a second one — this workflow only queries it (Status = Complete, sorted by Detected At) and never writes to it, so there is no schema fight. The only contract this workflow depends on: Status is a Select with a "Complete" option, and Detected At is a Date property with time — the rest of that schema is irrelevant here. If no such database exists yet, create one named "Incidents" with at least those two properties, so this entry runs standalone today and slots cleanly into a future postmortem workflow without a rename.

  3. 03

    Share both databases with your integration and resolve their data source ids

    Internal connections (app.notion.com/developers/connections) carry no access until you open each database → ••• → Connections → + Add connection and add yours explicitly — a token that can see the workspace but was never added to a specific database page gets object_not_found back, which reads like a typo, not a permissions problem. If you are reusing an "Incidents" database shared with another workflow, add this workflow's own connection to that page too; sharing it with one integration does not share it with another. Then call GET /v1/databases/{database_id} once per database and keep data_sources[0].id from the response — every query below addresses that id, not the database id.

  4. 04

    Query a runbook by service name

    Filter the Runbooks data source on the exact PagerDuty service name pulled from the escalation policy's services[] in the PagerDuty step above.

    POST /v1/data_sources/{RUNBOOKS_DATA_SOURCE_ID}/query · Notion-Version: 2026-03-11
    {
      "filter": { "property": "Service", "title": { "equals": "checkout-api" } },
      "page_size": 1
    }
  5. 05

    Query the last three completed postmortems

    Same shape whether the data source is an "Incidents" database shared with another workflow or your own standalone one, because both expose the same two properties this filter touches.

    POST /v1/data_sources/{INCIDENTS_DATA_SOURCE_ID}/query · Notion-Version: 2026-03-11
    {
      "filter": { "property": "Status", "select": { "equals": "Complete" } },
      "sorts": [{ "property": "Detected At", "direction": "descending" }],
      "page_size": 3
    }

Slack

Delivers the briefing to the incoming engineer

  1. 01

    Add Bot Token Scopes

    api.slack.com/apps → your app → OAuth & Permissions → Bot Token Scopes. Add the three scopes below, then reinstall the app for the new scopes to take effect.

    Bot Token Scopes
    chat:write          # post the briefing to a channel or a DM
    users:read.email    # look up a Slack user id from a PagerDuty user's email
    im:write            # open a DM with the incoming engineer
  2. 02

    Install to workspace and invite the bot

    Install to Workspace, then /invite @your-app-name into #on-call-handoff if any part of the briefing goes to a channel — a DM-only setup does not need this step.

  3. 03

    Map PagerDuty email to a Slack user id

    users.lookupByEmail assumes the PagerDuty and Slack accounts share the same email address, which holds under a single SSO domain and can silently miss otherwise — a routine assumption for any email-based Slack lookup, not a gap specific to this workflow. If a lookup misses, still post to the channel with the plain PagerDuty name rather than failing the whole run.

    GET users.lookupByEmail
    curl -s -G "https://slack.com/api/users.lookupByEmail" \
      -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
      -d "email=incoming.oncall@acme.com"
  4. 04

    Post the handoff briefing

    One message: open incidents, the runbook link, the last three postmortems — link out rather than pasting a wall of text, since the incoming engineer can pull each thread from the message itself. Post to a channel, DM the incoming engineer, or both (one extra chat.postMessage call) — most teams end up wanting both, since a channel keeps every handoff searchable while a DM guarantees the incoming engineer actually sees it. To DM, first call conversations.open with a single user id (one id is what makes it a DM rather than a group) and post to the channel id it returns.

    POST chat.postMessage
    curl -s -X POST "https://slack.com/api/chat.postMessage" \
      -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
      -H "Content-Type: application/json; charset=utf-8" \
      -d '{
        "channel": "C0123ONCALL",
        "text": "On-call handoff: <@U_OUTGOING> -> <@U_INCOMING>. 2 open incidents: <link1>, <link2>. Runbook: <link>. Last 3 postmortems: <link1>, <link2>, <link3>."
      }'