Notification strategies¶
Each mapped repository has a notification strategy. It decides when Notifycat tells Slack about a new PR.
| Strategy | What the channel sees |
|---|---|
per-event (default) |
One message per PR, posted the moment the PR opens, then updated with reviews, merge, and close. |
scheduled |
Nothing at once. New PRs wait in a queue. On a schedule, one message per channel lists every PR queued since the last send. |
With no notifications: block anywhere, every repository uses per-event. That is how Notifycat always worked.
Turning it on¶
Set notifications: on a mapping tier — an org "*" tier or a repository tier:
mappings:
acme:
"*":
channel: C0TEAMDEFAULT
notifications:
strategy: scheduled
mode: hourly # every hour at :00
payments:
channel: C0PAYMENTS0
notifications:
strategy: scheduled
mode: at
at: ["10:30", "15:00"] # HH:MM, in digest.timezone
billing:
channel: C0BILLING00
notifications:
strategy: scheduled
mode: cron
cron: ["0 9 * * *", "0 14 * * *"]
infra:
channel: C0INFRA0000
notifications:
strategy: per-event # opt out of the org "*" schedule
Each team picks its own strategy. In this example, acme/infra keeps instant messages while every other acme repository gets lists.
Modes¶
mode |
Extra key | Sends |
|---|---|---|
hourly |
none | Every hour at :00, around the clock. |
at |
at: — a non-empty list of HH:MM times |
Once a day at each listed time. |
cron |
cron: — a non-empty list of 5-field cron specs |
On each cron tick. |
All times use digest.timezone (default UTC). See Weekends, holidays, and timezone.
Rules¶
The server checks the block at startup. notifycat-config validate and notifycat-doctor run the same checks. A bad block fails startup.
strategyis required:per-eventorscheduled.per-eventtakes no other key.scheduledrequiresmode.hourlytakes noatand nocron.atneeds a non-emptyat:list. Each item isHH:MM(24-hour). Nocron.cronneeds a non-emptycron:list. Each item is a standard 5-field cron spec. Noat.- Unknown keys fail.
The block is atomic. The most specific tier that sets notifications: wins as a whole. Keys are not merged across tiers. So a repository tier that sets only strategy: per-event fully replaces a scheduled "*" tier, and a half-inherited schedule cannot happen.
The strategy is not part of the lock file hash. Changing it does not revalidate the entry and needs no new Slack scope.
What a list looks like¶
:eyes: @payments-team, 3 PRs ready for review:
• acme/payments #42: Add retries · by octocat
• acme/payments #43: Fix refund rounding · by hubot
• acme/web #7: New checkout page · by mona
- The headline pings the channel's
mentions. When several repositories send to one channel, their mentions are joined. - Each line links to the PR.
- A very long list is cut at Slack's block limit and ends with
…and N more. - A channel with nothing queued gets no message.
What happens to each event¶
| Event | scheduled behavior |
|---|---|
| PR opened / ready for review | Channels resolve as usual — channel:, channels: fan-out, and paths: rules. The PR is queued once per channel. No Slack call. |
| Schedule tick | Every queued PR of a still-open PR is posted in one list per channel, then marked sent. |
| Reviews and comments | No Slack call — there is no per-PR message to react on. |
| Merged or closed | The PR leaves the queue if it was not sent yet. |
| Converted to draft | The PR leaves the queue. Ready for review again → queued again. |
Each PR is listed once per channel. A webhook redelivery after the send does not list it again.
If the post to one channel fails, that channel's PRs stay queued and go out on the next tick. The other channels are not affected.
Weekends, holidays, and timezone¶
Scheduled sends share the stuck-PR digest calendar:
digest.timezonesets the clock for every mode.- Weekends are always skipped.
- Public holidays are skipped when
digest.countryis set.
A skipped tick keeps the queue. PRs opened on Saturday go out with the first tick on Monday.
You do not need to enable the digest for this. digest.timezone and digest.country apply even when digest.enabled is off.
No PR twice: the stuck-PR digest¶
The digest and a scheduled list can both run in one channel. Notifycat keeps a PR out of one channel's digest when, in that same channel, the PR is:
- still queued for a scheduled list, or
- sent in a scheduled list since the start of today (in
digest.timezone).
So a PR is never in both messages on the same day. From the next day on, a still-idle PR shows in the digest as usual.
Logs¶
INFO queued scheduled notification provider=github repository=acme/payments pr=42 channels=1
INFO scheduled notifications posted schedule="30 10 * * *" channel=C0PAYMENTS0 prs=3
ERROR scheduled notification post failed schedule="30 10 * * *" channel=C0PAYMENTS0 prs=3 err=...
INFO skipped scheduled notifications schedule="0 * * * *" reason=weekend date=2026-07-04
channels is how many channels the PR was queued for. reason is weekend or holiday, the same values as the digest.
Limits¶
- List messages are never updated, reacted on, or deleted. They hold many PRs.
cleanup.delete_on_closedoes not apply to them. - Lists have no "Start review" button.
- Switching a repository from
scheduledback toper-eventdoes not post its queued PRs. They stay unsent, and stay out of the digest, until each PR closes. New PRs post at once. - A PR opened while its repository was
per-eventkeeps its own message after a switch toscheduled. Reviews and merges still update that message.