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:

TransitionEventReason picked from
COMMITTED → QUALIFIEDdeal_collapsedFinancing fell through, Inspection issue, Buyer changed their mind, Seller withdrew, Lost to a competing offer, Agreement expired, Other.
IN_CONTRACT → QUALIFIEDcontract_voidedFinancing fell through, Inspection issue, Appraisal gap, Title issue, Mutual release, Buyer withdrew, Seller withdrew, Other.
NEW → TRASHED or CONTACTING → TRASHEDbogus_contact_detectedPhone number is invalid, Email bounces, Number reaches the wrong person, Spam or bot submission, Duplicate of another lead, Other.

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)Per-seller status (legacy)
NEWnewleadlead
CONTACTINGcontactedleadlead
QUALIFIEDqualifiedsearching (default; also covers touring)preparing
COMMITTEDqualified (fallback)offer_pendinglisted
IN_CONTRACTqualified (fallback)under_contractunder_contract
CLOSEDconvertedclosedclosed
ARCHIVEDlost (fallback)derivedderived
TRASHEDlost (fallback)derivedderived

A lead’s pipeline_type decides which of the two client columns it writes: buyer leads keep the per-buyer status in step, seller leads the per-seller status, and each leaves the other alone.

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 → ARCHIVEDtimeout_no_response, fired today by the Archive: no response buttonThe 21-day timeout is not scheduled yet; archiving is an agent’s decision.
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, the Re-engage buttonThe lead drops back into the active workflow. If the original agent can no longer receive leads, routing picks a new one and Scribe writes an Agent reassigned row.
TRASHED → NEWcontact_info_updated, the Contact info fixed buttonFresh SLA clock. Standard intake flow.

Every transition above is recorded through one control. The Lifecycle card on the lead’s detail page offers a button for each event that is legal from the current stage, and the board offers the same set through each card’s ⋯ menu and by drag. All routes open one dialog that asks only for what the event needs: a channel for contact attempts and responses, a reason for setbacks and bogus marks, an optional note on everything, and a This happened earlier… toggle for logging something after the fact. See how you fire these for the button per stage.

For integrations, the same thing is one endpoint:

POST /api/v1/admin/leads/{id}/transitions with a JSON body of {event_type, reason_code?, note?, channel?, occurred_at?}. It returns the refreshed timeline, which also carries available_transitions (what is legal next) and warnings (active communication flags). Errors: 400 for a malformed request (unknown event, missing or unknown reason or channel), 404 when the lead is out of the caller’s scope, 409 when the event is not legal from the lead’s current stage, 422 when a backdated occurred_at would make the recorded history impossible (before the lead was received, in the future, or out of order with a later event).

Timeline events carry actor_entered_at when they were backdated; the displayed time is when the event happened. Rows the migration reconstructed from a legacy status carry payload.backfilled = true and render with the reconstructed marker.

GET /api/v1/admin/leads/board returns available_transitions (buyer) and seller_transitions per column, pipeline_type per card, sla_remaining_seconds and sla_target_seconds per card, generated_at, can_filter_agents, and the agents list, and accepts ?assigned_agent_id= to narrow to one agent.

PATCH /api/v1/admin/leads/{id} handles assignment and notes. It accepts reassign_reason and writes an agent_assigned or agent_reassigned row. It refuses status changes with 409 and points at the transitions endpoint; stage changes only happen through events.

Scope is enforced everywhere: agents see and act only on leads assigned to them, brokers and owners see their brokerage, admins see all. An agent account that predates its agent record is matched by email.

  • 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
SMS providers other than TwilioText message alerts go through Twilio: each brokerage’s own account, saved in the admin with the token encrypted, or the platform’s (TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER) as a fallback. There is no other SMS provider.

Most of what used to sit on this list has shipped. The seller-side pipeline: every lead carries a pipeline_type (buyer or seller, derived from the inquiry subject and editable while the lead is NEW or CONTACTING), the eight states are shared, and seller leads get listing_agreement_signed, listing_activated, offer_accepted and listing_withdrawn in place of the buyer paperwork events; see the seller pipeline. The rest: backdated entry (the This happened earlier… toggle, with actor_entered_at as the second timestamp and a replay check against time paradoxes), re-routing on re-engagement (automatic when the original agent can no longer receive leads), reassignment as a timeline event (agent_assigned / agent_reassigned with the actor), the historical backfill (rows marked reconstructed), automatic timeouts (Scribe archives quiet CONTACTING leads after 21 days and stalled QUALIFIED leads after 180, per-brokerage), and SLA breach alerts (one email per lead when the response window closes; per-brokerage target and switch). The sweep behind the last two runs inside the API about once a minute under an advisory lock, with POST /api/v1/admin/leads/lifecycle/sweep for operators and a scheduled workflow as a backstop. Walk-in and phone leads can be handed off to a client record from the lead page (Create client profile), and market report and tour confirmation emails feed open and click signals into the temperature score. The escalation ladder: a reminder to the agent, then an email to the brokers, then optional automatic reassignment, then a final escalation a day after intake that tells the brokers and owner once more and, with reassignment on, routes the lead past every agent who has had it; each per brokerage and each at most once per lead. Every rung goes out on every channel the brokerage has turned on: email always, a short text through Twilio when the brokerage enables it and the recipient has a phone (agents can opt out), and a browser push when enabled. Follow-ups: every lead carries one next follow-up, booked by the agent or by the brokerage default after a contact, reminded once when due through the same channels, and cleared by the next contact or by leaving the pipeline; see schedule a follow-up.

Follow-up sequences (drips) have shipped as well. A brokerage defines a sequence per trigger stage (NEW, CONTACTING, QUALIFIED, ARCHIVED) with ordered, delayed plain-text steps; leads enroll on intake or on the transition into that stage, and the same sweep sends due steps one per enrollment per pass. A sequence stops for a lead when the stage changes (by default), when Unsubscribed, Do Not Contact or No Email is set, when the lead clicks the unsubscribe link every email carries, when staff stop it, or after three failed sends. See set up follow-up sequences and the drip sequences reference.

Browser push has shipped as a second channel for the response-window ladder: once the platform holds a VAPID key pair, each admin user turns on the bell in the sidebar per browser, and the sweep pushes the same breach, reminder and escalation alerts it emails, to the same recipients, with the notification opening the lead. Subscriptions are pruned on delivery (gone at once on a 404/410, otherwise after five straight failures). See enable push notifications.