Skip to main content

What lifecycle stages are

A lifecycle stage describes where a Contact sits in your sales funnel as a person — independent of any specific deal they’re part of. The same vocabulary every B2B sales team uses: Lead → MQL → SQL → Opportunity → Customer, with Disqualified as the terminal off-ramp. Connie speaks this vocabulary as a convention on the Contacts Stage column, not a separate entity. There’s no lifecycle picker UI, no new table, no special migration. Just a workspace config field plus a helper that downstream analytics read.

The default seed

Every new workspace gets this list out of the box. Override it any time without touching code: Terminal stages exit the funnel. They’re how analytics distinguish “won” / “lost” from “still in flight” — for example, the rep performance breakdown (G.4) computes win-rate by counting is_terminal: true && value == 'Customer' against the total terminal count.

Where it lives

workspaces.settings.lifecycle_stages — a JSON array on the existing settings blob. No new table. When the field is unset, the API returns the default seed above with is_default: true. As soon as you PATCH a custom list, that’s what reads back.

Configuring it

Validation rules

The PATCH endpoint enforces two invariants that the schema alone can’t:
  • Distinct values. Two stages with the same value (case-insensitive) would make the Stage cell ambiguous.
  • At least one terminal. Without a terminal stage, the win-rate analytics in G.4 divide by zero and emit a meaningless number — so the endpoint rejects a list where every entry has is_terminal: false.
If you need a funnel without an explicit Customer step (e.g., consumer onboarding), still mark the last step is_terminal: true so analytics treat it as the end.

How analytics read it

The isTerminalLifecycleStage(stages, value) helper in @conigma/shared/lifecycle-stages resolves a stage value against the configured list — case-insensitively — and returns whether it’s a terminal stage. G.4 (rep performance) calls this to split deals into “completed” (terminal) and “in-flight” buckets before computing win-rate and cycle-time. This is the only thing reading the config today. Future analytics (G.1 funnel conversion, G.2 stage velocity) can plug into the same helper.

What this is NOT

  • ❌ A lifecycle picker UI. The Contacts Stage column’s existing select-options dropdown is the picker.
  • ❌ A new entity / lookup table. It’s a JSON array on workspaces.settings.
  • ❌ A migration that touches existing Stage column data. Existing workspaces keep whatever Stage values they already configured.
  • ❌ A soft-sync warning system between this config and the Stage column’s actual select-options. They drift independently by design; the config informs analytics, the column drives data entry.

When in doubt

Treat lifecycle stages as vocabulary, not as a feature. The shape exists so cross-tool handoffs between Connie and a customer’s existing HubSpot/Salesforce instance don’t require translating “Connie’s Stage” into “their Lifecycle Stage.” If you find yourself building UI around it, you’ve probably scoped past the design.