# Lead Lost & Re-engagement

**Date:** 2026-05-31  
**Status:** Draft  
**Spec:** 111  
**Tier:** All tiers (re-engagement sequences: Business+)  
**Depends on:** `marketing-leads-pipeline`, `email-marketing-sequences`, `leads-detail-view`, `foundation-auth-rbac`  
**Referenced by:** `marketing-leads-pipeline`, `email-marketing-sequences`

---

## Overview

Spec 22 (`marketing-leads-pipeline`) defines `leads.stage` with a `'LOST'` terminal state. This spec defines the "Mark as Lost" flow (capture reason, optional cool-down), the lost leads view, and the re-engagement pathway (reopen lead or trigger a re-engagement sequence).

---

## Mark as Lost

From the leads detail view (`/leads/:id`) or Kanban card: **[Mark Lost]** button (visible when stage ≠ 'LOST' and ≠ 'WON').

```
┌──────────────────────────────────────────────────────────────┐
│  Mark lead as lost                                    [✕]    │
│                                                              │
│  Why was this lead lost?                                     │
│  ○ Price too high                                            │
│  ○ Chose competitor                                          │
│  ○ Not the right time                                        │
│  ○ No budget                                                 │
│  ○ Unresponsive                                              │
│  ○ Other: [__________________________________]               │
│                                                              │
│  Re-engage after:                                            │
│  ○ Never   ○ 30 days   ○ 90 days   ● 6 months               │
│                                                              │
│  [Cancel]              [Confirm lost]                        │
└──────────────────────────────────────────────────────────────┘
```

On confirm:
- `leads.stage = 'LOST'`
- `leads.lost_reason` set (new column)
- `leads.reengagement_at` set (new column, `NULL` if 'Never')
- Activity log entry: `type = 'stage_changed'`, `metadata = { from: prevStage, to: 'LOST', reason }`

---

## Lost Reasons (Tenant-Configurable)

Default reasons above. Tenant can customize via `tenant_settings.lead_lost_reasons JSONB` (array of strings). Managed at `/settings/crm`.

---

## Lost Leads View

Filter on `/leads` pipeline: **[Lost]** tab (separate from Kanban — shown as sortable list):

```
┌──────────────────────────────────────────────────────────────┐
│  Leads — Lost                                                │
│                                                              │
│  Search: [__________]  Reason: [All ▾]  Period: [All ▾]     │
│                                                              │
│  Lead              Reason              Lost on   Re-engage   │
│  ─────────────────────────────────────────────────────────── │
│  Acme Corp         Price too high      May 20    Nov 20 📅    │
│  BuildCo Ltd       Unresponsive        May 15    —           │
│  TechStart         Chose competitor    Apr 30    Jul 30 📅    │
│                                                              │
│  [Reopen] on each row                                        │
└──────────────────────────────────────────────────────────────┘
```

`Re-engage` column shows `reengagement_at` date. 📅 icon turns red when past due.

---

## Re-engagement Cron

Daily cron `lead-reengagement` (07:00 UTC):

```sql
SELECT l.*, u.name as owner_name
FROM leads l
JOIN users u ON l.assigned_to = u.id
WHERE l.stage = 'LOST'
  AND l.reengagement_at <= now()
  AND l.reengagement_notified_at IS NULL
  AND l.tenant_id = :tenantId
```

For each: create notification `type = 'lead_reengagement_due'` for the assigned user (extends spec 97 NotificationType taxonomy — add `'lead_reengagement_due'` to the union):
> "Re-engage Acme Corp — cool-down period ended"

Sets `reengagement_notified_at = now()` to prevent repeat notifications.

---

## Reopen Lead

**[Reopen]** on any lost lead:

```
┌──────────────────────────────────────────────────────────────┐
│  Reopen lead                                          [✕]    │
│                                                              │
│  Move back to:                                               │
│  ● New   ○ Contacted   ○ Qualified                           │
│                                                              │
│  Note: [_______________________________]                     │
│                                                              │
│  [Cancel]              [Reopen]                              │
└──────────────────────────────────────────────────────────────┘
```

- Resets `leads.stage` to chosen stage
- Clears `leads.reengagement_at`, `leads.reengagement_notified_at`
- Logs activity: `type = 'stage_changed'`, `metadata = { from: 'LOST', to: newStage }`

---

## Re-engagement Sequence (Business+)

From the lost leads list: **[Start sequence ▾]** per lead:

Dropdown shows active email sequences (spec 99 `email-marketing-sequences`) tagged for `trigger = 'manual'`. Selecting one enrolls the lead's email in that sequence immediately.

`POST /api/email-sequences/:seqId/enroll` — body: `{ leadId }` (uses `leads.email`).

---

## Schema Delta

```sql
ALTER TABLE leads ADD COLUMN lost_reason TEXT;
ALTER TABLE leads ADD COLUMN reengagement_at TIMESTAMPTZ;
ALTER TABLE leads ADD COLUMN reengagement_notified_at TIMESTAMPTZ;
```

`tenant_settings` delta:
```sql
ALTER TABLE tenant_settings ADD COLUMN lead_lost_reasons JSONB DEFAULT
  '["Price too high","Chose competitor","Not the right time","No budget","Unresponsive"]';
```

---

## API

```
PATCH /api/leads/:id
      → existing endpoint; accepts { stage: 'LOST', lost_reason, reengagement_at? }
        Requires: marketing:write

PATCH /api/leads/:id/reopen
      → reopen lost lead
        body: { stage: 'NEW'|'CONTACTED'|'QUALIFIED', note?: string }
        Requires: marketing:write

GET /api/leads?stage=LOST
    → existing list endpoint; existing filter param
      Requires: marketing:read
```

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| lost_reason as free-text | Not foreign key to enum table | Tenants need custom reasons; JSONB config for defaults + "Other" free text covers all cases without complexity |
| re-engagement notification (not auto-sequence) | Notify only | Auto-enrolling in a sequence on a re-engagement timer is risky — staff should decide whether to reach out; notification is safer |
| Lost tab separate from Kanban | Not a Kanban column | Lost leads are a historical view, not a work-in-progress column; mixing them with active pipeline clutters the board |
