---
title: Drip (queue-and-trickle inbox feeder)
---

# Drip (queue-and-trickle inbox feeder)

## What it is

**Drip** is a queue-and-trickle inbox feeder. You stash mixed content — short text notes, links you want to revisit, files you want to remember to look at, folders you want walked for new files — into named **drips**, and Drip releases items into your inbox on a cadence you control. One drip = one named queue + one cadence (e.g. "Weekend reading — Saturdays at 9am") + the pending pile of items waiting to be released. The metaphor is a slow drip rather than a firehose: instead of dumping a 60-link reading list into your inbox at once, you let three a week trickle in.

Drip is **on by default** under Settings → Features (flag `dripEnabled`). When enabled, a "Drip" virtual project appears in the Omniscio sidebar group with the standard amc-builtin treatment — sidebar row, virtual project pane, settings search hit at `Settings → Features → Enable Drip`.

A drip has three statuses: `active` (the cron fires on cadence, releasing `itemsPerRelease` items per tick from the head of the pending queue), `paused` (cron is suspended; the user explicitly stopped it, but pending items survive so the drip can be resumed without losing state), and `archived` (the scanner has released the last pending item and auto-transitioned the drip to archived — distinguishes "you stopped me" from "I'm out of things to give you"). `archived` is NOT strictly terminal: adding a releasable item back into an auto-archived drip wakes it to `active` so the item releases (`reactivateArchivedDripOnAdd`), whereas a user-`paused` drip is never auto-woken — see the [drip add-path contract](/.claude/memory/contracts/drip-add-path-contract.md). Pausing a single released item via the standard inbox snooze can optionally bump a `pendingSkipCount` (per-drip "skip the next release" knob via the `skipNextOnSnooze` toggle) so the user can buy breathing room without pausing the whole drip.

Items are typed via a discriminator: **text** (free-form prose / markdown lives in `textContent`), **link** (URL lives in `textContent` with best-effort og: metadata in `linkTitle` / `linkDescription` / `linkImageUrl`), **file** (the file lives on disk under `userData/drip/<dripId>/` with original-name preserved in `fileName` and the on-disk path in `filePath`), **book-pages** (a single releasable unit — one PDF page, one EPUB chapter, or one ~400-word run — of a `drip_book_sources` row, referenced by FK with `book_unit_start` / `book_unit_end` selecting the slice), **completion** (a synthetic terminal item the scanner emits exactly once when a drip drains, so the user sees a "queue empty" inbox card instead of the drip silently going quiet), and **divider** (an inert visual separator with an optional short label in `textContent` — "Week 2", "Phase 2", "Done in 2026" — that the user drops anywhere in the pending queue to group items; the scanner NEVER releases a divider, so it lives in the management view as a permanent layout aid and never reaches the inbox).

A drip can also have **folder sources** — watched directories on disk that the folder scanner walks on a slow cadence. New files in a watched folder are copied into the drip's `userData/drip/<dripId>/` storage (so the original file can move or be deleted later without breaking the drip) and queued as `file` items with `sourceKind: 'folder'`. The `drip_folder_seen_files` bookkeeping table tracks `(filePath, mtime, fileSize)` so the scanner doesn't re-queue unchanged files on every walk.

A folder can also be imported **once** rather than watched. The Add-folder form has a **One-time import** checkbox: with it ticked, submitting walks the directory a single time, copies every matching file into the drip's `userData/drip/<dripId>/` storage, and queues them as ordinary manual `file` items — but no watched-source row is persisted and no `drip_folder_seen_files` ledger is written, so the folder is never scanned again. The IPC channel is `DRIP_FOLDER_IMPORT_ONCE` (the one-shot sibling of `DRIP_FOLDER_SOURCE_ADD`); its handler returns an `{ added, errors, error }` result so a per-file copy failure is reported without aborting the whole import. Use a one-time import for "queue this folder's current contents and forget it"; use a watched source for "keep feeding me new files as they land here."

A drip can also have **book sources** — PDFs and EPUBs that the drip releases one unit at a time rather than as a single file item. The `drip_book_sources` row carries the on-disk path under `userData/drip/<dripId>/books/<sourceId>/`, the `kind` (`pdf` = one page per release, `epub-chapter` = one spine item per release, `epub-word` = one ~400-word chunk per release across the concatenated spine), and `total_units` (page count / spine length / chunk count, computed once at add time via the `drip-book-extractor-service`). Each scheduled release inserts a `book-pages`-typed `drip_item` whose `book_source_id` points at the source and whose `book_unit_start` / `book_unit_end` window selects the slice to extract at release time — the full book never sits in memory between cadence ticks. Magic-byte sniff (`%PDF-` for PDFs, ZIP-with-`mimetype`-first for EPUBs) gates the kind so a `.pdf` extension over an `.exe` body can't reach the extractor; failed extractions surface as a typed `success: false` reason in the IPC envelope, the file is GC'd by the Phase 4 sweep.

