gateway.yaml and every scheduled agent result is gated for you.
This is the policy layer — the whether and what to notify. It sits above Push Delivery Store (delivery guarantee) and before Real-Time Push Notifications (transport).
Quick Start
1
Turn it on with one YAML block
Add With these defaults a scheduled
gateway.notifications to your gateway.yaml. Scheduled agent results are now gated automatically.ops-agent result (agent_finished) is not opted in, so it is suppressed.2
Set a per-recipient preference
Key a recipient by
platform:channel_id and override any default.telegram:12345 now receives full scheduled results, except between 22:00 and 07:30 London time.3
Validate and hot-reload
A bad entry fails at startup, not at send time:Edit
gateway.yaml and reload. The new policy applies without a restart:How Categorised Sends Flow Through the Gateway
Every proactive send that carries acategory passes through the gate before the idempotency check and the channel.
- Suppressed sends count as handled —
deliver()returnsTrue, so the scheduler and outbox do not retry, and no idempotency key is claimed. - Uncategorised sends are never gated — direct replies, hook replies and restart notices pass straight through.
- Quiet hours use the recipient’s timezone — the gate passes an aware UTC
now, so the window is checked in the recipient’stimezone, not the gateway host’s clock. - No
notificationsblock, no gating — behaviour is unchanged when the block is absent.
What Gets Categorised Automatically
Today only scheduled results (
agent_finished) are wired. The gateway does not mark sends as urgent, so urgent_overrides_quiet has no effect on gateway sends yet — it applies when you call evaluate(..., is_urgent=True) yourself.Where Recipients Come From
A recipient key is the resolvedplatform:channel_id of the delivery target — the same pair the gateway logs on each proactive send (Delivered scheduled result to telegram:12345, or notification suppressed for telegram:12345 when gated). A recipient with no entry falls back to defaults, and anything missing from defaults falls back to DEFAULT_PREFERENCE.
The Three Concerns
Categories
A recipient opts into each event class independently; an un-opted category is suppressed with reasoncategory_not_subscribed.
Quiet Hours
A per-recipient local window suppresses non-urgent categories; urgent events override whenurgent_overrides_quiet is true.
[start, end). When start == end the window is empty (never quiet). When start > end it wraps past midnight — (22:00, 07:30) means 22:00–24:00 ∪ 00:00–07:30. A naive now is treated as already local; an aware now is converted to prefs.timezone via stdlib zoneinfo. When calling evaluate() directly, an unknown timezone name is ignored silently and now is left unconverted (so an aware UTC now is checked in UTC); the gateway rejects it at config load instead.
Detail Levels
Three levels control how much content leaves the gateway.Which Detail Level Should I Pick?
Match the level to how trusted the destination surface is.YAML Reference
All keys live undergateway.notifications.
<entry> is defaults or any recipients entry. A key missing from a recipient entry inherits from defaults; a key missing from defaults inherits from DEFAULT_PREFERENCE.
Validation and Hot-Reload
- Fails at startup — loading
gateway.yamlbuilds the gate, so an unknown category or detail, a malformedquiet_hours, or an unknown timezone raises before the gateway serves traffic. - Published schema —
praisonai gateway schemaprints the JSON schema, including thenotificationsblock, for editor validation. - Hot-reload — on file change or
SIGHUP, the gateway applies the newnotificationsblock to the live delivery router. Removing the block clears gating. See Gateway Hot Reload.
Under the Hood: evaluate()
The gate wraps one pure function, which you can also call from your own notifier before delivery; the decision carries the send flag and the already-redacted content.
evaluate() is pure and side-effect-free, so any process can call it at delivery-decision time without I/O. Pass prefs=None to fall back to DEFAULT_PREFERENCE; pass now=None to use the host’s naive local clock (prefs.timezone is not applied). To honour the recipient’s timezone, pass an aware now such as datetime.now(timezone.utc), as the gateway does.
Python Reference
NotificationPreference
Also exposes
.allows(category) → bool, whether this recipient opted into category.
NotificationDecision
evaluate(...)
DEFAULT_PREFERENCE
Applied when a recipient has set nothing: the two interaction-blocking categories are on, at the identified (no-content) detail level, with no quiet hours.
Common Patterns
Mute scheduled results overnight, keep approvals —agent_finished is suppressed inside the window.
detail: private so the lock-screen never shows content.
evaluate() directly from your own notifier.
Gate a scheduler completion notification — the recipient opts into AGENT_FINISHED, and it is not urgent.
DetailLevel.PRIVATE so the lock-screen never shows content.
Best Practices
Key recipients by platform:channel_id, not by display name
Key recipients by platform:channel_id, not by display name
The gateway looks up each send by the resolved
platform:channel_id (e.g. telegram:12345). A key that does not match — a username, a channel name — silently falls back to defaults. Copy the pair from the gateway’s delivery log line.Reload instead of restart
Reload instead of restart
Edit
gateway.yaml and save, or send SIGHUP / systemctl reload praisonai-gateway. The new policy is applied to the live delivery router without restarting the gateway.Pick a sensible default per product surface
Pick a sensible default per product surface
Match the default
detail to where notifications land. Use PRIVATE for mobile lock-screens where anyone nearby can read the preview, IDENTIFIED for shared team channels that route by agent, and DETAILED only for trusted single-user desktops. DEFAULT_PREFERENCE uses IDENTIFIED as a safe middle ground.Reserve urgency for interaction-blocking events
Reserve urgency for interaction-blocking events
Treat
APPROVAL_REQUESTED and AGENT_QUESTION as urgent by convention — they block an agent from continuing until you answer. Pass is_urgent=True for these so quiet hours never strand a paused agent. Keep failure alerts (SCHEDULED_TASK_FAILED, BACKGROUND_TASK_FAILED) non-urgent unless your operations demand overnight paging.Get quiet-hours edge cases right
Get quiet-hours edge cases right
The window is half-open
[start, end). A same-day window needs start < end (e.g. (09:00, 17:00)); an overnight window needs start > end (e.g. (22:00, 07:30)), which wraps across midnight. Setting start == end produces an empty window — never quiet. Always set timezone to the recipient’s IANA zone and pass an aware now so it localises correctly; a naive (or None) now is assumed already local, and an unknown zone leaves now unconverted.Balance lock-screen privacy against actionability
Balance lock-screen privacy against actionability
Lower detail protects content but forces the recipient to open the app to act. If a notification only exists to prompt an approval,
IDENTIFIED is usually enough — the recipient knows which agent to open. Reserve DETAILED for events where the content itself is the action (e.g. a failure stack trace) and the destination is trusted.User Interaction Flow
An operator sets a preference once ingateway.yaml; later scheduled results are gated automatically.
Related
Real-Time Push Notifications
The PushClient / WebSocket transport this policy sits above
Push Delivery Store
The durable at-least-once delivery this policy composes with
Gateway Schedules
Scheduled jobs — the producer of
agent_finished sendsGateway Hot Reload
How file-change and SIGHUP reloads apply a new policy

