AWS Step Functionsで連絡先リストにAmazon Connectアウトバウンドキャンペーンを実行し、緊急出勤可否をIVRで自動確認してみた

AWS Step Functionsで連絡先リストにAmazon Connectアウトバウンドキャンペーンを実行し、緊急出勤可否をIVRで自動確認してみた

2025.08.27

はじめに

前回、Amazon Connect Customer Profilesを利用せずに、アウトバウンドキャンペーンをAWS Step Functionsから実行する方法を以下の記事でご紹介しました。

https://dev.classmethod.jp/articles/step-functions-connect-outbound-campaigns-without-profiles/

今回は、AWS Step FunctionsとAmazon DynamoDBを組み合わせて、緊急時の出勤可否確認システムを構築してみました。
このシステムでは、DynamoDBに保存された従業員の連絡先リストに対してアウトバウンドキャンペーンを実行し、IVRで出勤可否を確認した結果を自動的にDynamoDBに保存します。

このシステムは緊急出勤確認以外にも、IVRを使った様々な自動応答システムとして活用できます。想定される活用シーンは以下の通りです。

  • 緊急時の出勤確認: 災害時や緊急事態発生時に従業員への自動出勤可否確認を実施し、IVR応答(1=出勤可能、2=出勤不可)で迅速に人員状況を把握
  • 災害時の安否確認システム: 地震や台風などの災害発生時に従業員や顧客への一斉安否確認を実施し、IVR応答(1=安全、2=軽傷、3=重傷・要支援)で安全状況を自動収集
  • システム障害時の連絡確認: 重要システムの障害発生時に運用チームへの障害通知確認状況を一斉チェックし、IVR応答(1=対応可能、2=対応不可)で情報伝達状況を効率的に把握
  • 顧客満足度調査の自動実施: サービス利用後の顧客への満足度調査を自動実施し、IVR応答(1=非常に満足、2=満足、3=普通、4=不満、5=非常に不満)でフィードバックを効率的に収集

システム概要

構築するシステムのステートマシン処理フローは以下の通りです。

  1. DynamoDBから連絡先取得: 緊急連絡先リストから従業員情報(電話番号と名前)を取得
  2. アウトバウンドキャンペーン実行: Amazon Connect Campaigns V2 APIで一斉発信
  3. 通話完了待機: 5分間待機して通話処理の完了を待つ
  4. 通話結果確認: 各コンタクトの詳細情報を並行取得
  5. 結果処理: 各コンタクトの通話結果とIVR応答結果を整理
  6. DynamoDBに保存: 処理結果を出勤可否結果テーブルに保存
  7. サマリー生成: ステートマシンの実行結果に処理結果を出力

cm-hirai-screenshot 2025-08-27 7.58.21

コンタクトフローの作成

緊急時出勤確認用のコンタクトフローを作成します。以下の機能を含める必要があります。

  1. 音声ガイダンス: 「緊急出勤の確認です。出勤可能な方は1を、出勤できない方は2を押してください」
  2. IVR入力受付: ユーザーからの1または2の入力を受け付け
  3. コンタクト属性設定: 入力値をattendanceResponse属性として保存

cm-hirai-screenshot 2025-08-26 15.05.05

アウトバウンドキャンペーンの有効化

Amazon Connectインスタンスでアウトバウンドキャンペーン機能を有効化する必要があります。インスタンスの設定画面から「アウトバウンドキャンペーン」を有効にしてください。

cm-hirai-screenshot 2025-06-30 15.49.36

アウトバウンドキャンペーンの作成

アウトバウンドキャンペーンの作成画面から、[外部キャンペーンをホスト] をクリックします。

cm-hirai-screenshot 2025-07-01 10.42.27

以下の設定でキャンペーンを作成します。作成時には先ほど作成したコンタクトフローを指定してください。

cm-hirai-screenshot 2025-08-21 14.35.36

キャンペーン作成後、キャンペーンID を確認し、コピーしておきます。このIDは後のステートマシンで使用します。

cm-hirai-screenshot 2025-08-21 14.39.46

DynamoDBテーブルの作成

以下の2つのテーブルを作成します。

  • 緊急連絡先リストテーブル(EmergencyContactList)
  • 緊急通話結果テーブル(EmergencyCallResults)

緊急連絡先リストテーブル(EmergencyContactList)

以下の構成でテーブルを作成します。

  • phoneNumber(パーティションキー):電話番号
  • employeeName:従業員名

