> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ravenna.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Reminders

> Automatically nudge pending approvers and ticket assignees on a configurable schedule until they act, the work completes, or the reminder cap is reached.

Reminders automatically nudge people who have not acted on work assigned to them. Ravenna handles the follow-ups, posting a public message on the ticket and @mentioning whoever still needs to respond. There are two types:

* **Approval reminders** nudge approvers who have not yet responded to a pending [approval round](/documentation/tickets/approvals/rounds).
* **Assignment reminders** nudge the current assignee of a ticket that has been sitting in an open state.

Both are configured per workspace in **Settings** → **Workspace** → **Automation**. Each workspace owns its own policies, so IT and HR can run on different cadences.

<CardGroup cols={2}>
  <Card title="Approval reminders" icon="check" href="/documentation/tickets/reminders#approval-reminders">
    Nudge approvers who have not yet responded to a pending approval round.
  </Card>

  <Card title="Assignment reminders" icon="user-check" href="/documentation/tickets/reminders#assignment-reminders">
    Nudge the current assignee of a ticket that has been sitting in an open state.
  </Card>
</CardGroup>

## Mental model

Approval and assignment reminders are workspace-level automation policies that schedule recurring nudge messages on tickets. Both use the shared `Reminder` infrastructure, the same scheduling primitive keyed by a URN, and both dispatch a `REMINDER_EXPIRED` ticket event when the cap is hit.

Each policy is configured once per workspace. There is no per-ticket, per-queue, per-form, per-round, or per-assignee override.

* **Approval reminders** are keyed by `(ticketId, roundId)`. Scheduled when a round opens, cancelled when the round closes (approve, decline, reset).
* **Assignment reminders** are keyed by `ticketId`. Scheduled when a ticket is assigned, cancelled when the ticket is unassigned, reassigned, archived, or moves to `Done` or `Closed`.

***

## Configuration

Each workspace has at most one policy of each type. Both expose the same fields:

