---
title: Snooze any inbox item
---
# Snooze any inbox item

## What it is

Every row in the Omniscio Inbox is **snoozable** — sessions, SMS conversations, Telegram chats, fired alarms, cron approvals, automation approvals, recipe approvals, recipe authoring approvals, CLI Pending Actions, RSS articles, RSS digests, drip campaigns, the daily digest, and weekly summaries. Snoozing temporarily hides the row from the Inbox and from every other "needs you" / sidebar / list filter until a time you pick, then the row reappears automatically.

The mechanic is unified: one **Snooze Palette** (same UI as session snooze), one event (`snooze-entity`), one gate helper (`isInboxItemSnoozed`), and one expiry sweep (60-second tick). Different inbox kinds use slightly different storage on the backend, but the user-facing behaviour is identical across every row.

This is distinct from [snoozing a session](snooze-a-session.md): session snooze drops a hoisted amber chat marker into the conversation and survives until you reply. Snoozing a non-session inbox row (an SMS thread, a cron approval, etc.) **does not** insert a chat marker — there is no shared conversation to mark — but it does hide the row from every list until the snooze expires or you act on it.

**A snooze sticks to the alert, not to the card.** Many alerts are raised by something that keeps
watching a condition: it posts a card when the condition is true, clears the card when it resolves,
and posts again the next time it breaks. That second posting is a brand-new row behind the scenes.
Snoozing is keyed to the alert's own identity rather than to the row that happened to deliver it, so
if you snooze a card for a week and the same alert is cleared and re-raised twice in the meantime, it
stays hidden for the week you asked for. Three limits keep that honest: an expired snooze is never
revived (a deferral cannot outlive its own end time), the longest deferral wins when more than one is
on record, and this applies to alerts only — every other inbox kind already has a stable row and is
unchanged.

## Where to find it

Right-click any row in the Omniscio Inbox for a **Snooze** submenu of presets, with **Custom...** at the bottom for a time you type.

## How it behaves

### How to use it

1. **Right-click any inbox row** (desktop) — the row's context menu shows a **Snooze** submenu with preset durations. The presets follow the same contextual logic as session snooze: 2 Hours From Now, This Morning (only during 12 AM–6 AM, resolves to 8 AM the same day), This Evening, Tomorrow Morning, Tomorrow Afternoon, Day After Tomorrow, This Weekend, Next Week, End of Week, Next Weekend, Next Month. Only the first 4 contextually relevant presets render in the submenu. **With several rows selected**, right-clicking one of them snoozes the whole selection and the item reads **"Snooze N selected..."** — see [bulk-select-sidebar.md](bulk-select-sidebar.md). Right-clicking a row *outside* your selection still snoozes just that row.
2. **Pick a preset, or open the full palette** — clicking a preset snoozes immediately. To type a custom time ("in 2 hours", "friday 9am", "tonight 11", or compact shorthand like "805p" / "8p"), click **Custom...** at the bottom of the submenu — the full Snooze Palette opens pre-targeted at the row. The same smart AM/PM defaulting applies (hours 1–6 → PM, `tonight 9` → PM, explicit AM/PM always wins).
3. **The row disappears from the Inbox immediately.** No undo toast, no separate confirmation — the row vanishing is the feedback. The badge counts on the sidebar and the inbox group header update live via the `INBOX_SNOOZE_CHANGED` push event.
4. **Snoozes expire automatically.** A background sweep runs every 60 seconds, clears the snooze record, and re-emits the row into the Inbox — no refetch, no refresh.
   - **Snoozing the row you're viewing carries you to the next inbox item** — snooze is a triage gesture just like Archive, so it advances the cursor (and closes to "All clear" when it was the last item) instead of leaving the just-snoozed item's pane open. This holds for **every** inbox kind — sessions, alerts, SMS, approvals, drips, and the daily-digest / weekly-summary cards alike. Snoozing a _different_ row via right-click while viewing another leaves your place put. A build guard makes it impossible for any snooze surface to regress this — see [inbox-snooze-coverage-contract.md](/.claude/memory/contracts/inbox-snooze-coverage-contract.md) `snoozing-the-open-row-advances-the-cursor` and the cross-cutting [inbox-navigation-contract.md](/.claude/memory/contracts/inbox-navigation-contract.md) `action-advance-coverage-manifest`.
