---
title: Alarms (part 2)
---

# Alarms (part 2)

## What it is

This is part 2 of the [Alarms](alarms.md) page. It carries the advanced scheduling behaviour and the edge cases: how many times an alarm may be snoozed, the math challenge that gates Dismiss, the Test fire preview, fire limits, what happens to 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.

## Where to find it

Alarms live in the Alarms virtual project in the Omniscio sidebar, with the global defaults at Settings → Notifications → Alarms; [Alarms](alarms.md) walks that surface end to end, including the header quick-add, folders, recurrence and sounds. Everything on this page is reached from the same Alarms detail editor (Pane 3), the Settings → Notifications → Alarms section, or the local CLI control server on 127.0.0.1:19519.

## How it behaves

### Max snoozes per alarm

The **Max snoozes** setting caps how many times an alarm can be snoozed during a single ring chain — a defense against the human reflex of mashing Snooze, Snooze, Snooze and not actually waking up.

- **Unlimited** (`-1`, default) — Phone-alarm convention. The Snooze button is always visible.
- **Dismiss-only** (`0`) — The Snooze button is hidden from the very first ring; only Dismiss is available.
- **N** (any positive integer; the UI offers 1 / 2 / 3 / 5 / 10) — N snoozes allowed. After the Nth snooze the Snooze button disappears on the next fire of that chain and only Dismiss is available.

The count is **per ring chain, not per alarm-lifetime**. A recurring 7 AM alarm with `maxSnoozes: 3` gives you 3 snoozes today, then resets to 3 fresh snoozes tomorrow morning. The count is held in memory in the alarm service and is cleared on Dismiss (or when Omniscio restarts) — it isn't persisted, so the user never lands in a state where Dismiss-then-restart silently still blocks Snooze.

