---
title: Pomodoro
---

# Pomodoro

## What it is

A built-in pomodoro timer that runs **focus blocks** (default 25 min) followed
by **breaks** (5 min short, 15 min long every 4th cycle). You pick a **preset**
— a named bundle of those durations plus alarm overrides and an optional
default project — click Start, and Omniscio drives a single
active run from focus → short-break → focus → … → long-break → … → complete,
chiming at each phase change.

The feature is paired with the Alarms virtual project in the Omniscio sidebar
group, accessed via the **Pomodoro** tab of the Alarms project's pane-2 tab
control.

> When the (in-development) **Time Tracker** is enabled, each completed focus
> session also appears on its unified timeline as a `focus` block — a one-way
> projection that leaves the Pomodoro engine untouched. See
> [time-tracker.md](time-tracker.md).

Two side-projects on top of "just start a timer":

- **Schedules** auto-start a preset at a recurring wall-clock time
  ("every weekday at 9 AM, start the Deep Work preset").
- **Stats** roll up the focus seconds into Today / This Week / by
  project, with a "planned vs actual" today block.

The whole feature is local-only — nothing leaves your machine, no AI is
involved at any layer, no cost. The only cross-feature integration is with
Alarms (shared sound picker, focus-mode pierce, foregrounding) and with
Projects (the optional project attribution).

## Where to find it

The Pomodoro feature is rendered inside the **Alarms virtual project**, not
as its own sidebar row. Open the Alarms virtual project in the Omniscio sidebar
group, and at the top of pane 2 there is a tab control with **Alarms** /
**Pomodoro** tabs — click **Pomodoro** to switch the rest of the project
panel to the pomodoro UI.

Inside the Pomodoro tab:

- **Pane 2 (sidebar)** — the **preset list** with a per-row Play button for
  quick-start. Header has three icon toggles: a calendar-clock icon (Schedule
  editor), a history icon (History view), and a gear (Settings panel).
  Clicking a preset row selects it and loads its editor into pane 3; clicking
  Play on a row starts a run immediately using that preset's default project
  (if set).
- **Pane 3 (detail)** — when a preset is selected, the **preset editor**: name,
  focus minutes, short-break minutes, long-break minutes, cycles-until-long-break,
  total cycles (or "unlimited"), default project (dropdown of all your Omniscio
  projects + "Unassigned"), and an Advanced section with the per-preset alarm
  overrides (sound, modal vs. banner assertiveness, bring-to-foreground,
  pierce-focus-mode). When the **Schedule** toggle is on, pane 3 swaps to the
  Schedule editor (list of recurring rows, each with day-of-week chips +
  HH:MM time + preset picker + project picker + label + enabled toggle); when
  the **History** toggle is on, pane 3 swaps to the History view (today /
  week / by project rollup + a planned-vs-actual block). When a run is
  active, pane 3 also shows the **Session view** above the editor: big
  countdown, phase label, **Pause / Resume**, **Skip Phase**, and **Stop**
  buttons. Skip Phase advances immediately to the next phase in the ladder
  (focus → break, break → focus) without waiting for the countdown to reach
  zero; it counts the phase as completed (focus seconds for the phase are
  added even though you skipped early).

The "Pomodoro is a tab of Alarms" layout is deliberate — it keeps both
notification-surface features (timers + reminders) under one cognitive
heading and lets them share the same sound picker, foregrounding rules, and
focus-mode interplay without duplicating settings UI.

## How it behaves

### How to start a run

1. **Open the Alarms virtual project** in the Omniscio sidebar.
2. **Click the Pomodoro tab** at the top of pane 2.
3. **Click the Play button** on any preset row in pane 2 (or select the preset
   and click Start in pane 3). The run begins immediately — pane 3 swaps to
   show the **Session view** with a live countdown.

If the preset has a **Default project** set, the run is attributed to that
project automatically (stats roll up under it). You can clear the default
project on the preset to be prompted/unassigned, or override per-run by
starting from a different preset.

### Phase transitions

The active run advances itself. A 1-second background tick checks whether the
current phase has ended; when it has, Omniscio transitions atomically and emits a
push to the renderer, which:

1. **Plays the chime** for the resolved sound (per-preset override or the
   Alarms-default sound).
