Skip to content

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.

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 question or media_capture node 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).

The flow-execution worker is a plain state machine:

  1. Load the conversation and the current node.
  2. If the node is message, send the text, advance to the next node, save state, loop.
  3. If the node is question or media_capture, set awaitingInput, save state, exit. The conversation parks until the next inbound message.
  4. If the node is validation or condition, evaluate and route to the correct outgoing edge.
  5. If the node is delay, schedule a follow-up job at the delay expiry, save state, exit.
  6. If the node is submission, write an entries row, bump campaign_stats.completed_count, set status: completed, and exit.
  7. If the node is end, set status: completed without 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.

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 failed with a policy-violation error.

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.

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).

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:

SymptomLikely cause
Conversation stuck on a question nodeContact hasn’t replied yet. Check last_seen_at on the contact.
Conversation stuck on a conditionVariable the condition reads was never set — upstream question stored to the wrong name.
All conversations for a campaign idleMeta credentials recently rotated or the access token expired. Check Settings → Meta credentials → Recent events.
Conversation completed without writing an entryThe 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.