---
title: Alarms
---

# Alarms

## What it is

Alarms are natural-language scheduled reminders that ring inside Omniscio at a wall-clock time you pick. You type "remind me to take meds at 8 PM" or "every weekday at 7:30 AM stand-up" into a single textarea and Omniscio saves the alarm, schedules the fire, and surfaces it however you've asked it to surface (full-screen modal, toast banner, or silent inbox row). When the alarm fires you get the standard phone-style affordances — Snooze for a few minutes, or Dismiss to clear it. Recurring alarms re-arm themselves automatically; one-off alarms vanish from the sidebar the moment they fire (phone-alarm semantics) — only the inbox `alarm-fired` row remains, where you can Snooze or Dismiss the missed fire like any other inbox card.

Alarms are a separate feature from **cron jobs** even though both are time-triggered. The line is that cron jobs run code (a script, a recipe, a session spawn) and post their result to the inbox; alarms only ever ring at the user. There is no command, no working directory, no exit code, no cost, no agent spawned. You can think of cron as the scheduler for Omniscio's automation surface and alarms as the scheduler for Omniscio's reminder surface. They live in different sidebar rows, different SQLite tables, and the UIs share nothing except the visual language of the rest of the app.

The feature ships behind a master toggle. Settings → Notifications → Alarms → **Enable Alarms** is on by default; flipping it off hides the sidebar row, suppresses the rest of the settings, and stops the in-process scheduler from ticking.

## Where to find it

### Where it lives in the UI

The Alarms entry sits in the **Omniscio sidebar group** (the same column as Cron, Skills, Recipes, Ask Omniscio). Clicking it opens the alarms virtual project with the usual 3-pane layout:

- **Pane 2 (sidebar)** — the natural-language input at the top, then time-based sections: **Today**, **This Week**, **Recurring**, **Disabled**. Below those, an optional **Folders** area shows your own user-defined groupings (see "Organizing alarms with folders" below) and an **Uncategorized** bucket for alarms not filed into any folder. Each row shows the label, the next fire time, and a small recurrence chip ("Daily" / "Weekdays" / "Custom" etc.); rows in folders also show a tiny folder badge so you can tell at a glance where an alarm lives. **Recurring alarm rows carry an on/off toggle so you can mute the schedule without deleting it; one-off alarm rows have no toggle** — they're inherently single-use, so the only meaningful actions are "open the detail pane to edit or cancel" and "let it fire" (after which it auto-deletes). Click an alarm to load it into Pane 3.
- **Pane 3 (detail)** — a full editor for the selected alarm: label, time, recurrence, **folder picker** (dropdown listing your folders + Uncategorized), sound picker with preview button, snooze duration, fire-limit (or "fire forever"), plus an Advanced section for the three per-alarm overrides (assertiveness / bring-to-foreground / pierce Focus Mode).

There is also a settings section at **Settings → Notifications → Alarms** that holds the global defaults — these are the values any new alarm inherits unless its per-alarm overrides say otherwise.

The sidebar row, the virtual project pane, and the settings section are wired to the registry in [src/shared/integration-registry.ts](../../src/shared/integration-registry.ts) (entry `id: 'alarms'`).

### How to create an alarm

1. **Open the Alarms virtual project** in the Omniscio sidebar.
2. **Type the alarm in plain English** into the textarea at the top of Pane 2. The placeholder shows an example: `Try "every weekday at 7:30 AM"`.
3. **Press Enter to save.** Shift+Enter inserts a newline if you want to spread the description across multiple lines, but Enter alone always submits.
4. **The new alarm appears in the appropriate section** — Today, This Week, Recurring, or Disabled — and Pane 3 loads its editor. You can refine the label, switch the sound, change the snooze duration, or flip individual override toggles from there.

If the parser can't make sense of what you typed, the textarea shows an inline hint underneath ("Could not parse — try `at 8pm` or `every Monday at 9am`"). The hint is informational only — nothing has been saved, no toast pops, and your input stays in the textarea so you can edit it.

The Pane 2 implementation is [AlarmsSidebar.tsx](../../src/renderer/src/features/alarms/AlarmsSidebar.tsx); the Pane 3 editor is [AlarmDetail.tsx](../../src/renderer/src/features/alarms/AlarmDetail.tsx).

#### Quick add from the header (toolbar button + Ctrl+Shift+A)

