When a call hits a phone number AppEngine manages, Twilio POSTs to /connect/webhook/twilio/ivr-menu. The IVR Flow Orchestrator looks up the routing record, decides which mode to run, and returns TwiML — either inline (forward, simple menu) or by handing off to the automation engine (automation flow).
The routing entry point
/connect/webhook/twilio/ivr-menuNo authTwilio's payload includes From (caller), To (called number), CallSid, AccountSid, plus any Digits / SpeechResult for in-call follow-ups. AppEngine derives the org from the ?orgid=... query parameter that was burned into the webhook URL when the number was provisioned.
The handler:
- Loads the routing record for the called number from
phone_routing. - Resolves business-hours state —
open,holiday,after_hours, oroff_day. - Picks the flow id:
normalHoursFlowId/afterHoursFlowId/holidayFlowId. - Decides the mode:
forward|ai-assistant|simple_menu|automation_flow. - Runs the mode (described below).
forward — direct ring
Generates <Dial> TwiML against the configured forwarding group. Supports optional greeting, whisper, and screening. When the dial completes (answer, busy, or no-answer), Twilio re-POSTs and the orchestrator runs the configured fallback:
| Fallback | Outcome |
|---|---|
voicemail | Inline <Record> with the routing's voicemail config |
ai-assistant | <Connect><Stream> to simple-voice/stream/{orgId}/{assistantId} |
automation | Trigger the configured fallbackAutomationId |
hangup | <Hangup> |
No automation execution involved — the orchestrator just emits TwiML.
ai-assistant — AI receptionist
Generates <Connect><Stream> immediately with custom parameters carrying caller and routing identifiers. The simple-voice gateway picks them up, loads the routing JSON from the cache, and the assistant runs the conversation. See Voice streaming.
If the assistant calls its transfer_call tool, the gateway re-POSTs the active call to /connect/webhook/twilio/ivr-ai-transfer, which generates <Dial> against the chosen forwarding group.
simple_menu — DTMF menu
Inline <Gather> TwiML with options enumerated from the routing record. Each option carries an action:
action | Behavior |
|---|---|
forward | Ring the named forwarding group |
transfer-phone | Direct dial a number |
transfer-queue | Enqueue to a Twilio queue |
voicemail | Inline voicemail recording |
send-sms | Queue an SMS to the caller |
submenu | Nest into another menu |
ai-assistant | Hand off to a specialty assistant |
automation | Run an automation flow |
When the caller presses a digit, Twilio POSTs back to the same handler with Digits=<n> and the orchestrator resolves the action. Catch-all defaultAction runs after maxRetries of unrecognized input.
automation_flow — full builder
Spins up an automation execution and returns the TwiML the first action emits. From there, every action that pauses execution attaches a webhook URL with executionId so Twilio can POST back and resume:
/connect/webhook/twilio/ivr-input?executionId=...&saveAs=...&nextStepId=...
/connect/webhook/twilio/ivr-menu-input?executionId=...&saveAs=...&options=...
/connect/webhook/twilio/ivr-recording?executionId=...&saveAs=...&nextStepId=...
/connect/webhook/twilio/ivr-payment-status?executionId=...&saveAs=...&nextStepId=...
/connect/webhook/twilio/ivr-continue?executionId=...&stepId=...
The orchestrator finds the execution, stores the captured value under saveAs, and dispatches the next step.
Manual routing test
There is no public REST endpoint that routes a call without Twilio — the routing logic is driven by Twilio's webhook payload. To unit-test a flow:
/automation/{automationId}/executeJWT{
"variables": {
"caller": "+14155550000",
"calledNumber": "+14155551234",
"callSid": "CA-test"
}
}
The action results return the TwiML each step would have rendered. For end-to-end tests, point a Twilio test account's number at your dev environment.
Outbound calls
Outbound calls don't hit /ivr-menu. They use one of:
- Softphone dial — browser
Twilio.Device.connect({ params: { To: '+1...' } }). Twilio runs the App's voice URL, which is/connect/webhook/twilio/voice. The handler reads the dialing identity, applies caller-ID rules, and returns<Dial>. - AI outbound — the
make_calltool (CRM tool registry) creates a Twilio call whose URL is/connect/webhook/twilio/ivr-ai-outbound. That handler returns TwiML opening a<Stream>tosimple-voice/stream/{orgId}/{assistantId}.
Status callbacks
Every leg's lifecycle (initiated, ringing, answered, completed, busy, no-answer, failed) is POSTed to:
/connect/webhook/twilio/statusNo authThe handler updates the call_log, increments usage counters, and emits NestJS events that automations / CRM activities listen to.
Sequence diagram (automation flow path)
Caller --(call)--> Twilio
Twilio --(POST /connect/webhook/twilio/ivr-menu)--> AppEngine
AppEngine: lookup routing → automation_flow → start execution → run step 1
AppEngine --(TwiML)--> Twilio
Twilio --(<Gather> plays, gets digits)--> AppEngine
POST /connect/webhook/twilio/ivr-menu-input?executionId=...
AppEngine: resume execution → run next step → render TwiML
... repeat until terminal action ...
AppEngine --(<Hangup> | <Dial> | <Stream>)--> Twilio
Twilio --> Caller
The orchestrator is stateless between webhook calls — every resume reads the execution from the data layer. Pods can come and go mid-call without losing context, which is why every redirect URL carries executionId and stepId instead of relying on memory.