2. **Shows a transition toast** with the new phase label and remaining time.
3. **Brings the Omniscio window to the foreground** only if both layers say so —
   see "Foregrounding" below; the default is **no** for phase changes.

The phase ladder is:

- **focus → short-break** when `cyclesCompleted % cyclesUntilLongBreak !== 0`.
- **focus → long-break** when `cyclesCompleted % cyclesUntilLongBreak === 0`.
- **break → focus** always (the cycle counter advances on the focus → break
  edge, so the break never re-counts).
- **focus → complete** when `cyclesCompleted >= totalCycles` (the preset's
  `totalCycles` is nullable — `null` means unlimited, so the run only ends
  when you click End).

#### Pause / resume / skip / stop

The Session view has four controls below the countdown:

- **Pause** freezes the countdown without losing time. Pause records the
  moment you paused in memory; **Resume** extends `phase_ends_at` by the
  pause duration and persists the new value, so the remaining time after
  resume matches what you saw when you paused.
- **Skip Phase** advances immediately to the next phase (no confirmation —
  it's a discrete state transition). The skipped phase is counted as
  completed; its focus-seconds (if it was a focus phase) are still added to
  the stats rollup.
- **Stop** ends the entire run after a confirm dialog (to avoid accidental
  loss of cycles).

**Caveat**: pause state is in-memory. If you close Omniscio while a run is paused,
the run is reaped as `crashed` on the next boot — pausing isn't crash-safe.
This trade-off avoids a `paused_at` DB column for what is normally a few
minutes of pause; the alternative is to End the run before closing Omniscio.

### Foregrounding (an important deliberate split)

Three settings interact at phase-change time:

| Setting                                | Default   | What it does                                                           |
| -------------------------------------- | --------- | ---------------------------------------------------------------------- |
| `pomodoroPhaseForeground`              | **false** | Whether ANY phase change can pull Omniscio's window to the foreground. |
| `alarmsBringToForeground`              | true      | The Alarms feature's foregrounding default (NOT consulted here).       |
| per-preset `bringToForegroundOverride` | null      | When non-null, overrides `pomodoroPhaseForeground` for THIS preset.    |

The deliberate split: a 5-minute focus block ending should chime + show a
tray badge but **not** yank you out of whatever you were doing — so the
pomodoro default is `false`. The alarm default is `true` (an alarm exists
precisely to interrupt). Per-preset `bringToForegroundOverride` is the
escape hatch when you want a particular preset to pierce.

If you change `alarmsBringToForeground` you do **not** change pomodoro
behavior. They're independent, and the Settings UI labels both clearly.

### Focus Mode integration (auto-silence during focus blocks)

A pomodoro run can automatically enable [Focus Mode](focus-mode.md) — the
toolbar bell pill that batches inbox notifications — while you're in a focus
block, then release it during breaks. The intent is to pair "I'm starting a
focus timer" with "stop pinging me about every push notification" without
forcing you to remember to click the bell manually.

#### The setting

**Settings → Notifications → Alarms → Auto-Enable Focus Mode During Focus Blocks**
(`pomodoroAutoEnableFocusModeDefault`, default **on**) is the global default.
Every per-preset **Auto-enable Focus Mode** override (Advanced section of
the preset editor) is a tri-state:

| Override value   | What it means                                                                        |
| ---------------- | ------------------------------------------------------------------------------------ |
| `null` (default) | Inherit the global `pomodoroAutoEnableFocusModeDefault`.                             |
| `true`           | This preset **always** auto-enables Focus Mode, even when the global default is off. |
| `false`          | This preset **never** touches Focus Mode, even when the global default is on.        |

#### When Focus Mode flips during a run

When the run is in a state that warrants silence — actively in a focus
phase — Focus Mode is **on**. Any other state (break, paused, ended) drops it.
Concretely:

- **Start a run** → if a focus phase is the opening phase (it always is),
  Focus Mode is enabled.
- **focus → short-break** or **focus → long-break** → Focus Mode is disabled
  (you get your alerts back).
- **break → focus** (start of the next cycle) → Focus Mode re-enables.
- **Pause during a focus phase** → Focus Mode drops (pause is treated as a
  break — you're stepping away, so let notifications through).
- **Resume after a pause** → Focus Mode re-enables only if the run resumes
  into a focus phase.
- **End the run** (manually or by reaching `totalCycles`) → Focus Mode
  drops back to whatever it was before the run started.
- **Omniscio crashes / closes** → because Focus Mode state is settings-persisted
  and the run row is marked `crashed` on the next boot, there's no orphaned
  Focus Mode lock. If you had Focus Mode on for an unrelated reason before
  the run, that state is restored.

#### Manual-toggle protection

If you **click the bell icon manually** at any point during a managed run —
either turning Focus Mode on when the run had dropped it, or off when the
run had it enabled — Pomodoro **stops managing Focus Mode** for the rest of
that run. The reasoning: your manual click is a deliberate statement
("actually, I want it the other way right now"), and the timer shouldn't
override you on the next phase transition. The block lasts only for the
current run; the next run starts fresh.

This is wired through a `FOCUS_MODE_MANUAL_TOGGLE` push that the Pomodoro
service listens to. The bell-icon click is the single chokepoint
([src/main/ipc/focus-mode-handlers.ts](../../src/main/ipc/focus-mode-handlers.ts))
— programmatic enable/disable from the Pomodoro service itself does NOT
emit the push, so the service never accidentally locks itself out.

#### Per-preset escape hatches

The per-preset **Auto-enable Focus Mode** override is the way to opt a
single preset out (or in) without flipping the global default. Common
shapes:

- **"Deep Work" preset** — override = `true`. Always silences, even if
  you've globally disabled the integration.
- **"Quick admin pomodoro" preset** — override = `false`. Stays
  notification-permissive even when you've turned the integration on
  globally.
- **All other presets** — override = `null`. Follows the global default.

### Schedules — auto-start at a wall-clock time

A schedule is a recurring row that calls Start for you. Open the **Schedule
editor** from pane 2's calendar-clock icon, and add rows of:

- **Day of week** — chips for Sun / Mon / … / Sat; pick one day per row.
- **Start time** — `HH:MM` in your local timezone.
- **Preset** — dropdown of all your presets.
- **Project** (optional) — overrides the preset's default project for this
  schedule. Empty = use the preset's default. Set explicitly to "Unassigned"
  to opt out per-schedule.
- **Label** (optional) — surfaces in the run row and stats so you can tell
  apart "Mon 9am — Deep Work" from "Wed 9am — Writing".
- **Enabled** — toggle to mute the schedule without deleting it.

#### How the resolver works

A 1Hz background tick (the same one that drives phase transitions) evaluates
every enabled schedule for the current minute. For each match:

1. **30 seconds before the scheduled minute**, Omniscio shows a **pre-start toast**
   ("Pomodoro starting in 30s — Deep Work") so you can dismiss it or just be
   ready. Toasts are per-`(schedule, minute)` so a re-render doesn't double up.
2. **At the scheduled minute exactly**, Omniscio atomically claims the minute and
   calls Start. If another run is already active (you manually started one,
   or the previous schedule's run hasn't ended yet), the new schedule is
   skipped — manual starts always win, no displacement.
3. **Idempotency**: a DB column `last_fired_minute` on the schedule row stores
   the last minute it fired. The atomic SQL UPDATE that claims a minute also
   bumps this column, so two concurrent ticks (or a restart mid-minute) can
   never both fire the same schedule. Lose-on-restart never happens — if Omniscio
   crashed at 8:59:30, the resolver after restart at 9:00:05 still fires the
   9 AM schedule, because `last_fired_minute < 540` (9 AM in minute-of-day).
   Resume in the same minute = idempotent skip.

#### The master kill switch

**Settings → Notifications → Alarms → Pomodoro: Enable schedules** (`pomodoroScheduleEnabled`,
default **on**) controls whether the resolver runs at all. When off, no
pre-start toasts, no auto-starts, no schedule-related DB reads — but
manually-started runs continue ticking normally. This is the
"silence the schedule" toggle when you want to keep your preset library and
schedule rows intact but stop the auto-fires (a vacation week, a deadline
crunch, etc).

Individual schedule rows also have their own `enabled` toggle so you can
silence just one without flipping the master.

### Stats — Today, This Week, by Project

Open the **History view** from pane 2's history icon. Three rollups:

- **Today** — sum of `focus_seconds_total` across runs started today, in your
  local timezone (resets at local midnight). The engine increments
  `focus_seconds_total` on every focus-phase completion, so a run you stopped
  mid-break still contributes the focus seconds from the phases that finished
  before you stopped. Phases skipped via **Skip Phase** also count as
  completed.
- **This Week** — same sum since the most recent Monday 00:00 local.
- **By Project** — table with rows for each `projectId` that has any focus
  seconds this week, sorted DESC by weekly total. Rows include today and
  weekly columns. Runs with `projectId: null` aggregate under an
  **"Unassigned"** bucket (never silently dropped — see invariant I6 in the
  contract).

#### Planned vs actual

Above the rollups is a **"Today's planned vs actual"** block:

- **Planned** sums today's enabled schedules × their preset's `focusMinutes`.
  E.g. 4 schedules today at 25 min each = 100 planned focus minutes.
- **Actual** is the same number as the "Today" rollup (completed focus
  seconds, converted to minutes).

This is a rough target — the planned number is what you'd accomplish if you
ran every scheduled session to completion. Schedules that haven't fired yet
still count toward "planned"; if you delete or disable a schedule, it drops
out of planned immediately. Soft-deleted presets and disabled schedules are
both excluded.

### Presets — what you can customize

Each preset row in pane 2 corresponds to a row in `pomodoro_presets`. The
editor lets you set:

| Field                             | Default   | Notes                                                                   |
| --------------------------------- | --------- | ----------------------------------------------------------------------- |
| Name                              | "Default" | Free text. Shown in the sidebar list, schedule rows, history.           |
| Focus minutes                     | 25        | The focus phase duration.                                               |
| Short-break minutes               | 5         | The short-break phase duration.                                         |
| Long-break minutes                | 15        | The long-break phase duration (every Nth cycle).                        |
| Cycles until long break           | 4         | After this many focus-block completions, the next break is long.        |
| Total cycles                      | null      | `null` = unlimited (only ends on End). Number = run completes at N.     |
| Default project                   | null      | Pre-fills the run's `projectId` when Start is clicked from this preset. |
| Auto-start breaks                 | true      | Editor toggle, currently inert — see "Known gap" below.                 |
| Auto-start focus                  | false     | Editor toggle, currently inert — see "Known gap" below.                 |
| Sound override (Advanced)         | null      | Per-preset chime; `null` = inherit `alarmsDefaultSound`.                |
| Assertiveness (Advanced)          | null      | `modal` / `banner` / `silent-banner`; `null` = inherit.                 |
| Bring-to-foreground (Advanced)    | null      | `null` = inherit `pomodoroPhaseForeground` (default false).             |
| Pierce focus-mode (Advanced)      | null      | `null` = inherit `alarmsPierceFocusMode` (default false).               |
| Auto-enable Focus Mode (Advanced) | null      | `null` = inherit `pomodoroAutoEnableFocusModeDefault` (default on).     |

The `null = inherit` pattern means a single change at Settings → Notifications → Alarms
propagates to every preset that didn't override, while presets that did
override are immune (intentionally so — you set them up that way).

Presets soft-delete (`is_deleted = 1`) when removed, so any run rows that
reference them in stats keep working. A soft-deleted preset is silently
dropped from the sidebar, the schedule's preset dropdown, and the planned
rollup.

#### Known gap — Auto-start toggles are inert

The **Auto-start breaks** and **Auto-start focus** toggles render in the
preset editor (and persist to `pomodoro_presets.auto_start_breaks` /
`auto_start_focus`), but the engine's `transition()` in
[src/main/services/pomodoro-service.ts](../../src/main/services/pomodoro-service.ts)
**doesn't read them** — every phase change unconditionally starts the next
phase. In other words, both toggles behave as if they were always on for
breaks and always on for focus. Flipping either toggle changes the stored
value but produces no observable behavior. This is a UI/engine wiring gap,
not a feature decision — track it as such if you see it.

### Settings

All Pomodoro settings live under **Settings → Notifications → Alarms** (the section is
shared with Alarms; the **Pomodoro** subsection groups its own toggles).
Three settings are exclusive to Pomodoro:

- **`pomodoroScheduleEnabled`** (default **on**) — the master kill switch
  described above. Hides nothing in the UI; only stops the resolver.
- **`pomodoroPhaseForeground`** (default **off**) — whether phase-change
  transitions can pull Omniscio to the foreground. Default off because a focus
  block ending shouldn't yank you out of your deep-work window.
- **`pomodoroAutoEnableFocusModeDefault`** (default **on**) — whether a
  focus block automatically enables Focus Mode for the duration of that
  phase (releases on breaks / pause / end). See "Focus Mode integration"
  above for the per-run lifecycle and manual-toggle protection.

Three settings are inherited from Alarms when a preset has the matching
override set to `null`:

- `alarmsDefaultSound` → preset's `soundId`
- `alarmsAssertiveness` → preset's `assertivenessOverride`
- `alarmsPierceFocusMode` → preset's `pierceFocusModeOverride`

(`alarmsBringToForeground` is **NOT** inherited — see "Foregrounding" above.)

### What it deliberately doesn't do

- **No cross-device sync.** Stats and runs are local SQLite. If you run Omniscio
  on two machines, they each have their own pomodoro history.
- **No automatic break enforcement.** If you skip a break by manually ending
  the run mid-break and starting a fresh focus, the engine doesn't try to
  stop you. The chime + tray badge are the nudge; the rest is on you.
- **No "tomato" gamification.** No streaks, no leaderboards, no badges. The
  stats are a usage record, not a habit-tracking layer.
- **No mobile pomodoro controls.** The Pomodoro tab is desktop-only. The
  mobile sidebar shows the Alarms virtual project, and Alarms surfaces are
  still reachable on phone — but starting / pausing / configuring presets
  and schedules is desktop only.
- **No project picker per-run.** The Start button always uses the preset's
  default project (or no project, if unset). To change per-run project, edit
  the preset's default or set a schedule with an override.
- **No tray-icon countdown overlay.** The tray badge updates on phase
  changes, but the seconds count down only in the Session view inside Omniscio.

### Where to look when it goes wrong

- **A schedule didn't auto-start at the scheduled minute** — first check
  **Settings → Notifications → Alarms → Pomodoro: Enable schedules** is on (the master
  kill switch). Then check the schedule row's `enabled` toggle. Then check
  whether a different run was already active at the scheduled minute (manual
  starts win — schedules don't displace active runs). Finally check the
  schedule's day-of-week — Mon=1, …, Sun=0 in storage.
- **The pre-start toast fired but the auto-start didn't** — most likely a
  run was already active. The toast fires unconditionally in the 30s window;
  the actual auto-start checks `getActiveRun()` first.
- **A phase change played the wrong sound** — the resolution is per-preset
  `soundId` (non-null) → `alarmsDefaultSound` (Settings → Notifications → Alarms). If the
  preset's `soundId` is non-null, the global default is bypassed.
- **Omniscio pulled itself to the foreground when I didn't want it to** — check
  the preset's `bringToForegroundOverride`. If non-null, it's overriding
  `pomodoroPhaseForeground`. Set it back to `null` to inherit the (off)
  default.
- **Focus Mode didn't enable when my focus block started** — first check
  **Settings → Notifications → Alarms → Auto-Enable Focus Mode During Focus Blocks** is on.
  Then check the preset's per-preset **Auto-enable Focus Mode** override —
  if it's `false`, this preset is opted out. If you toggled the bell icon
  manually during the run, the service stops managing Focus Mode for the
  rest of that run (deliberate — your click wins); restart the run to
  re-enable management.
- **Focus Mode stayed on after my run ended** — only happens if you
  toggled the bell manually during the run (the service handed control
  back to you and didn't drop it on end). Click the bell to disable.
- **My stats look wrong** — the Today rollup uses your local timezone
  midnight; the Week rollup uses the most recent local Monday 00:00. If
  you've recently changed timezones, "today" can include yesterday's runs
  (or vice versa). The rollup queries are in
  [src/main/db/queries-pomodoro.ts](../../src/main/db/queries-pomodoro.ts) —
  search for `getPomodoroStats`.
- **An active run survived an Omniscio restart and now nothing can start** —
  this shouldn't happen because `reapStaleActiveRuns()` runs at service
  boot and marks any leftover `active`/`paused` rows as `end_reason =
'crashed'`. If it does, manually flipping the row's `ended_at` in the
  DB clears the lock, but file a bug — the reaper has a hole.

## For agents

### CLI access

**Pomodoro is fully reachable from the CLI control server** (`127.0.0.1:19519`),
so an external agent or script can drive a run, edit your presets and schedules,
and read your stats headlessly:

- **Runs** — `POST /pomodoro/runs/start`, `POST /pomodoro/runs/:id/pause`,
  `POST /pomodoro/runs/:id/resume`, `POST /pomodoro/runs/:id/skip-phase`,
  `POST /pomodoro/runs/:id/stop`, `GET /pomodoro/runs/active`,
  `GET /pomodoro/runs/history`.
- **Presets** — `GET` / `POST /pomodoro/presets`, `PATCH` / `DELETE /pomodoro/presets/:id`,
  `POST /pomodoro/presets/:id/set-default`.
- **Schedules** — `GET` / `POST /pomodoro/schedules`, `PATCH` / `DELETE /pomodoro/schedules/:id`,
  `POST /pomodoro/schedules/:id/toggle`.
- **Stats** — `GET /pomodoro/stats` (Today / This Week / by-Project).

Two of those are **DELETEs**, so an agent you point at this family can remove a
saved preset or schedule. The routes are registered unconditionally (pomodoro
has no unreleased-feature flag) and take the usual CLI bearer token.

The **desktop/mobile gap is in the UI, not the API**: the Pomodoro tab itself
does not render on a paired phone, as noted above — but the routes above answer
headlessly either way, and `GET /pomodoro/stats` is the supported way to read
those numbers rather than opening the local SQLite file.

### Architecture pointers (for code edits)

If you're editing the feature, read the contract first:
[.claude/memory/contracts/pomodoro-contract.md](../../.claude/memory/contracts/pomodoro-contract.md)
— it names the test that locks each invariant, the state machine, and the
"changing the engine" checklist.

The major files are:

- DB schema migration v230 in
  [src/main/db/database.ts](../../src/main/db/database.ts) — adds the
  `project_id` column to `pomodoro_runs`, the `default_project_id` column to
  `pomodoro_presets`, and the `pomodoro_schedules` table.
- DB queries in
  [src/main/db/queries-pomodoro.ts](../../src/main/db/queries-pomodoro.ts) —
  every CRUD operation, `claimScheduleMinute()` for the atomic firing gate,
  `getPomodoroStats()` for the rollups.
- Service in
  [src/main/services/pomodoro-service.ts](../../src/main/services/pomodoro-service.ts) —
  the 1Hz tick, phase transitions, schedule resolver, and the foregrounding
  resolution.
- IPC handlers in
  [src/main/ipc/pomodoro-handlers.ts](../../src/main/ipc/pomodoro-handlers.ts).
- Renderer store in
  [src/renderer/src/stores/pomodoro-store.ts](../../src/renderer/src/stores/pomodoro-store.ts) —
  caches presets, schedules, stats, active run; subscribes to pushes.
- Renderer UI under
  [src/renderer/src/features/alarms/](../../src/renderer/src/features/alarms/) —
  `PomodoroSidebar.tsx`, `PomodoroPresetEditor.tsx`,
  `PomodoroScheduleEditor.tsx`, `PomodoroSessionView.tsx`,
  `PomodoroHistoryView.tsx`, `PomodoroPhaseTransitionToast.tsx`,
  `PomodoroSchedulePrestartToast.tsx`.

## Related

[Alarms](alarms.md) is the paired feature in the same virtual project, sharing
the sound picker, the foregrounding helper, and the focus-mode pierce.
[Focus Mode](focus-mode.md) is the batching gate that pomodoro chimes route
through, and a preset's Pierce Focus Mode is the bypass around it. And
[Use Recipes](use-recipes.md) covers recipes, which run on agent time rather
than wall-clock time — if you want "every Monday at 9 AM, spawn an agent"
instead of "every Monday at 9 AM, start a focus timer", use a cron job, not a
pomodoro schedule.