For the common case "I just want to add an alarm right now, I don't need to navigate to the Alarms project first", Omniscio has a header **quick-add** affordance. It's available whenever **Enable Alarms** is on.

- **Toolbar button** — a `BellPlus` icon labelled "New alarm" lives in the toolbar item list. Pin it to your header (Settings → Widgets → drag it into the pinned area, or use the overflow menu) and clicking it opens the quick-add modal in the centre of the screen.
- **Keyboard shortcut** — press **Ctrl+Shift+A** from anywhere in Omniscio (including while typing in another field — the shortcut is global) and the same modal opens.

The modal has:

1. **A natural-language textarea at the top** — same parser as Pane 2's box. Type something like `every weekday at 7am` and press **Enter** to fill the structured fields below. Enter parses + fills, it does NOT save — so you can tweak before committing.
2. **Editable form fields** — Label, Time, Recurrence, plus a one-off Date picker (when Recurrence = Once) or a row of weekday chips (when Recurrence = Custom days). You can skip NL entirely and fill these directly; you can also tweak what the parser produced before saving.
3. **Footer buttons** — Cancel (or Esc / backdrop click / X) closes without saving; **Create alarm** (or **Ctrl+Enter** from anywhere in the dialog) saves. On success the modal closes and the alarm joins your list. On failure (validation error, IPC error) the modal stays open with an inline hint so you can fix the input and retry.

The defaults on each open are `time = 08:00`, `recurrence = daily` — a single Ctrl+Enter from a blank modal creates a daily 8 AM alarm called "New alarm". Folder is always Uncategorized (move the alarm into a folder from the sidebar afterwards if you want).

The quick-add modal lives at [QuickAddAlarmModal.tsx](../../src/renderer/src/features/alarms/QuickAddAlarmModal.tsx). The toolbar item id is `'alarm-quick-add'` in [toolbar-items.ts](../../src/renderer/src/features/toolbar/toolbar-items.ts); the shortcut id is `'openAlarmQuickAdd'` in [keybindings.ts](../../src/shared/keybindings.ts). Both routes funnel through `useAlarmsStore.openQuickAdd()` which flips the `quickAddOpen` slice — the modal is mounted globally in App.tsx and reacts to that flag.

#### Natural-language examples that work

The parser handles the shapes you'd expect from a phone alarm app, plus a few Omniscio-specific ones:

- `at 8pm` — one-off, today (or tomorrow if 8 PM has already passed).
- `805p` / `8p` / `1230a` — compact shorthand for `8:05 PM` / `8:00 PM` / `12:30 AM`. A normalizer expands these to `H:MM AM/PM` before the parser runs, so compact times resolve on the free offline path (no AI call). The am/pm letter is required — a bare `1530` or `411` is left alone so a room or extension number is never read as a time.
- `tomorrow at 9:30am` — one-off on a named day.
- `every day at 7am` — recurring, daily.
- `every weekday at 7:30 AM stand-up` — recurring, Mon–Fri only, label "stand-up".
- `every Monday at 9am` — recurring, single weekday.
- `Mon Wed Fri at 6:45 PM yoga` — recurring, Custom (specific weekdays).
- `weekends at 10am` — recurring, Sat + Sun.
- `remind me to take meds at 8pm` — the label-hint phrase ("take meds") is preserved alongside the time.
- `tonight at 11:45 take out the trash` — one-off, descriptive label.
- `2025-12-25 at 7am open presents` — one-off, ISO date.

#### How the parser works (under the hood)

The parser at [src/main/services/alarm/alarm-nlp-parser.ts](../../src/main/services/alarm/alarm-nlp-parser.ts) is a two-stage offline-first hybrid:

1. **Chrono first (free, offline).** The chrono-node library parses the time and recurrence locally. If chrono produces a confident time and the input doesn't trip the recurrence-indicator regex, the alarm saves with zero network calls and zero cost. This is the path for the vast majority of phrasings.
2. **Haiku fallback (paid, capped).** When the input is ambiguous, hints at recurrence chrono can't infer (e.g. "every weekday"), or chrono fails outright, Omniscio calls Claude Haiku with a tight schema-constrained prompt and a 200-token cap. The model returns a `ParsedAlarm` object that the same downstream save path consumes. Failure is soft — if Haiku errors or the daily cost cap is exceeded, the inline hint surfaces and nothing is saved.

