Skip to main content
Back to The LibraryFor CTOs & AI Engineers

Two State Machines in a Trench Coat

A proposal can be "draft" and "approved" and "sent" at the same time.

The Architecture Series · 8 minute read · By Ed | Founder & CEO, DealiOS


Every B2B deal has a status. Actually, every B2B deal has two statuses.

The proposal has a status. The account has a pipeline stage. They're related but they're not the same thing. A proposal can be "draft" while the account is in "negotiation." Two entities, two lifecycles, two state machines, all pretending to be one thing in your CRM's dropdown menu.

Most platforms pick one state machine and stuff everything into it. The result is a dropdown with 14 options, six of which mean roughly the same thing, and a rep who picks whichever one is closest to the truth without being technically wrong.

That's not state management. That's multiple choice with no correct answer.

Two entities, two lifecycles, two state machines, all pretending to be one thing in your CRM's dropdown menu.


The Dropdown Problem

Open any CRM. Find the deal status field. Count the options.

If there are more than eight, someone tried to cram multiple lifecycles into a single field. You'll see entries like "Proposal Sent," "Proposal Viewed," "Negotiating," "Contract Out," "Verbal Commit," and "Closed Won" all living in the same dropdown, competing for attention like contestants on a reality show where nobody gets eliminated.

The fundamental issue: a single status field can only represent one dimension of state. But a B2B deal exists across at least two dimensions simultaneously. The proposal has its own lifecycle (draft, in_progress, published, viewed, archived). The account has its own pipeline stage that persists beyond any individual proposal (lead through closed_won).

When you collapse two dimensions into one, you get impossible states. A proposal marked "draft" on an account marked "closed_won." A pipeline showing "engaged" for an account where every proposal expired six months ago. A deal in "negotiation" with no active proposal attached.

Reps learn to work around these impossibilities. They pick the status that's closest to correct and move on, because the dropdown doesn't offer correct. It offers approximately not wrong. Over time, the data becomes a loose approximation of reality rather than a representation of it. Pipeline reviews turn into oral history sessions where the rep explains what the status actually means, which is never what the dropdown says it means.

Asking one dropdown to manage two lifecycles is like asking one person to be the pilot and the flight attendant. They can technically do both. The passengers are going to notice the quality drop somewhere around the beverage service.

The fix isn't a better dropdown. The fix is two separate state machines that know how to talk to each other.


The Two Machines

Here's what the actual architecture looks like.

Machine 1: Account Pipeline Stage. Eight stages: lead, qualified, proposal_in_progress, proposal_delivered, engaged, negotiation, contract_out, closed_won. This tracks the account's overall position in the sales pipeline. It advances based on the highest-signal activity across all items on the account. An account in engaged means someone opened a published proposal and spent time with it. An account in contract_out means a contract has been sent for signature. The pipeline stage belongs to the account, not any individual item.

Machine 2: Item Status. Five states: draft, in_progress, published, viewed, archived. This tracks where a specific proposal, contract, or other deliverable is in its own lifecycle. A single account can have multiple items, each in a different status. One proposal might be published while another sits in archived from three months ago. The item status belongs to the item, not the account.

The two machines interlock through event mappings. When a proposal is published, an event fires. That event advances the account pipeline from proposal_in_progress to proposal_delivered. But it only does so through a guard condition that checks the current stage first. You can't advance from lead to proposal_delivered. That would skip qualified. The guard catches it, the transition fails silently, and someone needs to figure out why there's a published proposal on an unqualified lead. (The answer is almost always a data quality issue upstream, and the state machine just found it for you.)

This is where the two-machine design earns its keep. A proposal can be draft while the account is in negotiation. That's not a bug. It's a second proposal being drafted for an account that's already deep in the sales process. A single-dropdown CRM can't represent that. It would force you to pick: is this deal in "negotiation" or "proposal draft"? The answer is both, because those are two different questions about two different entities. Two machines, two answers, both correct simultaneously.

Interlocking state machines don't just manage state. They enforce business logic. A guard condition isn't validation. It's a policy.

This is the beauty of interlocking state machines: they don't just manage state. They enforce business logic. A guard condition isn't validation. It's a policy. "You cannot deliver a proposal to an unqualified lead" is a business rule expressed as a transition constraint. The state machine didn't just prevent bad data. It prevented a bad process.


Guards, Regression, and Self-Healing

Two state machines talking to each other is elegant until they disagree. And they will disagree, because distributed systems are built on the promise that something will eventually go wrong.

Guard conditions prevent impossible transitions. The pipeline can't jump from lead to closed_won. That would skip six intermediate states, each of which represents a real milestone in the sales process. The guard checks: is the current state a valid predecessor for the target state? If not, the transition is rejected. Simple. But the implication is profound. Every forward movement in the pipeline has to be earned. No shortcuts. No manual overrides that skip steps. The state machine is more disciplined about process than most sales managers.

Deletion regression handles the reverse. Delete the last proposal on an account. What happens to the pipeline? It was sitting at proposal_delivered, but now there's no proposal. The pipeline should regress to qualified, because that's the last state that's still true without a proposal. This regression is automatic. The system notices the orphaned state, checks what's still valid, and falls back.

Most platforms don't do this. They leave orphaned states floating in the pipeline like abandoned shopping carts. Except abandoned shopping carts don't mislead your quarterly forecast.

