Back to blog
Backend Systemsintermediate

Amazon Connect IVR: Contact Flows, Routing & Business-Hours Logic

Build production IVR contact flows in Amazon Connect — DTMF menus, business-hours checks, Lambda data dips, queue transfers, voicemail, and exporting flows as JSON for version control.

LearnixoApril 16, 20265 min read
Amazon ConnectIVRContact FlowAWSTelephonyCall Center
Share:𝕏

What Is a Contact Flow?

A Contact Flow is the script that runs when a patient calls your phone number. It's a directed graph of blocks connected by branches.

Common block types: | Block | What It Does | |-------|-------------| | Play prompt | Say something to the caller (TTS or audio file) | | Get customer input | Wait for DTMF digits or speech | | Check hours of operation | Branch on whether a queue is open | | Set working queue | Point to the queue to transfer to | | Transfer to queue | Actually send the call to agents | | Invoke AWS Lambda | Look up patient data, check schedules | | Set contact attributes | Store data that flows through the call | | Disconnect | End the call |


A Typical Optometry IVR Flow

Inbound call
    ↓
Play greeting: "Thank you for calling Sunrise Eye Care"
    ↓
Check hours of operation
    ├─ Open → Get input (menu)
    │           ├─ 1: Schedule appointment → Transfer to scheduling queue
    │           ├─ 2: Insurance questions → Transfer to insurance queue
    │           ├─ 3: Existing patient → Lambda: look up patient → transfer
    │           └─ Timeout/Invalid → Repeat menu (max 3x) → Transfer to general
    └─ Closed → Play after-hours message → Offer callback → Disconnect

Contact Flow JSON Structure

Connect stores flows as JSON (importable/exportable). This enables version control:

JSON
{
  "Version": "2019-10-30",
  "StartAction": "greeting-block-id",
  "Actions": [
    {
      "Identifier": "greeting-block-id",
      "Type": "MessageParticipant",
      "Parameters": {
        "Text": "Thank you for calling Sunrise Eye Care. Your call is important to us."
      },
      "Transitions": {
        "NextAction": "hours-check-id",
        "Errors": [{ "NextAction": "hours-check-id", "ErrorType": "NoMatchingError" }]
      }
    },
    {
      "Identifier": "hours-check-id",
      "Type": "CheckHoursOfOperation",
      "Parameters": {
        "HoursOfOperationId": "arn:aws:connect:us-east-1:123:instance/xxx/operating-hours/yyy"
      },
      "Transitions": {
        "NextAction": "main-menu-id",
        "Conditions": [
          { "NextAction": "after-hours-id", "Condition": { "Operator": "Equals", "Operands": ["False"] } }
        ],
        "Errors": [{ "NextAction": "main-menu-id", "ErrorType": "NoMatchingError" }]
      }
    },
    {
      "Identifier": "main-menu-id",
      "Type": "GetParticipantInput",
      "Parameters": {
        "Text": "Press 1 to schedule an appointment. Press 2 for insurance questions. Press 3 for our after-hours team.",
        "InputTimeLimitSeconds": "5",
        "DTMF": {
          "NumberOfDigits": "1",
          "DisableCancel": "false"
        }
      },
      "Transitions": {
        "NextAction": "transfer-general-id",
        "Conditions": [
          { "NextAction": "set-scheduling-queue-id", "Condition": { "Operator": "Equals", "Operands": ["1"] } },
          { "NextAction": "set-insurance-queue-id",  "Condition": { "Operator": "Equals", "Operands": ["2"] } },
          { "NextAction": "after-hours-id",          "Condition": { "Operator": "Equals", "Operands": ["3"] } }
        ],
        "Errors": [
          { "NextAction": "transfer-general-id", "ErrorType": "InputTimeLimitExceeded" },
          { "NextAction": "main-menu-id",        "ErrorType": "NoMatchingCondition" }
        ]
      }
    }
  ]
}

Lambda Data Dip Mid-Call

One of the most powerful Connect features: invoke Lambda during a call to personalize the experience.

Use case: Caller presses "3" (existing patient). Look up the patient by phone number, get their next appointment, and announce it.

Python
# lambda/connect_patient_lookup/handler.py

import boto3
import os
from datetime import datetime

table = boto3.resource("dynamodb").Table(os.environ["TABLE_NAME"])

