State Machine
How leads move through stages and statuses
Every lead has two fields that control its lifecycle: status and stage. Status is the high-level bucket (open or resolved). Stage is the specific workflow position within that status.
Statuses
| Status | Meaning |
|---|---|
open | Lead is active and being worked |
resolved | Lead has reached a terminal outcome |
Stages
Open stages
These represent where the lead is in the contact-to-appointment workflow.
| Stage | Meaning | How a lead gets here |
|---|---|---|
new | Just created, no contact attempt yet | Initial state on creation |
attempted | Agent has tried to contact | First contact_attempted activity |
callback | Lead requested a callback | callback_requested activity, or appointment cancelled |
scheduled | Appointment is booked | appointment_scheduled activity |
no_show | Lead didn't show for appointment | appointment_no_show activity |
Resolved stages (terminal outcomes)
These end the lead's lifecycle. Once resolved, no further stage transitions occur.
| Stage | Meaning | Constraints |
|---|---|---|
sale | Lead purchased | Only from scheduled stage |
no_sale | Presentation given, no purchase | Only from scheduled stage |
refused | Lead explicitly refused | Any open stage |
wrong_number | Phone number belongs to someone else | Any open stage |
bad_number | Phone number disconnected/invalid | Any open stage |
unresponsive | Lead stopped responding | Cannot be applied from new (must have been contacted first) |
duplicate | Duplicate of another lead | Any open stage |
dnc | Do Not Contact — lead requested no further contact | Any open stage |
Transition diagram
new
│
│ contact_attempted
▼
attempted
│
├─── callback_requested ──► callback
│ │
│ │ appointment_scheduled
│ ▼
├─── appointment_scheduled ──► scheduled ◄── appointment_scheduled ── no_show
│ │ │ ▲
│ │ │ appointment_no_show │
│ │ └────────────────────────────────────┘
│ │
│ │ appointment_cancel
│ └──► callback
│
│ (from any open stage)
├──► refused [resolved]
├──► wrong_number [resolved]
├──► bad_number [resolved]
├──► duplicate [resolved]
├──► dnc [resolved]
│
│ (from any except 'new')
└──► unresponsive [resolved]
sale ◄── (only from scheduled) [resolved]
no_sale ◄── (only from scheduled) [resolved]How transitions happen
Transitions are not managed by the UI. They happen automatically in the database via the update_lead_status_based_on_activity trigger function, which fires on every lead_activity INSERT.
The trigger:
- Reads the new activity's
type - Looks up the lead's current
stageandstatus - If
status = 'resolved', exits early (no further transitions) - Applies the transition rules and updates
leads.stageandleads.status
This means the UI only needs to log the correct activity type — it never directly writes to stage or status.
Terminal outcome rules
The trigger enforces two constraints on terminal outcomes:
-
saleandno_salecan only be applied whenstage = 'scheduled'. The presentation flow enforces this — you can't start a presentation without an appointment. -
unresponsivecannot be applied whenstage = 'new'. A lead must have been contacted at least once before being marked unresponsive.
All other terminal outcomes (refused, wrong_number, bad_number, duplicate, dnc) can be applied from any open stage.
Re-opening leads
There is currently no mechanism to re-open a resolved lead. Once resolved, the lead stays resolved. If needed, a new lead can be created manually.