事前に発信したい連絡先データを以下の形式で保存しておきます。

{
  "phoneNumber": "+8190xxxxxxxx",
  "employeeName": "平井裕二"
}

cm-hirai-screenshot 2025-08-26 14.59.53

緊急通話結果テーブル(EmergencyCallResults)

以下の構成でテーブルを作成します。

  • phoneNumber(パーティションキー):電話番号
  • TTL設定を有効化

ステートマシンの作成

緊急出勤可否確認システム用のステートマシン定義を作成します。

cm-hirai-screenshot 2025-08-27 8.04.33

  • InstanceIdには、ConnectインスタンスARNを設定してください
  • テーブル名は以下を設定しています
    • EmergencyContactList
    • EmergencyCallResults
EmergencyWorkAvailabilityCheck
{
  "Comment": "Emergency Work Availability Check - 緊急時出勤可否確認システム",
  "QueryLanguage": "JSONata",
  "StartAt": "GetContactList",
  "States": {
    "GetContactList": {
      "Type": "Task",
      "Comment": "DynamoDBから緊急連絡先リストを取得",
      "Resource": "arn:aws:states:::aws-sdk:dynamodb:scan",
      "Arguments": {
        "TableName": "EmergencyContactList"
      },
      "Assign": {
        "campaignId": "{% $states.input.campaignId %}",
        "sourcePhone": "{% $states.input.sourcePhone %}",
        "executionTime": "{% $string($now()) %}",
        "contactList": "{% $states.result.Items %}"
      },
      "Retry": [
        {
          "ErrorEquals": ["States.TaskFailed"],
          "IntervalSeconds": 2,
          "MaxAttempts": 3,
          "BackoffRate": 2
        }
      ],
      "Next": "PrepareOutboundRequests"
    },
    "PrepareOutboundRequests": {
      "Type": "Pass",
      "Comment": "アウトバウンドキャンペーン用のリクエストを準備",
      "Assign": {
        "outboundRequests": "{% ( $currentTime := $now(); $expirationTime := $fromMillis($toMillis($currentTime) + 60000); [$contactList ~> $map(function($contact, $index) { { \"ClientToken\": 'emergency-' & $string($toMillis($currentTime)) & \"-\" & $string($index + 1), \"ExpirationTime\": $expirationTime, \"ChannelSubtypeParameters\": { \"Telephony\": { \"DestinationPhoneNumber\": $contact.phoneNumber.S, \"ConnectSourcePhoneNumber\": $sourcePhone, \"Attributes\": { \"CallType\": \"Emergency\", \"EmployeeName\": $contact.employeeName.S, \"CustomerIndex\": $string($index), \"BatchId\": 'emergency-' & $string($toMillis($currentTime)) } } } } })] ) %}"
      },
      "Next": "ExecuteOutboundCampaign"
    },
    "ExecuteOutboundCampaign": {
      "Type": "Task",
      "Comment": "Amazon Connect Campaigns V2 APIでアウトバウンドキャンペーンを実行",
      "Resource": "arn:aws:states:::aws-sdk:connectcampaignsv2:putOutboundRequestBatch",
      "Arguments": {
        "Id": "{% $campaignId %}",
        "OutboundRequests": "{% $outboundRequests %}"
      },
      "Assign": {
        "contactMappings": "{% [$states.result.SuccessfulRequests ~> $map(function($request, $index) { { \"phoneNumber\": $contactList[$index].phoneNumber.S, \"employeeName\": $contactList[$index].employeeName.S, \"contactId\": $request.Id } })] %}",
        "totalRequests": "{% $count($outboundRequests) %}",
        "successfulRequests": "{% $count($states.result.SuccessfulRequests) %}",
        "failedRequests": "{% $count($states.result.FailedRequests) %}"
      },
      "Retry": [
        {
          "ErrorEquals": ["States.TaskFailed"],
          "IntervalSeconds": 2,
          "MaxAttempts": 3,
          "BackoffRate": 2
        }
      ],
      "Next": "WaitForCallsToComplete"
    },
    "WaitForCallsToComplete": {
      "Type": "Wait",
      "Comment": "通話完了まで5分間待機",
      "Seconds": 300,
      "Next": "CheckAllContactsStatus"
    },
    "CheckAllContactsStatus": {
      "Type": "Map",
      "Comment": "全コンタクトのステータスを並行チェック",
      "Items": "{% $contactMappings %}",
      "MaxConcurrency": 10,
      "ItemProcessor": {
        "ProcessorConfig": {
          "Mode": "INLINE"
        },
        "StartAt": "GetContactDetails",
        "States": {
          "GetContactDetails": {
            "Type": "Task",
            "Comment": "個別コンタクトの詳細情報を取得",
            "Resource": "arn:aws:states:::aws-sdk:connect:describeContact",
            "Arguments": {
              "InstanceId": "arn:aws:connect:ap-northeast-1:アカウントID:instance/インスタンスID",
              "ContactId": "{% $states.input.contactId %}"
            },
            "Output": {
              "phoneNumber": "{% $states.input.phoneNumber %}",
              "employeeName": "{% $states.input.employeeName %}",
              "contactId": "{% $states.input.contactId %}",
              "answeringMachineStatus": "{% $states.result.Contact.AnsweringMachineDetectionStatus %}",
              "attendanceResponse": "{% $exists($states.result.Contact.Attributes.attendanceResponse) ? $states.result.Contact.Attributes.attendanceResponse : null %}"
            },
            "Retry": [
              {
                "ErrorEquals": ["States.TaskFailed"],
                "IntervalSeconds": 1,
                "MaxAttempts": 5,
                "BackoffRate": 1.5
              }
            ],
            "End": true
          }
        }
      },
      "Next": "ProcessContactResults"
    },
    "ProcessContactResults": {
      "Type": "Pass",
      "Comment": "コンタクト結果を処理し、DynamoDB保存形式に変換",
      "Assign": {
        "processedResults": "{% ( $ttlValue := $toMillis($now()) / 1000 + 604800; [$states.input ~> $map(function($contact) { { \"phoneNumber\": $contact.phoneNumber, \"employeeName\": $contact.employeeName, \"contactId\": $contact.contactId, \"executionTime\": $executionTime, \"callStatus\": $contact.answeringMachineStatus, \"ivr_response\": ( $contact.attendanceResponse = \"1\" ? \"AVAILABLE\" : $contact.attendanceResponse = \"2\" ? \"UNAVAILABLE\" : \"NO_RESPONSE\" ), \"ttl\": $number($ttlValue) } })] ) %}"
      },
      "Next": "SaveResultsToDynamoDB"
    },
    "SaveResultsToDynamoDB": {
      "Type": "Map",
      "Comment": "処理結果をDynamoDBに並行保存",
      "Items": "{% $processedResults %}",
      "MaxConcurrency": 25,
      "ItemProcessor": {
        "ProcessorConfig": {
          "Mode": "INLINE"
        },
        "StartAt": "PutItemToDynamoDB",
        "States": {
          "PutItemToDynamoDB": {
            "Type": "Task",
            "Comment": "個別結果をEmergencyCallResultsテーブルに保存",
            "Resource": "arn:aws:states:::aws-sdk:dynamodb:putItem",
            "Arguments": {
              "TableName": "EmergencyCallResults",
              "Item": {
                "phoneNumber": {
                  "S": "{% $states.input.phoneNumber %}"
                },
                "contactId": {
                  "S": "{% $states.input.contactId %}"
                },
                "executionTime": {
                  "S": "{% $states.input.executionTime %}"
                },
                "employeeName": {
                  "S": "{% $states.input.employeeName %}"
                },
                "callStatus": {
                  "S": "{% $states.input.callStatus %}"
                },
                "ivr_response": {
                  "S": "{% $states.input.ivr_response %}"
                },
                "ttl": {
                  "N": "{% $string($states.input.ttl) %}"
                }
              }
            },
            "Retry": [
              {
                "ErrorEquals": ["States.TaskFailed"],
                "IntervalSeconds": 1,
                "MaxAttempts": 3,
                "BackoffRate": 2
              }
            ],
            "End": true
          }
        }
      },
      "Next": "GenerateExecutionSummary"
    },
    "GenerateExecutionSummary": {
      "Type": "Pass",
      "Comment": "実行結果のサマリーを生成",
      "Output": {
        "executionSummary": {
          "totalContacts": "{% $totalRequests %}",
          "availableCount": "{% $count($processedResults[ivr_response=\"AVAILABLE\"]) %}",
          "unavailableCount": "{% $count($processedResults[ivr_response=\"UNAVAILABLE\"]) %}",
          "noResponseCount": "{% $count($processedResults[ivr_response=\"NO_RESPONSE\"]) %}",
          "successfulRequests": "{% $successfulRequests %}",
          "failedRequests": "{% $failedRequests %}"
        },
        "detailedResults": "{% $processedResults %}"
      },
      "End": true
    }
  }
}

