---
title: Scheduled Messages (compose-to-self, fire-into-inbox)
---

# Scheduled Messages (compose-to-self, fire-into-inbox)

## What it is

**Scheduled Messages** lets you write a rich-markdown note to yourself — a subject line plus a Markdown body, with optional inline images — schedule it to arrive once at a future time or on a recurring cadence, and have Omniscio silently drop it into your unified inbox when it fires. Think "a letter to your future self": a reminder, a checklist, a standing weekly prompt, a bit of context you want resurfaced next Monday morning. When the message fires it lands as a persistent inbox item that survives an app restart; you read it and archive it (and **Ctrl+Z** brings it right back if you archive one by mistake), and that's the whole lifecycle — there are no approvals and nothing to respond to.

Scheduled Messages is **on by default**. The capability (flag `scheduledMessagesEnabled`) ships enabled for everyone — existing installs are flipped on once, respecting a later opt-out — so the background scanner runs and a fired note lands in your unified inbox whether or not you ever open the feature's UI. What **is** hidden by default is the standalone **"Scheduled Messages" sidebar row and its compose surface**, gated by a separate UI-only flag (`scheduledMessagesSidebarEnabled`, default off), exactly the way Alerts works. Reveal the row at `Settings → Features → "Show Scheduled Messages in sidebar"` (or from the discovery nudge, which flips the same toggle); hiding it never affects the scanner or the inbox delivery. Turning the capability itself off — the opt-out on the same screen — silences the scanner; messages and deliveries already in the inbox are preserved.

**Not the same as "Send Later".** Send Later (a.k.a. scheduled responses) defers a _reply you are sending into a live Claude session_ — it queues your typed message and dispatches it to the agent at a chosen time. Scheduled Messages never touches a session and never talks to an agent: it delivers a note **to you**, into your inbox, full stop. If you want to nudge a running agent later, use Send Later; if you want to remind _yourself_ of something later, use Scheduled Messages.

A scheduled message has two states: **Active** (enabled — the scanner fires it on its schedule and, for recurring messages, advances to the next fire time) and **Paused** (disabled — the schedule is suspended but the message is kept so you can resume it). Recurrence reuses Omniscio's shared recurrence vocabulary: **once**, **daily**, **weekdays**, **weekends**, or specific **days of the week**, each paired with a local time of day.

## Where to find it

### How to use it

1. **Reveal it in the sidebar.** The feature is already on, but its sidebar row is hidden by default. Turn on Settings → Features → **"Show Scheduled Messages in sidebar"** to surface the "Scheduled Messages" row in the Omniscio group. (You can also jump straight in from the discovery nudge, which flips the same toggle.)
2. **Open the Scheduled Messages virtual project** in the sidebar. The first launch shows an empty state with **+ New message**.
3. **Compose a message.** Give it a subject (up to 200 characters) and a Markdown body (rendered live in a preview pane via `ProseBlock`). Use **Add image** to embed an inline image — it is copied into the message's own attachment storage and referenced by an `attachment://` URL, so the original file can move or be deleted later without breaking the message.
4. **Set the schedule.** Pick a recurrence — **Once**, **Daily**, **Weekdays**, **Weekends**, or **Custom days** (choose which days of the week) — and a time of day (defaults to 08:00 local). Or, faster: type the timing in plain English into the **Schedule (plain English)** box above those controls — _"every weekday at 8pm"_, _"tomorrow at 2:30pm"_, _"weekends at 9am"_ — and Omniscio fills the Time, Recurrence, and (for one-offs) Date for you, with a one-line preview of what it understood. The box only ever fills the _schedule_; your subject and body are never touched, and you can still adjust any field by hand. Save with the button or with Ctrl+Enter / Cmd+Enter.
5. **Receive it in your inbox.** When the message fires, it appears in your unified inbox as a **Scheduled Messages** integration row. Clicking the row opens the delivery in the **Scheduled message inbox viewer** on the right: the subject as a heading, a "Delivered <timestamp>" line, and the Markdown body rendered as rich text. Archive it with the Archive button to clear it from the inbox — and if you archive one by mistake, press **Ctrl+Z** to bring it straight back (the archive is reversible).
6. **Act on it with one click.** The card also carries a **Start session** button in its bottom bar (the same shared affordance drip and agent-alert cards have, keyboard shortcut **N**). Click it to spin up a new Claude session pre-filled with the reminder's subject and body, in the repo you pick — handy when the note is really a to-do. Starting a session leaves the reminder in your inbox (archive it yourself when you're done).
7. **Manage existing messages.** The management pane (and the sidebar) split messages into **Active** and **Paused** sections with live search. Each row offers **Pause** / **Resume** and **Delete** (delete is confirmed via `ConfirmDialog` — it stops all future deliveries, but deliveries already in your inbox are kept).

## How it behaves

### Related docs

- `Integration registry contracts` — the wiring assertions this feature's registry entry has to honor.
- [Drip](drip.md) — the queue-and-trickle inbox feeder this feature is modeled on (Drip is the richer, multi-item cousin).
- [Send Later](send-later.md) — defers a reply into a live agent session; the distinct feature people most often confuse this with.

## For agents

### How it works

Data lives in two tables, added in schema migration **v258** in [`src/main/db/incremental-migrations.ts`](/src/main/db/incremental-migrations.ts):

- `scheduled_messages` — one row per message: `subject`, `body_md`, the recurrence + `days_of_week` + `fire_time_local` schedule, `attachment_scope_id` (the folder its inline images live under), the computed `next_fire_at`, `enabled`, `fire_count`, and timestamps. Indexed on `(enabled, is_deleted, next_fire_at)` so the scanner's "what's due?" query is cheap.
- `scheduled_message_deliveries` — one row per fired delivery, FK'd back to its message. Each delivery **snapshots** `subject_snapshot` + `body_snapshot` at fire time (so editing the message later never rewrites history) plus `fired_at`, `read_at`, and `archived_at`. Indexed on `fired_at` where `is_deleted = 0 AND archived_at IS NULL` — the unified inbox's "unarchived deliveries" query.