The half of this page that carries the mechanics — the tables and services a drip is built from, the CLI surface that drives it, and what the inbox does with a released item — is [Drip (part 2)](drip-part-2.md).

## Where to find it

### How to use it

1. **Turn it on.** Settings → Features → **Enable Drip** is on by default; turning it off hides the sidebar row and silences the scanner.
2. **Open the Drip virtual project** in the sidebar. The first launch shows an empty state with **+ New drip**.
3. **Create a drip.** Pick a name, set a cadence (progressive-disclosure listbox — one input box, a list of presets beneath it that filters live as you type, exactly like the Snooze palette), set `itemsPerRelease` (default 1), toggle `skipNextOnSnooze` (default off). Typing `e` highlights every preset starting with "Every…"; typing `every fri` narrows to Friday entries; typing a full plain-English cadence the parser understands ("every weekday at 9am") shows a single **Custom** row at the bottom of the list with the resolved cron beside it; pasting a 5-field cron expression like `0 9 * * 1-5` is auto-detected (no separate "Raw cron" mode). Press ↑/↓ + Enter to pick from the list. Nothing turns red until you actually hit Save with something the parser can't resolve — the old eager "Couldn't read a recurrence pattern" toast that fired on the first keystroke is gone. The form also exposes two optional **Start-session defaults**: a **Session prompt** preset (leads the opening prompt whenever you launch a Claude Code session from one of this drip's inbox items) and a **Default repo** (which repo that session opens in — leave it at _Claude (default)_ to decide at launch instead). Both are optional; a blank prompt or the _Claude (default)_ repo stores no saved default.
4. **Add items.** Paste text, drop a link, attach a file, or add a folder source. The add-item zone carries three buttons beside the text box — **Attach files**, **Bulk add**, and **Divider**. **Bulk add** opens a modal with an editable textarea: paste or type a list and a split preview re-parses live as you edit (debounced ~250 ms), so you can fix a URL split across two lines or delete a blank row _before_ committing — then **Split** the content into separate queue items or **Add as single** entry. The same modal opens automatically when you paste something long into the add box (titled "Bulk paste detected" in that case, "Bulk add" when opened from the button). **Divider** inserts an inert labelled separator ("Week 2", "Phase 2") to group the queue visually — dividers reorder by drag and can be deleted, but are never released into the inbox. When adding a folder source, the **One-time import** checkbox flips the form from "watch this folder" to "copy its current files once and stop" (the submit button relabels to **Import once**). The drip's management pane shows the pending queue with reordering, plus four segmented tabs — **Pending** (default), **Released** (audit trail of items already trickled), **Archived** (items the user archived from the inbox; each row has a **Restore** button that puts it back into the inbox unread), and **All** (everything regardless of state).
5. **Receive on cadence.** Each release fires a `DRIP_ITEM_RELEASED` push, the item appears in the inbox as a `drip` integration row (header shows `DRIP` with a cyan droplet icon, sourced from the `DRIP_PROJECT_ID` branch in [`src/renderer/src/features/dashboard/inbox-helpers.ts`](/src/renderer/src/features/dashboard/inbox-helpers.ts)), and the standard inbox snooze / archive actions work on it. Clicking a row opens it in the **Drip inbox viewer** on the right: text items render as Markdown through the same shared alert text body every inbox alert uses (the chat-message renderer), link items render a clickable og: preview card (title / description / hero image, with a graceful image-fallback) via `LinkPreviewCard`, **image** file items render the picture inline (the bytes are lazy-loaded from `userData/drip/` as a data URL via the read-only `DRIP_ITEM_FILE_READ` channel and shown through `ReservedImage` in [`src/renderer/src/features/drip/DripInlineImage.tsx`](/src/renderer/src/features/drip/DripInlineImage.tsx), falling back to the pill PLUS an honest "could not be loaded — it may have been removed" note on a read error, e.g. the backing file was deleted), **HTML** file items (`.html`/`.htm`) render LIVE in a sandboxed preview — styling and JavaScript executing — when **Render HTML files live** is on (see "Live HTML preview" below), **video / audio / PDF** file items now play or preview INLINE (streamed over the `drip-media://` scheme — see "Inline media & openable files" below), any OTHER file item shows an **openable card** (Open in the OS default app / Reveal in folder on desktop, or a download link on mobile) rather than a dead name / mime / size pill, and the synthetic "queue drained" item shows a success message. Every releasable item's viewer also carries a **Start session** button (a `MessageSquarePlus` icon beside Archive): clicking it — or pressing **N** while the item is open — opens a small dialog that pre-fills an opening prompt composed from the item's context (led by the drip's saved preset, if any) and a target repo (the drip's saved default if it still exists and is spawnable, otherwise the Claude repo), both editable; confirming spawns a Claude Code session already focused on that item. It is the shared `StartSessionButton` (the same one every agent alert carries), so the bare-**N** shortcut appears on the button's hover tooltip — Omniscio's universal way of showing a control's key, never an always-visible badge. The button is hidden on the synthetic "queue drained" card and disabled — with a repo-mention tooltip — if you have no spawnable repo (**N** does nothing then either). The viewer's footer action bar carries an **"Edit drip" button** (shown for every card except the drained "queue empty" sentinel) that opens that drip's editor — the same name / cadence / repeat-forever / session-prompt / repo `CadenceModal` the Drips panel uses — right over the inbox, so a released item is one click from editing its **schedule and every setting**. The editor opened this way also gains a confirm-gated **Delete drip** button (footer, left of Cancel/Save): confirming deletes the whole drip and all its inbox cards, then advances the inbox to the next item — the inbox is now a full path to manage the drip, not just view it (it was previously the only Drip surface with no way back to the drip). The header **drip name** is also a link to the same editor (a secondary affordance alongside the footer's Edit button). The one exception is the synthetic "queue drained" card, whose name + pencil are absent (an auto-archived drip has nothing useful to edit); and a drip that can't be loaded (e.g. it was deleted out from under the item) reports a toast rather than a dead click. The name-link lives in the header **title** slot and the Edit button in the footer action bar (locked by [inbox-alert-contract](/.claude/memory/contracts/inbox-alert-contract.md) I8/I18), so the edit affordance never crowds Snooze / Archive and a phone never strands a lone pencil floating in the header's stacked actions row (where the snooze + archive pins leave it) below the title. Deleting a drip prunes its cards from the inbox slice instantly (independent of the debounced `DRIP_CHANGED` refresh, so it also clears on mobile); the whole-drip delete is `deleteDrip` in [`drip-store.ts`](/src/renderer/src/stores/drip-store.ts) (returns a boolean, rolls both slices back on failure). The `DRIP_ARCHIVED` push fires once when the queue drains and the drip auto-transitions to archived.
6. **Archive and restore inbox items.** Archiving a drip inbox row — pressing **E**, middle-clicking the sidebar row on desktop, the detail viewer's Archive button, or the mobile swipe — calls `drip:item-archive` and, from **any** of those surfaces, arms a **Ctrl+Z** undo labelled `Archived "<drip name>"`, then navigates to the next inbox row via `navigateAfterInboxDripItemDismiss`. The undo is registered inside the store's `archiveInboxItem` (not the individual archive surfaces), so no surface can forget it — press Ctrl+Z right after archiving to bring the item back. The archived item disappears from the inbox but is preserved on the row — it shows up in the drip management pane's **Archived** tab with a **Restore** button that calls `drip:item-archive-undo` to unset `inbox_archived_at` and bring it back to the inbox (`releasedAt` is preserved; the inbox treats it as freshly-released again). The symmetric `archiveInboxItem` / `unarchiveInboxItem` actions (both the store-armed undo and the Restore button) live in [`drip-store.ts`](/src/renderer/src/stores/drip-store.ts).