Input is capped at 500 characters before either parser sees it, so a paste of an entire email body will be truncated rather than billed.

## How it behaves

### Recurrence options

Alarms support five recurrence shapes, exposed in the editor as a segmented control:

| Recurrence   | Meaning                                                   | NL trigger phrase examples                 |
| ------------ | --------------------------------------------------------- | ------------------------------------------ |
| **Once**     | Fires once at the chosen date+time, then stops.           | "at 8pm", "tomorrow at 9am", ISO dates     |
| **Daily**    | Fires every day at the chosen time.                       | "every day", "daily"                       |
| **Weekdays** | Fires Mon–Fri only.                                       | "every weekday", "weekdays"                |
| **Weekends** | Fires Sat + Sun only.                                     | "weekends"                                 |
| **Custom**   | Fires on the specific days-of-week you pick (e.g. M/W/F). | "every Monday", "Mon Wed Fri", "Tue & Thu" |

In the editor, picking **Custom** reveals a row of seven day-of-week chips (Sun through Sat) that you toggle on or off. The full list lives in [AlarmDetail.tsx](../../src/renderer/src/features/alarms/AlarmDetail.tsx) (the `RECURRENCE_OPTIONS` and `DAY_LABELS` constants).

### Organizing alarms with folders

The time-based sections at the top of Pane 2 (Today / This Week / Recurring / Disabled) show every alarm you have. Below them, you can create flat user-defined **folders** to group related alarms together — "Work", "Meds", "Kids", whatever makes sense for you. Folders are purely cosmetic: an alarm in the "Meds" folder fires identically to an alarm with no folder, and an alarm shows up in BOTH its time section AND its folder section (they're two ways of looking at the same row, not a move).

#### Folder layout in the sidebar

The folder area appears beneath the time sections with these rows:

- **+ New folder** — a button at the top that opens an inline text input. Type a name, press Enter, the folder appears.
- **Uncategorized** — a built-in bucket for alarms that aren't in any user folder. Hidden when empty AND you have at least one user folder defined; visible otherwise so it's always discoverable.
- **Your folders** — one section per user folder, in your chosen order. Each folder's header has a left-side disclosure caret (collapse/expand), the folder name, and on hover two action icons appear: a pencil (rename inline) and a trash can (delete with confirm dialog).

Collapsing a folder hides its alarm rows but keeps the header visible so you can drop new alarms in. The collapsed state is per-session — restarting Omniscio re-expands everything.

#### Two ways to file an alarm into a folder

**1. Drag and drop (sidebar).** Grab any alarm row in Pane 2 — works in either the time sections or in a folder section — and drag it onto a folder header. The header highlights as you hover. Drop to file. Dragging onto **Uncategorized** removes the alarm from whatever folder it was in. Dragging onto an alarm's current folder is a no-op (no IPC round-trip, no visual flicker). Drag-and-drop uses a custom MIME type so random text drags or file drops can't accidentally trigger a folder highlight.

**2. Folder dropdown (Pane 3 editor).** Open any alarm in the detail editor and the **Folder** field is a normal dropdown listing all your folders plus "Uncategorized" at the top. Picking a folder saves on the next debounce tick like every other field in the editor.

Both paths route through the same `update(id, { folderId })` action on the store, so the result is identical no matter which you use.

#### Creating, renaming, deleting folders

- **Create** — click **+ New folder**, type a name, press Enter. Escape cancels. Empty names are rejected by the schema (no folder is created).
- **Rename** — hover the folder header, click the pencil. The name turns into an input. Type, press Enter to save, Escape to cancel; clicking away (blur) also saves. Renames affect only the folder's display name — the alarms inside it are untouched.
- **Delete** — hover the folder header, click the trash can, confirm in the dialog. **Alarms in the folder are NOT deleted.** They lose their folder assignment (the row's `folder_id` is set to NULL) and reappear in the Uncategorized bucket. This is a single atomic SQLite transaction at the backend, so neither half can land without the other — you can't get into a state where the folder is deleted but its alarms still point at the dead folder id.

The natural-language quick-add box at the top of Pane 2 doesn't accept a folder hint today. New alarms created via NL always land in Uncategorized; if you want them in a folder, drag the row down or pick a folder in Pane 3 afterward.

#### What folders deliberately don't do

- **No nesting.** Folders are flat — you can't put a folder inside a folder. The use case (group ~10 related alarms together) doesn't need tree structure, and a flat list keeps drag-and-drop unambiguous.
- **No fire-time inheritance.** A folder is just a label. Folders don't have their own settings, sound, or fire rules — every alarm carries its own settings.
- **No mobile editing.** Folder management (create / rename / delete / reorder / drag-to-assign) is desktop-only, same as the rest of the alarm editor. Mobile sees alarm rows in their time sections via the inbox; folder grouping is a desktop-side organization feature.

#### Where folders live in code

- DB schema: migration v199 in [src/main/db/database.ts](../../src/main/db/database.ts) adds the `alarm_folders` table (id, name, display_order, is_deleted, created_at, updated_at) and the `folder_id` column on `alarms`.
- CRUD: [src/main/db/queries-alarm-folders.ts](../../src/main/db/queries-alarm-folders.ts) (list/get/insert/rename/delete/reorder). Delete runs in a transaction that also sets `alarms.folder_id = NULL` for any row that referenced the deleted folder.
- IPC: channels `ALARM_FOLDERS_LIST` / `ALARM_FOLDER_CREATE` / `ALARM_FOLDER_RENAME` / `ALARM_FOLDER_DELETE` / `ALARM_FOLDERS_REORDER` in [src/shared/ipc-channels/index.ts](../../src/shared/ipc-channels/index.ts); handlers in [src/main/ipc/alarms-handlers.ts](../../src/main/ipc/alarms-handlers.ts). Push channel `ALARM_FOLDERS_CHANGED` fans changes out to other renderers.
- Renderer state: the `folders` slice on `useAlarmsStore` in [src/renderer/src/stores/alarms-store.ts](../../src/renderer/src/stores/alarms-store.ts), with a 150 ms debounce on push-driven reloads (independent of the alarm-list debounce so both can fire in parallel).
- UI: folder sections + drag-and-drop in [src/renderer/src/features/alarms/AlarmsSidebar.tsx](../../src/renderer/src/features/alarms/AlarmsSidebar.tsx); folder dropdown in [src/renderer/src/features/alarms/AlarmDetail.tsx](../../src/renderer/src/features/alarms/AlarmDetail.tsx).

### What happens when an alarm fires

The scheduler is an in-process loop that ticks every 30 seconds. When the tick passes an alarm's `nextFireAt` timestamp, Omniscio routes the fire through whatever assertiveness the alarm resolves to:

- **Modal** (default) — a full-screen `AlarmRingModal` opens with a live clock (HH:MM:SS, ticking every second), the alarm label, and two buttons: **Snooze 9m** and **Dismiss**. The sound starts playing on a loop. Ctrl+Enter (or Cmd+Enter on Mac) is bound to Dismiss as the primary action. Escape closes the dialog the same way Dismiss does. Only one modal can be visible at a time; if a second alarm fires while the first is open, it queues until the first resolves.
- **Banner** — a non-blocking toast appears at the top of the Omniscio window with the label and the same Snooze / Dismiss affordances. The sound plays once. This is the right choice for alarms you want to be aware of but don't want to interrupt typing or video calls.
- **Silent banner** — same toast as banner, but **no sound**. For passive reminders ("water plants", "stretch") that you want to notice when you look at the screen but never want to hear.

The modal lives at [AlarmRingModal.tsx](../../src/renderer/src/features/alarms/AlarmRingModal.tsx). The sound-playback gating happens in the main process — the renderer just renders whatever assertiveness the fired-event payload says.

#### Bring Omniscio to foreground

When the **Bring Omniscio to foreground** setting (or per-alarm override) is on, Omniscio raises its own window above other apps **once, at the moment the alarm starts ringing**, so you actually see the modal even if you've been working in another window — it does **not** keep pulling you back on every scheduler tick while the alarm continues to ring (the scheduler re-serves a still-ringing alarm every 30s; the foreground raise fires only at ring-start). This uses the same `forceForegroundWindow()` helper that the rest of Omniscio's notification surface uses — see [src/main/services/alarm/alarm-service.ts](../../src/main/services/alarm/alarm-service.ts).

#### Interaction with Focus Mode

Focus Mode batches non-urgent alerts into a single periodic notification instead of letting each one interrupt you. Alarms run through Focus Mode's `evaluate()` gate before they ring — and most alarms WILL be suppressed during a focus block, because by default alarms are subject to the same batching rules as every other notification surface.

The **Pierce Focus Mode** setting (or per-alarm override) is the escape hatch. With pierce on, the alarm bypasses Focus Mode entirely and fires immediately even mid-focus-block. Use it for the alarms you genuinely never want to miss — meds, picking up kids, hard meeting times — and leave it off for everything else.

When Focus Mode suppresses an alarm, the alarm doesn't disappear: it's recorded as a missed fire and surfaces in the inbox via the `alarm-fired` row (see "What happens if you're away" below).

### Snooze and Dismiss

**Snooze** delays the alarm for N minutes. The duration comes from `alarmsDefaultSnoozeMinutes` (5 / 9 / 15 / 30 — same options as a phone alarm app) or the per-alarm `snoozeMinutes` override. Snoozing a recurring alarm does NOT skip its next regular fire — if your 7 AM alarm rings and you snooze it 9 minutes, it'll ring again at 7:09 AND at 7 AM tomorrow morning.

**Dismiss** advances the schedule. For recurring alarms, Dismiss schedules the next occurrence — tomorrow at 7 AM, next Monday at 9 AM, etc. — and clears the current ring. For one-off alarms, the schedule-advance step **soft-deletes the alarm row** (phone-alarm semantics — a one-off vanishes from the sidebar the instant it fires), so Dismiss on a one-off is really just acknowledging the inbox card. The `alarm-fired` inbox row preserves the event regardless, so you can always still see that it happened.

Both actions are **idempotent at the service layer**. If a double-click or a flaky network produces two Snooze events for the same fire, the second one no-ops cleanly. You can't accidentally double-snooze.

**Snooze or Dismiss resolves the alarm on every device you have open**, not just the one you clicked. Dismiss it on your phone and the ring clears on the desktop too — the backend owns the ringing state and announces the ring ending, and each client drops it. A device that was asleep or offline when that happened clears the stale ring the next time its window regains focus, by re-checking with the backend which alarm (if any) is actually ringing.

#### Max ringing time

If you walk away mid-ring without clicking Snooze or Dismiss, Omniscio won't ring forever. The **Max Ringing Time** setting (default 15 minutes, range 1–60) caps how long the alarm rings before Omniscio auto-dismisses it to the inbox so it surfaces as a missed fire instead of locking the modal open all afternoon. This is the watchdog timer in [alarm-service.ts](../../src/main/services/alarm/alarm-service.ts).

### Sounds

Each alarm picks a sound from three sources, all surfaced in the same picker dropdown:

- **Built-in** — the small set of bundled WAV files that ship with Omniscio. The default for new alarms is `mission-alert` (a real built-in id; it MUST match a `BUILTIN_SOUNDS` entry — an id no source carries resolves to nothing and rings silently, the bug the old `needs-you-default` default caused).
- **System** — Windows / macOS system notification sounds discovered by the OS sound enumerator.
- **Custom** — any audio file you've added under Omniscio's user sound directory (`<userData>/sounds/`). The Settings → Notifications tab is where custom sounds are added today.

**Never silent.** When an alarm fires, the renderer resolves its sound id and — if the id genuinely can't be found (a stale default, a since-deleted custom sound) — falls back to a guaranteed built-in via `resolveAlarmSoundUrl`, so an alarm always rings rather than failing silently. The fallback runs only after the sound cache has loaded, so a valid-but-not-yet-cached system/custom sound still plays correctly, and sound-**preview** buttons keep using `buildSoundUrl` directly (a preview plays exactly the chosen sound, never a substitute). A deliberately silent alarm (`silent-banner` assertiveness) still plays nothing — the fallback lives only on the audible ring/banner paths.

Next to the picker is a **Play** button that previews the selected sound at the current notification volume. The preview uses Omniscio's `sound://` protocol (a custom Electron protocol that resolves builtin/system/custom prefixes to their on-disk paths). Selecting a sound saves immediately; the preview is a sanity check, not a separate save step.

The ringing alarm modal also carries a **View alarms** link in its bottom-left: it opens the Alarms list and honestly resolves the current ring (snoozing it while snoozing is allowed, else dismissing) so it is never left in a stuck "ringing" state.

The sound-list IPC is `IPC.NOTIFICATION_SOUNDS_LIST` and the picker component (`SoundPicker`) lives inline at [AlarmsSettings.tsx](../../src/renderer/src/features/settings/sections/alarms/AlarmsSettings.tsx) and is mirrored inside [AlarmDetail.tsx](../../src/renderer/src/features/alarms/AlarmDetail.tsx) for per-alarm overrides.

### Per-alarm overrides vs. global defaults

Many settings can be overridden per alarm. The pattern is uniform: `T | null` where `null` means "inherit the global default at fire time" — the global value is read fresh each fire, so changing the global default later updates every alarm that didn't override it. Override one alarm and only that alarm changes; flip the global and every alarm with `null` for that field picks up the new value on its next ring.

| Setting                 | Global (Settings → Notifications → Alarms) | Per-alarm (AlarmDetail)             |
| ----------------------- | ------------------------------------------ | ----------------------------------- |
| Sound                   | `alarmsDefaultSound`                       | `soundId` (null = inherit)          |
| Snooze duration         | `alarmsDefaultSnoozeMinutes`               | `snoozeMinutes` (null = inherit)    |
| Assertiveness           | `alarmsAssertiveness`                      | `assertivenessOverride` (null)      |
| Bring to foreground     | `alarmsBringToForeground`                  | `bringToForegroundOverride` (null)  |
| Pierce Focus Mode       | `alarmsPierceFocusMode`                    | `pierceFocusModeOverride` (null)    |
| Sound play mode         | `alarmsDefaultSoundPlayMode`               | `soundPlayMode` (null = inherit)    |
| Sound duration          | `alarmsDefaultSoundDurationSeconds`        | `soundDurationSeconds` (null)       |
| Fade-in seconds         | `alarmsDefaultSoundFadeInSeconds`          | `soundFadeInSeconds` (null)         |
| Volume override         | `notificationSoundVolume` (shared)         | `soundVolumeOverride` (null)        |
| Speak label aloud       | `alarmsDefaultSpeakLabel`                  | `speakLabel` (null = inherit)       |
| Max snoozes             | `alarmsDefaultMaxSnoozes`                  | `maxSnoozes` (null = inherit)       |
| Require math to dismiss | _(no global — per-alarm opt-in)_           | `requireMathToDismiss` (null = off) |

The `Alarm` type in [src/shared/types.ts](../../src/shared/types.ts) is the source of truth for the column shapes. The seven sound-config columns landed in schema v236; every existing alarm starts with all seven set to `NULL` and inherits the global defaults — no upgrade step required.

### Sound play mode, duration, and fade-in

When an alarm fires, three knobs together control how the sound is shaped:

- **Sound play mode** — `once` (default) plays the sound file one time and stops. `loop` plays the sound on a loop. **Loop applies to both ring shapes**: a `modal` alarm with `loop` keeps looping while the modal is open; a `banner` (toast) alarm with `loop` also keeps looping in the background while the toast is visible. This matters for assertive reminders ("I want it to keep nagging me until I dismiss it") even if you've picked the lighter-weight banner assertiveness.
- **Sound duration** — caps how long the sound is allowed to ring for. Presets are 5s / 10s / 30s / 1m / 2m / 5m / Forever. Once mode usually doesn't need this — the sound stops on its own when the file ends. Loop mode does: without a duration cap, a `loop` alarm rings until you click Snooze or Dismiss (or hit the global Max Ringing Time cap). Setting `Forever` makes the duration cap a no-op so the modal's Max Ringing Time is the only stop signal.
- **Fade-in seconds** — ramps the volume up over N seconds at the start of the ring. `0` (default) starts at full volume. `2` is a quick crescendo, `5` is a noticeable rise, `15`/`30`/`60` are a gentle wake. Omniscio steps `audio.volume` in software, so any sound file works — no special encoding required. The fade-in starts from silence and ends at the resolved volume (per-alarm volume override or global `notificationSoundVolume`).

These three combine for a few common shapes:

| Goal                                  | play mode | duration | fade-in |
| ------------------------------------- | --------- | -------- | ------- |
| Phone-style "loop until I dismiss it" | `loop`    | Forever  | 0       |
| Loud-but-quick (5s loop, no fade)     | `loop`    | 5s       | 0       |
| Gentle wake (60s ramp + loop forever) | `loop`    | Forever  | 60      |
| Default                               | `once`    | 30s      | 0       |
| Quick crescendo                       | `once`    | 30s      | 2 or 5  |

Both controls live in Pane 3 → **Advanced** for the per-alarm override and in Settings → Notifications → Alarms for the global defaults.

### Volume override per alarm

The global notification volume slider (Settings → Notifications) sets the system-wide audible level for all of Omniscio's notification sounds. Individual alarms can override that for ring time only — useful for a "wake me up loudly" alarm in a household where you've otherwise turned Omniscio's volume down.

The per-alarm control is a preset dropdown (10% / 25% / 50% / 75% / 100%) plus "Use default (global)" for the inherit case. It only affects this one alarm's ring — chat notifications, toast pops, and other Omniscio sounds still ride the global slider.

The volume value flows through the same `playOneShot()` audio path the rest of Omniscio uses, so the resolved volume applies to the fade-in's end point too: a 100% override with a 5-second fade-in ramps from silence to 100%, regardless of where the global slider is set.

### Speak label aloud

Turn on **Speak label aloud** (per alarm or globally) and Omniscio speaks the alarm's `label` text via the browser's `SpeechSynthesisUtterance` API at fire time, on top of whatever sound the alarm plays. The voice is the OS-default voice that the renderer picks up — no Omniscio-side voice configuration is exposed today.

This is handy for two cases:

1. **Accessibility** — a user who can't read the modal at fire time still gets the alarm's purpose announced.
2. **Across-the-room reminders** — an alarm called "take meds at 8 PM" speaking that phrase from a desk in another room is more useful than a generic ring.

The label-to-speech happens on the renderer side (the same process that plays the sound). The main process does not invoke any TTS. Speaking is **additive** to the sound — picking a silent banner with `speakLabel: true` is the closest Omniscio has to a "voice-only" alarm, but the chosen sound (if any) still plays alongside.

### Where to look when it goes wrong

- **An alarm didn't ring at all** — first check Settings → Notifications → Alarms → Enable Alarms is on, then check the alarm's `enabled` flag in Pane 3. If both are on, check whether Focus Mode is active and the alarm doesn't have Pierce Focus Mode set — that's the most common silent suppression. The scheduler tick logs live in `<userData>/logs/main.log` filterable by `[AlarmService]`.
- **The ring modal opened but no sound played** — likely the alarm's assertiveness resolved to `silent-banner`, or the system notification volume in Settings is at 0. Try the **Play** preview button next to the sound picker; if the preview is silent too, the issue is the system-volume side, not the alarm.
- **"Could not parse" hint on every input** — likely the NLP daily cost cap has been hit. Check today's spend in the Omniscio cost dashboard (look for `source = alarm-nlp`). Raise the cap in Settings → Notifications → Alarms if needed.
- **Missed alarms never appear in the inbox** — push channel may have been disabled. The `ALARM_FIRED` and `ALARMS_CHANGED` channels must be wired through the registry and web-access push filter — see [src/shared/integration-registry.ts](../../src/shared/integration-registry.ts) for the alarms entry, and [src/main/services/web/web-access-push-filter.ts](../../src/main/services/web/web-access-push-filter.ts) for the channel allow-list.
- **A recurring alarm fires at the wrong time after DST** — the `fireTimeLocal` is local wall-clock time, not UTC, so a 7 AM alarm rings at 7 AM both before and after the clocks change. If you're seeing drift across DST, file a bug.

## Related

- Cron jobs — the time-triggered automation surface (runs scripts/recipes/sessions); compare with alarms (rings a reminder, no agent spawned).
- [Focus Mode](focus-mode.md) — the batching gate that alarms route through; per-alarm Pierce Focus Mode is the bypass.
- [Mobile Access](mobile-remote-access.md) — how the inbox `alarm-fired` row reaches your phone.
- [Chat attachments](chat-attachments.md) — unrelated, but a useful cross-reference for the "where do features live in settings vs. virtual projects" pattern.
- [Alarms (part 2)](alarms-part-2.md) — the rest of this page: the advanced scheduling behaviour and edge cases (how many times an alarm may be snoozed, the math challenge that gates Dismiss, Test fire, fire limits, a fire you were away for, the mobile versus desktop split, the natural-language parse cost cap) and the CLI routes an agent uses to drive alarms.
