---
title: Tasks (markdown outliner, in development) (part 4)
---
# Tasks (markdown outliner, in development) (part 4)

## What it is

This is part 4 of the [Tasks (markdown outliner, in development)](tasks-v2.md) page. It carries the next stretch of the material on that page, moved here because a single page is capped at 40,000 characters.

## Where to find it

Reach this part through [Tasks (markdown outliner, in development)](tasks-v2.md) — it lists every part and explains where the feature lives in the product. Everything below is reached from the same place.

## How it behaves

Everything below is the behaviour, detail and edge cases that belong to this stretch of the Tasks (markdown outliner, in development) page.

### Deadline & snooze reminders

Tasks doesn't just show deadlines and snoozes on the row — it **tells you when they come
up**. A quiet background check (running whenever Tasks is on) raises **one Inbox alert** the
moment:

- a task's **deadline arrives** (its due date/time passes), or
- a **snoozed task comes back** (its snooze runs out).

Each alert shows the item and **where it lives in your project** — the project name on its own
line at the top, then any parent tasks stacked beneath it as an indented outline, so you see the
full path down to the item — plus two buttons: **Open in Tasks** (opens that task **in its own
list, highlighted and scrolled to the middle** — and on a phone it also switches you to the Tasks
screen) and **Archive task** (files the task away and clears the alert). **Snoozing the reminder
re-snoozes the whole _task_** until the time you pick (and clears the reminder), so it genuinely
defers the task instead of only hiding the notice — which is why it no longer just comes right
back. It fires **once** per
event (re-scheduling a deadline re-arms it for the new date), it's **free** (no AI), and a big
backlog of already-due / already-back items trickles in a few a minute rather than flooding you.
The reminders land in your Inbox's **Alerts** area with the project name shown inside. Mechanism

- invariants: `inbox-reminder` in [`.claude/memory/contracts/tasks-v2-contract.md`](/.claude/memory/contracts/tasks-v2-contract.md).

### Waiting on someone — the follow-up clock

Some tasks aren't blocked on _you_ — they're **waiting on someone else** (a reply, a delivery, a
sign-off). Tasks lets you mark that, then quietly reminds you to chase it so it doesn't slip:

- The **⏳ "waiting" toggle** sits with the row's other action icons on the **right** and appears
  when you **hover** the row, so a resting row stays clean — just its number + text (it's shown
  plainly on the focused row, and on the focused row on a phone). Click it to open a clean
  **centered popup** (the same style as the Snooze popup): type **who or what** you're waiting on — and the people and things you've **waited on before** drop down as a **clickable list** from the name box (it appears when you click into or type in the box, and floats over the popup instead of pushing it bigger) to fill it in — optionally type **when to be reminded to chase it** in plain language (**"in 2 days"**, **"next friday"**, or a quick preset — the same smart date box the due-date field uses, with a live preview of the exact date), and confirm — or just press **Enter**
  to mark it waiting without a name. Marking it starts a **follow-up timer at the reminder time you picked, or 2 days out by
  default** if you left it blank, saved on the task so it holds even if you never reopen it. The name's first word is **capitalized automatically** — like your task titles, "sarah" is saved as "Sarah". Once a
  task is waiting, the same ⏳ popup shows the current reminder and lets you edit who, **set a new
  reminder time** (which re-schedules the chase), or **Clear waiting** — and a time it can't read is
  refused with a small hint rather than saved wrong.
- **A waiting task tucks into a "Waiting" section — exactly like Snoozed.** The moment you mark it,
  the task leaves the main list and collects under a single collapsible **⏳ Waiting (N)** row
  pinned at the bottom (below the Snoozed section), so your list stays focused on what you can act
  on now. Open the section to see your waiting tasks **grouped under the person or thing** you're
  waiting on (see the next point), each rendered **just like a regular numbered task row** with a
  calm **waiting** chip (or a gentle amber **follow up** once the follow-up date passes — a nudge,
  not an alarm; amber "Needs You", never red) and a "+N sub-tasks" count. To drop one back into your
  list, open the row's **⋯ menu** and choose **Clear waiting** — the old always-on button is gone, so
  the rows stay calm and read like any other task. This works in your per-project lists **and** the
  **All Projects** view (one section per project); the **Today** plate keeps waiting tasks inline
  (they show the full `waiting · <who>` / `follow up · <who>` chip on the row). A task that's both
  snoozed and waiting shows under **Snoozed** only.
- **The section always groups by who — each person/thing a numbered top-level row.** Every person or
  thing you're waiting on becomes its own **numbered top-level row** (**1.** Dana, **2.** Alex) with a
  count and a collapse chevron, and the tasks you're waiting on nest **beneath it, numbered one level
  down** (**A.**, **B.**) — so you see who you're chasing at a glance. The person you most need to
  **chase** floats to the **top** (any overdue follow-up leads). This happens **even for a single
  person** — one numbered group, not a flat list; anything with **no name set** collects in a muted
  **"No one specified"** group at the bottom. Because the person is named at the top, the tasks
  beneath drop the redundant "· <who>" and just read **waiting** / **follow up**. Applies in your
  per-project lists and the All Projects view alike.
