Documentation

IVR flow builder

Compose call flows from typed nodes — speak, collect, menu, transfer, record, pay — and store them as automations.

The IVR builder isn't a separate engine; it's the automation engine with a focused set of action types. An IVR flow is an automation record whose steps use the ivr_* actions. The same trigger system, condition system, and execution engine that drive marketing automations drive call flows — which means you can mix IVR steps with send_email, create_task, or add_tag in the same flow.

What gets built

A complete phone-routing setup involves three layers:

  1. Routing record — per phone number. Decides which mode runs the call: forward | ai-assistant | simple_menu | automation_flow. Defines forwarding groups, business hours, holidays.
  2. IVR flow — the automation, when routingType: 'automation_flow'. Holds the step graph.
  3. Steps — typed ivr_* actions chained by nextStepId.

For simpler routing modes (forward, ai-assistant, simple_menu) you don't author a flow at all — the orchestrator generates TwiML from the routing record directly.

Available IVR actions

ActionPurpose
ivr_speakText-to-speech message
ivr_collect_inputCollect DTMF digits or speech
ivr_menuPresent DTMF menu options
ivr_transferTransfer to phone, agent, or queue
ivr_forwardRing group: simultaneous or sequential
ivr_recordRecord caller audio
ivr_voicemailVoicemail with transcription + notifications
ivr_play_audioPlay an audio file by URL
ivr_send_smsSend SMS during the call
ivr_send_emailSend email during the call
ivr_ai_assistantHand off to the AI voice assistant via <Stream>
ivr_twilio_payPCI-compliant payment collection
ivr_hangupEnd the call

Source: appengine/src/automation/actions/ivr/. Each action validates its config, generates TwiML, and (where it waits for caller input) sets pauseExecution: true so the orchestrator parks the run until Twilio calls back.

Variable interpolation

Every action supports {{variable}} substitution against the call context:

  • {{caller}} — caller's E.164 number
  • {{calledNumber}} — the IVR number that was dialed
  • {{callSid}} — Twilio's call ID
  • {{variables.<name>}} — anything stored via saveAs from a previous step

So Thanks for choosing {{variables.selectedDept}} reads back what the menu step captured.

Flow JSON shape

A flow is an automation record. The IVR-relevant fields:

{
  "name": "Sales support menu",
  "trigger": { "type": "ivr-incoming-call" },
  "steps": [
    {
      "id": "greet",
      "type": "ivr-action",
      "actionType": "ivr_speak",
      "config": {
        "message": "Welcome to Acme. Please listen carefully.",
        "voice": "Polly.Joanna-Neural"
      },
      "nextStepId": "menu"
    },
    {
      "id": "menu",
      "type": "ivr-action",
      "actionType": "ivr_menu",
      "config": {
        "greeting": "For sales press 1, for support press 2, or stay on the line.",
        "saveAs": "selection",
        "options": [
          { "digit": "1", "label": "sales",   "nextStepId": "transfer-sales"   },
          { "digit": "2", "label": "support", "nextStepId": "transfer-support" }
        ],
        "onTimeout": "voicemail"
      }
    },
    {
      "id": "transfer-sales",
      "type": "ivr-action",
      "actionType": "ivr_transfer",
      "config": {
        "transferType": "phone",
        "to": "+14155550100",
        "record": true
      }
    },
    {
      "id": "transfer-support",
      "type": "ivr-action",
      "actionType": "ivr_forward",
      "config": {
        "ringMode": "simultaneous",
        "ringDuration": 25,
        "targets": [{ "type": "phone", "value": "+14155550101" }],
        "fallback": "voicemail"
      }
    },
    {
      "id": "voicemail",
      "type": "ivr-action",
      "actionType": "ivr_voicemail",
      "config": {
        "greeting": "Sorry we missed you. Leave a message after the tone.",
        "maxLength": 180,
        "transcribe": true,
        "notifyEmail": "[email protected]"
      }
    }
  ]
}

The id values are referenced by nextStepId (and by menu options' per-option nextStepId). Branching is implicit in those references.

Authoring through the API

Flows are created the same way as any automation:

POST/automationJWT
curl -X POST https://appengine.appmint.io/automation \
  -H "orgid: my-org" -H "Authorization: Bearer <jwt>" \
  -H "Content-Type: application/json" \
  -d '{ "data": { "name": "...", "trigger": {...}, "steps": [...] } }'

Update with PUT /automation/:automationId, delete with DELETE /automation/:automationId.

Wiring a flow to a number

Once the flow exists, attach it to a phone routing record:

{
  "phoneNumber": "+14155551234",
  "routingType": "automation_flow",
  "normalHoursFlowId": "<automationId>",
  "afterHoursFlowId": "<automationId>",
  "businessHours": { "enabled": true, "officeHours": [...], "holidays": [...] }
}

After-hours and holiday flows are optional. If unset, the normal-hours flow runs around the clock.

Testing without a phone

The action layer is testable in isolation:

POST/automation/{automationId}/executeJWT

Pass synthetic variables in the body and the orchestrator runs the flow as if a call had come in. The TwiML is returned in the step results so you can eyeball what Twilio would have rendered.

Pause / resume mechanics

Most IVR actions are synchronous: speak, hangup, send-sms. A few — ivr_collect_input, ivr_menu, ivr_record, ivr_voicemail, ivr_twilio_pay — set pauseExecution: true and emit TwiML with an action URL pointing at:

/connect/webhook/twilio/ivr-{input|menu-input|recording|payment-status}?executionId={id}&saveAs={var}&nextStepId={id}

Twilio calls that endpoint with the user's input. The webhook stores the captured value (under saveAs), looks up the flow execution, and resumes from nextStepId — generating the next TwiML in the response.

Recommended structure

For anything beyond a 3-step happy path, organize the flow as a directed graph:

  • One greeting / ivr_speak
  • One ivr_menu per branch point with explicit onTimeout fallback
  • Leaf actions: ivr_transfer, ivr_forward, ivr_voicemail, ivr_hangup
  • Reusable error path that ends in ivr_voicemail so callers always have an exit

Flows that don't terminate (no ivr_hangup and no transfer) leave Twilio holding an open call until its 4-hour cap. Always make sure every leaf is a terminal action.

The IVR builder UI in the Appmint Studio admin is a graph editor on top of this same JSON. It surfaces validation errors from each action's validate() so configuration mistakes are caught before publishing.