--- name: Storage tasks and subtasks CRUD type: architecture-map-page slice: 05-core-storage-hotkey-build last_verified: 2026/05/09 --- # Storage tasks and subtasks CRUD > **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Storage tasks CRUD **Plain English summary.** Tasks are the things the user wants to do. Subtasks are tasks with a `parent_task_id` set. Both live in the same `tasks` table with a self-referential foreign key. The completion path includes a small bit of business logic: completing the last subtask auto-completes the parent (and stamps `auto_completed = 1` so the daily-completion analytics can exclude cascade-completed parents). ## At a glance - Source: `crates/storage/src/database.rs:301-631`. - Public surface: `insert_task`, `list_tasks`, `get_task_by_id`, `update_task`, `set_task_energy`, `complete_task`, `uncomplete_task`, `delete_task`, `insert_subtask`, `list_subtasks`, `complete_subtask_and_check_parent`, `list_recent_completions`. - Public types: `TaskRow`, `DailyCompletionCount`. - Consumers: slice 2 task commands; the LLM micro-step decomposer (slice 4) calls `insert_subtask`; the gamification badge on the Tasks page reads `list_recent_completions`. ## Public types ### `TaskRow` — `crates/storage/src/database.rs:825` ```rust pub struct TaskRow { pub id: String, pub text: String, pub bucket: String, pub list_id: Option, pub effort: Option, pub notes: String, // v4 pub done: bool, pub done_at: Option, pub created_at: String, pub source_transcript_id: Option, pub parent_task_id: Option, // v3 pub energy: Option, // v11; "high" | "medium" | "brain_dead" | None } ``` ### `DailyCompletionCount` — `crates/storage/src/database.rs:573` ```rust #[derive(serde::Serialize)] pub struct DailyCompletionCount { pub day: String, // "YYYY-MM-DD" in local time pub count: u32, } ``` Serialised because slice 2 forwards it to the frontend sparkline. ## Functions ### `insert_task(pool, id, text, bucket, source_transcript_id, list_id, effort, energy)` — `crates/storage/src/database.rs:301` Positional signature, eight arguments. Documented at `database.rs:293` as deliberately flat — it mirrors the `tasks` schema columns. Refactor to a params struct only if another nullable lands after `energy`. ### `list_tasks(pool) -> Result>` — `crates/storage/src/database.rs:328` `SELECT ... FROM tasks ORDER BY created_at DESC`. Returns every row, including completed ones. Frontend filters. ### `get_task_by_id(pool, id) -> Result>` — `crates/storage/src/database.rs:341` Single-row select. ### `update_task(pool, id, text, bucket, list_id, effort, notes, energy)` — `crates/storage/src/database.rs:361` Updates the user-editable fields. `done` and `done_at` are not updated through this function — completion has its own pair (`complete_task` / `uncomplete_task`). ### `set_task_energy(pool, id, energy) -> Result` — `crates/storage/src/database.rs:402` Single-column update. Returns the row post-update so the frontend gets confirmation. Energy must be one of `"high"`, `"medium"`, `"brain_dead"`, or `None` — enforced by the DB-level `CHECK` from migration v11. ### `insert_subtask(pool, parent_id, id, text, bucket)` — `crates/storage/src/database.rs:415` Same as `insert_task` but with `parent_task_id` set. Inherits the `bucket` from the parent. ### `list_subtasks(pool, parent_id) -> Result>` — `crates/storage/src/database.rs:431` `SELECT ... FROM tasks WHERE parent_task_id = ? ORDER BY created_at ASC`. ### `complete_subtask_and_check_parent(pool, subtask_id) -> Result<()>` — `crates/storage/src/database.rs:447` Business logic, in one function so it lives in one transaction: 1. Mark the subtask done. 2. If every sibling subtask is now done, mark the parent done with `auto_completed = 1`. The `auto_completed` flag (v13) lets `list_recent_completions` exclude cascade-completed parents from the daily-count sparkline so the user sees one count per real action, not one count per parent action plus one per subtask completion. ### `complete_task(pool, id)` — `crates/storage/src/database.rs:498` `UPDATE tasks SET done = 1, done_at = datetime('now') WHERE id = ?`. ### `uncomplete_task(pool, id)` — `crates/storage/src/database.rs:507` `UPDATE tasks SET done = 0, done_at = NULL, auto_completed = 0 WHERE id = ?`. This is the function flagged in the 2026-04-22 review as RB-15 ("doesn't reopen auto-completed parents"). The current implementation only un-completes the row passed in. If a user un-completes a child of an auto-completed parent, the parent stays done. The fix is to walk up the `parent_task_id` chain and reset auto-completed parents whose subtasks are no longer all-done. Tracked as a release-blocker (per `code-review-2026-04-22.md`); resolution status to verify against current `HANDOVER.md`. ### `delete_task(pool, id)` — `crates/storage/src/database.rs:550` `DELETE FROM tasks WHERE id = ?`. The `parent_task_id REFERENCES tasks(id) ON DELETE CASCADE` from migration v3 removes child subtasks automatically. ### `list_recent_completions(pool, days) -> Result>` — `crates/storage/src/database.rs:578` The Phase 8 momentum sparkline. Steps: 1. Clamp `days` to `[1, 365]`. 2. Group `tasks` rows by `DATE(done_at, 'localtime')` for `done = 1 AND auto_completed = 0`. 3. Compose a fixed-length output array by left-joining the grouped result against an N-day "spine" generated by re-querying SQLite for each offset (so SQLite owns the calendar arithmetic, including DST and month boundaries). Today the frontend always asks for `days = 7`. The clamp prevents a wild value producing an empty or huge series. ## Watch-outs - **`uncomplete_task` does not reopen auto-completed parents.** RB-15 in the 2026-04-22 review. Status: see slice debt section in [`README.md`](README.md). - **`list_tasks` returns every task ever created.** Soft-delete is not modelled; deleted tasks are gone. Completed tasks pile up over time. Frontend is responsible for filtering. - **Energy column accepts NULL but the `CHECK` only fires on non-NULL values.** Migration v11's check is `CHECK (energy IN ('high', 'medium', 'brain_dead') OR energy IS NULL)`. Worth a regression test that asserts an invalid string fails. - **`auto_completed` flag is set in code, not via trigger.** A future migration that adds another path to "complete this task" (eg an LLM-driven auto-complete) needs to remember to set the flag. Worth a row-level audit. ## See also - [Schema and migrations](storage-schema-and-migrations.md) — v3, v4, v11, v13 are the relevant migrations. - [Storage overview](storage-overview.md) - [Slice 4 LLM decompose-task](../04-llm-formatting-mcp/README.md) — caller of `insert_subtask`.