5. **Open any inbox item and snooze from its header** — every inbox detail view shows a **Snooze (clock) button beside Archive**, on both desktop and mobile. It's the shared [InboxSnoozeButton](/src/renderer/src/components/inbox/InboxSnoozeButton.tsx) — the twin of the Archive button — on the alert, drip, scheduled-message, SMS, daily-digest and weekly-summary viewers, and the `snoozeTarget` clock on the approval-shell panes (recipe run, the Telegram/RSS/Jira/Linear/Granola/Notion redirect pane, and bug-intake). Clicking it opens the same palette. This is the primary path **on mobile**, where there is no right-click menu; the desktop right-click submenu and the `S` shortcut stay available too. (The per-row mobile Snooze clock that used to sit beside the dismiss X was **removed 2026-06-06** — the affordance lives in the opened-item header, not on the row; see [inbox-snooze-coverage-contract.md](/.claude/memory/contracts/inbox-snooze-coverage-contract.md) `no-per-row-snooze-button` / `every-archivable-viewer-also-surfaces-snooze`. `alarm-fired` is the one carve-out — it keeps its own "Snooze 9m" button. `MobileInboxDetailHeader` was the original intended mechanism but is now dead code.)

**Special case — alarm-fired rows.** A fired-alarm row already shows an inline **Snooze 9m** button (the legacy quick-action). The right-click → Snooze submenu now coexists with it: 9m stays as the one-tap default, the full palette is reachable via the context menu for any custom duration.

### How it works

Inbox rows split into two storage paths based on whether the underlying entity already has a `snoozed_until` column on its own table:

**Three reserved kinds keep their per-table snooze field** (because they already had one before universal snooze shipped):

- `session` — `sessions.snoozed_until`, drives the chat marker and the rich session-snooze behaviour ([snooze-a-session.md](snooze-a-session.md)).
- `digest` — `daily_digests.snoozed_until`, hides today's digest from the Inbox.
- `weekly-summary` — `weekly_summaries.snoozed_until`, hides this week's summary.

**Every non-reserved inbox kind routes through the `inbox_snoozes` table** (v200 migration). The set is DERIVED — `getUniversalInboxKinds() = INBOX_SOURCE_INTEGRATIONS` minus the 3 reserved kinds ([inbox-snooze-kinds.ts](/src/renderer/src/lib/inbox-snooze-kinds.ts); a LAZY getter, not an eager const, so the module is safe to import from anywhere in the `inbox-source-behaviors ↔ inbox-sources` ESM cycle) — so a new inbox source becomes snoozeable automatically. Currently 24 kinds: `sms`, `telegram`, `rss`, `rss-digest`, `cron-approval`, `automation-approval`, `cli-pending-approval`, `recipe-approval`, `recipe-authoring-approval`, `alarm-fired`, `recipe`, `drip`, `scheduled-message`, `pr-merge-queue`, `pr-inbox`, `support-chat`, `intake-triage`, `jira-inbox`, `alert`, `supermail`, `linear-inbox`, `granola-inbox`, `notion-inbox`, `feature-nudge`. (The Context Size / doc-token and MCP overhead warnings moved off their own inbox kinds onto the central alert system in 2026-09 — they snooze as ordinary `alert` cards now, not their own `doc-token-alert` / `mcp-bloat-alert` kind.)

