Skip to content

Lifecycle

If you only read one page about the lifecycle feature, read this one. It’s the whole conceptual model in one place: the eight canonical pipeline states, the orthogonal engagement temperature, the communication flags, how they fit together, and what’s still being figured out.

Every lead’s lifecycle position is a tuple of three orthogonal facts: what pipeline stage they’re in, how engaged they are right now, and what communication channels are open. Those three things change at different cadences for different reasons, so they live on three independent axes — not one combined “lead score” column.

Eight discrete stages, ordered by progress through the buying process. Two of them (ARCHIVED, TRASHED) are explicitly not terminal — a lead can come back from either.

stateDiagram-v2
    direction LR
    [*] --> NEW: contact_form_submission

    NEW --> CONTACTING: contact_attempted
    NEW --> TRASHED: bogus_contact_detected

    CONTACTING --> QUALIFIED: motivation_confirmed
    CONTACTING --> ARCHIVED: timeout_21d_no_response
    CONTACTING --> TRASHED: bogus_contact_detected

    QUALIFIED --> COMMITTED: buyer_agreement_signed
    QUALIFIED --> ARCHIVED: timeout_180d_no_progress

    COMMITTED --> IN_CONTRACT: contract_executed
    COMMITTED --> QUALIFIED: deal_collapsed

    IN_CONTRACT --> CLOSED: closing_complete
    IN_CONTRACT --> QUALIFIED: contract_voided

    ARCHIVED --> CONTACTING: lead_re_engaged
    TRASHED --> NEW: contact_info_updated

The state list, with one-line meanings:

StateWhat it means
NEWInquiry arrived. No outreach yet. 5-minute SLA window is running.
CONTACTINGFirst outreach made. Awaiting response.
QUALIFIEDMotivated and shopping. Searching + touring share this column.
COMMITTEDBuyer’s agreement signed. Working with you, not just browsing.
IN_CONTRACTOffer accepted; in escrow.
CLOSEDFunded. Keys exchanged.
ARCHIVEDSet aside after timeout or manual archive. Re-engageable.
TRASHEDBad contact data. Re-enters NEW once the data is updated.

Earlier sketches considered finer-grained columns (separating “searching” from “touring”, separating “drafting offer” from “buyer’s agreement signed”). The design collapsed them where the distinction wasn’t load-bearing for the boardQUALIFIED covers both searching and touring because the operational question (“is this lead actively engaging with property options?”) is the same, and the finer distinction is preserved in the secondary status that older parts of the UI read.

Three transitions move a lead “backward” in the pipeline and the state machine refuses them without an attached reason code:

TransitionReason codeCaptures
COMMITTED → QUALIFIEDdeal_collapsedBuyer’s agreement fell apart before contract.
IN_CONTRACT → QUALIFIEDcontract_voidedContract executed but voided before closing — financing, inspection, mutual release.
NEW → TRASHED or CONTACTING → TRASHEDbogus_contact_detectedPhone is invalid, email bounces, name is gibberish.

The reason code is the reason demotions become countable across the board. See why event sourcing matters for the broker-side argument on why this is enforced rather than optional.

The platform has older status fields from earlier versions (the per-lead status, the per-buyer status, the per-seller status). The canonical state is the new single source of truth, and the legacy fields are kept in sync as projections — translations from canonical state to the older vocabulary.

A simplified view of the projection:

Canonical statePer-lead status (legacy)Per-buyer status (legacy)
NEWnewlead
CONTACTINGcontactedlead
QUALIFIEDqualifiedsearching (default; also covers touring)
COMMITTEDqualified (fallback)offer_pending
IN_CONTRACTqualified (fallback)under_contract
CLOSEDconvertedclosed
ARCHIVEDlost (fallback)derived
TRASHEDlost (fallback)derived

The projection is lossy in the inverse direction — a legacy qualified value could correspond to canonical QUALIFIED, COMMITTED, or IN_CONTRACT, and the reverse projection picks the safest fallback. You usually don’t think about projection; it’s the mechanism that keeps older parts of the UI consistent while the canonical state owns the truth.

A numeric score derived from behavioral signals — site visits, favorites, replies to outreach, time-since-last-activity. Three bands:

stateDiagram-v2
    direction LR
    [*] --> cold
    cold --> warm: score >= 30
    warm --> hot: score >= 70
    hot --> warm: score < 70
    warm --> cold: score < 30

Temperature is orthogonal to pipeline stage. A QUALIFIED lead can be Hot or Cold and the tactic for each is different (more on this in why state and temperature are separate). The score decays over time when a lead is silent, climbs when they engage, and is recomputed lazily — you don’t see it tick in real time, but it’s correct at the moment you load a page.

The per-signal weights are still being calibrated against real conversion data, so the bands (30 / 70) are firm but individual signal contributions are subject to change as the model is backtested. See temperature signals for the signal table.

Five flags, each blocking a specific channel or set of channels:

