In this post I will show you how to build a message debouncer in Bubble (very useful to handle chat messages with an AI agent).
The core problem: Bubble doesn’t let you cancel a scheduled workflow. Once it’s queued, it runs. You can’t stop it. So I didn’t cancel timers. I invalidated them with a validity flag.
The Problem
I’m building Jaguara — a recruitment platform that turns WhatsApp into a CRM for recruiters. The whole pitch is that recruiters keep using WhatsApp normally, while an AI assistant interprets their natural language commands behind the scenes to help them manage the flow of candidates.
Simple enough. Until I ran the first real test.
During onboarding, the user typed:
“Hi”
“How are you?”
“I saw Jaguara’s post”
“and I wanted to know more”
By the time the third message arrived, my agent had already replied to the first one. Classified “Oi” as an error. Sent a confused greeting. Interrupted the user mid-thought.
That’s not how we use WhatsApp. Nobody sends one neat paragraph. People chat one line at a time.
I needed the agent to wait for all the messages before he starts to reply.
The fix was a debounce pattern applied to conversational AI.
The data model
InboundMessage
-> fromnumber: text
-> body: text
InboundMessageBuffer
-> fromnumber: text
-> messages: list of InboundMessage
-> processed: boolean
InboundMessageBatch
-> fromnumber: text
-> isvalid: boolean
-> schedulefordate: date
-> buffer: InboundMessageBuffer
-> isaborted: boolean
Plus an option set InboundBatchConfig with a debounce_seconds attribute, so the waiting period is not hardcoded into a workflow.
The flow
msg 1 ──► Buffer [ msg1 ] Batch A (isvalid=true) ──► fires at T+4s
│
msg 2 ──► Buffer [ msg1, msg2 ] Batch A (isvalid=FALSE) ◄───┘
Batch B (isvalid=true) ──► fires at T+4s
│
msg 3 ──► Buffer [ msg1,2,3 ] Batch B (isvalid=FALSE) ◄────┘
Batch C (isvalid=true) ──► fires at T+4s
│
4s silence ▼
isvalid=true ✓
→ call AI Agent with
[ msg1, msg2, msg3 ]
Batch A and B wake up, check isvalid, find false, abort. Only Batch C does real work.
The workflows
_Workflow 1 — Handle Inbound Message
Runs on every incoming message.
1_ If Buffer doesn’t exist for this number → create one (processed = false)
2_ Append the message to Buffer.messages
3_ Search for existing Batch where fromnumber = sender AND isvalid = true → set isvalid = false
4_ Create a new Batch: isvalid = true, schedulefordate = now + debounce_seconds, linked to that Buffer
5_ Schedule “Process Batch” at schedulefordate
Step 3 is the cancel. You’re not stopping the old scheduled run — you’re just flipping the flag it will check when it wakes up.
_Workflow 2 — Process Batch
Runs after the silence window.
1_ If Batch.isvalid = false → set isaborted = true, terminate (That’s the guard. Stale batches exit before they touch anything.)
2_ Walk the chain: Batch → Buffer → messages → build the prompt with full conversation context
3_ Call the AI with all buffered messages as a single concatenated input
4_ Parse the JSON response, store it back on the Batch
5_ Mark Buffer.processed = true
6_ Schedule the send-reply workflow immediately
The pattern in plain terms
Every inbound message invalidates the previous batch and creates a new one. The scheduled workflows still all fire on schedule — but only the last one passes the isvalid check. The rest self-terminate on the first action.
It’s a soft-cancel. The timer runs regardless; the flag decides whether anything happens.
The debounce window is 4 seconds. Living in an option set so I can change it without opening the workflow editor.
If you’re building on Bubble and need timer reset behavior — this is the pattern. No plugins, no external queues.
Hope this helps. ![]()