## How it behaves

### Repeat forever (loop)

A drip can be set to **repeat forever** via the `loop` flag (the "Repeat forever" toggle in the cadence form; column `drips.loop`, default 0, migration `20260622132725`). A loop drip **never archives on drain**: instead of consuming its queue once and transitioning to `archived` when empty, it re-delivers its queued item(s) on every cadence tick, indefinitely — turning a one-shot trickle into a recurring reminder.

Mechanically, a loop drip's queued items are perpetual **templates** — they stay `pending` and are never consumed. On each tick the scanner **clones** the next `itemsPerRelease` template(s) (round-robin, cursor = `totalReleased % templateCount`) into fresh **released** `drip_items` + inbox alerts, so every delivery is a distinct, archivable card. Dividers are skipped; an empty / divider-only queue simply reschedules (it never archives, and never divides by zero). Both the normal trickle path and the loop path go through one shared `releaseDripItemAsAlert` helper in [`drip-scanner-service.ts`](/src/main/services/drip/drip-scanner-service.ts), so the alert reuses the clone's `drip_item` id (archive-sync invariant preserved) and the two paths can't drift. The loop path still advances `next_run_at` to the next cron slot every tick — without that the 60s scanner would re-fire every minute and flood the inbox.

Round-robin cannot pin a specific item to a specific time (which template lands on which tick depends on first-fire parity), so for a time-pinned schedule (e.g. photo A at 9 AM, photo B at 4 PM) use **two single-item loop drips**, one per slot. Trade-off: clone-on-release grows `drip_items` + `inbox_alert_items` by ~1 row/tick (trivial for SQLite; the inbox surface is capped at 200 and excludes archived). Full invariants + the tests that lock them: [`.claude/memory/contracts/drip-loop-contract.md`](/.claude/memory/contracts/drip-loop-contract.md).

