Graine AI

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.

WhereNarrows byApplies to
Agent → Custom Webhook → Send these eventsevent typeevery delivery from that agent
Workflow → Webhooks → Run On Eventsevent, outcome, reason, expressionone webhook URL
The workflow's trigger nodeevent, outcome, reason, expressionthe 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:

completed · busy · no_answer · failed · canceled
queued · initiated · ringing · in_progress

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 status reports as completed or no_answer
  • Insufficient balance, Disconnected by the platform, Platform error and Agent did not take the call, which status reports as failed
  • Stopped by user, which status reports as canceled
  • 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:

payload.call_duration > 30 && sub_status !== 'voicemail_detected'

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:

{
  "ok": true,
  "skipped": true,
  "status": "filtered",
  "reason": "Webhook \"Post-call\": status \"busy\" is not in [completed]",
  "event": "call_completed",
  "callStatus": "busy",
  "callSubStatus": null,
  "filteredBy": "webhook"
}

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.

Next

On this page