**One special case inside `alert`:** a Tasks **reminder** alert (a task's deadline / back-from-snooze notice) does NOT write an `inbox_snoozes` row. The backend handler detects its `tasks-v2-…` dedup key and instead snoozes the underlying **task** (sets `tasks_v2.snoozed_until`) and archives the reminder — so the task actually defers and the reminder doesn't just reappear. See [inbox-snooze-coverage-contract.md](/.claude/memory/contracts/inbox-snooze-coverage-contract.md) "Known edge" and [tasks-v2-contract.md](/.claude/memory/contracts/tasks-v2-contract.md) `inbox-reminder`. Every OTHER `alert` (and every other non-reserved kind) routes through `inbox_snoozes` as below.

The `inbox_snoozes` row shape is `(kind TEXT, target_id TEXT, snoozed_until TEXT, note TEXT?)` keyed on `(kind, target_id)`. The `target_id` is the item's **`payloadRef.id`** — the canonical snooze key, which can be coarser than the row's `item.id` (e.g. `pr-merge-queue` snoozes per-repo while rendering per-PR rows; `intake-triage` keys on `intake-class-<k>`/`intake-issue-<id>`). Keying on `item.id` instead silently breaks those kinds. Composite keys mean two different kinds can share an id without colliding.

**Service** ([/src/main/services/inbox/inbox-snooze-service.ts](/src/main/services/inbox/inbox-snooze-service.ts)) is a per-kind dispatcher. It rejects the three reserved kinds at the service boundary so the universal table can never accumulate rows for them (those go through their existing lifecycle services). Every write emits `INBOX_SNOOZE_CHANGED` so the renderer cache updates without a refetch.

**Usage tracking.** On a successful snooze, the inbox handler (like the digest, weekly-summary, and email handlers) records a `snooze.applied` event with a duration `bucket` via `recordSnoozeUsage` — feeding the always-on snooze-usage stats and the (Lab, default-OFF) personalized snooze ordering. These cross-kind paths record the bucket only; the preset ranking is session-driven. See [snooze-a-session.md](snooze-a-session.md) "Personalized ordering (Lab)" + [snooze-recommendations-contract.md](/.claude/memory/contracts/snooze-recommendations-contract.md).

**Expiry sweep** ([/src/main/services/inbox/inbox-snooze-service.ts](/src/main/services/inbox/inbox-snooze-service.ts) `checkExpiredInboxSnoozes`) runs every 60 seconds via `createPeriodicTask`, deletes rows where `snoozed_until <= now`, and fires `INBOX_SNOOZE_CHANGED { removed: true }` for each. Runs in parallel with (not nested inside) the session snooze sweep — same cadence, separate tasks.

**Renderer cache** ([/src/renderer/src/stores/inbox-snooze-store.ts](/src/renderer/src/stores/inbox-snooze-store.ts)) is a Zustand store keyed by composite `kind|entityId`. `loadActive()` pulls all non-expired rows on boot; the `INBOX_SNOOZE_CHANGED` push listener applies upserts and deletes incrementally.

**Gate helper** ([/src/renderer/src/lib/inbox-snooze-gate.ts](/src/renderer/src/lib/inbox-snooze-gate.ts)) `isInboxItemSnoozed(kind, entityId)` is the **single source of truth** every inbox/attention filter consults. For `session` / `digest` / `weekly-summary` it reads the entity's own store; for the universal kinds it reads `inbox-snooze-store`. In both cases it compares `snoozedUntil > Date.now()` so an expired snooze stops hiding the row immediately, even before the next 60-second sweep fires.

**Lint enforcement.** [inbox-snooze-gate-routing.test.ts](/tests/unit/lint/inbox-snooze-gate-routing.test.ts) bans bare `!row.snoozedUntil` reads (they leave rows stuck-snoozed past expiry; non-inbox concerns like tasks are allow-listed). [inbox-snooze-coverage.test.ts](/tests/unit/lint/inbox-snooze-coverage.test.ts) asserts every non-reserved kind is in the derived allow-list, every kind's projector calls `isInboxItemSnoozed`, and the dispatch keys on `payloadRef.id`. Contract: [inbox-snooze-coverage-contract.md](/.claude/memory/contracts/inbox-snooze-coverage-contract.md).

**UI surface.** Snooze fires a `snooze-entity` `CustomEvent` (the palette stays the universal entry point). Three producers, all building the payload via the shared `buildSnoozeEntityDetail` ([inbox-snooze-kinds.ts](/src/renderer/src/lib/inbox-snooze-kinds.ts)), keyed on `payloadRef.id`: the desktop right-click submenu ([InboxItemContextMenu](/src/renderer/src/features/dashboard/InboxItemContextMenu.tsx)); the `S` shortcut; and — the opened-item path, desktop AND mobile — the shared [InboxSnoozeButton](/src/renderer/src/components/inbox/InboxSnoozeButton.tsx) in each detail viewer's header (plus `ApprovalPaneShell`'s `snoozeTarget` on the approval-shell panes). The button resolves the open item's target through `resolveActiveInboxSnoozeTarget()` / `snoozeActiveInboxItem()` ([inbox-source-behaviors.ts](/src/renderer/src/stores/inbox-source-behaviors.ts), the snooze twin of `dismissActiveInboxItem`), so it's keyed on the right `payloadRef.id` for every kind — the two reserved-kind viewers (digest / weekly-summary) pass an explicit target instead. Coverage is locked by [inbox-snooze-button-coverage.test.ts](/tests/unit/lint/inbox-snooze-button-coverage.test.ts) (archive ⟺ snooze on every viewer; `AlarmDetailPane` carve-out). (The per-row mobile Snooze button on [UnifiedInboxRow](/src/renderer/src/features/dashboard/UnifiedInboxRow.tsx) was removed 2026-06-06.) The palette is the shared [SnoozePalette](/src/renderer/src/components/ui/SnoozePalette.tsx); its `SnoozeTarget.type` is `'session' | 'email' | 'digest' | 'weekly-summary' | UniversalInboxKind`, where `UniversalInboxKind` is derived from the registry (so a new kind is covered at compile time).

