# Notifications

<!-- BEGIN GENERATED: guide-map -->
## Guide map

Read only the section needed for the next decision. Line ranges are exact
for this generated runtime target.

| Section | What it covers | Lines |
| --- | --- | --- |
| [Purpose](#purpose) | Scope, ownership, and the central operating question. | 28-35 |
| [Primary SDK Paths](#primary-sdk-paths) | Operating guidance and contracts for this section. | 36-48 |
| [Supporting References](#supporting-references) | Capability, provider, policy, and runtime guides for this outcome. | 49-52 |
| [Use This When](#use-this-when) | Tasks and conditions that belong in this guide. | 53-63 |
| [Do Not Use This For](#do-not-use-this-for) | Boundaries and the correct routes for excluded work. | 64-72 |
| [Choose A Path](#choose-a-path) | Decision hierarchy for selecting the right capability or provider. | 73-83 |
| [Mental Model](#mental-model) | Concepts and state boundaries needed for correct operation. | 84-110 |
| [Preflight](#preflight) | State, authority, inputs, and readiness to verify before execution. | 111-120 |
| [Risk And Side Effects](#risk-and-side-effects) | Effects, approvals, cost, privacy, and retry boundaries. | 121-135 |
| [Canonical Operating Pattern](#canonical-operating-pattern) | The normal end-to-end execution sequence. | 136-150 |
| [Core SDK Surfaces](#core-sdk-surfaces) | Exact source-backed callable contracts owned by this guide. | 151-170 |
| [Data Contracts](#data-contracts) | Required identities, fields, evidence, handles, and durable shapes. | 171-191 |
| [Verification](#verification) | Evidence required before reporting success. | 192-201 |
| [Troubleshooting](#troubleshooting) | Failure diagnosis and corrective routes. | 202-218 |
| [Related References](#related-references) | Adjacent boundaries and when to cross them. | 219-226 |
<!-- END GENERATED: guide-map -->

## Purpose

Operate the workspace's Slack notification layer: which platform events ping
which Slack channel or DM, how loud follow-ups are allowed to be, when the
channel goes quiet after a human takes a conversation over, and who gets
@-mentioned. The central question this reference answers: "how do I make the
team hear about the right reply moments, once, in the right place?"

## Primary SDK Paths

<!-- BEGIN GENERATED: primary-sdk-paths -->
| Primary function | Choose it when | Required inputs | Returns | Exact contract |
| --- | --- | --- | --- | --- |
| `vf.notifications.capabilities` | Use first to verify Slack connection, destination support, and missing scopes before configuring or testing delivery. | None | `notification_capabilities` | [`notifications.md`](notifications.md) · installed SDK: `verbiflow/notifications.py:313-327` |
| `vf.notifications.rules` | Use to inspect existing rule coverage and health before creating a duplicate or diagnosing delivery. | None | `notification_rule_list` | [`notifications.md`](notifications.md) · installed SDK: `verbiflow/notifications.py:27-41` |
| `vf.notifications.create_rule` | Use to persist a new event-to-Slack delivery rule after scope, destination, matching conditions, and behavior are explicit. | `destination`, `event_keys`, `name` | `notification_rule` | [`notifications.md`](notifications.md) · installed SDK: `verbiflow/notifications.py:43-107` |
| `vf.notifications.matching_events` | Use before creation or repair to preview which historical workspace events a rule would match. | `event_keys` | `notification_matching_events` | [`notifications.md`](notifications.md) · installed SDK: `verbiflow/notifications.py:255-311` |
| `vf.notifications.test_rule` | Use to send or simulate the product's bounded test delivery for one existing rule and inspect its receipt. | `rule_id` | `notification_test_result` | [`notifications.md`](notifications.md) · installed SDK: `verbiflow/notifications.py:192-217` |
| `vf.notifications.catch_up` | Use to deliver missed matching events for an existing rule over a bounded historical window. | `rule_id` | `notification_catch_up_result` | [`notifications.md`](notifications.md) · installed SDK: `verbiflow/notifications.py:219-253` |
<!-- END GENERATED: primary-sdk-paths -->

## Supporting References

This outcome has no supporting guide.

## Use This When

- Creating, updating, listing, or deleting Slack notification rules
  (workspace-wide or scoped to specific sequences).
- Reading or changing the workspace channel-ping setting (`quiet_down` vs
  `keep_pinging`).
- Previewing which past events a rule shape would match before creating it.
- Delivering a rule's earlier matches on demand (catch-up).
- Mapping sender accounts to Slack users for @-mentions.
- Diagnosing "why did/didn't this reply ping the channel".

## Do Not Use This For

- Reading or answering the replies themselves: `inbox.md` owns reply triage
  and customer-visible replies.
- Sender/mailbox identity and readiness: `senders.md`.
- Sequence lifecycle (publish, staging, audience): `sequences.md`.
- Slack workspace connection itself: `notifications.capabilities` reads its
  status, but installing/reconnecting Slack is a dashboard operation.

## Choose A Path

1. For a new alert, inspect capabilities and existing rules, preview matching
   events, then create one rule with an explicit destination.
2. For a missing or noisy alert, inspect rule health, matching events, handoff
   mode, delivery history, and throttle state before editing anything.
3. For a real delivery test or backlog catch-up, keep the customer-visible
   Slack effect separate and confirm the exact rule and destination first.
4. For reply content or an actual response, leave notifications and use
   `inbox-and-replies.md`.

## Mental Model

A rule maps events to one Slack destination. Rules have an event list
(`reply.classified`, `reply.received`, lifecycle keys), a sequence scope
(`{"kind": "all"}`, `{"kind": "sequences", "sequence_ids": [...]}`, or
`{"kind": "inbox"}`), optional reply-content filters (categories, priority
bands), a destination (`slack_channel` or `slack_dm`), and a delivery mode
(`instant` or `digest`).

Delivery renders one card per conversation per channel. The card silently
re-edits to the newest state with an update counter; thread replies carry the
full history; channel pings (broadcasts) are rationed.

Ping precedence, in order: **handoff beats filters, filters beat the model,
the model only judges what the rule left broad, and nothing loud escapes the
throttle** (max one ping per conversation per ~3 minutes).

- Handoff mute (email only, under `quiet_down`): when the latest human reply
  came from the owner's own email inbox, the conversation stops pinging; the
  card keeps updating. One model-judged rescue ping fires if the lead keeps
  writing and nobody answers. A reply through the product re-arms pings.
- Reply-content filters (categories/bands) always ping: the filter is the
  user's attention judgment.
- Broad rules delegate the ping decision to the reply classifier's
  attention verdict; junk (bounces, auto-replies) stays quiet.
- `keep_pinging` disables the mute and the verdict; the throttle stays.

## Preflight

1. `vf.notifications.capabilities()` — confirm `slack_connected` is true and
   note `dm_enabled`/`missing_scopes` before creating DM rules or mentions.
2. `vf.notifications.rules()` — check for an existing rule covering the same
   events and channel before creating a near-duplicate.
3. For a new rule shape, `vf.notifications.matching_events(...)` — see what
   it would have matched; a surprising `total_matching` means the scope or
   filters are wrong.

## Risk And Side Effects

- `rules`, `capabilities`, `settings`, `mention_mappings`, and
  `matching_events` are platform reads.
- `create_rule`, `update_rule`, `delete_rule`, `set_handoff_mode`,
  `set_mention_mapping`, and `delete_mention_mapping` are platform writes to
  workspace notification config.
- `test_rule` and `catch_up` post **real messages into the customer's
  Slack**. Catch-up can send many at once (a large backlog folds into
  summary posts). Confirm with the user before either unless they explicitly
  asked. `create_rule(catch_up={"enabled": True, ...})` triggers the same
  delivery at create time.
- `set_handoff_mode` changes ping behavior for the whole workspace, not one
  rule.

## Canonical Operating Pattern

1. `vf.notifications.capabilities()` — Slack connected, scopes present.
2. `vf.notifications.rules()` — inspect existing rules and their `health`.
3. Preview: `vf.notifications.matching_events(event_keys=[...], ...)`.
4. Create: `vf.notifications.create_rule(name=..., event_keys=[...],
   destination={...})`; print the returned `rule_id`.
5. Prove delivery: `vf.notifications.test_rule(rule_id)` (with user
   consent — it posts).
6. Adjust later with `vf.notifications.update_rule(rule_id, filters={...})`
   — partial fields merge over the current rule.
7. For backlog delivery, confirm intent, then
   `vf.notifications.catch_up(rule_id, window="last_week")` and report the returned
   counts.

## Core SDK Surfaces

<!-- BEGIN GENERATED: core-sdk-surfaces -->
| Function | Use When | Inputs | Output And Handles To Preserve | Side Effect | Source |
| --- | --- | --- | --- | --- | --- |
| `vf.notifications.capabilities` | Use for Slack notification rules, ping settings (handoff mode), mention mappings, match previews, and catch-up delivery. | No inputs. | `dict` with `slack_connected`, `team_id`, `team_name`, `dm_enabled`, `email_match_enabled`, and `missing_scopes` for the workspace Slack installation.<br><strong>Common next calls</strong>: `vf.notifications.rules`, `vf.notifications.settings` | read | installed SDK: `verbiflow/notifications.py:313-327` |
| `vf.notifications.catch_up` | Use for Slack notification rules, ping settings (handoff mode), mention mappings, match previews, and catch-up delivery. | <strong>Required</strong>: `rule_id` (str; required): Rule id from notifications.rules or create_rule.<br><strong>Optional</strong>: `include_delivered` (bool; optional; default `False`): Queue events that already have delivery state again; keep False for idempotent catch-up.; `window` (str; optional; default `'all_time'`): Backlog window: all_time, last_day, last_3_days, last_week, last_2_weeks, last_month, or last_3_months. | `dict` with `sent_individually`, `summarized`, and `already_delivered` counts for the catch-up run.<br><strong>Common next calls</strong>: `vf.notifications.matching_events` | platform_write | installed SDK: `verbiflow/notifications.py:219-253` |
| `vf.notifications.create_rule` | Use for Slack notification rules, ping settings (handoff mode), mention mappings, match previews, and catch-up delivery. | <strong>Required</strong>: `destination` (JsonObject; required): Slack destination: {kind: slack_channel, team_id, channel_id} or {kind: slack_dm, team_id, slack_user_id}.; `event_keys` (list[str]; required): Notification event keys to match, e.g. reply.classified or reply.received.; `name` (str; required): Rule display name shown in notification settings.<br><strong>Optional</strong>: `analysis` (JsonObject \| None; optional; default `None`): Optional Mochi analysis: {enabled: bool, instruction: str}.; `catch_up` (JsonObject \| None; optional; default `None`): Create-time backlog delivery: {enabled: true, window: all_time\|last_week\|...}. Posts real Slack messages.; `delivery_mode` (JsonObject \| None; optional; default `None`): {kind: instant} (default) or {kind: digest, digest_interval: hourly\|daily}.; `enabled` (bool; optional; default `True`): Whether the rule is active.; `filters` (JsonObject \| None; optional; default `None`): Typed filters: reply_categories, priority_bands, bounce_classes, and first_touch_only. Each filter applies only to its event family.; `notify_slack_user_ids` (list[str] \| None; optional; default `None`): Static Slack member ids to @-mention on loud posts.; `sequence_scope` (JsonObject \| None; optional; default `None`): Scope: {kind: all} (default), {kind: sequences, sequence_ids: [...]}, or {kind: inbox}. | `dict` with `rule_id`, `enabled`, `name`, `event_keys`, `sequence_scope`, `filters`, `destination`, `delivery_mode`, `analysis`, `notify_slack_user_ids`, `version`, and delivery `health`. Preserve `rule_id` and `version`.<br><strong>Common next calls</strong>: `vf.notifications.test_rule`, `vf.notifications.rules` | platform_write | installed SDK: `verbiflow/notifications.py:43-107` |
| `vf.notifications.delete_mention_mapping` | Use for Slack notification rules, ping settings (handoff mode), mention mappings, match previews, and catch-up delivery. | <strong>Required</strong>: `sender_account_id` (str; required): Sender account whose outbound identity maps to this Slack user.<br><strong>Optional</strong>: `sequence_id` (str \| None; optional; default `None`): Scope the mapping to one sequence; omit for the workspace default. | `dict` with `deleted: bool` confirming the removal.<br><strong>Common next calls</strong>: `vf.notifications.mention_mappings` | platform_write | installed SDK: `verbiflow/notifications.py:425-456` |
| `vf.notifications.delete_rule` | Use for Slack notification rules, ping settings (handoff mode), mention mappings, match previews, and catch-up delivery. | <strong>Required</strong>: `rule_id` (str; required): Rule id from notifications.rules or create_rule.<br><strong>Optional</strong>: none | `dict` with `deleted: bool` confirming the removal.<br><strong>Common next calls</strong>: `vf.notifications.rules` | platform_write | installed SDK: `verbiflow/notifications.py:165-190` |
| `vf.notifications.matching_events` | Use for Slack notification rules, ping settings (handoff mode), mention mappings, match previews, and catch-up delivery. | <strong>Required</strong>: `event_keys` (list[str]; required): Notification event keys to match, e.g. reply.classified or reply.received.<br><strong>Optional</strong>: `bounce_classes` (list[str] \| None; optional; default `None`): Bounce classes to filter on: hard, soft, or unknown.; `exclude_delivered_for_rule` (str; optional; default `''`): Rule id: count only events a catch-up run for that rule would still send.; `first_touch_only` (bool; optional; default `True`): For email opens and clicks, match only the first tracked touch.; `limit` (int; optional; default `5`): Preview row count (1-25); total_matching is always the full count.; `priority_bands` (list[str] \| None; optional; default `None`): Lead priority bands to filter on, e.g. hot, warm.; `reply_categories` (list[str] \| None; optional; default `None`): Reply categories to filter on, e.g. positive_interest, meeting_requested.; `sequence_ids` (list[str] \| None; optional; default `None`): Sequence ids when sequence_scope_kind is sequences.; `sequence_scope_kind` (str; optional; default `'all'`): Scope kind to preview: all, sequences, or inbox.; `window` (str; optional; default `'all_time'`): Backlog window: all_time, last_day, last_3_days, last_week, last_2_weeks, last_month, or last_3_months. | `dict` with `events` (event_id, event_key, occurred_at, payload, sequence_id, conversation_group_id) and `total_matching` for the previewed rule shape.<br><strong>Common next calls</strong>: `vf.notifications.create_rule`, `vf.notifications.catch_up` | read | installed SDK: `verbiflow/notifications.py:255-311` |
| `vf.notifications.mention_mappings` | Use for Slack notification rules, ping settings (handoff mode), mention mappings, match previews, and catch-up delivery. | No inputs. | `dict` with `mappings` rows (`sender_account_id`, `slack_user_id`, optional `sequence_id`).<br><strong>Common next calls</strong>: `vf.notifications.set_mention_mapping` | read | installed SDK: `verbiflow/notifications.py:369-383` |
| `vf.notifications.rules` | Use for Slack notification rules, ping settings (handoff mode), mention mappings, match previews, and catch-up delivery. | No inputs. | `dict` with `rules` rows including `rule_id`, config, `health` (`ok`/`never_fired`/`erroring`), `last_fired_at`, and `last_error`.<br><strong>Common next calls</strong>: `vf.notifications.create_rule`, `vf.notifications.matching_events` | read | installed SDK: `verbiflow/notifications.py:27-41` |
| `vf.notifications.set_handoff_mode` | Use for Slack notification rules, ping settings (handoff mode), mention mappings, match previews, and catch-up delivery. | <strong>Required</strong>: `mode` (quiet_down \| keep_pinging; required): quiet_down mutes conversations a teammate answers from their own inbox; keep_pinging pings on every match.<br><strong>Optional</strong>: none | `dict` with `handoff_mode` (`quiet_down` or `keep_pinging`).<br><strong>Common next calls</strong>: `vf.notifications.settings` | platform_write | installed SDK: `verbiflow/notifications.py:345-367` |
| `vf.notifications.set_mention_mapping` | Use for Slack notification rules, ping settings (handoff mode), mention mappings, match previews, and catch-up delivery. | <strong>Required</strong>: `sender_account_id` (str; required): Sender account whose outbound identity maps to this Slack user.; `slack_user_id` (str; required): Slack member id (U...) to @-mention on loud notifications.<br><strong>Optional</strong>: `sequence_id` (str \| None; optional; default `None`): Scope the mapping to one sequence; omit for the workspace default. | `dict` with the saved mapping: `sender_account_id`, `slack_user_id`, and optional `sequence_id` override scope.<br><strong>Common next calls</strong>: `vf.notifications.mention_mappings` | platform_write | installed SDK: `verbiflow/notifications.py:385-423` |
| `vf.notifications.settings` | Use for Slack notification rules, ping settings (handoff mode), mention mappings, match previews, and catch-up delivery. | No inputs. | `dict` with `handoff_mode` (`quiet_down` or `keep_pinging`).<br><strong>Common next calls</strong>: `vf.notifications.set_handoff_mode` | read | installed SDK: `verbiflow/notifications.py:329-343` |
| `vf.notifications.test_rule` | Use for Slack notification rules, ping settings (handoff mode), mention mappings, match previews, and catch-up delivery. | <strong>Required</strong>: `rule_id` (str; required): Rule id from notifications.rules or create_rule.<br><strong>Optional</strong>: none | `dict` with `ok` and optional `error` for the test Slack post.<br><strong>Common next calls</strong>: `vf.notifications.rules` | platform_write | installed SDK: `verbiflow/notifications.py:192-217` |
| `vf.notifications.update_rule` | Use for Slack notification rules, ping settings (handoff mode), mention mappings, match previews, and catch-up delivery. | <strong>Required</strong>: `rule_id` (str; required): Rule id from notifications.rules or create_rule.<br><strong>Optional</strong>: `analysis` (object; optional): Optional Mochi analysis: {enabled: bool, instruction: str}.; `delivery_mode` (object; optional): {kind: instant} (default) or {kind: digest, digest_interval: hourly\|daily}.; `destination` (object; optional): Slack destination: {kind: slack_channel, team_id, channel_id} or {kind: slack_dm, team_id, slack_user_id}.; `enabled` (boolean; optional): Whether the rule is active.; `event_keys` (array; optional): Notification event keys to match, e.g. reply.classified or reply.received.; `filters` (object; optional): Typed filters: reply_categories, priority_bands, bounce_classes, and first_touch_only. Each filter applies only to its event family.; `name` (string; optional): Rule display name shown in notification settings.; `notify_slack_user_ids` (array; optional): Static Slack member ids to @-mention on loud posts.; `sequence_scope` (object; optional): Scope: {kind: all} (default), {kind: sequences, sequence_ids: [...]}, or {kind: inbox}. | `dict` with `rule_id`, `enabled`, `name`, `event_keys`, `sequence_scope`, `filters`, `destination`, `delivery_mode`, `analysis`, `notify_slack_user_ids`, `version`, and delivery `health`. Preserve `rule_id` and `version`.<br><strong>Common next calls</strong>: `vf.notifications.rules` | platform_write | installed SDK: `verbiflow/notifications.py:109-163` |
<!-- END GENERATED: core-sdk-surfaces -->

## Data Contracts

- Rule rows carry `rule_id` (preserve it; update/delete/test/catch-up need
  it), `version` (optimistic concurrency; `update_rule` handles it), and
  `health`: `ok`, `never_fired`, or `erroring` with `last_error`.
- `destination` is `{"kind": "slack_channel", "team_id", "channel_id"}` or
  `{"kind": "slack_dm", "team_id", "slack_user_id"}`. A channel already bound
  to a different organization's hosted agent is rejected with `409`.
- `sequence_scope` kinds: `all`, `sequences` (with `sequence_ids`), `inbox`.
- `filters`: `reply_categories`, `priority_bands`, `bounce_classes`,
  `first_touch_only`. Each filter applies only to its event family.
  Non-empty categories/bands make every match ping (see Mental Model).
- `catch_up` result counts: `sent_individually`, `summarized`,
  `already_delivered`. The legacy `already_delivered` field name counts events
  with any existing delivery state, so present it as already processed.
  Windows: `all_time`, `last_day`, `last_3_days`,
  `last_week`, `last_2_weeks`, `last_month`, `last_3_months`.
- `handoff_mode`: `quiet_down` (default) or `keep_pinging`.
- Mention mappings: `sequence_id=None` is the workspace default; a value
  scopes an override to one sequence.

## Verification

- After create/update: `vf.notifications.rules()` shows the rule with the
  expected config; print `rule_id` and `health`.
- After `test_rule`: result `ok` is true, and the user can see the test post
  in the destination channel. `ok=False` returns the Slack error in `error`.
- After `catch_up`: report the three counts. Describe `already_delivered` as
  already processed. A growing value on repeat runs proves idempotent delivery.
- After `set_handoff_mode`: `vf.notifications.settings()` echoes the mode.

## Troubleshooting

- Rule `health` is `erroring`: read `last_error`. `channel_not_found` /
  `not_in_channel` means the Slack app was removed from the channel;
  `missing_scope` means the install predates a required scope — check
  `capabilities().missing_scopes` and ask the user to reconnect Slack.
- Rule exists but nothing pings: check the ping ladder before assuming
  breakage — a teammate replying from their own email inbox mutes that
  conversation under `quiet_down`, and broad-rule routine follow-ups stay
  quiet by design. The conversation card still updates in place.
- `update_rule` returns 409 `version_conflict`: someone changed the rule
  concurrently; re-run the call (it re-reads the current rule).
- `matching_events` returns 400: an `event_keys` entry is not a valid
  notification event key.
- `set_handoff_mode` returns 404 `slack_install_missing`: Slack is not
  connected for this workspace.

## Related References

| If you are doing this | Read next |
| --- | --- |
| Reading or answering the replies that triggered pings | `inbox.md` |
| Sender identity behind mention mappings | `senders.md` |
| Sequence lifecycle the rules are scoped to | `sequences.md` |
| Approval classes for customer-visible actions | `safety.md` |
