Inbound Message Flow

WhatsRB Cloud — Complete pipeline from WhatsApp to webhook

Every inbound WhatsApp message follows this pipeline: debounce batching → pre-filter rules → human takeover check → nano LLM routing (filters acknowledgments + routes to agents) → agent execution (with conversation context) → webhook dispatch. Each stage can exit early, saving tokens and quota.

Step 1
Message Received
Meta Cloud API webhook fires → CloudApiWebhookHandler parses the 31 message types + statuses → creates BusinessMessage record → enqueues RouteInboundBatchJob.
enqueue with debounce delay
Step 2
can skip
Debounce Batch
Waits agent.debounce_seconds (1-60s) to collect multiple rapid messages from the same phone into one batch. If a newer job exists for this conversation, the current one exits — only the latest job runs.
no newer job exists → proceed
Step 3
new
Human Takeover Check
ConversationPause.active_for?(ba_id, phone) — checks if a human has taken over this conversation. Pauses are created manually (API), on outbound message, or by agent proposal. Normalizes +33 vs 33 formats.
If paused
Exit immediately — no agent run, no LLM cost. Human handles the conversation.
Auto-expire
Default 30 min — configurable via human_takeover_duration. Each manual outbound resets the timer.
not paused → proceed
Step 4
can skip
PreFilter Rules
PreFilterRules — free, rule-based pre-filter (no LLM). Checks regex patterns, spam detection, min length, excluded numbers. Filters out greetings, emojis, "ok", "thanks" without spending tokens.
passes pre-filter → LLM territory
Step 5
Inbound Router
InboundRouter — single LLM call (gpt-4.1-nano, temp=0) classifies the message and routes to the right agent(s). Runs for all cases — including single-agent setups — to filter acknowledgments like "ok thanks", "got it", "super merci". Returns actionable (bool) + reason + agent_ids. If not actionable → exit. On error → fail-open to all agents.
Model
gpt-4.1-nano — configurable via ROUTING_MODEL env var
Smart filter
Catches acknowledgments, small talk, and thank-you messages that pass PreFilter regex — saves agent runs and quota
actionable → per-agent filter
Step 6
can skip
Inbound Filter (per-agent)
InboundFilter — per-agent rules: trigger_on (all/text_only), patterns, min_message_length, max_messages_per_hour, excluded_numbers. No LLM cost — pure config.
passes filter → agent run
Conversation Context Loop
Step 7a
new
Fetch Previous Context
fetch_previous_run — finds the last completed AgentRun for this phone + agent within the TTL window. Uses metadata @> conversation_key (jsonb + GIN index).
TTL
context_ttl_minutes — default 60, set to 0 to disable. If last run is older → no context (new topic).
Injected
context_summary (text) + last_agent_output (intent + actions from previous run)
{ "messages": ["medium ones"], "context_summary": "Guest in B12 requested towels, asked about size", "last_agent_output": { "intent": "request_towel", "actions": [{ "type": "hospitality.room_service.request", "payload": { "room": "B12", "size": null }}] } }
enriched context → LLM
Step 7b
Agent Runner (LLM Call)
AgentRunner — builds system prompt + enriched context → calls AI::ModelAdapter → LLM returns structured output via propose_actions tool. Output includes intent, actions, suggested_reply, and the new context_summary.
Model
gpt-4o-mini primary, deepseek-chat fallback
Cost per run
~500-600 input + ~100-200 output tokens
output → save + loop back
Step 7c
new
Save Context Summary
The LLM-generated context_summary is saved on the AgentRun record. The next inbound message from this phone (within TTL) will pick it up in step 7a — closing the loop.
// Saved on AgentRun: "context_summary": "Guest in B12 wants medium towels, order fulfilled" // Next run will receive this ↑ as context
completed → post-processing
Step 8a
Handle Special Actions
System actions (system.noop.skip, system.conversation.pause) are auto-injected into all agents and stripped from webhook payloads. system.conversation.pause triggers ConversationPauseManager. Internal only — never reaches user's backend.
Step 8b
Webhook Dispatch
WebhookDispatcher sends agent_run.completed event to user's endpoint. HMAC-signed, with retry. Payload includes output, tokens, latency.

Context Summary — How It Flows

Run N
First message
"I need towels for room B12"

No previous context → agent processes from scratch.
Output: intent=request_towel, room=B12, size=null

Saves: "Guest in B12 requested towels, asked about size"
Run N+1
Follow-up (within TTL)
"medium ones"

Context injected → agent understands "medium ones" = towel size.
Output: intent=request_towel, room=B12, size=medium

Saves: "Guest in B12 wants medium towels, fulfilled"
2h later (beyond TTL)
Run N+2
New topic
"I also need a taxi for tomorrow at 8am" — TTL expired, no context injected. Agent treats it as a fresh conversation. Clean slate.

Early Exit Points

Exit @ Step 2
Newer job exists
Debounce: a more recent batch job was scheduled → this one exits. Cost: 0 tokens.
Exit @ Step 3
new
Human takeover active
Pause active for this phone → skip entirely. Human is handling it. Cost: 0 tokens.
Exit @ Step 4
PreFilter rejects
Rule-based filter catches spam, greetings, too-short messages. Cost: 0 tokens.
Exit @ Step 5
Router: not actionable
Nano router catches acknowledgments ("ok thanks", "got it"), small talk, and non-actionable messages. Cost: ~450 tokens (nano only, no agent run). Saves quota.