Spec: F06 — Recurring Tasks
Status: shipped · Audited: 2026-06-10
1. Problem & CEO Job
See clearly. SME operations run on rhythms — weekly debt collection calls, monthly payroll prep, daily cash count. When a rhythm breaks, that’s a CEO-grade signal. Recurring tasks encode the rhythm so missing it becomes visible (an overdue generated task feeds the Brief) instead of silently forgotten.
2. User Stories & Acceptance Criteria
- As a manager, I want to define “every Friday: chase receivables”, so that it materializes as a real task without anyone remembering.
- Given an active daily/weekly/monthly template, when the generation run executes for a due date, then exactly one Task is created (
source_type=recurring) — never duplicates for the same template+date. - Given
end_datepassed oris_active=false, then no further tasks generate.
- Given an active daily/weekly/monthly template, when the generation run executes for a due date, then exactly one Task is created (
- As a CEO, I want missed recurring work to surface, so that broken rhythms reach the Brief.
- Given a generated task past
due_date, then it appears in Brief section “overdue” like any task (no special-casing needed).
- Given a generated task past
- As a manager, I want to pause a rhythm without deleting its history.
- Given
is_active=falsethen re-enabled, thennext_run_atrecomputes from today, not from the gap.
- Given
- As a manager, I want a temporary refresh outage not to erase my operating rhythms.
- Given a populated shared Android/iOS recurring list, when refresh fails, then the last committed templates and Run now actions remain visible behind one concise warning with explicit Retry. Repeated failure and create reconciliation retain that snapshot; only an initial empty failure is blocking.
- Given a populated shared Android/iOS recurring detail, when refresh fails, then the exact template facts and Edit/Delete/Run now actions remain visible behind one concise warning with explicit Retry. Repeated failure retains that snapshot; only an initial empty failure is blocking.
- As a manager in more than one organization, I want the recurring directory to follow the active organization.
- Given Android switches organizations, when Work Graph reports recurring work and the user opens Recurring, then the destination creates a fresh organization-scoped model instead of rendering the previous tenant’s cached list.
3. Scope Boundaries
- In: template CRUD, daily/weekly/monthly (+
rrulestring for custom), generation command, active toggle, business-impact inheritance to generated tasks. - Out: calendar UI of future occurrences, per-occurrence editing, reminders (Telegram brief covers visibility).
4. Data Model
work.RecurringTask — template fields (template_title, template_description), schedule (frequency, rrule, start_date, end_date, next_run_at, last_run_at), is_active. The persisted auto_create_task field still exists in the model/DTOs for compatibility, but shipped generation ignores it and always auto-materializes active recurring templates. Generated Task carries source_type="recurring".
5. API Contract
| Method | Path | Min role | Notes |
|---|---|---|---|
| CRUD | /api/recurring-tasks/ | member (write) / viewer (read) | Router ViewSet |
| POST | /api/recurring-tasks/<id>/run-now/ | manager | Immediate generation; guards: inactive → 400, past end_date → 400 |
Generation: management command (invoked by Celery beat hourly), iterates active templates where next_run_at <= today, creates Task, advances next_run_at, stamps last_run_at. Idempotent per (template, date).
6. UI / UX
/work/recurring(RecurringPage.tsx) — template list with frequency, next run, active toggle.- Mental model for users: “việc lặp lại” = a checklist that writes itself. Creating one looks exactly like creating a task + picking a frequency. No cron syntax ever shown;
rruleis power-user/API-only. - States: empty (explain the value: “Việc lặp lại sẽ tự tạo task — thử ‘Thu công nợ mỗi thứ 6’”), loading skeletons.
- Edit + Delete per template (added 2026-06-19): Edit opens a dialog pre-filled with template_title/template_description/frequency/start_date →
PATCH /recurring-tasks/:id/; confirmation-gated Delete →DELETE /recurring-tasks/:id/. NewuseUpdateRecurring/useDeleteRecurringhooks (hooks/queries/useRecurring.ts) +recurringApi.delete. Test idsrecurring-edit-{id}/recurring-delete-{id}. - Recurring detail shows
next_run_atas a localized human-readable fact for every parseable date, with the semantic urgency badge alongside overdue/today values. Tomorrow remains relative; later dates use the shared absolute formatter with the active locale’s month resources. It must never expose the backend ISO date directly. - The recurring list uses the same active-locale month resources for later next-run dates; the translated short-date resource owns month/day order (
July 14in English,14 Tháng 7in Vietnamese). It must not hard-code English month abbreviations. - Shared mobile detail/edit: the detail header keeps Edit and Delete inside the viewport; Edit opens the populated shared form, whose in-flow Save action stays above the software keyboard. Emulator proof is diagnostic only. PM-owned Android floor-device and iPhone 12 evidence remains required.
- Shared mobile list recovery: populated refresh failures keep the last committed template directory and Run now actions visible behind one compact warning. Retry re-enters refresh without discarding stale rows, and a successful create from stale state appends without dropping existing templates. Initial empty failure remains blocking.
- Shared mobile detail recovery: populated refresh failures keep the exact recurring template facts plus Edit, Delete, and Run now visible behind one compact warning. Retry re-enters refresh with that same snapshot; initial empty failure remains blocking.
- Android organization switching: the Recurring route keys its model to the active organization. Switching organizations remounts and reloads the directory, so Work Graph counts and the destination cannot diverge because of a previous tenant snapshot. iOS already constructs the recurring controller with the current organization ID.
- i18n:
recurring.
7. Test Plan
| # | Case | Type | Where |
|---|---|---|---|
| 1 | Template CRUD | backend | tests/test_api.py::TestRecurringTask, tests/test_lumi.py::TestRecurringTask |
| 2 | Generation: creates task, advances next_run, idempotent, respects end_date/is_active | backend | tests/test_api.py::TestRecurringGeneration |
| 3 | run_now guards (inactive → 400, past end_date → 400) | backend | TestF06RunNowGuards |
| 4 | Reactivation: next_run_at recomputes from today | backend | TestF06Reactivation |
| 5 | List + create flow | e2e | describe("Recurring Tasks") |
| 6 | List and detail next-run dates stay localized and humanized for overdue/today/later states | shared JVM + native audit | RecurringCardMetaTest · test_f06_recurring_detail_date_policy.py · WRK-07.U3 and WRK-08.U7 device tours |
| 7 | Mobile detail actions remain visible; edit Save remains keyboard-safe | shared JVM source invariant + named-device runtime | RecurringDetailHeaderSourceTest; .maestro/critical/57_recurring_edit_keyboard.yaml mechanics flow; FRM-12 PM walkthrough |
| 8 | Populated mobile list survives repeated refresh failure and explicit Retry without losing templates or Run now actions | shared JVM + source policy + Android lifecycle | RecurringScreenModelTest.repeatedRefreshFailureAndRetryPreserveStaleRecurringDirectory; test_f06_recurring_mobile_recovery_policy.py; .maestro/critical/158_android_recurring_refresh_recovery.yaml |
| 9 | Populated mobile detail survives repeated refresh failure and explicit Retry without losing the exact template facts or actions | shared JVM + source policy + Android lifecycle | RecurringDetailScreenModelTest.repeatedRefreshFailureKeepsRecurringDetailAndExplicitRetryRecovers; test_f06_recurring_mobile_recovery_policy.py; .maestro/critical/158_android_recurring_refresh_recovery.yaml |
| 10 | Android organization switching remounts the Recurring directory with a fresh model keyed to the active organization | Android JVM source contract | LumiAppOrgScopeTest.recurringRootCreatesFreshModelInsideEachOrganizationScopedRemember |
8. Current Behavior
Shipped. Recurring task definitions and scheduled generation exist, with the Celery beat worker path creating ordinary tasks from daily, weekly, and monthly rules inside the same tenant-scoped work model. Scheduler wired (2026-06-10) — no longer “works when poked.” Reactivation fix (a9b72cc0d) — next_run_at recomputes from today on is_active False→True. run_now guards: inactive → 400, past end_date → 400. The stored auto_create_task boolean is currently compatibility-only; the scheduler does not branch on it. Native recurring list and detail surfaces format every parseable next-run fact through the localized shared date vocabulary and reserve detail-header width for Edit/Delete. Populated list and detail surfaces now preserve committed template context and actions through refresh failure with explicit recovery. Android now remounts the recurring directory for the active organization instead of retaining a process-lifetime tenant snapshot; iOS already creates the controller from the current organization ID. Named-device WRK-07.U3, WRK-08.U7, FRM-12, organization-switch runtime, and cross-platform recovery evidence remain PM-owned and open.
9. Error States
| # | Error | Response | Notes |
|---|---|---|---|
| ES1 | Missing required template_title | 400 {"template_title": ["This field is required."]} | Implicit in CRUD tests |
| ES2 | Missing required start_date | 400 {"start_date": ["This field is required."]} | Implicit in CRUD tests |
| ES3 | Invalid frequency value | 400 {"frequency": ["\"yearly\" is not a valid choice."]} | test_create_recurring_task_rejects_invalid_frequency |
| ES4 | Cross-tenant recurring task access | 404 | test_recurring_tenant_isolation |
| ES5 | run_now on non-existent template | 404 | DRF get_object behavior |
| ES6 | end_date before start_date | 200 — silently succeeds; template never generates (end_date < today guard) | test_create_recurring_task_with_end_date_before_start_accepted |
| ES7 | Frontend load failure | toast.error(t('error_load')) | No E2E test |
| ES8 | Frontend create failure | toast.error(t('error_create')) | No E2E test |
| ES9 | Celery beat not running | Silent failure — tasks stop generating; no error, no alert | No monitoring or health check for beat liveness |
| ES10 | Celery worker not running | Task queued but never executed; same silent failure | No monitoring |
| ES11 | DB constraint violation on generated task | Exception bubbles up; template’s next_run_at not advanced; retry on next cycle | Not unit testable (requires injecting DB failure into Celery task) |
| ES12 | run_now on inactive template | 400 {"detail": "Cannot run a recurring task that is inactive."} | Fixed; TestF06RunNowGuards::test_run_now_inactive_returns_400 |
| ES13 | run_now past end_date | 400 {"detail": "Cannot run a recurring task past its end date."} | Fixed; TestF06RunNowGuards::test_run_now_end_date_past_returns_400 |
| ES14 | Populated native list refresh fails | Keep last committed templates and Run now actions behind one compact warning; explicit Retry refreshes | Shared JVM/source policy plus guarded Android flow 158; iOS/named-device/accessibility/store/deployed/production proof open |
| ES15 | Android switches organization after Recurring was previously loaded | Remount the Recurring route with a fresh model keyed to the selected organization | Android source regression; reporter-device organization-switch confirmation remains open |
10. Diagrams & Docs Touched
backend/DIAGRAMS.md.
11. Implementation Audit (2026-06-13)
| # | Claim | Status | Evidence |
|---|---|---|---|
| 1 | Template CRUD, active toggle | ✅ match | TestRecurringTask; test_recurring_tenant_isolation |
| 2 | Generation: one task per (template, date); advances next_run_at; respects end_date/is_active | ✅ match | TestRecurringGeneration::test_generate_recurring_tasks_idempotent; test_end_date_equals_today_generates_task |
| 3 | Celery beat scheduler wired (hourly); no longer “works when poked” | ✅ match | CELERY_BEAT_SCHEDULE in config/settings/base.py; apps/work/tasks.py::generate_recurring_tasks — closes §9 blocking gap |
| 4 | Inactive re-enable: next_run_at recomputes from today (not from gap) | ✅ match | a9b72cc0d; TestF06Reactivation::test_reactivation_recomputes_next_run_from_today |
| 5 | run_now inactive → 400 guard | ✅ match | Fixed this PR; TestF06RunNowGuards::test_run_now_inactive_returns_400 |
| 6 | run_now past end_date → 400 guard | ✅ match | Fixed this PR; TestF06RunNowGuards::test_run_now_end_date_past_returns_400 |
| 7 | Monthly frequency: both generate and run_now use relativedelta(months=1) — consistent | ✅ match | apps/work/tasks.py:53 (relativedelta(months=1)); apps/work/views.py:907 (relativedelta(months=1)); divergence reported in prior audit has been fixed |
| 8 | Concurrent Celery execution: no select_for_update, no idempotency key on (template, date) | ⚠️ gap | E7; two concurrent workers can produce duplicate tasks; test_generate_recurring_tasks_idempotent covers sequential not concurrent |
| 9 | auto_create_task remains in the schema/DTO contract as a compatibility field, but shipped recurring generation always auto-creates tasks and does not branch on it. | ✅ match | RecurringTask.auto_create_task exists; generate_recurring_tasks_for_date() never checks it; test_auto_create_task_false_still_generates proves generation still occurs |
| 10 | Custom rrule frequency falls through to daily (+1 day advance) — rrule string never parsed | ⚠️ gap | E16; test_custom_frequency_advances_by_one_day; custom rrule is API-only and effectively = daily |
| 11 | Visual QC: recurring-task create CTA uses explicit task-language in Vietnamese | ✅ match | 2026-06-18 screenshot pass; vi/recurring.json uses + Tạo công việc định kỳ; RecurringPage.test.tsx |
| 12 | Visual QC: recurring templates explain the self-writing checklist model and label schedule inputs plainly | ✅ match | 2026-06-18 Recurring pass; RecurringPage.tsx adds helper copy plus visible Tần suất / Ngày bắt đầu labels and chip-style schedule metadata |
| 13 | Android Recurring directory remounts when the active organization changes | ✅ match | LumiAppFeatureEntries.kt uses remember(orgScopedScreenKey(currentOrgId, "recurring")); LumiAppOrgScopeTest.recurringRootCreatesFreshModelInsideEachOrganizationScopedRemember |
Summary: 11/13 match; 2 gaps (concurrent Celery race, rrule never parsed). Monthly relativedelta divergence (formerly row 7 ⚠️) confirmed fixed — both paths use relativedelta(months=1).
Edge Cases Catalog (formerly §13, 2026-06-12; E1–E16): Key items integrated above. Remaining: E1 past next_run_at catch-up (one task per cycle, not batch); E2 monthly edge via run_now (FIXED — row 7, both paths now use relativedelta(months=1)); E3 timezone crossing midnight (no timezone awareness, UTC server date); E4 end_date in the past on create (silently succeeds, never generates — ES6); E5 end_date == next_run_at today (generates, correct — end_date < today check); E6 task creation failure mid-batch (exception bubbles, template retries — ES11); E8 auto_create_task=False remains a compatibility-only field rather than a separate shipped mode (row 9); E9 inactive re-enable (FIXED — row 4); E10 run_now on inactive (FIXED — row 5); E11 run_now past end_date (FIXED — row 6); E12 owner SET_NULL (tested); E13 project/team SET_NULL (tested); E14 monthly timedelta(days=30) divergence (FIXED — row 7); E15 next_run_at=NULL never generates (tested). Full catalog in git history (2026-06-12 changelog entry).
12. Open Questions
- Custom
rruleis API-only — leave undocumented in UI until someone needs more than daily/weekly/monthly.
13. Bug / Change Log
2026-08-11 — Android recurring directory follows the active organization
Type: Tenant-state consistency bug fix (Android)
Summary:
- Work Graph already rebuilt its summary model after an organization switch, but the Recurring destination reused one lazy process-lifetime model and could still show the previous tenant’s empty snapshot.
- The Android route now creates the recurring model inside an organization-scoped
remember, so switching organizations remounts and reloads the directory before it is compared with the new Work Graph count. - A focused Android host regression protects both the factory and route-key ownership. Sentry
lumi-androidhad no matching exception at the 2026-08-11 pre-fix check; reporter-device runtime confirmation remains open.
2026-08-10 — Mobile recurring detail keeps its exact rhythm through refresh failure
Type: Recovery UX bug fix (shared Android/iOS)
Summary:
- The detail model stored the last recurring template in
LoadState.Error.last, but the UI treated every error as blocking; Retry usedload()and could discard that stored snapshot. - Populated failures now keep the exact template facts and Edit/Delete/Run now actions visible behind one compact one-line warning. Explicit Retry refreshes with that same snapshot, including across repeated failure. Initial empty failures remain blocking.
- Focused shared tests and exact-artifact guarded Android flow 158 cover repeated exact-detail outage, explicit recovery, template custody, evidence retention, cleanup, and runtime shutdown. iOS, named-device, screen-reader-service, store, deployed, and production evidence remain open.
2026-08-10 — Mobile recurring refresh recovery preserves operating rhythms
Type: Recovery UX bug fix (shared Android/iOS)
Summary:
- The model retained populated templates in
LoadState.Error.last, but the list replaced them with a blocking error; Retry and create reconciliation then read only successful state. - Populated failures now keep templates and Run now actions visible behind one concise warning. Explicit Retry refreshes with the same snapshot, and create reconciliation appends to current-or-stale data. Initial empty failures remain blocking.
- Focused shared tests and exact-artifact guarded Android flow 158 cover repeated outage, explicit recovery, template/frequency/active custody, evidence retention, cleanup, and runtime shutdown. iOS, named-device, screen-reader-service, store, deployed, and production evidence remain open.
2026-08-03 — Localize later-date labels in the recurring list (WRK-07.U3)
Type: Bug Fix (shared mobile presentation)
Summary:
- Replaced the recurring list’s hard-coded English
Jan–Dectable with the existing localized month resources and a translated month/day ordering resource. - English later dates now read
July 14; Vietnamese reads14 Tháng 7. Relative today/tomorrow, weekday labels, overdue urgency, and the detail fact table keep their existing behavior.
Reason:
- The list localized weekday names but leaked English month abbreviations for dates at least 14 days away, so WRK-07/08 failed the universal language gate even when the account language was Vietnamese.
Scope boundary:
- Source and shared-JVM proof close the hard-coded-copy defect. Named Android/iPhone rendering and PM score promotion remain open; this change does not claim device evidence.
Tests:
F06RecurringDetailDatePolicyTests.test_list_path_uses_localized_compact_date_formatRecurringCardMetaTest.short_date_uses_locale_owned_month_and_day_order- Guarded full
:shared:jvmTestsuite
2026-07-15 — Mobile: restore reachable recurring detail actions
Type: User-facing UX bug fix (shared KMP detail layout)
Summary:
LumiPageTitlefills available width by design. The recurring detail header omitted the establishedModifier.weight(1f)constraint, so Edit and Delete were laid out beyond the right edge and users could not enter the edit form.- Weighted the title inside the action-bearing row and added a focused source invariant.
- Exact-APK testing then exposed a second layer: the Android phone shell applied
WindowInsets.safeDrawingto persistent bottom navigation. BecausesafeDrawingincludes the IME, the bottom bar grew to keyboard height and collapsed the routed edit surface. The shell now reserves navigation-bar and display-cutout insets only and leaves Scaffold measurement while the IME is visible, placing the in-flow action at the form-owned keyboard edge. - Kept FRM-12 named-device keyboard proof explicit rather than inferring it from an emulator.
Tests:
RecurringDetailHeaderSourceTestPhoneNavigationInsetsSourceTest- Exact-APK Android Maestro
.maestro/critical/57_recurring_edit_keyboard.yaml
2026-07-14 — Mobile: keep Edit Recurring Save above the keyboard (FRM-12)
Type: UX fix (shared KMP form layout)
Summary:
- Edit Recurring now uses a weighted scroll region with an in-flow Save CTA so the keyboard cannot cover the action.
- Added stable screen/action tags and a source guard against bottom-overlay Save regressions.
Scope boundary:
- Source hardening only. FRM-12 populated edit, validation, and named-device keyboard evidence remain PM-owned mobile-audit work.
Tests:
git diff --checkPYENV_VERSION=system python3 scripts/check_docs_truth.pybash scripts/check-platform-parity.sh
2026-07-16 — Mobile: remove raw ISO dates from recurring detail (WRK-08.U7)
Type: Bug Fix (mobile presentation)
Summary:
- Recurring detail previously rendered
next_run_atwithtake(10)for overdue and today states, leaking values such as2026-07-14beside an urgency badge. - The detail path now uses the localized shared absolute-date formatter for every parseable date while preserving the existing tomorrow-relative and malformed-input behavior.
Reason:
- The earlier raw-date class sweep fixed the list and most detail surfaces, but its source invariant did not catch this local-variable render path. A device-caught U7 regression remained possible.
Impact:
- Users see a human-readable date in recurring detail; no API or persistence behavior changes. PM must still re-verify WRK-08 in English and Vietnamese on named devices.
Tests:
RecurringCardMetaTestcovers overdue, today, tomorrow, later, and malformed dates.scripts/check-platform-parity.shand the guarded shared JVM suite.
2026-07-13 — Contract Graph migration and recurring-task authorization boundary
Type: Contract and authorization traceability
Summary:
- Added the canonical F06 CUE contract for the seven template CRUD/run-now endpoint surfaces and attached executable proof IDs for CRUD, invalid frequency, tenant isolation, generation, guards, and role boundaries.
- Contract proof collection found that
RecurringTaskViewSetapplied onlyIsOrgMemberto every action, allowing viewers to mutate templates and any active member to call manager-onlyrun_now. AddedIsRecurringWriterfor template mutations andIsOrgManagerforrun_now. - Generation remains linked to the same contract through
generate_recurring_tasks_for_date; custom rrule parsing, concurrent idempotency, scheduler liveness, and named runtime evidence remain gaps.
Impact: Viewer recurring-template access is now read-only; run_now requires effective manager
capabilities; existing active non-viewer template writes remain allowed.
Tests: The Contract Graph verifier collects and passes all 10 F06 pytest proof records. F06 remains
partial because the known scheduler/rrule/runtime gaps are explicit in contracts/features/F06.cue.
2026-07-02 — Documentation truth: §11 row 7 ⚠️ → ✅ (monthly relativedelta divergence fixed)
Type: Documentation Change
Summary:
- §11 row 7 claimed
run_nowusestimedelta(days=30)whilegenerateusesrelativedelta(months=1)— a divergence that would cause incorrect next_run dates in February. Verified against live code:apps/work/tasks.py:53andapps/work/views.py:907both importdateutil.relativedelta.relativedeltaand userelativedelta(months=1). The prior ⚠️ gap no longer applies. Row 7 upgraded to ✅; §11 summary updated from 9/12 to 10/12 match. - Edge Cases Catalog note updated to mark E2 and E14 as FIXED.
Reason:
grep -n "relativedelta\|timedelta.*30\|days=30\|months=1"on both files confirmed the fix is already shipped. The divergence was a code bug that was corrected at an earlier date; the spec was never updated.
Impact:
- Documentation only. No backend, frontend, or mobile behavior changed.
Tests:
- None (documentation change; code fix already merged).
2026-07-02 — Clarify auto_create_task as compatibility-only
Type: Documentation Change
Summary:
- Corrected §4, §8, and §11 so F06 no longer overstates
auto_create_taskas an active behavioral switch. - Documented the real shipped contract: recurring templates always auto-materialize tasks when due; the stored boolean remains in the schema/DTO surface but the generator does not branch on it.
- Reclassified audit row 9 from spec drift to an explicit compatibility note so the remaining gaps focus on real scheduler debt.
Reason:
generate_recurring_tasks_for_date()does not inspectauto_create_task, and the existing regression test already provesauto_create_task=Falsestill generates a task.
Impact:
- Documentation only. No backend, frontend, or mobile behavior changed.
Tests:
- None (documentation change).
2026-06-19 — Add recurring-template Edit + Delete
Type: Feature (CRUD completion)
Summary: RecurringTaskViewSet (ModelViewSet) supported PATCH/DELETE but the UI had only create + run-now — no edit or delete, and no mutation hooks existed. Added useUpdateRecurring/useDeleteRecurring (new hooks/queries/useRecurring.ts), recurringApi.delete, and per-template Edit dialog + confirmation Delete in RecurringPage.tsx. i18n keys added to en/vi. See §6.
2026-06-18 — Visual QC: make recurring templates read like self-writing checklists
Type: UX Fix
Summary:
- The Recurring Tasks page was technically correct but visually flat: the form controls were unlabeled in the visible UI, and each template card collapsed all schedule metadata into one washed-out line.
- Added a short helper band that explains the core value, made
Tần suấtandNgày bắt đầuvisible labels in the create form, and restyled schedule metadata into clearer chips so each template reads like a checklist that writes itself.
Verification:
frontend/screenshots/08-recurring-tasks/01-recurring-tasks-list.pngfrontend/screenshots/08-recurring-tasks/02-add-recurring-task-form.pngpnpm test -- src/test/RecurringPage.test.tsxpnpm exec playwright test tests/e2e/screenshot-flow.spec.ts --project=chromium --reporter=list -g "08-recurring-tasks"./node_modules/.bin/tsc --noEmit
2026-06-18 — Visual QC: clarify recurring-task creation copy
Type: UX Fix
Summary:
- The Vietnamese Recurring Tasks screenshot showed
+ Tạo định kỳ, a clipped CTA beside the page titleCông việc định kỳ. - The CTA now says
+ Tạo công việc định kỳ, matching the page’s mental model: a task checklist that writes itself.
Verification:
frontend/screenshots/08-recurring-tasks/01-recurring-tasks-list.pngpnpm test -- src/test/RecurringPage.test.tsxpnpm exec playwright test tests/e2e/screenshot-flow.spec.ts --project=chromium --reporter=list -g "08-recurring-tasks"
2026-06-15 — Frontend route namespaced
Frontend route namespaced: /recurring -> /work/recurring (app-wide IA restructure; API paths unchanged).
2026-06-14 — Spec restructured to TEMPLATE.md canonical order (§1–§13)
Type: Documentation Change
Summary:
- §3 Competitive Positioning and §4 Success Metrics moved to §14/§15 appendix positions.
- §5 Scope → §3, §6 Data Model → §4, §7 API → §5, §8 UI → §6, §9 Test Plan → §7, §10 Current Behavior → §8, §11 Error States → §9, §12 Diagrams → §10, §13 Implementation Audit → §11, §14 OQ → §12, §15 BL → §13.
- Updated §11 row 3 reference: “closes §11 blocking gap” → “closes §9 blocking gap.”
Reason:
- Align with TEMPLATE.md canonical section order per SP fleet restructuring pass.
Impact:
- Spec authors and future implementation agents.
2026-06-13 — F06 run_now guards: inactive + past end_date (E10, E11)
Type: Bug Fix
Summary:
run_nownow returns 400 for inactive templates (E10) and for templates past their end_date (E11).- Tests:
TestF06RunNowGuards—test_run_now_inactive_returns_400,test_run_now_end_date_past_returns_400, regression guards.
2026-06-13 — Reactivation recomputes next_run_at from today (E9, a9b72cc0d)
Type: Bug Fix
Summary:
RecurringTask.save()detectsis_activeFalse→True and setsnext_run_at = max(today, start_date).- Tests:
TestF06Reactivationintest_red_r33_h.py.
2026-06-12 — Add Edge Cases (E1–E16) and Error States (ES1–ES11) to F06 spec
Type: Documentation Change
2026-06-10 — Wire Celery beat scheduler for recurring task generation
Type: Feature
Summary:
- Added
apps/work/tasks.py::generate_recurring_tasks(Celery shared task), registered hourly inCELERY_BEAT_SCHEDULE. - Added
config/celery.py,worker/beatservices indocker-compose.yml,make worker/make beattargets. - Closes §9 blocking gap: “a rhythm feature that depends on a human running a management command is a broken promise.”
14. Competitive Positioning (Appendix)
vs Trello (no recurrence)
Trello has no native recurring task functionality. Users must manually duplicate cards each cycle or install Butler power-up with per-board rules. Neither surfaces when a recurring task is missed — the CEO must remember to check. Lumi’s pulse-linked recurrence auto-generates tasks from a one-field template and surfaces skipped runs in the Brief without anyone asking.
vs ClickUp (over-engineered repetition)
ClickUp offers recurrence on every task with granular options (repeat every N days/weeks/months, skip weekends, end after X occurrences). This is feature-rich but requires a trained user to set up correctly. For an SME manager who wants “check receivables every Friday”, ClickUp’s UI is intimidating. Lumi asks for one frequency pick (daily/weekly/monthly) and one optional end date. The template hides all cron/RRULE complexity behind a simple “việc lặp lại” mental model.
vs Asana (timeline-only repetition)
Asana recurrence is tied to timeline/project schedules — it does not connect to daily check-ins or CEO signal extraction. Lumi’s recurring tasks are cross-project and cross-team: a “bi-monthly cash count” pattern generates tasks regardless of which project it belongs to, and the CEO sees the overdue generated task in the morning Brief alongside pulse-extracted risks.
Lumi differentiator: pulse-linked recurrence
Lumi is the only tool where a recurring task can be discovered from a pulse: a team member submits “cần nhắc khách công nợ mỗi tuần” and a manager converts it into a RecurringTask template with one click. The generated task becomes a first-class Task (source_type=recurring) with business impact, due date, and owner. The Brief does not distinguish between “task created by human” and “task created by recurrence template.” Missed runs surface identically.
15. Success Metrics (Appendix)
| Metric | Target | Why it matters |
|---|---|---|
| Recurrence setup time | <30s after task-like name entry | One field (template_title) + one frequency picker — no cron, no rrule |
| Auto-generation success rate | >99% of scheduled runs create a task | Generation is idempotent per (template, date); Celery beat fires hourly |
| CEO interaction rate | >40% of generated recurring tasks viewed or acted on via Brief | Silent generation is waste |
| Template activation toggle usage | >60% of templates toggled at least once per quarter | Pause/resume is the primary management action |
| No-show generation alerts | 100% of missed generation cycles produce an alert | Celery beat/worker downtime must be monitored |
| Pulse-to-template conversion rate | >5% of relevant pulse entries become RecurringTask templates | Signal that recurring tasks are discovered from team input |