### Over the CLI

The same universal snooze is reachable without the UI, so a script, a hotkey or an outside AI can
put a row away and bring it back on a timer of its own:

- **`POST /inbox/snooze`** — body `{ kind, entityId, snoozedUntil, note?, title? }`. It **upserts**,
  so snoozing the same row again simply moves its wake time rather than stacking a second snooze.
- **`POST /inbox/unsnooze`** — body `{ kind, entityId }`. Brings the row back immediately, and is
  **idempotent**: unsnoozing something that is not snoozed is a success, not an error.
- **`GET /inbox/snoozes`** — the rows that are snoozed right now, as `kind`, `entityId`,
  `snoozedUntil` and `note`. Bounded by `?limit=`.

**Three kinds are deliberately not snoozable this way:** `session`, `digest` and
`weekly-summary`. Each carries its own snooze field on its own row rather than living in the
universal snooze table, so asking the universal routes for one is refused with a `400` that names
the kind and tells you to use that kind's own snooze instead. The UI hides this distinction
entirely — every row in the inbox snoozes the same way from the right-click menu — so it is only a
caller's concern.

Mutations are charged to the shared mutation budget and reads to the read budget, like the rest of
the CLI surface.

## Related

- [snooze-a-session.md](snooze-a-session.md) — sessions get the rich chat-marker behaviour on top of the gate; the snooze entry point is the session header overflow menu (or right-click on a sidebar row), not the inbox right-click.
- [inbox-overview.md](inbox-overview.md) — every row in the Inbox is now snoozable; the snooze badge counts and group counts update live via the `INBOX_SNOOZE_CHANGED` push event.
- [bulk-select-sidebar.md](bulk-select-sidebar.md) — Shift+J/K multi-select followed by bulk Snooze applies the same palette to every selected inbox row in a single shot.
- [alarms.md](alarms.md) — the inline **Snooze 9m** button on a fired-alarm row remains for one-tap dismissal; the right-click submenu opens the full palette for custom durations.