ワークフローの処理内容

  1. GetContactList: DynamoDBから緊急連絡先リストをスキャンして取得
  2. PrepareOutboundRequests: Connect Campaigns V2 API用のリクエスト形式に変換
  3. ExecuteOutboundCampaign: アウトバウンドキャンペーンを実行
  4. WaitForCallsToComplete: 通話処理完了まで5分間待機
    • 各コンタクトのステータス完了までの待機時間です。通常2~3分で完了しますが、安全のため5分待機します
  5. CheckAllContactsStatus: 各コンタクトの詳細情報を並行取得(最大10並行)
  6. ProcessContactResults: 各コンタクトの通話結果とIVR応答結果を整理し、データ形式に変換
    • TTLも1週間で生成
  7. SaveResultsToDynamoDB: 処理結果をDynamoDBに並行保存(最大25並行)
  8. GenerateExecutionSummary: 実行結果のサマリーを生成

なお、TTLは1週間に設定していますが、要件に応じて変更してください。

Step Functions実行ロールの権限設定

マネジメントコンソール上でステートマシンを作成する際、実行ロールに以下のポリシーを手動で追加する必要があります。

アカウントIDは各環境に合わせて変更してください。

cm-hirai-screenshot 2025-08-26 8.59.08

ステートマシンに追加するポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "dynamodb:Scan"
            ],
            "Resource": [
                "arn:aws:dynamodb:ap-northeast-1:アカウントID:table/EmergencyContactList"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "connect-campaigns:PutOutboundRequestBatch"
            ],
            "Resource": "arn:aws:connect-campaigns:ap-northeast-1:アカウントID:campaign/*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "connect:DescribeContact"
            ],
            "Resource": "arn:aws:connect:ap-northeast-1:アカウントID:instance/*/contact/*"
        }
    ]
}

