onCallEndstableOn call ended
Fires once when the call hangs up, regardless of where the main flow was. The downstream chain runs in isolation, so this is the safe place for final webh…
What it does
Fires once when the parent (caller) leg hangs up, no matter where the main
flow happened to be. The chain runs in an isolated state branch after the
worker has finished its own teardown housekeeping (closing the queue entry,
committing any pending voicemail), which means the call is already gone
by the time your downstream nodes run — there is no audio to play, no agent
to talk to, no caller leg to interact with. What you do have is the full
vars snapshot accumulated during the call: vars.lastRecordingUrl,
vars.gatheredDigits, vars.transferTo, anything you stamped along the
way.
This is the right place for the work that has to happen every call,
regardless of which IVR path the caller took or where they dropped off:
final webhooks, CRM "call ended" updates, summary Slack pings, custom
analytics. Because the chain runs in isolation, even if the main flow had
already terminated normally, your onCallEnd chain still runs — it's the
"finally" block of the call.
When to use it
- POST a final summary to your CRM with duration, recording URL, and
vars.gatheredDigits. - Send a Slack message to
#saleswhenever a sales-line call ends, with the caller number and a link to the recording. - Mark a row in your own database as "completed" with the same metadata.
- Kick off an after-call survey by writing a job to a queue (the call is gone — anything synchronous-voice belongs in the main flow before hangup).
If you need a teardown step that runs before hangup (e.g. play a
goodbye message), put it in the main flow before hangup. Once
onCallEnd fires, the carrier connection is already torn down.
Configuration
_label and _note are author-only metadata; this trigger has no runtime
configuration of its own.
Fires once when the call hangs up, regardless of where the main flow was. The downstream chain runs in isolation, so this is the safe place for final webhooks, CRM updates, Slack notifications, or analytics — it always runs even if the caller dropped mid-menu.
| Field | Label | Type | Required | Default | Notes |
|---|---|---|---|---|---|
_label | Trigger label | text | Optional | — | Friendly name shown in the canvas and run logs. Optional. |
_note | Internal note | textarea | Optional | — | Notes for your team. |
Outgoing events: triggered
Examples
Final summary webhook
Posts a JSON payload to your own service every time a call ends, no matter how it ended.
{
"id": "end-trigger",
"type": "onCallEnd",
"config": { "_label": "Post call summary to webhook" },
"on": { "triggered": "post-summary" }
}
[onCallEnd] ──► [httpCall POST /webhooks/call-ended
body: {
from: "{{event.from}}",
to: "{{event.to}}",
recording: "{{vars.lastRecordingUrl}}",
digits: "{{vars.gatheredDigits}}"
}] ──► [end]
Gotchas
- No audio side-effects work here. The caller leg is already gone by
the time
onCallEndfires.say,playAudio,record,holdCall, and anything else that talks to the carrier will fail or no-op silently. Do outbound HTTP,setVar,customScript, orrunFlowwork only. - Trigger errors are swallowed. If a node in the chain throws (the
third-party API is down, an
httpCalltimes out), the worker logs the error and moves on — it doesn't retry, and there's noonErrorpath on the trigger itself. Build retries into your destination service if you need at-least-once delivery semantics. - Side-chain runs in an isolated state branch. The chain shares the
call's final
varssnapshot but cannot mutate flow position — by this point there is no flow position to mutate. Mutations tovarsfrom inside the chain are not persisted anywhere durable; the run row is about to be marked completed. - Pending voicemail is committed before the trigger runs. If the
caller hung up mid-voicemail, the worker calls
commitVoicemail()first, so by the time your chain runsvars.lastRecordingUrlis populated. Don't try to commit the voicemail yourself.
