# Recurring Tasks & Templates

**Date:** 2026-05-31  
**Status:** Draft  
**Spec:** 113  
**Tier:** All tiers  
**Depends on:** `tasks-board-engine`, `projects-module`, `foundation-auth-rbac`  
**Referenced by:** `tasks-board-engine`, `projects-module`

---

## Overview

Spec 8 (`tasks-board-engine`) defines the `tasks` table with no recurrence support. This spec adds recurring task definitions (cron-like schedule) that auto-generate task instances, and task templates that accelerate task creation with pre-filled fields.

---

## Recurring Task Definition

A recurring task is a template row with a recurrence rule. Separate table from `tasks`:

```sql
CREATE TABLE recurring_tasks (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES tenants(id),
  project_id UUID REFERENCES projects(id),
  title TEXT NOT NULL,
  description TEXT,
  assignee_id UUID REFERENCES users(id),
  estimated_hours NUMERIC(6,2),
  priority TEXT DEFAULT 'MEDIUM',  -- 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT'
  labels TEXT[] DEFAULT '{}',
  recurrence TEXT NOT NULL,         -- RFC 5545 RRULE, e.g. 'FREQ=WEEKLY;BYDAY=MO'
  advance_days INTEGER DEFAULT 1,   -- create instance N days before due date
  status_id UUID REFERENCES task_statuses(id),  -- starting status for generated tasks
  is_active BOOLEAN DEFAULT true,
  created_by UUID NOT NULL REFERENCES users(id),
  created_at TIMESTAMPTZ DEFAULT now(),
  last_generated_at TIMESTAMPTZ
);
```

---

## Recurring Task UI

`/tasks/recurring`:

```
┌──────────────────────────────────────────────────────────────┐
│  Recurring Tasks                        [+ New recurring]    │
│                                                              │
│  Monthly VAT filing       Monthly (1st)   Finance   ✓ Active │
│  Weekly team standup      Weekly Mon       General   ✓ Active │
│  Quarterly review         Every 3 months  Mgmt      ✓ Active │
│                                                              │
│  [Edit]  [Pause]  [Delete] per row                           │
└──────────────────────────────────────────────────────────────┘
```

**[+ New recurring]** → create modal with RRULE picker (daily, weekly, monthly, custom).

RRULE picker:

```
┌──────────────────────────────────────────────────────────────┐
│  New recurring task                                   [✕]    │
│                                                              │
│  Title:  [Monthly VAT filing______________________]          │
│  Project: [Finance ▾]   Assignee: [Alex Cohen ▾]            │
│  Estimated: [2.0 h]   Priority: [HIGH ▾]                    │
│                                                              │
│  Repeats:                                                     │
│  ○ Daily   ○ Weekly   ● Monthly   ○ Custom (RRULE)           │
│  Every [1] month on day [1]                                  │
│                                                              │
│  Create task [1] day(s) before due date                      │
│                                                              │
│  [Cancel]                  [Save]                            │
└──────────────────────────────────────────────────────────────┘
```

---

## Task Generation Cron

Daily cron `recurring-task-generator` (01:00 UTC):

For each active `recurring_tasks` row, expand RRULE to find next occurrence dates within the next `advance_days` window:

```
For each recurring_task where is_active = true:
  occurrences = rrule.between(now(), now() + advance_days)
  for each occurrence:
    if NOT EXISTS (tasks WHERE recurring_task_id = :id AND due_date = :occurrence):
      INSERT INTO tasks (
        tenant_id, project_id, title, description,
        assignee_id, estimated_hours, priority,
        status_id, due_date, recurring_task_id
      ) VALUES (...)  -- returns new task id
      for each label in recurring_task.labels:
        INSERT INTO task_labels (task_id, label) VALUES (:newTaskId, :label)
  UPDATE recurring_tasks SET last_generated_at = now()
```

RRULE expansion: rrule.js (Node, same library as calendar). Duplicate guard: `tasks.recurring_task_id + due_date` unique index prevents re-creation.

---

## Schema Delta

```sql
-- Link generated tasks back to their recurring definition
ALTER TABLE tasks ADD COLUMN recurring_task_id UUID REFERENCES recurring_tasks(id);

CREATE UNIQUE INDEX idx_tasks_recurring_due
  ON tasks(recurring_task_id, due_date)
  WHERE recurring_task_id IS NOT NULL;
```

---

## Task Templates

Templates are reusable task blueprints — not automatically generated, but available as one-click creation starting points.

```sql
CREATE TABLE task_templates (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES tenants(id),
  name TEXT NOT NULL,                          -- template display name
  title TEXT NOT NULL,                         -- task title (can have {{customer}}, {{project}} vars)
  description TEXT,
  estimated_hours NUMERIC(6,2),
  priority TEXT DEFAULT 'MEDIUM',
  labels TEXT[] DEFAULT '{}',
  created_by UUID NOT NULL REFERENCES users(id),
  created_at TIMESTAMPTZ DEFAULT now()
);
```

---

## Template Usage

On "New task" button, a **[From template ▾]** option lists available templates. Selecting one pre-fills the new task form with template values (all editable).

Template variables in `title` and `description`: `{{customer}}` and `{{project}}` replaced with the target project's customer name and project name on creation. No compile step — simple string replace client-side.

`GET /api/task-templates` → list templates for tenant.  
`POST /api/task-templates` → create template.  
`DELETE /api/task-templates/:id` → delete.

---

## API

```
GET /api/tasks/recurring
    → list recurring task definitions
      Requires: tasks:read (OWNER/ADMIN)

POST /api/tasks/recurring
     → create recurring task definition
       body: { title, project_id?, assignee_id?, estimated_hours?, priority?,
               recurrence, advance_days, status_id? }
       Requires: tasks:write (OWNER/ADMIN)

PATCH /api/tasks/recurring/:id
      → update / pause (is_active toggle)
        Requires: tasks:write (OWNER/ADMIN)

DELETE /api/tasks/recurring/:id
       → delete definition (existing generated tasks unaffected)
         Requires: tasks:write (OWNER/ADMIN)

GET /api/task-templates
    → list templates for tenant
      Requires: tasks:read

POST /api/task-templates
     → create template
       Requires: tasks:write

DELETE /api/task-templates/:id
       → delete template
         Requires: tasks:write
```

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| Separate `recurring_tasks` table | Not `tasks.recurrence` column | Recurring definition is a separate entity (blueprint); generated tasks are the instances; mixing schema conflates authoring and execution |
| advance_days window | Not generate months ahead | Generating too far ahead means tasks appear before staff are ready to see them; 1-day default keeps queue manageable |
| Unique index on recurring_task_id + due_date | Not idempotent insert | Unique index makes cron re-runs safe (if cron runs twice, second run no-ops) without requiring transaction-level dedup |
| Templates as separate table | Not recurring_tasks with is_template flag | Templates have no schedule — they're fundamentally different from recurring tasks; same table would require nullable columns and confusing nulls |