| Field           | Type                         | Default | Notes                                                                                                                                                                                                  |
| --------------- | ---------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `enabled`       | boolean                      | `false` | Master switch. `false` means no reminders are scheduled or dispatched even if other fields are set                                                                                                     |
| `intervalHours` | number ≥ 1                   | `24`    | Hours between the work starting (or the last reminder) and the next reminder                                                                                                                           |
| `maxReminders`  | integer ≥ 1 or `null`        | `3`     | Maximum reminders per round (approval) or per assigned ticket (assignment). `null` means unlimited                                                                                                     |
| `scheduleId`    | string or `null`             | `null`  | Optional [business schedule](/documentation/platform/workspaces/settings#business-schedules) ID used to defer delivery to working windows. Must belong to the same workspace                           |
| `filterGroups`  | filter-group array or `null` | `null`  | Optional ticket condition filter, expressed as an OR of AND groups. Empty/null means "remind for every ticket". Evaluated against the ticket's hydrated context every time a reminder is about to fire |

The UI exposes the interval in hours, days, or weeks, but the value is normalized to hours on save. Days and weeks are converted via `24` and `168` respectively.

`computeReminderFireAt(baseTime, intervalHours, schedule)` returns `baseTime + intervalHours` adjusted forward to the next working window of the policy's selected business schedule. If the policy has no schedule, the returned time is the raw wall-clock offset. The schedule is the one attached to the policy, not the workspace default. Upserting a policy with a `scheduleId` that does not belong to the same workspace is rejected.

***

## Approval reminder lifecycle

1. **Round opens.** When an approval round transitions to ACTIVE, an approval reminder is scheduled for the round.
2. **Policy check.** The workspace's approval reminder policy is loaded. If `enabled = false`, no reminder is scheduled.
3. **First reminder scheduled.** A reminder is enqueued with:
   * `uri`: `REMINDER_URN.approvalRound(ticketId, roundId)`, which guarantees idempotency per round.
   * `type`: `ReminderType.Approval`.
   * `fireAt`: `computeReminderFireAt(round.openedAt, intervalHours, policy.schedule)`.
   * `metadata`: `{ ticketId, roundId, workspaceId, organizationId, reminderCount: 0 }`.
4. **Reminder fires.** The handler runs:
   * Re-fetches pending approvers. Empty list → cancels the reminder URN and returns (round resolved between fire-at and execution).
   * Re-reads the policy. If `enabled = false` mid-cycle → cancels and returns.
   * Evaluates `policy.filterGroups` against the hydrated ticket context. On no match, the reminder is marked `skipped` and **no message is posted**; on match, posts the system message below.
   * Posts a system message authored by the workspace bot user with `@` mentions for each pending approver. Source is `SYSTEM`. Message metadata records `reminderType`, `reminderCount`, `intervalHours`, `maxReminders`, and `recipientIds`.
   * Computes `nextCount`. A delivered fire increments it; a `skipped` fire leaves it unchanged. If `maxReminders !== null && nextCount >= maxReminders`, dispatches a `REMINDER_EXPIRED` ticket event with `reminderType: Approval`. Otherwise, if the chain has been running less than `REMINDER_CHAIN_MAX_AGE_DAYS` (30 days from `chainStartedAt`), schedules the next reminder. Past that ceiling the chain stops without dispatching `REMINDER_EXPIRED`.
5. **Round closes.** When the round becomes APPROVED, DECLINED, or RESET, the reminder is cancelled via the URN.

URN:

```text theme={"system"}
ravenna:reminder:approval-round:{ticketId}:{roundId}
```

Scheduling with the same URN replaces any existing reminder for that key, so re-opening or re-activating a round does not double-schedule.

***

## Assignment reminder lifecycle

1. **Assignee set.** When a ticket gains an assignee, an assignment reminder is scheduled.
2. **Policy check.** The workspace's assignment reminder policy is loaded. If `enabled = false`, no reminder is scheduled.
3. **First reminder scheduled.** A reminder is enqueued with:
   * `uri`: `REMINDER_URN.assignment(ticketId)`, one in-flight reminder per ticket.
   * `type`: `ReminderType.Assignment`.
   * `fireAt`: `computeReminderFireAt(now, intervalHours, policy.schedule)`.
   * `metadata`: `{ ticketId, assigneeId, organizationId, workspaceId, reminderCount: 0 }`.
4. **Reminder fires.** The handler runs:
   * Reloads the ticket. If the ticket is missing or no longer assigned to `metadata.assigneeId`, is archived, or its status group is in `TERMINAL_STATUS_GROUPS` (`Done`, `Closed`) → cancels the URN and returns.
   * Re-reads the policy. If `enabled = false` mid-cycle → cancels and returns.
   * Evaluates `policy.filterGroups` against the hydrated ticket context. On no match, the reminder is marked `skipped` and **no message is posted**; on match, posts the system message below.
   * Posts a system message authored by the workspace bot user with an `@` mention for the assignee. Source is `SYSTEM`. Message metadata records `reminderType`, `reminderCount`, `intervalHours`, `maxReminders`, and `recipientIds`.
   * Computes `nextCount`. A delivered fire increments it; a `skipped` fire leaves it unchanged. If `maxReminders !== null && nextCount >= maxReminders`, dispatches a `REMINDER_EXPIRED` ticket event with `reminderType: Assignment`. Otherwise, if the chain has been running less than `REMINDER_CHAIN_MAX_AGE_DAYS` (30 days from `chainStartedAt`), schedules the next reminder. Past that ceiling the chain stops without dispatching `REMINDER_EXPIRED`.
5. **Assignment changes or ticket closes.** The URN is cancelled. Reassigning to a new user schedules a fresh cycle keyed on the same URN, which supersedes any prior schedule.

URN:

```text theme={"system"}
ravenna:reminder:assignment:{ticketId}
```

One reminder per ticket. Reassignment replaces the schedule rather than running two cycles in parallel.

***

## REMINDER\_EXPIRED dispatch

When `maxReminders` is hit, the handler dispatches a `TicketEventAction.REMINDER_EXPIRED` event with:

* `ticketId`: the ticket
* `reminderType`: `Approval` or `Assignment`
* `organizationId`
* `source`: `SYSTEM`

The **Reminder Expired** workflow trigger consumes this event. Filter by `Reminder Type` to target a specific reminder type. The round or ticket is **not** modified by the expiry. Any state change must come from a workflow or manual admin action.

***

## Constraints and gotchas

* Each workspace has at most one policy of each type. There is no per-queue, per-form, or per-ticket override.
* Approval reminders are per-round. A ticket with three sequential rounds gets three independent reminder cycles. Assignment reminders are per-ticket-per-assignee. Reassigning a ticket cancels the existing cycle (via the URN) and starts a new one with `reminderCount: 0`.
* The `intervalHours` clock restarts after every reminder. It is measured from "work started or last reminder", not from ticket creation.
* The cap (`maxReminders`) counts dispatched reminders, not scheduled ones. If a reminder fires and is cancelled early (no pending approvers, terminal status, mismatched assignee, archived), or is `skipped` because the ticket did not match `filterGroups`, the count is not incremented.
* `filterGroups` is evaluated against the ticket's hydrated context (reference id `reminder`, section `input`) on every fire. A non-matching fire is recorded with status `skipped` instead of `fired`, no message is posted, and the chain reschedules without advancing the count. Reminder records can be in one of four statuses: `pending`, `fired`, `skipped`, or `cancelled`.
* Reminder chains carry `chainStartedAt`, the wall-clock time the first reminder was scheduled. Once the chain age exceeds `REMINDER_CHAIN_MAX_AGE_DAYS` (30 days), the handler stops rescheduling and does **not** fire `REMINDER_EXPIRED`. This prevents a ticket that never matches its policy filters from rescheduling forever. Chains scheduled before this field existed have no cap.
* Reminder messages are public ticket messages, visible to everyone with access to the ticket including the requester via Slack mirrors and email.
* Tickets in any sub-status of the `Done` or `Closed` status groups stop assignment reminders. Tickets in `Waiting` do **not**, so move a ticket to `Done` if you want reminders to cease.
* Approval reminders do not send a fresh Slack DM. They rely on the existing approval DM plus the new public `@` mention. To re-prompt via DM, reset the round.
* Disabling a policy mid-cycle cancels future dispatches but does not delete already-scheduled reminders. The handler re-checks the policy at fire time and cancels the URN if disabled.