FlagWhat it blocks
unsubscribedAutomated email marketing. Personal email and other channels still allowed.
dncAll automated outreach. Manual contact requires a logged reason.
do_not_emailAll email — automated or templated.
do_not_textAll SMS — automated or templated.
do_not_callAll voice calls — automated or templated.

Flags compose. A lead can carry several at once and the effective restriction is the union of every active flag’s blocks. The state machine doesn’t read flag rows — they’re a separate axis. See communication flags for the full composition rules.

Flags are mutable with history. Setting unsubscribed, clearing it months later, and re-setting it after a future campaign — each set and each clear is preserved as a row in the audit trail. The system enforces “at most one active flag of each type per lead” without erasing the history.

The recurring temptation in lead-management products is to collapse all three axes into a single “lead score” or a single “lead category” (Hot / Watch / Nurture / etc.). The platform deliberately doesn’t, for three reasons:

  • They answer different questions. State asks “where is this lead in the buying process?” Temperature asks “should I prioritize this lead today?” Flags ask “what can I send and through which channel?” Collapsing them obscures one of the three.
  • They change at different cadences. State changes are discrete events (a signed agreement, a closing). Temperature drifts continuously. Flags are mutable preference declarations. A single column would either ignore the slow changes or be re-fired by every drift.
  • They have different actors. State changes are driven by agents (with a small number of automated timeouts). Temperature is computed by the system. Flags are typically set by the lead themselves. Mixing the audit trails collapses three distinct accountability stories into one.

For the worked-example version of this argument, see why state and temperature are separate.

The forward path through the pipeline is intuitive. The non-obvious cases are the ones worth highlighting:

From → ToTriggerNotes
NEW → CONTACTINGFirst logged outreach (call, email, text)Stops the 5-minute SLA clock.
CONTACTING → ARCHIVED21-day timeout with ≥6 contact attempts loggedSends a going-dark final email; clears active drips.
QUALIFIED → COMMITTEDBuyer’s agreement signedStops automated outreach; the lead transitions to manual handling.
IN_CONTRACT → QUALIFIEDContract voidedRequires reason code. Demotion event recorded. Search-tier drips resume.
ARCHIVED → CONTACTINGlead_re_engaged (manual reactivation or score climbed by ≥15 in 7 days)The lead drops back into the active workflow. The “who should this lead route to now?” recheck is on the roadmap (see open questions).
TRASHED → NEWcontact_info_updated (new valid email or phone)Fresh SLA clock. Standard intake flow.
  • Auditable transitions. Every canonical state change is event-driven and attributable. “Why is this lead in QUALIFIED?” has a single, scrollable answer.
  • Independent query dimensions. “CONTACTING + hot + not unsubscribed” is a clean filter, not a contortion.
  • Reversibility with reason codes. Demotion, archive recovery, and trash recovery are first-class — and the reason codes are what makes them analyzable later.
  • Decoupling from older status fields. The canonical state owns the truth; legacy fields stay materialized for backward compatibility without becoming a source of contradictions.
  • Multi-person leads. A lead with a co-buyer or partner is one lead with multiple contacts attached — the model doesn’t force one human per record.
  • Live demo data. A working demo with realistic leads sits behind /admin/leads/board on the demo environment, layered on a mirror of an actual brokerage.

These are real product gaps, not engineering hand-waves. Each is named and on the roadmap; each is something you might bump into in current usage and want context on.

Open questionStatus
Backdated event entryA NEW lead’s intake timestamp is always “now” — there’s no way today to log “I met with this client Friday but I’m only entering it now.” The data model needs one additional piece (a separate “entered at” timestamp distinct from “occurred at”) and validation against time paradoxes. Named as a precondition before this feature is rolled to all brokerages.
Re-routing on re-engagementWhen an ARCHIVED lead is reactivated, the lead currently keeps its original assigned agent — even if that agent is no longer active. The fix is to re-run agent matching on every lead_re_engaged event. On the roadmap.
Reassignment as a first-class eventReassigning a lead today is a direct change to the assignment, not an event in the log. The infrastructure to emit it as an event is in place; wiring it through is a small remaining piece.
Backfill of historical timelinesLeads created before the lifecycle feature shipped (2026-05-16) have empty timelines because the system has no record of what happened to them under the prior model. A backfill that synthesizes events from older status fields is on the roadmap; until it lands, expect older leads to look quiet on the detail page.
Seller-side state machineThe current state machine is buyer-shaped — COMMITTED means “buyer’s agreement signed.” A separate (or parameterized) seller path is deferred until the first seller-side lead actually lands in the system. The names are pipeline-agnostic enough that a parallel implementation is possible without renaming.
SLA alertingThe 5-minute target is observable on the board today, but a breach does not notify anyone. The proactive alerting layer requires background-job infrastructure that doesn’t yet exist on the platform. See SLA targets for the full breakdown.