Handling conversation state
Conversations are where the rubber meets the road. A flow is a static definition; a conversation is a contact running through that flow, one node at a time, across real clock time. This page is about the runtime.
The state record
Section titled “The state record”Every active conversation carries a single JSON blob in
conversations.state:
{ "flowId": 42, "flowVersionId": 17, "currentNodeId": 214, "vars": { "full_name": "Sarah", "sa_id": "8601011234089" }, "awaitingInput": 219}- flowId / flowVersionId — the contact is pinned to a specific version of the flow from the moment they enter. If you publish a new version of the flow mid-campaign, the in-flight conversation keeps running on the old version. This is by design.
- currentNodeId — where the contact is right now.
- vars — everything collected so far.
- awaitingInput — set when a
questionormedia_capturenode is blocking on the contact’s reply.
When an inbound message arrives, Voxa looks up the contact’s active
conversation, merges the new input into vars, and enqueues a
flow-execution job. The worker advances
the state until it hits the next node that needs a reply (or an end /
submission).
Advancing
Section titled “Advancing”The flow-execution worker is a plain state machine:
- Load the conversation and the current node.
- If the node is
message, send the text, advance to the next node, save state, loop. - If the node is
questionormedia_capture, setawaitingInput, save state, exit. The conversation parks until the next inbound message. - If the node is
validationorcondition, evaluate and route to the correct outgoing edge. - If the node is
delay, schedule a follow-up job at the delay expiry, save state, exit. - If the node is
submission, write anentriesrow, bumpcampaign_stats.completed_count, setstatus: completed, and exit. - If the node is
end, setstatus: completedwithout writing an entry.
Every transition is idempotent — a crashed worker replays the current
node cleanly. Every transition writes an audit_log row so you can
trace exactly what happened to any conversation.
The 24-hour WhatsApp window
Section titled “The 24-hour WhatsApp window”WhatsApp’s Business Messaging Policy allows free-form outbound messages only within 24 hours of the contact’s most recent inbound message. After that window closes, Meta requires you to use an approved template for the first outbound — and templates are a different content model (structured, pre-approved).
Voxa doesn’t track the 24-hour window as an explicit countdown yet. In practice:
- A contact hits your campaign → inbound → 24-hour window opens.
- You reply freely with message / question nodes as long as they keep responding; each response extends the window.
- If the contact goes dark mid-flow and you try to send an outbound
message more than 24 hours after their last inbound, Meta will
reject the send. The message status comes back as
failedwith a policy-violation error.
Parallel conversations per contact
Section titled “Parallel conversations per contact”A single contact can be in more than one active conversation —
one per campaign they’re currently running through. Voxa routes each
inbound to whichever conversation is marked awaitingInput on the
matching channel. Keyword collisions are resolved in campaign creation
order — be careful with overlapping keywords.
Abandonment
Section titled “Abandonment”Voxa does not eagerly time out conversations. A conversation that
hasn’t been touched in 48 hours is effectively abandoned, but nothing
runs to mark it that way today. Analytics queries compute abandonment
at read time by comparing updated_at against the campaign window.
If a contact re-engages a day later, their conversation is still live and continues from where it stopped — but only if that re-engagement arrives inside the 24-hour WhatsApp window (see above).
Inspecting a stuck conversation
Section titled “Inspecting a stuck conversation”When something goes wrong, the Conversations page shows every
active and recent conversation with the current node, last-updated
timestamp, and current vars. Common issues:
| Symptom | Likely cause |
|---|---|
Conversation stuck on a question node | Contact hasn’t replied yet. Check last_seen_at on the contact. |
Conversation stuck on a condition | Variable the condition reads was never set — upstream question stored to the wrong name. |
| All conversations for a campaign idle | Meta credentials recently rotated or the access token expired. Check Settings → Meta credentials → Recent events. |
| Conversation completed without writing an entry | The flow ended on an end node rather than a submission node. |
The Audit log for the conversation records every node transition with timestamp and variable delta — that’s the first place to look when a flow behaves unexpectedly.
- Production checklist
- Core concepts — refresher on flows vs. campaigns.