**Missing-file resilience.** A `file` item whose backing file under `userData/drip/<dripId>/` was deleted out from under the drip (e.g. the 2026-06-24 cross-instance wipe) is NOT released as a silent dead paperclip chip. The shared `releaseDripItemAsAlert` chokepoint checks `existsSync` and instead releases a clear "file unavailable — re-add it" **text** card; for a **loop** drip it also pauses the drip (`status → paused`) so it stops firing a blank card every cadence until the user re-adds the file. Already-released dead cards render an honest "could not be loaded" note in the inbox image viewer (above). The deletion itself is prevented at the source — see [`.claude/memory/postmortems/drip-datadir-cross-instance-deletion-postmortem.md`](/.claude/memory/postmortems/drip-datadir-cross-instance-deletion-postmortem.md).

**DATA_DIR-move portability.** A drip `file` item stores an absolute path in `drip_items.file_path`, but that directory prefix is **advisory**: the file always lives at `<currentDripRoot>/<dripId>/<leaf>`. Every drip-file read resolves the stored path against the _current_ drip root via the one chokepoint [`readDripItemFile`](/src/main/services/drip/drip-file-storage.ts) (→ `resolveDripItemFilePath`, re-derived from `dripId` + the filename), the scanner's presence check re-bases the same way, and the GC compares orphans by basename — so moving the database / `DATA_DIR` (e.g. the 2026-07-13 C:→H: move) does NOT make a present file read as missing/unreadable, falsely pause a loop drip, or get the moved files GC-deleted. This mirrors the already-portable `drip_book_sources.path` (stored relative). The security containment in `readDripFile` is unchanged; the re-base happens upstream. Locked by the move-portability tests in `drip-file-storage`, `drip-gc`, `drip-scanner`, `serve-drip-preview`, and the file-read handler; full detail in the postmortem above.

### `next_run_at` lifecycle

The scanner's `listActiveDripsReadyToFire` selector reads `drips.next_run_at` and skips any drip where it's NULL — so the column MUST be populated for the drip to ever fire. The CRUD handlers (not the scanner) own the lifecycle:

