# Lifecycle stages

A relationship sits at one of six points — prospect, qualified, opportunity, client, onboarding, active — plus *not proceeding*, which is the absence of a step rather than a further one. `journey.advance` moves a relationship forward, records who moved it and why, and refuses to make somebody a client on its own because that is a commercial commitment.

- **Status:** Available
- **Audience:** both
- **In the app:** #/relationships, #/pipeline
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/relationships/lifecycle-stages/

## The six, and the seventh that is not one

```text
prospect → qualified → opportunity → client → onboarding → active
```

That is the shortest sequence which still separates the things a business treats differently: a name on a list, somebody who replied, somebody who wants a quote, somebody who bought, somebody being set up, and a customer who occasionally needs something. Connect is not trying to be a CRM here — there are no custom pipelines, no probability weightings and no forecast. It needs to know which of the six applies so it can behave correctly.

*Not proceeding* (`closed`) sits deliberately outside the sequence. Giving it a position would let a move be refused for a customer who came back a year later; ranked as *before everything*, any real signal moves them out of it again. The same is true of `lost`, `not_interested` and the other terminal words — a relationship that was closed and writes to you again is starting over, not resuming at the end.

## What a move actually changes

| What changes | Where | Why it is there |
|---|---|---|
| The current position | `people.stage` | One answer to where the relationship stands |
| A transition event | `relationship_stage_events` | Who moved it, from what, to what, with a reason — a column alone cannot settle a disputed invoice |
| A timeline entry | Assembled by `timeline.py` | The move is an event, readable in the order it happened |
| An audit record | `relationship.stage_changed` in `audit.py` | A commercial fact — 'they became a client on the 3rd' — with the actor recorded |

The contact record is kept in step at the same moment, because the pipeline screen and the prospect list still read `contacts.stage`. Two answers to the same question is the drift this module exists to end.

## Three rules that hold everywhere

**History is never lost** — The position lives on the person and every transition is also written as its own event. You can always answer *when did they become a client, and on whose say-so?*
**Every transition is idempotent** — The same evidence arrives more than once — a reprocessed message, a retried request, a redelivered webhook — and must move the relationship once. `reference` is the key; a repeat returns the original event rather than writing a second.
**Becoming a client is not Connect's decision alone** — `client` is on the needs-a-human list. Connect may propose it; the workspace's own approval rules put it in front of a person, the same rules every outbound action uses.

A forward move that Connect can make on its own still records that it made it. The audit trail carries refusals as well as actions, because a refusal is a decision — [the decision log](/docs/autonomy/) is where that is read.

## Moving backwards, and other refusals

- **Backwards.** A move to a position at or behind the current one is refused unless it is asked for explicitly. Progress is not undone by a stray signal.
- **A word Connect does not know.** A destination outside the six is refused outright with `unknown_stage` rather than written as a new position. Reads are forgiving; writes are not — see [normalising a stage](/docs/relationships/stage-normalisation/).
- **A commitment awaiting a person.** Where the destination is `client` and Connect is the actor, the move is held and an approval is raised. The relationship stays where it was until somebody says yes.

> **Note** Position is read by the product rather than displayed for decoration: it shapes what the relationship summary emphasises and what the next best action looks like. Moving somebody by hand is therefore a real change to behaviour, not a label.

## Questions

### Can I add my own stages?

No. The six are fixed, deliberately — custom pipelines, weightings and forecasting are what a CRM is for, and Connect's job is to know which of the six applies so it can act correctly. Sales has its own [stage rules](/docs/sales/stage-rules/) for an opportunity, which is a different record.

### Why was a move refused as unknown?

Because the destination was not one of the six. `advance` refuses rather than inventing a position, which is what keeps the vocabulary from growing a seventh entry every time an import uses a different word.

### Who can move a relationship to client?

A person. Connect can propose it and the workspace's approval rules decide when it is put in front of somebody; the transition itself waits for a human yes, and the approval id is recorded on the event alongside the actor and the reason.

## Related

- [Normalising a stage](https://connectbyjbrh.com/docs/relationships/stage-normalisation/)
- [Relationships in Connect](https://connectbyjbrh.com/docs/relationships/)
- [People](https://connectbyjbrh.com/docs/relationships/people/)
- [Moving a relationship through its lifecycle](https://connectbyjbrh.com/docs/workflows/lifecycle-progression/)
- [Stage rules and refusals](https://connectbyjbrh.com/docs/sales/stage-rules/)
- [What Connect may do](https://connectbyjbrh.com/docs/autonomy/)

## What this page is based on

- `backend/app/journey.py` — `ORDER`, `NEEDS_HUMAN`, `advance` and the stage events
- `docs-source/sources/CHANNELS.md` §5 — `move_stage` and `stages()`
- `docs-source/sources/GENERAL.md` §5 — approvals and the audit trail
