A common failure in WhatsApp-to-CRM builds: the CRM pipeline says "Qualified" while the chat has been silent for a long time. The pipeline and the conversation drift apart because nothing connects their states.
The fix I keep coming back to in WhatsApp CRM automation projects for small teams is to model the conversation as an explicit state machine, and let only state transitions touch the pipeline.
The states
from enum import Enum
class Conv(Enum):
NEW = "new" # first inbound message
AWAITING_US = "awaiting_us" # customer spoke last
AWAITING_THEM = "awaiting_them" # we spoke last
NEEDS_HUMAN = "needs_human" # low confidence, price, complaint
QUOTED = "quoted" # approved quote sent
WON = "won"
LOST = "lost"
OPTED_OUT = "opted_out" # terminal for automated messages
The allowed transitions
ALLOWED = {
Conv.NEW: {Conv.AWAITING_US, Conv.NEEDS_HUMAN, Conv.OPTED_OUT},
Conv.AWAITING_US: {Conv.AWAITING_THEM, Conv.NEEDS_HUMAN, Conv.OPTED_OUT},
Conv.AWAITING_THEM: {Conv.AWAITING_US, Conv.LOST, Conv.OPTED_OUT},
Conv.NEEDS_HUMAN: {Conv.AWAITING_THEM, Conv.QUOTED, Conv.LOST, Conv.OPTED_OUT},
Conv.QUOTED: {Conv.AWAITING_US, Conv.WON, Conv.LOST, Conv.OPTED_OUT},
}
def transition(conv, new_state, actor):
if new_state not in ALLOWED.get(conv.state, set()):
raise ValueError(f"{conv.state} -> {new_state} not allowed")
if new_state in (Conv.QUOTED, Conv.WON) and actor != "human":
raise PermissionError("quotes and wins are confirmed by a person")
conv.state = new_state
sync_pipeline(conv) # the ONLY place the CRM stage is written
Why this helps
One writer. Only sync_pipeline writes the CRM stage, so you never have the bot, a Zap and a salesperson fighting over the same field.
Explicit human gates. Quotes and wins can only be set by a person. The AI can draft a quote; it cannot mark one sent.
Opt-out is terminal. Once OPTED_OUT, no automated template goes out, whatever any other rule says.
Silence becomes data. A scheduled job moves AWAITING_THEM to LOST after a quiet period you choose, and that shows up in the pipeline instead of rotting there.
Mapping to the CRM
Keep the mapping in config, because every CRM names stages differently:
pipeline_map:
needs_human: "Contacted"
quoted: "Proposal sent"
won: "Closed won"
lost: "Closed lost"
States the CRM does not care about (awaiting_us, awaiting_them) live only in your automation database, and drive reminders to staff instead.
Before building any of this, map the real workflow with the people who do it. This list for mapping who owns each handoff is a good start.
We integrate with all major CRMs (for example HubSpot, Zoho, Salesforce, Pipedrive and Freshsales, plus custom CRMs or Google Sheets setups) and work with all major WhatsApp Business providers and the official WhatsApp Business API (for example Meta Cloud API, Twilio, Gupshup, Interakt and WATI).
Disclosure: I co-founded NxFlowAI, an AI automation agency in Mumbai. Code is illustrative and free to adapt.












