Get a Slack DM when a GitHub pull request is waiting on your review.
A self-contained alternative for when the official GitHub Slack app isn't set up on a repository you review for. It runs entirely from your own account — a GitHub token and a Slack delivery target — so it needs no configuration on the repository side.
Runs on macOS, Linux, and Windows. Python 3.8+, standard library only: no pip, no venv, no dependencies.
Every couple of minutes it asks GitHub which open PRs are waiting on you:
is:open is:pr archived:false review-requested:@me
is:open is:pr archived:false team-review-requested:<org>/<team> (per team)
New results get a Slack message. Results that disappear (you reviewed it, or the request was withdrawn) are forgotten, so a later re-request notifies again.
Both queries matter. review-requested:@me does not match PRs where a
team you belong to was asked to review. If you only ran the first query you'd
have a tool that looks like it works while silently missing a whole class of
request. Put your teams in config.json.
git clone <this repo>
cd github-review-poller
python3 install.py # Windows: py install.pyThe installer walks through the three things it needs. Details below if you'd rather do it by hand or something goes wrong.
A classic PAT with the repo scope, from https://github.com/settings/tokens.
Add read:org if you want the installer to list your teams for you.
The most common failure: if the repo is in an org that enforces SSO, the token must be SSO-authorized for that org — on the token's settings page, Configure SSO. Without it, private repos return zero results with no error, so the poller looks like it's working and finds nothing. Run
python3 poller.py --check, which tests for this explicitly.
Three modes. workflow_webhook is the default because it's built entirely
from native Slack steps, so there's no app to install.
workflow_webhook — Workflow Builder DMs you (recommended)
In Slack: Automations → Workflows → New Workflow
- Trigger: From a webhook
- Add a variable named exactly
text, type Text - Add step: Send a message to a person → choose yourself → insert the
{text}variable as the message body - Publish, then copy the webhook URL
Requires a paid Slack plan (Workflow Builder isn't on Free). Webhook workflows are available to members by default, though a workspace can turn that off — if "From a webhook" isn't among the triggers, that setting is why.
Slack limits these webhooks to 1 request/second and rejects nested JSON. Both are handled: sends are paced, and the payload is flat.
Want richer messages? Declare variables named repo, number, title, url,
author, age, draft on the trigger and set "include_fields": true in
config.json. Every declared variable must exist or Slack rejects the request.
bot_token — your own Slack app
Create an app with the chat:write bot scope, install it, and supply the bot
token plus your own member ID (Profile → ⋯ → Copy member ID, looks like
U01ABCDEF). Best formatting (Block Kit), at the cost of having an app to set up.
incoming_webhook — private channel fallback
An incoming webhook is bound to the channel chosen at install time and cannot DM you — that's a Slack limitation, not an oversight here. Create a private channel, point a webhook at it, and set the channel to All messages so it still pings you.
install.py sets this up for you, per platform:
| OS | Mechanism | Inspect it with |
|---|---|---|
| macOS | launchd agent, StartInterval |
launchctl print gui/$(id -u)/com.github-review-poller |
| Linux | systemd user timer | systemctl --user list-timers github-review-poller.timer |
| Linux (no systemd) | crontab entry | crontab -l |
| Windows | Task Scheduler, run via pythonw.exe so no console window flashes |
schtasks /query /tn GitHubReviewPoller |
On Linux, run loginctl enable-linger $USER if you want it to keep polling
while you're logged out.
None of these fire while the machine is asleep; each runs shortly after wake.
python3 poller.py --check # diagnose everything, notify nothing
python3 poller.py --check --post-test # ...and send one real test message
python3 poller.py --dry-run # print what would be sent; writes nothing
python3 poller.py --seed # adopt the current backlog silently
python3 poller.py --seed --announce # ...but post one summary of it first
python3 poller.py # one poll (what the scheduler runs)
python3 poller.py --watch # poll forever, in the foreground
python3 poller.py -v # include debug linesThe first run seeds silently — it adopts everything already outstanding without notifying, so setting this up doesn't dump your whole backlog into Slack. Only requests that arrive afterwards ping you.
config.json in the config directory (see --check for the exact path). It
holds no secrets.
| Key | Default | Meaning |
|---|---|---|
teams |
[] |
"org/team" slugs to watch. Omitted teams are never noticed. |
ignore_drafts |
false |
Skip draft PRs |
interval_seconds |
120 |
Poll interval (floor: 30) |
slack.mode |
workflow_webhook |
workflow_webhook, bot_token, or incoming_webhook |
slack.include_fields |
false |
Send all flat variables, not just text |
slack.user_id |
"" |
Your U… ID; required for bot_token only |
Resolved environment variable first, then file:
GRP_GITHUB_TOKEN/GRP_SLACK_TARGETsecrets.jsonin the config directory, written0600
On Windows permissions follow NTFS ACLs, so secrets.json is not
chmod-protected — treat it as readable by your Windows account.
If you prefer env vars, set them before running install.py: the values are
baked into the scheduled job, because schedulers otherwise run with a nearly
empty environment and the job would silently find no credentials.
| macOS | Linux | Windows | |
|---|---|---|---|
| config | ~/Library/Application Support/… |
$XDG_CONFIG_HOME or ~/.config/… |
%APPDATA%\… |
| state | …/state |
$XDG_STATE_HOME or ~/.local/state/… |
%LOCALAPPDATA%\…\state |
| logs | ~/Library/Logs/… |
…/state/logs |
%LOCALAPPDATA%\…\logs |
Override with GRP_CONFIG_DIR, GRP_STATE_DIR, GRP_LOG_DIR.
Start with python3 poller.py --check. Then the log file (path is in --check).
No notifications at all, and --check shows 0 results for the direct query.
Almost always the SSO authorization above. Confirm by opening the same search on
github.com — if the website shows PRs the token doesn't, it's SSO or a missing
repo scope.
Nothing since I scheduled it, but manual runs work. The scheduled job runs
with a different environment. If your secrets come from env vars, re-run
install.py with them set so they're written into the job definition.
--check says a query "filter ignored". A team-review-requested slug that
GitHub can't resolve doesn't return zero — it silently drops the qualifier and
matches every open PR on GitHub (verified: ~85,000,000 results). The poller
refuses any query returning more than 200 results rather than sending you
hundreds of messages. Fix or remove that team in config.json. Direct-request
results are unaffected; the bad team is skipped and logged.
Team reviews aren't showing up. Check teams in config.json and that each
slug is org/team exactly as it appears in the team URL.
Duplicate messages. Expected if two machines poll at the same time — state is per-machine by design. Run it on one machine, or accept the duplicates.
A message failed to send. That PR is deliberately not recorded, so the next run retries it rather than dropping it silently.
python3 -m unittest discover -s tests -v45 tests, no network access, no credentials, nothing touched outside a temp directory. Worth running on each OS you install on — it covers the paths, atomic state writes, and flood guard that differ across platforms.
python3 install.py --uninstall # removes the scheduled jobThen delete the config and state directories (--check lists them) to remove
the token and webhook URL.