Soft-delete (`is_deleted`) applies to both tables. Query modules: [`src/main/db/queries-scheduled-messages.ts`](/src/main/db/queries-scheduled-messages.ts) and [`src/main/db/queries-scheduled-message-deliveries.ts`](/src/main/db/queries-scheduled-message-deliveries.ts).

Shared types live in [`src/shared/scheduled-message-types.ts`](/src/shared/scheduled-message-types.ts): `ScheduledMessage`, `ScheduledMessageDelivery` / `ScheduledMessageInboxItem`, the create/update input shapes, and the two push payloads (`ScheduledMessageDeliveredPayload` and `ScheduledMessageChangedPayload`, both carrying a `messageId`). Recurrence reuses the existing `AlarmRecurrence` union (`'once' | 'daily' | 'weekdays' | 'weekends' | 'days-of-week'`) rather than inventing a parallel vocabulary.

The compose modal's **plain-English schedule box** reuses the same natural-language parser the Calendar and Alarm Quick Add features use — it calls the shared `CALENDAR_PARSE_QUICK_ADD` IPC (regex-first, with a Haiku fallback only on a regex miss), so there is no extra parsing code or AI cost specific to this feature. The pure adapter [`src/shared/scheduled-message-quick-add.ts`](/src/shared/scheduled-message-quick-add.ts) maps the parser's interpretation into the four schedule fields, reusing the alarm mapper for the RRULE→legacy-enum collapse, and the box only ever fills the schedule (the parser's extracted title is discarded — the subject stays yours). Because a scheduled message can only repeat in the five `AlarmRecurrence` ways, a phrase the parser understands but the feature cannot store (e.g. _"every 3 weeks"_ or _"first Monday of the month"_) is **degraded to a one-off on its first matching date**, with a note in the preview saying the repeat could not be stored.

The background scanner is [`src/main/services/scheduled-message-scanner-service.ts`](/src/main/services/scheduled-message-scanner-service.ts), a 60-second periodic task. Each tick returns early when `settings.scheduledMessagesEnabled` is off, so the feature truly goes quiet when disabled. For each due message it runs a single transaction that inserts the snapshot delivery and advances `next_fire_at` (for a `once` message, it disables the message after firing). After the transaction commits it emits a `SCHEDULED_MESSAGE_DELIVERED` push and a `SCHEDULED_MESSAGE_CHANGED` push.

The ten IPC handlers live in [`src/main/ipc/scheduled-message-handlers.ts`](/src/main/ipc/scheduled-message-handlers.ts): message `LIST` / `GET` / `CREATE` / `UPDATE` / `DELETE`, inbox `INBOX_LIST` / `INBOX_ARCHIVE` / `INBOX_UNARCHIVE` / `INBOX_MARK_READ`, and `SAVE_IMAGE` (which enforces a 10 MB cap and returns an `attachment://<scopeId>/<filename>` URL). `INBOX_UNARCHIVE` is the inverse of archive that powers the **Ctrl+Z** undo — it clears `archived_at` so the delivery returns to the inbox feed. Create / update / delete emit `SCHEDULED_MESSAGE_CHANGED`; the idempotent inbox archive / unarchive / mark-read handlers deliberately do **not** push (mirroring Drip). On the renderer, [`src/renderer/src/stores/scheduled-message-store.ts`](/src/renderer/src/stores/scheduled-message-store.ts) holds the message list, the selected id, and the cross-message inbox slice, with optimistic create/update/delete + a debounced `handlePushUpdate` wired through [`src/renderer/src/hooks/useScheduledMessagePushEvents.ts`](/src/renderer/src/hooks/useScheduledMessagePushEvents.ts). One intentional divergence from the Drip template: `SCHEDULED_MESSAGE_UPDATE` takes a **flat** wire payload (`{ id, ...fields }`), not Drip's nested `{ id, fields }`, because the handler's `.strict()` Zod schema places `id` as a sibling of the field keys.

The integration is registered in [`src/shared/integration-registry.ts`](/src/shared/integration-registry.ts) with `kind: 'amc-builtin'`, `virtualProjectPath: SCHEDULED_MESSAGES_PROJECT_ID` (the `__scheduled_messages__` sentinel), `featureFlag: 'scheduledMessagesEnabled'`, `settingsSearchId: 'scheduled-messages-enabled'`, `inboxSourceIds: ['scheduled-message']`, and `approvalKinds: []` (one-way item — no approvals). Its two push channels are `SCHEDULED_MESSAGE_DELIVERED` and `SCHEDULED_MESSAGE_CHANGED`; only `SCHEDULED_MESSAGE_DELIVERED` is **critical** (Zod-validated on the emit side and able to wake mobile clients), because `SCHEDULED_MESSAGE_CHANGED` is a management-UI sync signal. The renderer UI wiring (sidebar + panel components, the CalendarClock icon) lives in [`src/renderer/src/integrations/ui-registry.ts`](/src/renderer/src/integrations/ui-registry.ts). The integration-completeness contract test ([`tests/unit/lint/integration-completeness.test.ts`](/tests/unit/lint/integration-completeness.test.ts)) fails CI if any of these claims drifts from its backing wiring.

## Related

[drip.md](drip.md) is the queue-and-trickle inbox feeder this feature is modelled on, and [send-later.md](send-later.md) covers the distinct feature most often confused with it — deferring one of your replies into a live agent session rather than delivering a note to you.
