Filtering Which Events Run a Workflow
Three places to narrow what reaches a workflow — the agent's Custom Webhook, a per-URL filter, and the trigger node — plus the two traps that send messages to people who never answered.
A Custom Webhook relays the whole life of a call, so a workflow whose job is "message the contact afterwards" runs once per event unless something narrows it. Four WhatsApp templates for one conversation is the usual way people discover this.
There are three places to narrow it. They are independent, and you will often want two of them.
| Where | Narrows by | Applies to |
|---|---|---|
| Agent → Custom Webhook → Send these events | event type | every delivery from that agent |
| Workflow → Webhooks → Run On Events | event, outcome, reason, expression | one webhook URL |
| The workflow's trigger node | event, outcome, reason, expression | the whole workflow |
Narrow at the source first
Ticking events in the agent dialog stops us sending them at all. The workflow filters discard on arrival, which is the right tool when you do not control the sender — an inbound integration, a relay, a URL several agents post to.
The two traps
Both of these send messages to people you never spoke to. Neither is obvious from the event name.
`call_completed` means the call ENDED, not that it succeeded
It fires on busy, no answer, failed and canceled exactly as it
fires on a real conversation. The outcome is in status, not in the event
name. Subscribing to call_completed alone will message every busy signal.
Add an outcome filter of Completed to get only calls that connected.
A voicemail can be reported as `completed`
An answering machine recognised after the call was recorded as connected is
status: "completed" with sub_status: "VOICEMAIL_DETECTED" — the usual case
is no_answer / voicemail_detected. No outcome filter can separate the
first from a real conversation, however it is worded — status is
deliberately coarse.
That is what the reason filter is for. Tick Customer hung up and Agent ended the call, or exclude voicemail in the advanced filter.
The four dimensions
Set on a trigger node, or per webhook URL. They AND together, and a blank one is not a constraint.
Events
Which event types may run the workflow. Nothing ticked means every event. See Event types for what each one carries.
Call outcomes
Matched against status. The published vocabulary is:
call-disconnected, error and balance-low are all reported as failed,
so one box selects them. Hyphen and underscore spellings are treated as the same
rule, so no-answer and no_answer both match.
Reasons (sub-status)
Matched against sub_status — the why behind an outcome, and the only place
some distinctions exist at all:
- Voicemail, which
statusreports ascompletedorno_answer - Insufficient balance, Disconnected by the platform, Platform error
and Agent did not take the call, which
statusreports asfailed - Stopped by user, which
statusreports ascanceled - How a connected call ended: customer hung up, agent ended it, silence timeout, duration cap, transferred
Casing is part of the contract — VOICEMAIL_DETECTED on a completed call,
voicemail_detected on a failed one — and one checkbox matches both. The
open-ended sip_<code> family is not listed; use the advanced filter for a
specific code.
Advanced filter
JavaScript, evaluated last, on top of the ticks above. It has payload,
event, status and sub_status in scope and must return true to run:
An expression that throws denies rather than admits — a filter its author believes is protecting them must not silently stop protecting them. The reason comes back in the response body, so a typo shows up on the first event.
Precedence
A webhook URL with its own filter is answered by that filter alone. It replaces the trigger node's rather than adding to it, which is the same most-specific-wins rule that resolves an agent's subscription against the organization-wide one.
Replace, not AND
ANDing them would let a URL-level "only call_completed" and a node-level
"only call_ringing" cancel out to a workflow that never runs, with nothing
on screen to say why. A webhook that sets nothing inherits the node's filter.
Across several trigger nodes the rules OR together — each trigger is an independent entry point.
What a filtered-out event receives
HTTP 200, with a body naming what happened:
200 rather than 4xx is deliberate: the sender did nothing wrong, and an error
code would put the delivery into the retry ladder and mark your endpoint
unhealthy for an event you asked to ignore. filteredBy says which filter
answered, so precedence is never a guess.
Filtering happens before an execution is created, so a discarded event leaves no run in the workflow's history and costs no storage.
Variables worth knowing
`{{call.transcript}}` is an array
It is a list of {role, content} turns. That is correct inside a JSON request
body, where it injects as a typed array — but in a text field, such as a
WhatsApp template parameter, it renders as [object Object].
Use {{call.transcript_text}} for readable lines. {{call.sub_status}} is
available too, for the voicemail distinction {{call.disposition}} cannot make.