実行してみる

以下の内容を入力してステートマシンを実行します。

{
  "campaignId": "ae613cdc-1ad5-4e67-9d10-5c31a5834940",
  "sourcePhone": "+8150xxxxxxxx"
}

入力パラメータは各環境に合わせて変更してください。

  • campaignId: 作成したアウトバウンドキャンペーンのID
  • sourcePhone: キャンペーン作成時に設定した発信元として使用するAmazon Connectの電話番号

実行すると、DynamoDBのEmergencyContactListテーブルに登録された全ての連絡先に対して自動的に電話がかかります。

実行結果の確認

ステートマシンの実行結果は以下のとおりです。

cm-hirai-screenshot 2025-08-26 15.08.42

実行結果のサマリーや各連絡先ごとに処理結果などの情報が確認できます。

GenerateExecutionSummaryの出力
{
  "executionSummary": {
    "totalContacts": 2,
    "availableCount": 0,
    "unavailableCount": 0,
    "noResponseCount": 2,
    "successfulRequests": 2,
    "failedRequests": 0
  },
  "detailedResults": [
    {
      "phoneNumber": "+8150xxxxxxxx",
      "employeeName": "山田太郎",
      "contactId": "d7eeaba0-b354-43fd-abea-0cb4e05de6ec",
      "executionTime": "2025-08-26T05:11:39.685Z",
      "callStatus": "AMD_UNANSWERED",
      "ivr_response": "NO_RESPONSE",
      "ttl": 1756790020.847
    },
    {
      "phoneNumber": "+8190xxxxxxxx",
      "employeeName": "平井裕二",
      "contactId": "d15b82a6-a0f1-453f-8b5a-87a953e9f25f",
      "executionTime": "2025-08-26T05:11:39.685Z",
      "callStatus": "AMD_UNRESOLVED_SILENCE",
      "ivr_response": "NO_RESPONSE",
      "ttl": 1756790020.847
    }
  ]
}

executionSummaryには以下の統計情報が含まれます。

  • totalContacts: 発信対象の連絡先総数
  • availableCount: 出勤可能と回答した人数(IVR応答「1」)
  • unavailableCount: 出勤不可と回答した人数(IVR応答「2」)
  • noResponseCount: 応答なしまたは未回答の人数
  • successfulRequests: 発信に成功したリクエスト数
  • failedRequests: 発信に失敗したリクエスト数