The Snooze button gates on the cap in two places: the ring modal/banner hides the button once the cap is reached, and the backend `snooze()` IPC handler also early-returns past the cap (belt-and-suspenders — a stale UI state can't sneak past the limit).

### Require math to dismiss (anti-snooze)

For alarms where you really need to wake up (e.g. early-morning standup, kids' school run), the per-alarm **Require math to dismiss** toggle gates the Dismiss button behind a small arithmetic challenge. When the alarm fires, the modal/banner shows a problem like `7 + 8 = ?` with a text input; you have to type the correct answer before Dismiss becomes clickable. Snooze is unaffected — you can still snooze without solving.

The math is intentionally small (two-digit addition / subtraction, no multiplication or division) — it isn't meant to be a brain teaser, it's meant to be enough of a friction nudge that you have to be actually awake to clear it.

This is **per-alarm opt-in only** — there is no global `alarmsDefaultRequireMathToDismiss` setting. A global default that gates dismiss across every alarm would be hard to recover from if forgotten (imagine a poorly-aimed click on a settings toggle blocking every alarm's dismiss tomorrow morning). The per-alarm scope keeps the risk surface narrow and the user's intent explicit.

The math gate is **forced off when test-firing** the alarm (see "Test fire" below) — you pressed a preview button, you didn't consent to being gated behind arithmetic just to clear the preview.

### Test fire — preview an alarm without scheduling it

The alarm detail editor (Pane 3) has a **Test fire** button in the footer, alongside Delete. Click it and the alarm fires immediately — the modal opens, the sound plays, fade-in ramps, the label is spoken if you've turned that on, the Snooze and Dismiss buttons render — but **the alarm's schedule is untouched**. The fire isn't counted toward `fireLimit`, `next_fire_at` isn't advanced, and no `alarm-fired` inbox row is created.

This is the right tool for:

- **Picking a sound** — preview the sound at the actual fade-in / volume / loop config you've set, not just the picker's "Play" button which uses the global volume and no fade-in.
- **Sanity-checking loop + duration** — confirm a 5-second loop really does what you expect before going to bed.
- **Hearing the spoken label** — verify the OS voice pronounces your alarm label correctly.
- **Testing the math gate** — see how hard the math is and whether you can solve it half-asleep, without waiting for 6 AM.

A few mechanical things to know:

- **Snooze and Dismiss in the preview are local-only.** They close the modal/toast like a real fire would, but they don't fire any backend handler — the alarm service never set `ringingId` for the preview, so a real `snooze(id)` IPC against the preview's `alarmId` is a no-op. This is what keeps the schedule untouched.
- **The `requireMathToDismiss` toggle is forced off for previews**, even if the alarm has it set. Math-gated alarms would otherwise gate the preview behind arithmetic the user didn't sign up for when they clicked Test fire.
- **Focus Mode is bypassed.** A normal fire routes through the Focus Mode evaluate() gate (suppress / batch / fire-immediately); a test fire skips that and fires straight to the UI. You asked to preview the alarm — Focus Mode shouldn't decide otherwise.
- **Bring Omniscio to foreground is also bypassed** for previews. The window is already focused (that's where the click came from), so re-foregrounding it is redundant and would steal focus from any other Omniscio pane the user was using.

Implementation: `alarm-service.testFire(id)` in [src/main/services/alarm/alarm-service.ts](../../src/main/services/alarm/alarm-service.ts) emits the same `ALARM_FIRED` payload that a real fire would, with `test: true` and `requireMath: false` spread over the top. The renderer's `useIpcListener(IPC.ALARM_FIRED)` consumes both real and test fires identically; the `test: true` flag tells the alarms store to skip pushing onto the firedAlarms slice (avoiding a phantom inbox row).

### Fire-limit and "fire forever"

Recurring alarms default to "fire forever" — they keep re-arming until you disable or delete them. If you want a recurring alarm to stop after N fires (e.g. "stand-up at 9am, but only this week — 5 fires then stop"), there's a **Fire forever** toggle in the editor's Advanced section. Turn it off and a number input appears for `fireLimit`. The alarm's `fireCount` increments on each fire; when `fireCount >= fireLimit` the alarm auto-disables.

This is independent of the recurrence shape — you can pair "Daily" with `fireLimit: 5` to get five consecutive days then auto-stop.

### What happens if you're away

If the Omniscio window is closed when an alarm fires, or if the ring modal times out at the max-ringing cap, or if the modal was dismissed without picking Snooze/Dismiss, the alarm surfaces as an **inbox row** in Omniscio's inbox. The row shows an amber AlarmClock icon, the alarm label, a relative timestamp ("3m ago"), and **Snooze 9m** + **Dismiss** buttons inline — so you can act on the missed fire without re-opening the ring modal.

Middle-click on the row also dismisses it (matches Omniscio's universal inbox-card contract). The row routes through the canonical inbox dispatcher (`resolveInboxAction('alarm-fired', alarmId, …)`) which captures the next nav target before the IPC await, so dismissing auto-advances your inbox cursor to the next item like every other inbox row.

The inbox surface is a generic inbox row plus the per-alarm [AlarmDetailPane.tsx](../../src/renderer/src/features/inbox/detail/AlarmDetailPane.tsx); the handler mapping lives in [src/renderer/src/stores/inbox-handlers/alarm-fired-handlers.ts](../../src/renderer/src/stores/inbox-handlers/alarm-fired-handlers.ts).

### Mobile vs. desktop

The split is "manage on desktop, react anywhere":

- **Desktop only**: the Alarms virtual project's Pane 2/3 (natural-language input, edit form, sound picker with preview, all per-alarm overrides). Creating and editing alarms is a desktop affordance.
- **Both desktop and mobile**: the inbox `alarm-fired` row. If you're triaging your inbox from your phone via Tailscale Funnel or LAN access ([mobile-remote-access.md](mobile-remote-access.md)) and a missed alarm landed there, you'll see it with the Snooze / Dismiss buttons and can act on it from the phone.

Mobile reach for the inbox row depends on the `ALARM_FIRED` and `ALARMS_CHANGED` push channels being in `INBOX_ALLOWED_CHANNELS` in [src/main/services/web/web-access-push-filter.ts](../../src/main/services/web/web-access-push-filter.ts) — they are, so missed-alarm rows reach the phone the same way SMS rows and other inbox surfaces do.

The full-screen ring modal is desktop only by design — a phone in your pocket can't reasonably present a full-screen modal Omniscio controls. The inbox row is the mobile-side surface for that case.

### NLP cost cap

The Haiku fallback for natural-language parsing costs a small amount of money per call. To keep Omniscio from accidentally billing $100 because some pasted bug report tripped the parser, the daily spend is capped.

Settings → Notifications → Alarms → **NLP Daily Cost Cap ($)** controls the cap. Default is **$0.10**, range is $0.05 to $5.00 (one-cent steps). At Haiku's pricing this works out to roughly a few hundred parser calls per day at the default — comfortable for normal use, hard to overshoot by accident.

When the cap is hit, the parser short-circuits before calling Haiku. The chrono.js fast-path keeps working (it's free), so simple shapes like `at 8pm` — and compact shorthand like `805p`, which a normalizer rewrites to `8:05 PM` before chrono sees it — still parse. Ambiguous inputs fail soft with the same inline hint the user sees on a chrono miss — "Could not parse — try `at 8pm`…". The cap resets at midnight local time.

Cost tracking goes through the standard `api_cost_log` table (source label `alarm-nlp`) via `trackApiCost()` in [src/main/services/api-cost-tracker.ts](../../src/main/services/api-cost-tracker.ts) so the cost dashboard shows alarm parsing spend alongside everything else.

## For agents

### CLI access

The alarms subsystem is accessible via the CLI Control REST API (`http://127.0.0.1:19519`). This allows external AI agents (like Claude Code) to create, list, update, delete, toggle, and NLP-parse alarms programmatically.

#### Routes

| Method | Path                     | Purpose                                                                       |
| ------ | ------------------------ | ----------------------------------------------------------------------------- |
| `GET`  | `/alarms/list`           | List all alarms. Optional query params: `?folderId=<id>`, `?enabledOnly=true` |
| `POST` | `/alarms/create`         | Create an alarm (body = `alarmCreateSchema`)                                  |
| `POST` | `/alarms/update`         | Update an alarm (body = `alarmUpdateSchema`: `{ id, patch }`)                 |
| `POST` | `/alarms/delete`         | Soft-delete (body = `{ id }`)                                                 |
| `POST` | `/alarms/toggle`         | Enable/disable (body = `{ id, enabled }`)                                     |
| `POST` | `/alarms/parse`          | NLP parse (body = `{ text }`). Returns structured alarm fields or null        |
| `GET`  | `/alarms/folders/list`   | List all folders                                                              |
| `POST` | `/alarms/folders/create` | Create a folder (body = `{ name }`)                                           |

All routes require Bearer token auth. Mutations share the 10/minute rate limit; reads share the 60/minute budget.

Mutations apply immediately — no approval gate. The scheduler picks up changes on its next 30-second tick.

For full endpoint documentation including request/response examples, see the CLI skill doc at `.claude/skills/omniscio-control/alarms.md`.

## Related

This page is the companion to [Alarms](alarms.md), which covers the feature itself: what it is, where to find it, how to create an alarm, recurrence, folders, sounds and the ring behaviour. For the time-triggered automation surface that runs code rather than ringing at you, see Cron jobs; for the batching gate alarms route through, see [Focus Mode](focus-mode.md); and for how a missed fire reaches your phone, see [Mobile Access](mobile-remote-access.md).
