Documentation

Call routing

From inbound Twilio call to running IVR flow — how AppEngine looks up routing, picks the mode, and starts the action sequence.

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

POST/connect/webhook/twilio/ivr-menuNo auth

Twilio'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:

  1. Loads the routing record for the called number from phone_routing.
  2. Resolves business-hours state — open, holiday, after_hours, or off_day.
  3. Picks the flow id: normalHoursFlowId / afterHoursFlowId / holidayFlowId.
  4. Decides the mode: forward | ai-assistant | simple_menu | automation_flow.
  5. 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:

FallbackOutcome
voicemailInline <Record> with the routing's voicemail config
ai-assistant<Connect><Stream> to simple-voice/stream/{orgId}/{assistantId}
automationTrigger 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:

actionBehavior
forwardRing the named forwarding group
transfer-phoneDirect dial a number
transfer-queueEnqueue to a Twilio queue
voicemailInline voicemail recording
send-smsQueue an SMS to the caller
submenuNest into another menu
ai-assistantHand off to a specialty assistant
automationRun 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:

POST/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_call tool (CRM tool registry) creates a Twilio call whose URL is /connect/webhook/twilio/ivr-ai-outbound. That handler returns TwiML opening a <Stream> to simple-voice/stream/{orgId}/{assistantId}.

Status callbacks

Every leg's lifecycle (initiated, ringing, answered, completed, busy, no-answer, failed) is POSTed to:

POST/connect/webhook/twilio/statusNo auth

The 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.