- You can also name **who** you're waiting on (and change the **follow-up date**) in the task's
  **details pane** — the same fields the ⏳ popup writes; leave the date empty for a calm reminder
  with no nudge.
- If that date passes while you're **not** looking at Tasks, a **one-time** inbox reminder appears,
  titled `<task> — follow up with <who>`, with **Open in Tasks** and **Archive**. It fires **once** per
  horizon — moving the follow-up date re-arms it — and, because
  Tasks works on your phone, the nudge reaches **mobile** too.
- Your **daily check-in** also learns who you're waiting on and who's overdue to chase, so it can
  gently remind you to follow up.

This mirrors the Tasks follow-up clock: a ~1-minute background scan (on whenever Tasks is on)
over waiting tasks whose `follow_up_at` horizon has passed — gated + fire-once via a
`follow_up_notified_at` stamp — persisting a **chase alert** (Open-in-Tasks / Archive buttons, no transient toast)
surfaced through the standard `ALERT_CREATED` push. Service:
`src/main/services/tasks/tasks-v2-followup-service.ts`.

### Always-on outline numbers

Every visible row carries an **outline number** — the same size and weight as your task text, with a
trailing period — just to the right of its checkbox: top level `1.` / `2.` / `3.`, children
`A.` / `B.`, grandchildren roman `i.` / `ii.`, great-grandchildren `a.` / `b.`, then the
marker style keeps cycling by depth (`1.` → `A.` → `i.` → `a.`, then repeats).
Each row shows only its own marker (a child reads `A`, not `2A`) — the indentation shows
which parent it belongs to, like a classic outline: **a sub-item is indented so its own marker
lines up under the start of its parent's text** (each nesting level steps in by the parent's
marker gutter, not a token amount). It is always on and follows whatever
sort/filter you're viewing (and a drilled-in view renumbers from `1`), so the number always
matches what's on screen. You see the numbers in **both** the default "All Projects" view
(where each project is numbered on its own, starting from `1`) and a single-project view.
The labels are computed purely from the visible rows (`outline-numbering.ts`) and rendered as
a **number the same size and weight as your task text, with a trailing period** (`1.` / `A.` / `i.`),
in a gutter kept _out_ of the task's own markdown — so a row whose text is itself a markdown
list never clashes with its number. The **checkbox sits to the left of the number**, and the
number sits on the row's first line beside the text (it never floats above the text like a
superscript); on a task that wraps onto several lines it stays beside that first line.

### Visual skin — Liquid Glass

Tasks renders in a **Liquid Glass** skin: frosted panels, soft refraction edges, and a restrained accent glow. It is **theme-aware and accent-derived** — the glass fills track the surface palette and the tint/glow track the user's accent (`--accent-rgb`), so it follows the active light/dark theme and chosen accent rather than a fixed dark/teal look. `backdrop-filter` blur runs on the **big panels only** (the list rail, the master-context card, the menus); the row "bubble" (fill + border + sheen, **no per-row blur**) rides **only on the active row** — the one you're focused on or have multi-selected — so the rest of the list renders flat and long lists stay smooth, and the blur drops on mobile and under reduced-motion.

The skin is **isolated to Tasks** — its own `--tv2-glass-*` CSS vars + `.tv2-glass-*` rules (`src/renderer/src/styles/globals.css`) and `TV2_*` class constants (`src/renderer/src/lib/styles.ts`). The shipped Tasks (v1) outliner, which shares the older `TASK_*` constants, is left untouched (the safe-change rule: never restyle v2 by editing a shared constant). **The only desktop right pane beside the list is the M4 Break-it-down interview** (`interviewTaskId`) — the list rail, header, and outliner all stay visible while it's open. The per-task **agent chat has no Tasks-tab pane**: it lives in the **Sessions tab** (see [Per-task agent chat](#per-task-agent-chat)); on a narrow screen an explicitly-opened chat still takes over the full screen. Context attachments still live in the modal context pane. Full styling invariants are in the contract (`.claude/memory/contracts/tasks-v2-contract.md` → "Liquid Glass styling").

## For agents

### Data model — the `tasks_v2` table

Created by migration `src/main/db/migrations/20260601091339-tasks-v2-table.ts`. Columns:

