Drawing 1 · Interactive
Follow one customer's booking state
- Idle to Awaiting service: taps Book
- Awaiting service to Awaiting date: picks service
- Awaiting date to Confirming: valid date
- Confirming to Booked: taps Yes
- Confirming to Awaiting date: taps Change
- Booked to Idle: done
- Awaiting service to Expired: silent 2 h
- Awaiting date to Expired: silent 2 h
- Expired to Idle: reset
- Awaiting date to With agent: asks for a person
- With agent to Idle: agent hands back
- Idle
- Awaiting service
- Awaiting date
- Confirming
- Booked
- Idle
Step 6 of 6 · end of walk
Idle
No journey in progress. Any message shows the main menu.
Arrived from Booked via done.
Can move to
- Awaiting service · taps Book
Happy path, step 6 of 6: Idle (via done). No journey in progress. Any message shows the main menu.
What is conversation state management?
Conversation state management is how a chatbot remembers, between messages, where each customer is in a journey. Every WhatsApp message reaches your server as an independent messages webhook with no memory of what came before, so the bot must store — and reload on every message — a small record per customer: the current step (awaiting_date), the answers collected so far, the last intent, timestamps and who currently owns the chat.
That record turns a series of stateless HTTP requests into a conversation. When a customer replies "tomorrow 5 pm", the stored step tells the bot this is the appointment time, not a new question. State is usually keyed by your phone_number_id together with the customer's WhatsApp ID (wa_id, or the business-scoped user ID where usernames apply), kept in a fast shared store such as Redis or a database table, and modelled as a finite state machine with explicit transitions.
It is your system's memory, not the model's. An LLM's context window holds what you send in one call and forgets it afterwards; conversation state persists across calls, survives restarts and can be read by your inbox, analytics and agents. It also has to respect time: a half-finished state should expire, and the 24-hour customer service window decides whether the bot can still pick up where it left off with free-form messages or needs a template.
Key takeaways
- Conversation state management stores each customer's place in a bot journey so stateless webhooks can be read in context.
- Key state by phone_number_id plus the customer's wa_id (or BSUID) and keep it in a shared store such as Redis, never in server memory.
- Save the last processed wamid: Meta retries webhooks for up to 7 days, and a duplicate must not advance the journey twice.
- Give every in-progress state a TTL and a reset path, and silence the bot while a human agent owns the chat.
- State is your system's memory; the LLM context window is what one model call can see and forgets afterwards.
Implementation
Conversation state management in code
import { createClient } from "redis";const redis = createClient({ url: process.env.REDIS_URL });await redis.connect();const TTL_SECONDS = 2 * 60 * 60; // abandon half-finished journeys after 2 hoursconst key = (phoneNumberId, waId) => `conv:${phoneNumberId}:${waId}`;export async function handleMessage(phoneNumberId, msg) {const k = key(phoneNumberId, msg.from);const state = JSON.parse((await redis.get(k)) ?? '{"step":"idle","owner":"bot"}');if (state.lastWamid === msg.id) return; // webhook retry: already handledif (state.owner === "agent") return; // a human owns the chatconst input = msg.interactive?.button_reply?.id ?? msg.text?.body ?? "";const next = transition(state, input); // pure: (state, input) -> state + replynext.lastWamid = msg.id;next.updatedAt = Date.now();await redis.set(k, JSON.stringify(next), { EX: TTL_SECONDS });if (next.reply) await sendWhatsApp(phoneNumberId, msg.from, next.reply);}
Load, transition, save with a TTL. In production, add a per-customer lock or optimistic version check so two parallel deliveries cannot race.
Operating sequence
How does conversation state management work?
Choose the key
Key state by business number plus customer:
phone_number_id+wa_id(or BSUID). One customer messaging two of your numbers has two separate conversations.Model the journey as states
List the steps — idle, awaiting_service, awaiting_date, confirming, booked, with_agent — and the events that move between them. Input that is not valid in the current state goes to a fallback.
Load, decide, save — atomically
On each webhook, load the record, compute the next state and reply, then save with a version check or a per-customer lock so two parallel deliveries cannot both advance it.
Make it idempotent
Store the last processed
wamid. When a retry delivers the same message again, skip it instead of moving the journey forward twice.Expire and reset
Give every in-progress state a TTL (30 minutes to a few hours is common). On expiry reset to idle and, if the customer returns, ask whether to continue. Support a reset word such as "menu".
Coordinate with humans
When the chat is handed to an agent, set the owner to the agent so the bot stays silent, and resume from a clean state when the agent hands back.
Design intent
Why does conversation state management matter?
Without reliable state, bots ask the same question twice, treat answers as new requests, or send two replies when a webhook is retried. Clean state management is what lets a bot cope with a customer who wanders off for an hour mid-booking, an agent who takes over and hands back, and thousands of conversations running in parallel across several server instances — without anyone losing their place.
Example: A Chennai salon's booking bot
A customer taps "Book" and picks "Haircut"; the bot saves {step: "awaiting_date", service: "haircut"}. She disappears for 40 minutes, then replies "Sunday 11". The record is still within its two-hour TTL, so the bot reads the reply as a date, offers 11:00 and 11:30 and moves to confirming. After a network blip Meta re-sends one webhook; the stored last wamid matches, so the duplicate is ignored. Had she replied the next evening, the state would have expired and the bot would have asked whether to restart the booking.
Failure modes
Common mistakes with conversation state management
Keeping state in server memory
An in-process map disappears on every restart and is invisible to other instances behind a load balancer. Use a shared store.
No expiry
A customer who abandoned a booking last week gets their next "hi" read as a date. Put a TTL on every in-progress state and reset to idle.
Race conditions
Two quick messages or a retried webhook can be processed in parallel, both reading the old state. Use a per-customer lock, a queue partitioned by customer, or optimistic versioning.
Bot and agent both replying
If handoff does not change state, the bot keeps answering while an agent types. Record the owner and silence the bot until the agent hands back.
Leaving the step to the model
Asking an LLM to infer the current step from chat history is fragile. Store the step explicitly and pass it to the model as a fact.
Specifications
Conversation state management: key facts and limits
- Key
- phone_number_id + wa_id (or BSUID)
- Typical store
- Redis or a database table, with a TTL
- Minimum fields
- step, collected slots, owner, last wamid, timestamps
- Webhook retries
- Up to 7 days — deduplicate on wamid
- Delivery order
- Not guaranteed — never assume sequence
- Free-form resume
- Within 24 hours of the customer's last message
Recommended practice
Best practices for conversation state management
- Keep the transition logic a pure function so every path is easy to unit-test.
- Log each transition with the triggering wamid for debugging.
- Offer escape words ("menu", "agent") that work from any state.
- Store when the 24-hour window closes so the bot knows when only templates will work.