How-To·8 min read

How to Keep a Slack @oncall User Group in Sync With PagerDuty Schedules

SyncMyGroup team · September 3, 2026

Every team that runs on-call ends up with the same Slack message: "who's on call for payments right now?" Someone opens PagerDuty, finds the schedule, reads off a name, and pastes it back. Multiply that by every team and every week.

The obvious fix is a Slack user group — @oncall-payments — that always contains whoever is currently on call. People mention the handle, the right person gets notified, and nobody has to look anything up. The catch is that a user group is a static list. The PagerDuty schedule rotates. Somebody has to keep the two in step, and by Thursday that somebody has usually forgotten.

This guide covers how to make that update automatic: the decisions you need to make first, what PagerDuty and Slack give you out of the box, a script you can run yourself, and a no-code option.

Decide what "on call" means before you automate it

Most broken @oncall handles are broken by design, not by tooling. Answer these before you write any code or connect any app.

Which schedule, or which escalation level? A PagerDuty escalation policy can have several levels, each pointing at a schedule. @oncall-payments almost always means level 1 — the person who gets paged first. If you want a secondary handle, make it a separate group (@oncall-payments-secondary) rather than one handle that pings both.

One handle per team, or one global handle? Per-team handles are what people actually use. A single @oncall across a 40-person rotation is only useful if there is genuinely one on-call engineer for everything.

What happens to overrides? If someone swaps a shift in PagerDuty, the handle should follow the override, not the base rotation. Whatever you build, test it with an override before you trust it.

What happens between shifts? Some schedules have gaps — no coverage overnight, or nobody on weekends. Slack won't let you set a user group to zero members (more on that below), so decide now whether the handle should fall back to a team lead, keep the last person, or be disabled.

Who else should always be in the group? An engineering manager or incident commander is often added permanently so mentions never go unanswered.

Write the answers down. They become your configuration.

What you get natively (as of 2026)

PagerDuty. PagerDuty's Slack integration covers incident notifications, acknowledging and resolving from Slack, and similar incident workflows. We couldn't find documentation (checked September 2026) for a built-in feature that keeps a Slack user group's membership equal to a schedule's current on-call. Plenty of third-party tools exist for this exact gap — Zapier publishes a template that updates a Slack user group from PagerDuty on an hourly schedule, and there are open-source scripts on GitHub and PyPI.

Slack. User groups are available on Slack's paid plans only. On the free plan the API returns plan_upgrade_required. Workspace owners and admins can create and edit user groups by default, and they can open that up to all members.

The Slack API method that matters is usergroups.users.update. Three behaviours from Slack's documentation shape everything:

  • It replaces the entire member list with the list you send. There is no "add one user" call; you always send the full set.
  • It can't be empty. An empty list returns no_users_provided or invalid_users. To clear a group you call usergroups.disable, and usergroups.enable to bring it back.
  • Guests and bot users can't be added. That rules out adding an alerting bot to the handle, and it means a contractor on a guest account can't be in @oncall even if they're on the schedule.

Option 1: Write the sync yourself

The logic is short. Ask PagerDuty who is on call for a schedule, look up those people in Slack by email, and set the user group to that list.

PagerDuty's GET /oncalls endpoint takes schedule_ids[] or escalation_policy_ids[] filters, and each entry includes the user, schedule, escalation_level, start and end. With no since/until, it returns the current on-calls.

import os, requests

PD = requests.Session()
PD.headers.update({
    "Authorization": f"Token token={os.environ['PD_TOKEN']}",
    "Accept": "application/vnd.pagerduty+json;version=2",
})
SLACK = requests.Session()
SLACK.headers.update({"Authorization": f"Bearer {os.environ['SLACK_TOKEN']}"})

MAPPINGS = {
    # Slack user group ID : (PagerDuty escalation policy ID, level)
    "S0123ABCD": ("PABC123", 1),
}
PERMANENT = {"S0123ABCD": ["lead@example.com"]}

def current_oncall_emails(policy_id, level):
    r = PD.get("https://api.pagerduty.com/oncalls", params={
        "escalation_policy_ids[]": policy_id,
        "include[]": "users",
    })
    r.raise_for_status()
    return {
        o["user"]["email"].lower()
        for o in r.json()["oncalls"]
        if o["escalation_level"] == level
    }

def slack_id(email):
    r = SLACK.get("https://slack.com/api/users.lookupByEmail", params={"email": email})
    body = r.json()
    return body["user"]["id"] if body.get("ok") else None

for group_id, (policy, level) in MAPPINGS.items():
    emails = current_oncall_emails(policy, level) | set(PERMANENT.get(group_id, []))
    ids = sorted(filter(None, (slack_id(e) for e in emails)))
    if not ids:
        print(f"{group_id}: nobody on call, leaving group unchanged")
        continue
    r = SLACK.post("https://slack.com/api/usergroups.users.update",
                   data={"usergroup": group_id, "users": ",".join(ids)})
    print(group_id, r.json().get("ok"), r.json().get("error"))

