A pull request that nobody notices sits for days. The usual fix is a Slack ping, but the way most teams get GitHub events into Slack, the /github subscribe app, fires a message on nearly every action and never on the one thing you actually want: "this PR has been waiting on you for a day." This post builds that missing reminder yourself with a scheduled workflow, then shows the exact points where a hand-rolled version costs more than it saves.
The 60-second version
- The default GitHub Slack app posts per event, so review requests drown in commit and comment noise.
- GitHub's app does have scheduled reminders, but they are per channel or team on a fixed time, with no idle-time or CI awareness.
- A cron-triggered GitHub Action can list open PRs, keep the ones with pending reviewers, and post a targeted Slack digest. Full code below.
- The DIY version is stateless: it re-pings every run, can't edit or thread messages, and leans on
updated_atas a rough proxy for "idle." - Once you want per-reviewer routing, dedupe, and CI status on the message, you're building a small service.
Why the default GitHub Slack app doesn't do this well
The official GitHub + Slack integration works by subscribing a channel to a repo with /github subscribe owner/repo. Out of the box that turns on issues, pulls, commits, releases, and deployments. The pulls feature notifies on PR changes and on draft PRs marked ready, so an active repo produces a steady stream: opened, each push, each review, each comment. The signal you care about, "reviewer X has not looked at PR #123 in a day," is buried in that stream.
To its credit, the app also has a scheduled reminders feature that posts pending review requests at set times. That is genuinely useful and worth turning on. Its ceiling is that reminders are coarse: they fire on a fixed schedule for a channel or team, they can't key off how long an individual review has been idle, and they carry no pipeline status. If you want "only ping when a PR has waited more than N hours, and tell me which check is red," you need your own logic.
The DIY version: a scheduled GitHub Action
The building blocks are all free. A scheduled workflow runs on cron, the REST pulls API lists open PRs with their requested_reviewers, draft, and updated_at fields, and a Slack incoming webhook posts the result. Store the webhook URL as a repository secret named SLACK_WEBHOOK_URL and drop this in as .github/workflows/review-reminders.yml:
name: Review reminders
on:
schedule:
- cron: "7 9 * * 1-5" # 09:07 UTC, Mon-Fri (off-hour on purpose)
workflow_dispatch: # lets you run it by hand while testing
jobs:
remind:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
with:
script: |
const { owner, repo } = context.repo;
const prs = await github.paginate(github.rest.pulls.list, {
owner, repo, state: "open", per_page: 100,
});
const IDLE_MS = 24 * 60 * 60 * 1000; // 1 day
const now = Date.now();
const waiting = prs.filter(pr =>
!pr.draft &&
pr.requested_reviewers.length > 0 &&
now - new Date(pr.updated_at).getTime() > IDLE_MS
);
if (waiting.length === 0) return;
const lines = waiting.map(pr => {
const who = pr.requested_reviewers.map(r => r.login).join(", ");
return `<${pr.html_url}|#${pr.number} ${pr.title}> — waiting on ${who}`;
});
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: "POST",
headers: { "Content-type": "application/json" },
body: JSON.stringify({ text: `Pending reviews:\n${lines.join("\n")}` }),
});That's the whole thing. actions/github-script runs on Node 20, so fetch is built in and you get an authenticated Octokit as github for free. Slack's <url|label> syntax renders each PR as a clickable link. Run it once with the workflow_dispatch button to confirm the message shape before you trust the schedule.
Full disclosure: we make PRFlow, which does this without the workflow. If you'd rather not own a reminder script, PRFlow posts one Slack message per pull request that updates in place with CI/CD status and threaded comments, on GitHub and GitLab. The snippet above is a fine place to start; keep reading for where it stops being fine.
Where the DIY reminder breaks
None of these are reasons to avoid the script. They are the maintenance you're signing up for, and they show up in this order.
It re-pings on every run. The workflow has no memory. If a PR is still idle tomorrow, it posts again, and again the day after. There's no state store in Actions, so deduping ("only remind once per PR per day") means writing to a cache, a Gist, or an external store and reading it back. That's the first afternoon you lose.
updated_at is a lie for "idle." It changes on any activity, including a bot comment, a label, or a rebase push. So a PR that got a "please fix" review 20 hours ago but had a linter bot comment 5 minutes ago looks fresh and gets skipped. Real "waiting on reviewer" state needs the reviews and timeline APIs, not one timestamp.
It pings a channel, not a person. requested_reviewers gives you GitHub logins. To @-mention someone in Slack you need their Slack member ID, which means maintaining a GitHub-to-Slack lookup table and keeping it current as people join and leave. Miss one and the reminder is addressed to nobody.
Incoming webhooks can only create messages. Slack's own docs are explicit that incoming webhooks do not allow you to delete a message, and they can't edit or thread one either. So you can't keep a single updating "these PRs are waiting" message; you get a new post every run, which is its own kind of noise. Editing in place needs a full OAuth Slack app with chat.update and stored message timestamps.
The schedule itself is soft. Per GitHub's docs, scheduled workflows can be delayed during periods of high load, and the top of every hour is the worst slot. The shortest interval is once every 5 minutes, and in public repos the schedule auto-disables after 60 days without repo activity. A quiet repo silently stops reminding.
There's no CI status. The reminder says a PR is waiting but not whether it's even mergeable. Adding "and checks are green" means another API call per PR (the checks or commit-status endpoints) and more code to correlate.
Build it or skip it?
- Build the Action if one repo, a channel-level ping, and "good enough" idle detection cover you. It's honest, free, and yours.
- Turn on GitHub's scheduled reminders too — they're zero maintenance and complement the Action for the simple cases.
- Reach for a maintained tool once you need per-reviewer routing, dedupe, one updating message, or CI status on that message. At that point you're maintaining a service, and running it yourself rarely pays off.
If the noise problem sounds familiar on the GitLab side too, the same trade-offs play out there — see every way to connect GitLab and Slack. And if reminders are a symptom of reviews piling up, the code review bottleneck is the underlying problem worth reading about. A close cousin of this post covers the other half of the noise problem: sending a Slack notification only when a PR is ready for review.
Bottom line
A scheduled GitHub Action is the right first move for reminders: it's a few lines, it's free, and it targets the exact PRs that are waiting. Just go in knowing you've adopted a small stateful problem in disguise — dedupe, reviewer mapping, and message editing are where the real time goes. Start with the script, add GitHub's built-in reminders, and switch to a maintained tool the moment "just one more field" turns your reminder into a service.