- **`drip:create`** computes `next_run_at` via `computeNextRunAt(cronExpression, Date.now())` and persists it on the new row.
- **`drip:update`** recomputes when `cronExpression` changes (from "now", to honor the user's intent that the new cadence starts ticking now).
- **`drip:setStatus` paused → active** recomputes from "now" so a drip paused for days doesn't dump a backlog on resume.
- **`drip:setStatus` active → paused** clears `next_run_at` to NULL.
- **`drip:setStatus` active → active** (idempotent or other transitions) preserves the existing `next_run_at`.

The scanner's tick has a defensive **self-heal pass**: any active drip with `next_run_at IS NULL` gets one written from `computeNextRunAt(cronExpression, Date.now())` and a warning logged. Self-terminating — the per-drip release txn always advances `next_run_at` or archives the drip, so the warning never loops. This catches drips on a real user's machine that were created during the pre-fix window where the handlers didn't populate `next_run_at`.

The full contract — including the original 2026-05-17 bug where handlers shipped without `next_run_at` population, the 2026-05-20 fix, and the regression-lock tests in [`tests/unit/ipc/drip-handlers-next-run-at.test.ts`](/tests/unit/ipc/drip-handlers-next-run-at.test.ts) — is in [`.claude/memory/postmortems/drip-next-run-at-handler-gap-postmortem.md`](/.claude/memory/postmortems/drip-next-run-at-handler-gap-postmortem.md).

### Retention and permanent deletion

Drip runs an **hourly janitor** — the `drip-gc` periodic service, started at every app launch by [`src/main/startup/tasks/1900-drip-gc.ts`](/src/main/startup/tasks/1900-drip-gc.ts) and **not gated behind the Drip feature flag**, so it sweeps whether or not you still use Drip. Two independent passes run each hour, plus one bookkeeping prune. The App tells you at delete time that a deleted drip cannot be undone; what no surface has said is what the janitor removes **on its own**.

**Deleting a drip is a 7-day hold, then a permanent purge.** Deleting a drip marks it deleted and cascades that mark to its items, folder sources, book sources and released inbox cards. There is **no undelete** surface in the App — the confirm dialog says so plainly. Once a drip has been deleted for **more than 7 days**, the janitor permanently deletes every row that references it (`drip_folder_seen_files` → `drip_folder_sources` → `drip_items` → `drips`) **and wipes its entire on-disk folder**, `userData/drip/<dripId>/` — every file copied in from a watched folder, and any imported PDF/EPUB under `books/`. Past day 7 the content is gone: there is no trash folder and no recovery from inside the App.

**Orphaned files inside a LIVE drip are removed after 24 hours — with no action from you.** For every drip that has *not* been deleted, the janitor lists the files in `userData/drip/<dripId>/` and **deletes any file that no undeleted item points at whose last-modified time is older than 24 hours**. You never deleted that file; the janitor did. The 24-hour floor exists because the folder scanner copies a file in *before* it writes the item row, so a tick landing mid-copy must not delete the file that was just placed.

Two things are deliberately safe from that sweep. A file referenced by any item that has **not** been deleted is kept — including an item you **archived** from the inbox, since archiving is not a delete and leaves the item (and its file) referenced. And the original in a **watched folder** is never touched: the scanner always **copies** a file into the drip's own storage rather than moving it, so neither deleting a drip nor the orphan sweep can reach the folder you pointed at.

**The watched-folder dedup ledger is pruned after 30 days.** `drip_folder_seen_files` records the files the folder scanner has already queued, so it does not re-queue unchanged files on every walk. A row whose `last_seen_at` is older than 30 days means that file has left the watched folder, so the row is dropped. This removes bookkeeping only — it never deletes an item or a file.

### Trickle tasks from Tasks

Drip is also the **engine behind "Trickle to inbox…"** in Tasks (gated, in-development). From a project (its open tasks) or a hand-picked set of tasks, that action creates a NEW Drip pre-loaded with those tasks as `text` items, so they trickle into the inbox a few at a time on a cadence you pick — reusing everything here (cadence, items-per-release, repeat-forever, pause/resume, snooze, the management pane). It's **free** (a local task ranker, no AI).

Two small additions make a drip "task-aware". Each task item carries a back-reference to its `tasks_v2` row (`drip_items.source_task_id`), and the per-trickle "archive each task once it's sent to my inbox" choice is stored on the drip (`drips.archive_source_on_release`). At release the scanner consults one seam, [`drip-task-source.ts`](/src/main/services/drip/drip-task-source.ts): a task that's been **finished, deleted, or archived since the snapshot drains silently** (no inbox row — it never nags you about done work), and an open task is **archived (cascade, reversible)** when the trickle opted in. Archive-on-release is forced off for a `loop` (repeat-forever) drip — a standing reminder must not archive its own source — and a loop clone copies the back-reference so a task finished mid-flight stops re-dripping too. The presence of `source_task_id` (not a new `source_kind`) is the "this is a task item" marker, so the two columns are plain additive ones — no table rebuild. Full invariants + the tests that lock them: [`.claude/memory/contracts/tasks-v2-trickle-contract.md`](/.claude/memory/contracts/tasks-v2-trickle-contract.md).

## For agents

The user-facing half of Drip is on this page. The internals — the five tables and the data model behind a drip, the shared types, the book extractor, and the full CLI surface — are in [Drip (part 2)](drip-part-2.md).

## Related

- `Integration registry contracts` — the wiring assertions Drip's registry entry has to honor.
- `Drip inbox image prefetch contract` — how released drip images are pre-warmed at inbox-load so a card paints instantly: LOCAL file bytes into a renderer data-URL cache, and REMOTE images (markdown images in text + link-preview images) into the browser HTTP cache via `new Image()`.
- Snooze and the time parser — the NL parser Drip reuses for cron input.
- Inbox row contract — how Drip rows render alongside other integration rows.
