Child Task Hierarchy
Executive Summary
The app currently has two hierarchy concepts: subtasks (Notion to_do blocks — lightweight checkboxes with no status, due date, or priority) and child tasks (full Task pages linked via Notion “Parent Task” self-referencing relation, with their own status, due date, priority, and categories).
Decision: Subtasks are being removed. Child tasks are the only hierarchy going forward.
This spike investigated how child tasks should work across the entire app. After competitive analysis of 6 industry leaders, wireframing 5 interactive screens, designing detailed interaction specs, and assessing technical feasibility against the existing codebase, we recommend:
- A flat list with expand/collapse — children render inline under the parent when expanded
- Inline quick-add for child creation in TaskDetail (no navigation required)
- Max 2 levels of nesting (parent → child)
- Lazy-loaded children with local cache for instant re-expand
- Bidirectional completion cascade: auto-complete parent when all children done (with undo); confirmation sheet for completing parent with active children
The backend infrastructure is ~80% built. Estimated total effort: M-L (1.5–2 sprints), reduced from L after agreeing to disable drag on nested tasks for v1.
Spike Deliverables
| # | Deliverable | Owner | Status | File |
|---|---|---|---|---|
| 1 | UX Research Document | UX Researcher | Done | child-tasks-ux-research.html |
| 2 | Interactive Wireframes | UI Designer | Done | child-tasks-wireframes.html |
| 3 | Interaction Design Spec | UX Researcher | Done | child-tasks-interactions.md |
| 4 | Technical Feasibility Notes | Frontend Dev | Done | child-tasks-technical-feasibility.md |
| 5 | Acceptance Criteria | Product Owner | Done | child-tasks-acceptance-criteria.md |
| 6 | Recommendation + Migration Plan | Business Analyst | Done | child-tasks-recommendation.md |
Key Decisions
These decisions were agreed across the team through PO review, UI designer review, and cross-deliverable synthesis.
| # | Decision | Choice | Source |
|---|---|---|---|
| 1 | Max nesting depth | 2 levels (parent → child) | Agreed Tech feasibility + Interaction spec |
| 2 | Children in main list | Hidden by default; shown inline when parent expanded | Agreed Tech feasibility |
| 3 | Section grouping | Children bypass grouping — render under parent’s section | Agreed Tech feasibility |
| 4 | Auto-complete parent | ON by default when all children Done (5s undo toast) | PO decision |
| 5 | Cascade complete children | OFF by default — confirmation sheet with explicit “Complete all” | PO decision |
| 6 | Child creation flow | Inline quick-add in TaskDetail (primary); QuickAdd with parent field (secondary) | Wireframes + Interaction spec |
| 7 | Drag on nested tasks | Disabled for v1 — re-parenting via TaskDetail | Agreed Tech feasibility |
| 8 | Child loading strategy | Lazy with cache + loading skeleton on first expand | Agreed Tech feasibility |
| 9 | Default expand state | Collapsed; persisted in localStorage per view | Interaction spec |
| 10 | Swipe gestures on children | Identical to regular tasks | Interaction spec |
| 11 | Detail sheet navigation | Stack: Parent → Child → swipe back → Parent | Interaction spec |
| 12 | Progress indicator | Text badge only for v1 (progress ring deferred to v2) | Interaction spec |
UX Research — Competitive Analysis
Studied 6 task management apps across 5 dimensions: creation flow, visual nesting, completion behaviour, navigation, and mobile patterns.
Apps Analysed
| App | Max Depth | Auto-Complete Parent | Mobile UX | Key Insight |
|---|---|---|---|---|
| Todoist | 4 levels | No | Excellent — dense flat rows | Inline creation, indent/outdent gestures |
| Things 3 | 1 level (checklists) | Optional | Best-in-class mobile | Headings for grouping, not deep nesting |
| TickTick | 5 levels | Configurable | Good | Flexible but overwhelming at depth |
| Linear | 1 level (sub-issues) | Yes (bidirectional) | Desktop-focused | Best auto-completion model — bidirectional cascade |
| Asana | 5+ levels | No | Cluttered on mobile | Powerful but orphaned subtask problem |
| Notion | Unlimited | No | Slow on mobile | Flexible but no task-specific UX |
Key Findings
- Optimal depth for mobile: 1–2 levels. Apps allowing 4+ levels see usability complaints on small screens. Industry is converging on shallow hierarchies.
- Linear’s bidirectional auto-close is best-in-class. All children done → parent auto-completes. Parent completed → explicit choice for children. We adopted a hybrid of Linear’s model.
- Inline quick-add is universal. Every app studied offers inline creation without navigating away from the parent context.
- Todoist’s visual pattern is our baseline. Dense flat rows, 16px indentation, no connector lines — matches our existing aesthetic.
- Full-featured children > lightweight checklists. Apps with status/date/priority on sub-items (Linear, Todoist) handle complex projects better than checkbox-only approaches (Things 3 checklists).
Wireframes
5 interactive screens designed at 375px mobile viewport, matching the Todoist-dense dark theme aesthetic. View wireframes →
Screen A — Task List (Expand/Collapse)
- Chevron (▶/▼) right of StatusCircle, 32×44px tap target (preserves consistent left edge)
- Child count badge (done/total) right-aligned before due date
- 16px indentation per level, no connector lines
Screen B — Parent Detail View
- Child tasks section replaces deprecated SubtaskList
- Each child shows: StatusCircle + title + due date, tappable to navigate
- Section header: “Child tasks (2/4)” with [+ Add] button
- [+ Add child task] inline form at bottom of child list
Screen C — Child Detail View
- “↳ Parent Title” link at top, navigates to parent detail
- “← Back to [Parent]” button below drag pill indicator
- All standard task fields: status, priority, due date, categories, notes
Screen D — Create Child Task Flow
- Option A (Recommended): Inline quick-add in TaskDetail — text input + date chip + submit
- Option B: Full create form with pre-filled parent
- NLP date parsing via chrono-node (same as QuickAdd)
- Defaults: Status → Inbox, Category → inherited from parent, Priority → None
Screen E — Card Variants
- Parent card: Chevron toggle + child count badge + progress text
- Child card: 16px indent + “↳ Parent Name” breadcrumb
- Standalone card: No hierarchy indicators (existing design)
Interaction Design
Completion Behaviour
| Scenario | Behaviour |
|---|---|
| Complete a child | Child → Done. If all siblings now Done → parent auto-completes + undo toast (5s) |
| Complete parent (incomplete children) | Confirmation sheet: “Complete all” / “Complete only parent” / “Cancel” |
| Complete parent (all children Done) | Immediate complete, no confirmation needed |
| Un-complete a child | Child → Inbox. If parent was auto-completed → parent reverts to previous status |
| Un-complete a parent | Parent → Inbox. Children unchanged |
Expand / Collapse
- Tap chevron: toggle expand/collapse with 150ms stagger animation (50ms between children)
- Tap task row: opens TaskDetail (does NOT toggle expand)
- Pull to refresh: preserves expand/collapse state
- Persist state in localStorage keyed by expand-state:{viewKey}
Swipe Gestures
- Right swipe (>100px): toggle Done/Inbox — triggers auto-complete check if last incomplete child
- Left swipe (<-100px): reschedule popup — identical to regular tasks
- Edge case: completing last child via swipe → double haptic (child + parent)
Reordering
- Within same parent: long-press drag, stored in localStorage (Notion relations have no sort order)
- Between parents: NOT supported in v1 — use TaskDetail “Change parent” flow
- Drag disabled on child rows and expanded parents (AGREED)
Navigation
- TaskDetail maintains navigation stack: Parent → Child → swipe back → Parent
- Swipe-down from child: returns to parent detail (not dismiss)
- Swipe-down from parent/top-level: dismisses sheet
- Stack depth matches 2-level nesting cap
Edge Cases
| Case | Handling |
|---|---|
| Depth limit exceeded | API returns 400; “Add child task” hidden on depth-2 tasks |
| Circular reference | API validates; task picker excludes self + descendants |
| Orphaned children (parent deleted) | Children promoted to top-level; toast notification |
| Offline operations | Expand/collapse local; mutations queued via mutate(); cascade evaluated client-side |
| >10 children in list view | Show first 5 + “Show N more” link; TaskDetail shows all (scrollable) |
Technical Feasibility
What Already Exists (~80% of backend)
| Layer | What’s Built | File |
|---|---|---|
| Types | parentId, parentTitle, childCount, depth on Task interface | frontend + backend types.ts |
| Mapper | notionToTask() reads Parent Task relation | mapper.ts:23 |
| Enrichment | queryTasks() builds in-memory map for parentTitle, childCount, depth | client.ts:72-94 |
| Child query | getChildTasks(parentId) via relation filter | client.ts:142-153 |
| API endpoint | GET /api/tasks/:id/children | routes/tasks.ts:81-90 |
| Frontend API | fetchChildTasks(taskId) | api.ts:92-94 |
| Rendering | Child count badge, parent link, depth indentation, child list in detail | TaskCard, TaskList, TaskDetail |
Key Gaps
| Gap | Impact | Fix |
|---|---|---|
| taskToNotionProperties() doesn’t map parentId | Cannot create/update Parent Task relation | Add “Parent Task” relation mapping to mapper.ts:30-62 |
| CreateTaskInput / UpdateTaskInput lack parentId | Backend types reject parentId | Add parentId?: string to both |
| optimisticCreate hardcodes parentId: null | Cannot create child tasks in UI | Add optional parentId parameter |
| No expand/collapse state | Can’t show/hide children in list | Add expandedTaskIds + childTasksCache to TaskContext |
| Section grouping splits families | Children sorted by date, not parent | Filter parentId != null from main list; inject inline on expand |
Notion API Constraints
- Querying children: Efficient — single API call with relation filter
- Recursive queries: Not supported — each level requires N additional calls
- Rate limit: 3 req/s per integration — rapid expansion of 5 tasks could take ~2s
- Page-scoped enrichment: parentTitle/childCount only accurate within 100-result query page
Recommendation
Consolidate on child tasks (full Notion pages via “Parent Task” relation). Remove subtasks (to_do blocks) entirely.
Why This Approach
- Unified model: One hierarchy concept instead of two. Eliminates user confusion between “subtask” and “child task.”
- Full capabilities: Child tasks have status, due date, priority, categories — unlike checkbox-only subtasks.
- Infrastructure exists: Backend is ~80% built. The Notion “Parent Task” relation is already in use. Frontend already renders parent links, child counts, and depth indentation.
- Industry alignment: All 6 apps studied use full-featured sub-items (or are moving toward them). Checkbox-only checklists are falling out of favour for task management.
- Incremental delivery: 6 phases, each independently shippable. No big-bang migration required.
What NOT to Do
- Don’t convert existing to_do subtasks to child tasks. Data model mismatch (text-only → full task), high API cost, low user value. Let them age out naturally.
- Don’t support 3+ nesting levels. 375px viewport can’t show it usably; Notion API costs scale per level.
- Don’t add drag-to-reparent in v1. Too error-prone on mobile. TaskDetail-based flow is safer.
Implementation Phases
Ordered by dependency. Total: ~15 working days across 2 sprints. Phases 1 & 2 can run in parallel.
| Task | File |
|---|---|
| Add parentId to CreateTaskInput and UpdateTaskInput | backend/src/types.ts |
| Map parentId to “Parent Task” relation in taskToNotionProperties() | backend/src/notion/mapper.ts |
| Add depth validation (reject > 2) | backend/src/routes/tasks.ts |
| Add circular reference validation | backend/src/routes/tasks.ts |
| Task | File |
|---|---|
| Add expand state + child cache to TaskContext | context/TaskContext.tsx |
| Persist expand state in localStorage | context/TaskContext.tsx |
| Filter children from main list (!t.parentId) | components/TaskList.tsx |
| Add chevron toggle to TaskCard | components/TaskCard.tsx |
| Render expanded children inline | components/TaskList.tsx |
| Loading skeleton on first expand | components/TaskList.tsx |
| Animate expand/collapse (150ms stagger) | components/TaskList.tsx |
| Task | File |
|---|---|
| Add parentId to optimisticCreate | context/TaskContext.tsx |
| Inline quick-add form in TaskDetail | components/TaskDetail.tsx |
| Wire NLP date parsing (reuse chrono-node) | components/TaskDetail.tsx |
| Add “Parent” field to QuickAdd | components/QuickAdd.tsx |
| Optimistic child count update | context/TaskContext.tsx |
| Task | File |
|---|---|
| Auto-complete parent when all children Done | context/TaskContext.tsx |
| Undo toast (5s timeout) | new: components/UndoToast.tsx |
| Confirmation sheet for parent complete | TaskDetail.tsx or TaskCard.tsx |
| Un-complete child reverts auto-completed parent | context/TaskContext.tsx |
| Batch status update API | backend/src/routes/tasks.ts |
| Action | Files to Remove / Modify |
|---|---|
| Delete SubtaskList component | Delete: SubtaskList.tsx; Remove from: TaskDetail.tsx |
| Remove subtask API functions | api.ts: lines 78–115 |
| Remove subtask backend routes | routes/tasks.ts: lines 94–150 |
| Remove subtask client functions | client.ts: lines 157–203 |
| Remove Subtask type | frontend/types.ts + backend types |
| Task | File |
|---|---|
| Navigation stack in TaskDetail | components/TaskDetail.tsx |
| “Back to [Parent]” indicator | components/TaskDetail.tsx |
| Rename “subtask” labels to “child task” | components/TaskCard.tsx |
| Disable drag on nested tasks | hooks/useDragReorder.ts |
| Orphan promotion on parent delete | backend/src/routes/tasks.ts |
Dependency Graph
Critical path: Phase 1 → Phase 3 → Phase 4 → Phase 5. Phases 1 & 2 run in parallel.
Migration Plan
Strategy: Cut Over, Don’t Convert
Do NOT convert existing to_do block subtasks to child tasks.
- Data model mismatch: Subtasks are text-only checkboxes. Child tasks are full pages with status, due, priority. No meaningful 1:1 mapping.
- API cost: Converting requires N×M API calls (create page + delete block per subtask). Risky on a live Notion database.
- Low user impact: Most subtasks are ephemeral checklists (“Pack X, pack Y”). Users can manually recreate important ones as child tasks.
Migration Phases
| Phase | What Happens | When |
|---|---|---|
| A. Soft Deprecation | Relabel “Subtasks” to “Checklist (legacy)”. Both systems visible in TaskDetail. Child task UI added. | Sprint 1, Week 1 |
| B. Hard Deprecation | Remove “Add subtask” input (read-only display remains). Child tasks feature-complete. Legacy section collapsed by default. | Sprint 1, Week 2 |
| C. Removal | Delete SubtaskList component, all subtask API functions, backend routes, and client functions. | Sprint 2, Week 1 |
| D. Cleanup | Remove Subtask type, verify tsc --noEmit, manual regression test. | Sprint 2, Week 1 |
What Happens to Existing Subtasks
Existing Notion to_do blocks are not deleted from the Notion database. They remain as block children of their parent page. The PWA simply stops rendering them after Phase C. Users who need those items as child tasks can recreate them manually.
Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Page-scoped enrichment inaccuracy | Medium | High | Children hidden from main list (mitigates orphan display). getChildTasks() returns full data. Accept edge-case inaccuracy for users with >100 active tasks. |
| Rate limiting under rapid expansion | Medium | Medium | Queue expand requests. Cache eliminates repeat fetches. Loading skeleton covers latency. |
| Completion cascade edge cases | Low | Medium | Track autoCompletedParents map. Only auto-revert if parent was auto-completed (not manually). |
| TaskContext complexity (41+ fields) | Medium | Medium | Consider extracting to useHierarchy hook in v2 refactor. Acceptable for v1. |
| Offline child creation with temp parent | Low | Low | Defer solving. Block child creation for offline-created parents in v1. |
| Circular references (via direct Notion edit) | Very Low | Low | API validates on create/update. Render as top-level with warning badge if detected. |
Deferred Items
| Feature | Reason | Target |
|---|---|---|
| Drag-to-reparent | Complex mobile UX, high accidental-reparent risk | v2 |
| Grandchildren (depth 3+) | 375px viewport too narrow; API cost scales per level | v2+ |
| Progress ring on StatusCircle | Text badge sufficient; SVG arc adds complexity | v2 |
| NLP parent assignment | Ambiguity in parent name matching | v2 |
| Folded screen optimization | PO deferred; 375px design works on both screens | Future |
| Accessibility tree semantics | PO deferred for v1; basic touch targets included | Future |
| Offline child creation (temp parent) | Edge case; needs post-sync ID resolution | v2 |
| Bulk child operations | Single-task operations sufficient initially | v2 |
| Child task reordering across parents | Relation change + complex drag UX | v2 |