Self-healing is the safety net underneath everything. A function called checkPipelineConsistency() runs on every account load. It examines the current pipeline stage, looks at what proposals exist and what state they're in, and asks a simple question: does the pipeline stage make sense given the current evidence? If the account is in proposal_delivered but every proposal is in draft, something drifted. A race condition. A failed webhook. A manual edit that bypassed the state machine. The self-healing function corrects the state and logs the drift.

A self-healing state machine is a system that assumes it made a mistake and checks. That's a level of self-awareness most people don't have. Your pipeline management software is more introspective than your sales team.

The important design decision: self-healing doesn't throw errors. It corrects silently and logs. Because a state inconsistency isn't a user-facing failure. It's a system-facing truth. The user doesn't need to know the pipeline drifted and recovered. They need the pipeline to be right when they look at it. The healing happens before the render.


Why Not a Workflow Engine

At this point, the reasonable question is: why not use Temporal, Step Functions, or a BPMN engine?

Workflow engines are designed for orchestration. They manage sequences of tasks, handle retries, maintain execution state across long-running processes. They're excellent tools when the problem is "run these seven things in order, handle failures, resume from where you left off."

But state machines solve a different problem. They answer the question: "What state is this entity in right now?"

The distinction matters. A workflow engine tracks what happened and what happens next. A state machine tracks what is. The pipeline stage isn't a step in a sequence. It's an identity. The account doesn't "become" engaged as a step in a workflow. It "is" engaged because the evidence supports that classification.

Two simple state machines with typed transitions are debuggable. You can look at the current state, the transition log, and the guard conditions and understand exactly why an account is where it is. The entire history is a list of events: proposal published, engagement detected, score recalculated, pipeline advanced. Each event maps to exactly one transition in exactly one machine.

A BPMN diagram with 47 nodes offers none of that clarity. It tells you what the process could do. It doesn't tell you what the entity is. And when the VP of Sales asks "why is this deal in negotiation?" the answer should be a two-line event log, not a flowchart that requires a PhD to interpret.

There's another practical argument: state machines compose. You can add a third machine (say, a more granular contract signing lifecycle) without redesigning the other two. Each machine owns its own states and transitions. The connection point is an event mapping: "when this happens in Machine A, check if this transition is valid in Machine B." The machines don't know about each other's internal structure. They communicate through events and guards.

Workflow engines tend toward monoliths. As the process grows, the workflow grows. Branching conditions multiply. Exception paths proliferate. Six months in, nobody understands the full flow, and changing step 23 requires regression testing steps 1 through 47. The state machines, by contrast, remain simple individually. The complexity lives in the connections between them, which are explicit, typed, and auditable.


The Terminal State

One state deserves special attention: closed_won.

In the pipeline state machine, closed_won is terminal. There is no transition out of it. You cannot regress from closed_won to negotiating. You cannot advance from closed_won to anything. The deal is done. The pipeline is resolved.

This is a deliberate architectural choice with real business consequences. Once a deal is closed, the revenue is recognized. The contract is signed. The account transitions from sales to customer success. Allowing the pipeline to regress from closed_won would retroactively un-recognize revenue, which is the kind of thing that makes CFOs develop involuntary eye twitches.

Terminal state protection means the state machine won't do it, even if a bug tries to trigger it. The guard on closed_won rejects all outbound transitions. The self-healing function treats closed_won as permanently valid. Even deletion regression won't touch it: delete every proposal on a closed_won account, and the pipeline stays at closed_won, because the fact that the deal was won is historical truth, not dependent on current proposal state.

The lesson is broader than just closed_won. Some states are irreversible because the business consequences of reversal are unacceptable. The state machine should enforce this, not just document it. A comment that says "don't regress from closed_won" is a wish. A guard condition that rejects the transition is a guarantee.

A comment that says "don't regress from closed_won" is a wish. A guard condition that rejects the transition is a guarantee.


The Universal Pattern

This architecture isn't specific to sales pipelines. It appears anywhere multiple entities have interrelated but independent lifecycles.

Order processing systems have order state and fulfillment state. An order can be confirmed while fulfillment is not_started. Two machines, interlocking. A healthcare treatment protocol has patient status and treatment status. A patient can be admitted while the treatment is scheduled. Manufacturing quality chains have assembly state and inspection state. Two machines, two lifecycles, one product. Add a third dimension when the domain demands it, but start with two and let the event mappings prove you need more.

In each case, the temptation is the same: collapse everything into one status field. And in each case, the result is the same: impossible states, lost information, and operators who work around the system instead of with it.

The alternative is always the same: separate the lifecycles, connect them through events and guards, build regression for orphaned states, and add self-healing as a trust layer. Simple systems with explicit bridges beat one complex system every time. Because simple systems can be understood, tested, debugged, and extended independently. Complex systems can only be feared.

The takeaway: When multiple entities have interrelated lifecycles, separate them into distinct state machines connected by event mappings and guard conditions. Simple systems with explicit bridges beat one complex system every time.


References

1. Harel, D. (1987). "Statecharts: A Visual Formalism for Complex Systems." Science of Computer Programming, 8(3), 231-274. 2. Temporal Technologies. (2024). "Workflows vs. State Machines: When to Use Each." Temporal.io Documentation. 3. Amazon Web Services. (2024). "AWS Step Functions Developer Guide: State Machine Concepts." AWS Documentation. 4. Hohpe, G. & Woolf, B. (2003). Enterprise Integration Patterns. Addison-Wesley.


This paper is part of The Architecture Series by Ed, Founder & CEO of DealiOS. For the weekly series and more technical deep-dives, visit dealios.app/library.

See how these principles come together. Explore the platform →