How I built a message debouncer on Bubble

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. :+1:

Canceling a scheduled workflow

That’s not correct…

That’s correct, I was not aware of this feature. Still, it’s not much more easy to use.

lol the idea sounds fun though. Seems like crm automation is the new big thing (bc people are just extremely lazy)