Run it from cron, a scheduled CI job or a small serverless function. A few things that will bite you:

  • Email mismatches. users.lookupByEmail only works if the PagerDuty email and the Slack email are the same. Contractors and people with aliases are where this breaks. Log every lookup miss.
  • Polling frequency. Shift handovers usually happen on the hour. A job running every few minutes is fine; one running hourly means the handle can be wrong for up to an hour after a handover or an override.
  • Rate limits. Slack returns ratelimited with a Retry-After header. Honour it, especially if you sync dozens of groups in one run.
  • Silent failure. The day this script stops running, nothing alerts you — the handle just slowly drifts. Add a heartbeat check.
  • Ownership. Someone has to own the token, the cron host and the mapping file. That's fine until they change teams.

For one or two handles, this is a reasonable afternoon of work. At ten teams with overrides, gaps and permanent members, it becomes a small internal product.

Option 2: A purpose-built sync

SyncMyGroup treats a PagerDuty on-call schedule as a source and a Slack user group as a destination, and keeps the destination matching the source. The setup is the same shape as any other sync in the product:

  1. Connect PagerDuty and Slack in app.syncmygroup.com. Both are OAuth connections; SyncMyGroup stores only email addresses and the integration credentials.
  2. Create a sync with the PagerDuty schedule or team as the source and a Slack user group as the destination. If the group doesn't exist yet, it can be created for you.
  3. Add permanent users. This is where the engineering manager or incident commander goes — fixed members kept in the group regardless of who is on call. It also handles the "never empty" problem for schedules with gaps.
  4. Read the preview. Before anything is written, you see exactly who will be added and removed. On a first run against a hand-maintained @oncall group, the removals list is usually the interesting part.
  5. Turn on notifications. In the channel where the team works, run @SyncMyGroups subscribe <team> and the bot posts each addition and removal as it happens. For on-call, that doubles as a handover announcement: "Priya added to @oncall-payments, Sam removed."

You can also go the other way and keep Slack channel membership in step — for example, auto-adding whoever is on call to #payments-incidents and removing them when their shift ends.

Naming patterns that hold up

HandleSourceNotes
@oncall-paymentsPayments escalation policy, level 1The one people mention
@oncall-payments-secondarySame policy, level 2Separate handle, used sparingly
@payments-teamGoogle Group or PagerDuty teamEveryone on the team, not just who's on call
@incident-commandersIC scheduleOften has permanent members

Keep on-call handles prefixed (oncall-) so they sort together in the user group list and nobody confuses "the team" with "the person on call right now". If your org is also deciding between a user group and a channel for this, see Slack user group vs channel.

Rollout checklist

  1. List every existing on-call handle and who maintains it today.
  2. Map each one to a schedule and escalation level. Retire the ones nobody can map.
  3. Confirm PagerDuty and Slack emails match for everyone on those schedules.
  4. Start with one team. Watch it through at least one handover and one override.
  5. Announce the change in the team channel so people stop editing the group by hand — any manual edit will be overwritten on the next sync.
  6. Expand team by team.

If you're also syncing the full team roster (not just the on-call person) from Google Groups into PagerDuty, that's a separate sync; see Keep PagerDuty teams in sync with Google Groups. And if your incident process pulls people into channels and handles beyond on-call, incident response team sync covers the wider pattern.

Cost

SyncMyGroup is free up to 5 users with every integration, and the Starter plan is $2 per user per month with unlimited users (pricing). For an on-call handle, the users that matter are the people on the schedule, so a small rotation often fits inside the free tier.

FAQ

Does PagerDuty update Slack user groups on its own?

As of 2026 we couldn't find a documented PagerDuty feature that keeps a Slack user group equal to the current on-call. The PagerDuty Slack app focuses on incident workflows. Teams fill the gap with scripts, Zapier, or a sync tool.

What happens to the @oncall group when nobody is on call?

Slack's usergroups.users.update rejects an empty member list, so the group can't simply go to zero. Either keep a permanent member (a team lead) in the group, leave the previous person in place until the next shift, or disable the group during the gap.

Can a bot or a guest be in the @oncall group?

No. Slack doesn't allow guests or bot users in user groups. If a contractor on a guest account is on the schedule, they'll be skipped; convert them to a full member or route mentions another way.

How quickly does the handle change at handover?

That depends on how often your sync runs. With a script, it's your polling interval. With SyncMyGroup, subscribe a channel to the team's notifications so you can see each change land, and check the preview if a handover looks wrong.


Want to see it against your own schedules? Book a walkthrough or browse all supported workflows.

Start syncing your favorite tools today

Free up to 5 users with all integrations. $2 per user per month when you grow.