detailedResultsには各連絡先の詳細結果が含まれます。

  • phoneNumber: 発信先電話番号
  • employeeName: 従業員名
  • contactId: ConnectのコンタクトID
  • executionTime: キャンペーン実行時刻
  • callStatus: Amazon Connectの通話ステータス(AMD_UNANSWERED等)
  • ivr_response: IVR応答結果(AVAILABLE/UNAVAILABLE/NO_RESPONSE)
  • ttl: DynamoDB TTL値(自動削除予定時刻)

DynamoDB結果テーブル

EmergencyCallResultsテーブルには以下の情報が自動保存されます。

  • phoneNumber: 電話番号(パーティションキー)
  • contactId: ConnectのコンタクトID
  • executionTime: 実行時刻(1つのキャンペーン内の全コンタクトは同じ実行時刻)
  • employeeName: 従業員名
  • callStatus: 通話ステータス(HUMAN_ANSWEREDAMD_UNANSWEREDVOICEMAIL_BEEPなど)
  • ivr_response: IVR応答結果(AVAILABLEUNAVAILABLENO_RESPONSE
    • AVAILABLE: 従業員が「1」を押下(出勤可能)
    • UNAVAILABLE: 従業員が「2」を押下(出勤不可)
    • NO_RESPONSE: IVR入力なし(電話に出なかった、または入力しなかった)
  • ttl: 1週間後の自動削除時刻

通話ステータスの詳細な値については、以下のドキュメントを参照してください。

https://docs.aws.amazon.com/ja_jp/connect/latest/adminguide/contact-events.html

通話結果パターン例

以下に各パターンでのDynamoDBに保存される情報の例を示します。

「1」を押した場合

  • callStatus:HUMAN_ANSWERED
  • ivr_response:AVAILABLE

cm-hirai-screenshot 2025-08-26 15.25.40

「2」を押した場合

  • callStatus:HUMAN_ANSWERED
  • ivr_response:UNAVAILABLE

cm-hirai-screenshot 2025-08-26 15.25.02

電話に出なかった場合

  • callStatus:AMD_UNANSWERED
  • ivr_response:NO_RESPONSE

cm-hirai-screenshot 2025-08-26 15.28.00

電話に出たが何も入力しなかった場合

  • callStatus:AMD_UNANSWERED
  • ivr_response:NO_RESPONSE

cm-hirai-screenshot 2025-08-26 15.28.41

話し中で電話に出られなかった場合

  • callStatus:AMD_UNRESOLVED_SILENCE
  • ivr_response:NO_RESPONSE

cm-hirai-screenshot 2025-08-26 15.26.35

複数回実行時の動作

1回のアウトバウンドキャンペーンでは、executionTimeが同じ値でDynamoDBに保存されます。そのため、1週間以内に複数回アウトバウンドキャンペーンを実行した場合、電話番号をパーティションキーとするテーブル構造により、最新のexecutionTimeの結果で上書きされます。

最新のアウトバウンドキャンペーン実行結果を確認したい場合は、executionTimeで降順ソートすることで効率的に確認できます。

cm-hirai-screenshot 2025-08-27 8.15.18

最後に

AWS Step FunctionsとDynamoDBを組み合わせることで、緊急時の出勤可否確認システムを完全自動化できました。

このシステムには以下の特徴があります。

  • 全自動化された処理: DynamoDBから取得→発信→結果保存まで完全自動
  • TTL機能: 結果データは1週間後に自動削除。複数回実行された場合は電話番号ごとに上書きされるため、最新のexecutionTimeを確認することで実行履歴を把握可能
  • 詳細な統計情報: GenerateExecutionSummaryステートの出力により、出勤可能人数、連絡不能者数等の統計情報を自動生成

Customer Profilesを使用せず、DynamoDBとStep Functionsの組み合わせで、コスト効率的かつ柔軟な緊急連絡システムを構築できるため、ぜひ参考にしてみてください。

参考

https://docs.aws.amazon.com/ja_jp/connect/latest/APIReference/API_connect-outbound-campaigns-v2_PutOutboundRequestBatch.html

https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/transforming-data.html

https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/workflow-variables.html#variable-scope

この記事をシェアする

facebookのロゴhatenaのロゴtwitterのロゴ

© Classmethod, Inc. All rights reserved.