def handler(event, context):
    """
    Amazon Connect passes the call context in event.
    Return a flat dict of string attributes.
    """
    # The caller's phone number (E.164 format: +12125551234)
    caller_phone = event["Details"]["ContactData"]["CustomerEndpoint"]["Address"]
    
    # Normalize to just digits for lookup
    normalized = caller_phone.replace("+1", "").replace("-", "").replace(" ", "")
    
    try:
        # Look up patient by phone number (GSI)
        response = table.query(
            IndexName="PhoneIndex",
            KeyConditionExpression="phone = :phone",
            ExpressionAttributeValues={":phone": normalized},
            Limit=1
        )
        
        if not response["Items"]:
            return {"Found": "false"}
        
        patient = response["Items"][0]
        next_appt = get_next_appointment(patient["id"])
        
        # Connect requires ALL values to be strings
        result = {
            "Found": "true",
            "PatientName": patient.get("first_name", ""),
            "ClinicName": patient.get("clinic_name", ""),
        }
        
        if next_appt:
            dt = datetime.fromisoformat(next_appt["datetime"])
            result["NextAppointment"] = dt.strftime("%A %B %d at %I:%M %p")
            result["AppointmentId"] = next_appt["id"]
        else:
            result["NextAppointment"] = ""
        
        return result
        
    except Exception as e:
        # Never crash the call — Connect will use error branch
        print(f"Patient lookup failed: {e}")
        return {"Found": "false"}

In the contact flow, after the Lambda block:

JSON
{
  "Type": "InvokeLambdaFunction",
  "Parameters": {
    "LambdaFunctionARN": "arn:aws:lambda:us-east-1:123:function:connect-patient-lookup",
    "InvocationTimeLimitSeconds": "3"
  },
  "Transitions": {
    "NextAction": "greet-patient-id",
    "Conditions": [
      { "NextAction": "new-patient-flow-id", "Condition": { "Operator": "Equals", "Operands": ["false"], "Variable": "$.External.Found" } }
    ]
  }
},
{
  "Type": "MessageParticipant",
  "Identifier": "greet-patient-id",
  "Parameters": {
    "TextToSpeechType": "ssml",
    "Text": "<speak>Welcome back, <say-as interpret-as='name'>$.External.PatientName</say-as>. Your next appointment is $.External.NextAppointment. How can we help you today?</speak>"
  }
}

Business Hours + Overflow Logic

JSON
{
  "Type": "CheckHoursOfOperation",
  "Parameters": {
    "HoursOfOperationId": "..."
  },
  "Transitions": {
    "NextAction": "main-menu-id",
    "Conditions": [
      { "NextAction": "after-hours-handler-id", "Condition": {"Operator": "Equals", "Operands": ["False"]} }
    ]
  }
},
{
  "Type": "CheckMetricData",
  "Identifier": "check-queue-capacity-id",
  "Parameters": {
    "Metric": { "Name": "CONTACTS_IN_QUEUE" },
    "QueueId": "arn:...:queue/appointment-scheduling",
    "ComparisonOperator": "LT",
    "Threshold": 10
  },
  "Transitions": {
    "NextAction": "transfer-to-queue-id",
    "Conditions": [
      { "NextAction": "queue-full-handler-id", "Condition": {"Operator": "Equals", "Operands": ["False"]} }
    ]
  }
}

Voicemail / Callback Flow

When agents are unavailable, offer a callback:

JSON
{
  "Identifier": "offer-callback-id",
  "Type": "GetParticipantInput",
  "Parameters": {
    "Text": "All agents are currently busy. Press 1 to leave a voicemail and we'll call you back within 2 hours. Press 2 to hold.",
    "DTMF": { "NumberOfDigits": "1" }
  },
  "Transitions": {
    "Conditions": [
      { "NextAction": "start-recording-id", "Condition": {"Operator": "Equals", "Operands": ["1"]} },
      { "NextAction": "transfer-to-queue-id", "Condition": {"Operator": "Equals", "Operands": ["2"]} }
    ]
  }
},
{
  "Identifier": "start-recording-id",
  "Type": "MessageParticipant",
  "Parameters": {
    "Text": "Please leave your name, callback number, and a brief message after the tone."
  },
  "Transitions": { "NextAction": "play-beep-id" }
},
{
  "Type": "StartMediaStreaming",
  "Parameters": {
    "MediaStreamingStartCondition": "AfterTransfer"
  }
}

Version Control for Flows

Export flows to JSON and store in Git:

Bash
# Export all flows
aws connect list-contact-flows \
  --instance-id $INSTANCE_ID \
  --contact-flow-types CONTACT_FLOW \
  --query 'ContactFlowSummaryList[*].Id' \
  --output text | \
while read id; do
  name=$(aws connect describe-contact-flow --instance-id $INSTANCE_ID --contact-flow-id $id --query 'ContactFlow.Name' --output text)
  aws connect describe-contact-flow --instance-id $INSTANCE_ID --contact-flow-id $id \
    --query 'ContactFlow.Content' --output text > "flows/${name}.json"
done

# Import (update) a flow
aws connect update-contact-flow-content \
  --instance-id $INSTANCE_ID \
  --contact-flow-id $FLOW_ID \
  --content file://flows/main-inbound.json

This lets you review IVR logic changes in pull requests just like code.

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.