Connect by JBRH Open Connect

Normalising a stage

journey.normalise turns whatever a stage column says into one of the six, or into closed. A word it does not recognise is returned unchanged rather than guessed at, and then ranks as *before everything* — the safe answer, because a stage Connect does not know must never block a real transition. Writes are strict; reads are total.

Status
Available What this means
Audience
both, developer
In the app
#/relationships
Last verified
Product version
6.3.2

One column, three vocabularies#

contacts.stage has carried three sets of words over this product's life and holds all three today. Anything reading that column has to accept all of them, which is why the translation lives in one place instead of at each call site.

VocabularyWritten byExample words
The engine's own progressionagent.upsert_contact, from the classified intentresearch, conversation, interested, qualification, demo, installation
The Owner pipeline's wordsRead by operator_ui.stage_fornew, contacted, replied, opportunity, client, closed
The six lifecycle stagesjourney.advance, keeping the contact in stepprospect, qualified, opportunity, client, onboarding, active

Words are compared after being lowered, trimmed and having spaces turned into underscores, so *Not Interested* and not_interested are one word. The empty value, new, lead and contacted all read as the first position — a person grown from an old contact row would otherwise appear to leap out of nowhere at their first real transition.

Strict on write, total on read#

Writing
advance refuses a destination outside the six with unknown_stage and a 422. The vocabulary cannot grow by accident.
Reading
normalise and contact_rank never raise. An unrecognised word comes back as itself and ranks -1, which reads as *before everything*.
Terminal words
closed, lost, not_fit, not_interested, dnc, wrong_person and duplicate carry no position at all. Somebody who was closed and then writes again is starting over.
Mapping direction
Translation is non-decreasing along the engine's progression, so a contact the engine advances never makes the relationship go backwards.

That asymmetry is the design. A closed vocabulary on the way in keeps the meaning of a position stable; a total function on the way out means no reader can be broken by a word written years ago by code that no longer exists.

The failure this replaced#

Before the translation had one home, agent read the column by calling list.index on the engine's nine words. Every other word raised — including new, which is what every contact bridged in from a customer workspace carries, and client, which advance itself writes.

  1. A message arrives for a contact whose stage word is new.

    Result The lookup raises rather than returning a position.

  2. The engine retries.

    Result It raises again, for the same reason, because nothing about the data has changed.

  3. Three attempts pass.

    Result The message is held — and never answered. Nothing on any screen said *this stage word is unreadable*; it read as a message that had simply not been dealt with.

What this means for imports and integrations#

  • An import may carry any word. It is read charitably and ranks last, so it never blocks a later move. It is not adopted into the vocabulary.
  • Setting a position through the API or the Assistant uses the six. Anything else is refused with unknown_stage rather than stored.
  • Casing and spacing do not matter. Comparison is on the normalised word.
  • Two screens can show different words for the same relationship while old rows survive — the lifecycle position is the one written by advance, and it is what other behaviour reads.

Questions#

What happens to a stage word Connect does not recognise?

It is returned unchanged and ranked -1, meaning *before everything*. Nothing is guessed and nothing is overwritten, and because it ranks last it cannot prevent a genuine forward move.

Can I add a word to the mapping?

The mapping is code, not configuration, and the six destinations are fixed. If an import uses your own vocabulary, map it on the way in — the alternative is a seventh position appearing by accident, which is what the strict write path exists to prevent.

Why do the prospect list and the relationship screen sometimes disagree?

Because the prospect list and the pipeline read contacts.stage, which holds older words, while the relationship reads people.stage. advance writes both at the same moment, so a relationship moved since that change agrees with itself; a record untouched since carries whatever it carried.