| Column                      | Type                            | Notes                                                                                                                                                                                                                                                                                                                               |
| --------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                        | TEXT PK                         | `randomUUID()`                                                                                                                                                                                                                                                                                                                      |
| `parent_id`                 | TEXT, self-FK                   | NULL = top-level row                                                                                                                                                                                                                                                                                                                |
| `sort_order`                | INTEGER                         | fractional, 1024-spaced (`SORT_GAP`); midpoint inserts; auto-renormalized when a gap collapses below `RENORMALIZE_THRESHOLD = 2`                                                                                                                                                                                                    |
| `text`                      | TEXT                            | the markdown source (≤ 10 000 chars, Zod-enforced)                                                                                                                                                                                                                                                                                  |
| `done`                      | INTEGER                         | 0/1                                                                                                                                                                                                                                                                                                                                 |
| `due_at`                    | TEXT                            | ISO timestamp, nullable — ALWAYS a `…Z` UTC instant. Writes accept a zoneless local wall clock / an offset / `Z` and normalize (see [Setting a time on a task](#setting-a-time-on-a-task-dueat-snoozeduntil-startat-waitingsince-followupat)); the deadline sweep compares this column LEXICALLY, so a non-`Z` value would break it |
| `snoozed_until`             | TEXT                            | ISO timestamp, nullable — same `…Z` normalization as `due_at`                                                                                                                                                                                                                                                                       |
| `snooze_history_json`       | TEXT                            | JSON array of `{ at, until }` — the durable per-task snooze log behind the "came back" alarm-clock icon + ×N count + hover; NULL = never snoozed; server-maintained (not settable via create/update); capped at 25 (migration `20260704161842-…`)                                                                                   |
| `due_notified_at`           | TEXT                            | reminder bookkeeping, nullable                                                                                                                                                                                                                                                                                                      |
| `waiting_since`             | TEXT                            | ISO timestamp — set when the task is marked "waiting on someone"; NULL = not waiting (the derived waiting state) (migration `20260713224000-…`)                                                                                                                                                                                     |
| `waiting_on_who`            | TEXT                            | free text — who/what is blocking (e.g. "Sarah"), nullable                                                                                                                                                                                                                                                                           |
| `follow_up_at`              | TEXT                            | ISO "nudge horizon" — when it passes the row flips to "chase" AND the follow-up clock fires; nullable (empty = calm reminder only)                                                                                                                                                                                                  |
| `follow_up_notified_at`     | TEXT                            | fire-once stamp for the follow-up clock; cleared on a `follow_up_at` change (the `updateTaskV2` re-arm) to nudge again; nullable                                                                                                                                                                                                    |
| `metadata_json`             | TEXT                            | arbitrary JSON object (≤ 50 000 chars serialized); `ctx` + `tags` flow to the mirror, while the `bold` / `star` / `highlight` emphasis keys stay DB-only                                                                                                                                                                            |
| `group_id`                  | TEXT                            | **always NULL in v2** — there is no task-group feature in this table                                                                                                                                                                                                                                                                |
| `importance` / `urgency`    | INTEGER                         | 0–3 priority dials (None/Low/Medium/High), nullable — the M1 fields                                                                                                                                                                                                                                                                 |
| `start_at`                  | TEXT                            | ISO scheduled-start date, nullable — the M1 field (migration `20260607220204-…`); same `…Z` normalization as `due_at`                                                                                                                                                                                                               |
| `estimated_minutes`         | INTEGER                         | optional estimated duration in whole minutes (1–6000 = 1 min…100 h), nullable — "how long will this take" (migration `20260712155750-…`)                                                                                                                                                                                            |
| `list_id`                   | TEXT, FK → `tasks_v2_lists(id)` | the list this task belongs to; backfilled to Inbox for pre-existing rows                                                                                                                                                                                                                                                            |
| `created_at` / `updated_at` | TEXT                            | ISO timestamps, bound in JS                                                                                                                                                                                                                                                                                                         |
| `is_deleted`                | INTEGER                         | 0/1 soft-delete                                                                                                                                                                                                                                                                                                                     |

Four partial indexes (all `WHERE is_deleted = 0`): `idx_tasks_v2_parent_order` on `(parent_id, sort_order)`, `idx_tasks_v2_due` on `(due_at)` where undone, `idx_tasks_v2_snooze` on `(snoozed_until)` where undone, and `idx_tasks_v2_due_followups` on `(follow_up_at)` covering only waiting + due + un-fired rows (the follow-up clock's per-tick scan).

The core module that reads or writes this table is `src/main/db/queries-tasks-v2.ts` — `listTasksV2`, `getTaskV2ById`, `listAllTasksV2`, `createTaskV2`, `updateTaskV2`, `completeTaskV2Cascade`, `softDeleteTaskV2Cascade`, `reorderTaskV2`, `indentTaskV2LeaveChildren` (the v2 Tab — indent a row alone, re-homing its direct children to the new parent in one atomic transaction), `moveTaskV2ToListCascade` (re-file a row + its subtree into another list), `countOpenTasksV2ByList` (per-list open-task counts for the rail badges), plus `listDueFollowUpsV2` + `markFollowUpNotifiedV2` (the waiting-on follow-up clock's per-tick scan + its fire-once stamp). The notification-clock reads (`listDueTasksV2` / `listSnoozeExpiredTasksV2` / `listDueFollowUpsV2` / `listDueTaskEventsV2` + their mark/clear stamps + `getTaskV2AncestorChain`) and the `rowToTask` row-mapper live in the sibling modules `queries-tasks-v2-clocks.ts` and `queries-tasks-v2-row.ts`, both re-exported from `queries-tasks-v2.ts` so every import path is unchanged. Both the IPC handlers and the CLI routes call into it, sharing one Zod validation surface.

## Related

The overview, the other parts, and everything else worth reading next all sit on [Tasks (markdown outliner, in development)](tasks-v2.md).
