8
n8n 한국어amn8n.com

AI 음성 통화 에이전트 - 시간대 인식(VAPI)

고급

이것은Support Chatbot, Multimodal AI분야의자동화 워크플로우로, 24개의 노드를 포함합니다.주로 If, Code, Webhook, Airtable, Schedule 등의 노드를 사용하며. VAPI 음성 AI 및 시간대 인텔리전스를 사용한 주문 확인 자동화

사전 요구사항
  • HTTP Webhook 엔드포인트(n8n이 자동으로 생성)
  • Airtable API Key
  • 대상 API의 인증 정보가 필요할 수 있음
워크플로우 미리보기
노드 연결 관계를 시각적으로 표시하며, 확대/축소 및 이동을 지원합니다
워크플로우 내보내기
다음 JSON 구성을 복사하여 n8n에 가져오면 이 워크플로우를 사용할 수 있습니다
{
  "name": "AI Voice Calling Agent - Timezone Aware (VAPI)",
  "tags": [
    {
      "id": "voice-ai",
      "name": "Voice AI",
      "createdAt": "2024-08-13T12:00:00.000Z",
      "updatedAt": "2024-08-13T12:00:00.000Z"
    },
    {
      "id": "ecommerce",
      "name": "E-commerce",
      "createdAt": "2024-08-13T12:00:00.000Z",
      "updatedAt": "2024-08-13T12:00:00.000Z"
    },
    {
      "id": "timezone-aware",
      "name": "Timezone Aware",
      "createdAt": "2024-08-13T12:00:00.000Z",
      "updatedAt": "2024-08-13T12:00:00.000Z"
    }
  ],
  "nodes": [
    {
      "id": "webhook-trigger",
      "name": "주문 웹훅",
      "type": "n8n-nodes-base.webhook",
      "position": [
        240,
        300
      ],
      "webhookId": "order-confirmation-webhook",
      "parameters": {
        "path": "order-confirmation",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 1
    },
    {
      "id": "validation-check",
      "name": "주문 데이터 검증",
      "type": "n8n-nodes-base.if",
      "position": [
        460,
        300
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "customer-has-phone",
              "operator": {
                "type": "string",
                "operation": "isNotEmpty"
              },
              "leftValue": "={{ $json.customer_phone }}",
              "rightValue": ""
            },
            {
              "id": "order-status-pending",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.order_status }}",
              "rightValue": "pending_confirmation"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "timezone-checker",
      "name": "시간대 및 통화 시간 확인",
      "type": "n8n-nodes-base.code",
      "position": [
        680,
        300
      ],
      "parameters": {
        "jsCode": "// Determine customer timezone and check calling hours\nconst orderData = $input.first().json;\n\n// Extract timezone from shipping address or phone number\nfunction getTimezoneFromAddress(address) {\n  const timezoneMap = {\n    // US States\n    'AL': 'America/Chicago', 'AK': 'America/Anchorage', 'AZ': 'America/Phoenix',\n    'AR': 'America/Chicago', 'CA': 'America/Los_Angeles', 'CO': 'America/Denver',\n    'CT': 'America/New_York', 'DE': 'America/New_York', 'FL': 'America/New_York',\n    'GA': 'America/New_York', 'HI': 'Pacific/Honolulu', 'ID': 'America/Denver',\n    'IL': 'America/Chicago', 'IN': 'America/New_York', 'IA': 'America/Chicago',\n    'KS': 'America/Chicago', 'KY': 'America/New_York', 'LA': 'America/Chicago',\n    'ME': 'America/New_York', 'MD': 'America/New_York', 'MA': 'America/New_York',\n    'MI': 'America/New_York', 'MN': 'America/Chicago', 'MS': 'America/Chicago',\n    'MO': 'America/Chicago', 'MT': 'America/Denver', 'NE': 'America/Chicago',\n    'NV': 'America/Los_Angeles', 'NH': 'America/New_York', 'NJ': 'America/New_York',\n    'NM': 'America/Denver', 'NY': 'America/New_York', 'NC': 'America/New_York',\n    'ND': 'America/Chicago', 'OH': 'America/New_York', 'OK': 'America/Chicago',\n    'OR': 'America/Los_Angeles', 'PA': 'America/New_York', 'RI': 'America/New_York',\n    'SC': 'America/New_York', 'SD': 'America/Chicago', 'TN': 'America/Chicago',\n    'TX': 'America/Chicago', 'UT': 'America/Denver', 'VT': 'America/New_York',\n    'VA': 'America/New_York', 'WA': 'America/Los_Angeles', 'WV': 'America/New_York',\n    'WI': 'America/Chicago', 'WY': 'America/Denver',\n    // Countries\n    'UK': 'Europe/London', 'GB': 'Europe/London', 'CANADA': 'America/Toronto',\n    'AUSTRALIA': 'Australia/Sydney', 'GERMANY': 'Europe/Berlin',\n    'FRANCE': 'Europe/Paris', 'INDIA': 'Asia/Kolkata', 'JAPAN': 'Asia/Tokyo'\n  };\n  \n  const address_upper = address.toUpperCase();\n  for (const [key, timezone] of Object.entries(timezoneMap)) {\n    if (address_upper.includes(key)) {\n      return timezone;\n    }\n  }\n  \n  // Default to customer's country code from phone number\n  const phone = orderData.customer_phone;\n  if (phone.startsWith('+1')) return 'America/New_York'; // US/Canada default\n  if (phone.startsWith('+44')) return 'Europe/London'; // UK\n  if (phone.startsWith('+91')) return 'Asia/Kolkata'; // India\n  if (phone.startsWith('+81')) return 'Asia/Tokyo'; // Japan\n  if (phone.startsWith('+49')) return 'Europe/Berlin'; // Germany\n  if (phone.startsWith('+33')) return 'Europe/Paris'; // France\n  if (phone.startsWith('+61')) return 'Australia/Sydney'; // Australia\n  \n  return 'America/New_York'; // Default fallback\n}\n\nfunction isWithinCallingHours(timezone) {\n  const now = new Date();\n  const customerTime = new Date(now.toLocaleString(\"en-US\", {timeZone: timezone}));\n  const hour = customerTime.getHours();\n  const day = customerTime.getDay(); // 0 = Sunday, 6 = Saturday\n  \n  // Check if it's a weekday (Monday-Friday) and between 10 AM - 3 PM\n  const isWeekday = day >= 1 && day <= 5;\n  const isCallingHours = hour >= 10 && hour < 15; // 10 AM to 3 PM\n  \n  return {\n    canCall: isWeekday && isCallingHours,\n    currentHour: hour,\n    currentDay: day,\n    isWeekday,\n    isCallingHours,\n    customerTime: customerTime.toLocaleString(),\n    timezone\n  };\n}\n\nfunction calculateNextCallTime(timezone) {\n  const now = new Date();\n  const customerTime = new Date(now.toLocaleString(\"en-US\", {timeZone: timezone}));\n  const hour = customerTime.getHours();\n  const day = customerTime.getDay();\n  \n  let nextCallTime = new Date(customerTime);\n  \n  // If it's weekend, schedule for next Monday at 10 AM\n  if (day === 0 || day === 6) { // Sunday or Saturday\n    const daysUntilMonday = day === 0 ? 1 : 2; // Sunday = 1 day, Saturday = 2 days\n    nextCallTime.setDate(nextCallTime.getDate() + daysUntilMonday);\n    nextCallTime.setHours(10, 0, 0, 0);\n  }\n  // If it's before 10 AM on weekday, schedule for 10 AM today\n  else if (hour < 10) {\n    nextCallTime.setHours(10, 0, 0, 0);\n  }\n  // If it's after 3 PM on weekday, schedule for 10 AM next day\n  else if (hour >= 15) {\n    nextCallTime.setDate(nextCallTime.getDate() + 1);\n    // If next day is Saturday, schedule for Monday\n    if (nextCallTime.getDay() === 6) {\n      nextCallTime.setDate(nextCallTime.getDate() + 2);\n    }\n    nextCallTime.setHours(10, 0, 0, 0);\n  }\n  \n  // Convert back to UTC for scheduling\n  const utcOffset = nextCallTime.getTimezoneOffset() * 60000;\n  const utcTime = new Date(nextCallTime.getTime() + utcOffset);\n  return utcTime.toISOString();\n}\n\n// Determine customer timezone\nconst customerTimezone = orderData.customer_timezone || \n  getTimezoneFromAddress(orderData.shipping_address || '');\n\n// Check calling hours\nconst callingStatus = isWithinCallingHours(customerTimezone);\n\nreturn {\n  json: {\n    ...orderData,\n    customer_timezone: customerTimezone,\n    calling_status: callingStatus,\n    can_call_now: callingStatus.canCall,\n    next_call_time: callingStatus.canCall ? null : calculateNextCallTime(customerTimezone),\n    scheduling_info: {\n      customer_local_time: callingStatus.customerTime,\n      current_hour: callingStatus.currentHour,\n      is_weekday: callingStatus.isWeekday,\n      is_calling_hours: callingStatus.isCallingHours\n    }\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "check-calling-hours",
      "name": "지금 통화 가능한가?",
      "type": "n8n-nodes-base.if",
      "position": [
        900,
        300
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "can-call-now",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.can_call_now }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "format-order-data",
      "name": "주문 데이터 포맷팅",
      "type": "n8n-nodes-base.code",
      "position": [
        1120,
        200
      ],
      "parameters": {
        "jsCode": "// Format order data for VAPI call with timezone awareness\nconst orderData = $input.first().json;\n\n// Create order summary\nconst items = orderData.items.map(item => \n  `${item.quantity} ${item.name} at $${item.price} each`\n).join(', ');\n\nconst orderSummary = `Order ${orderData.order_id} containing ${items}. Total amount: $${orderData.total}. Shipping to ${orderData.shipping_address}.`;\n\n// Get customer's local time for personalized greeting\nconst customerLocalTime = orderData.scheduling_info.customer_local_time;\nconst currentHour = orderData.scheduling_info.current_hour;\n\nlet timeGreeting = \"\";\nif (currentHour < 12) {\n  timeGreeting = \"Good morning\";\n} else if (currentHour < 17) {\n  timeGreeting = \"Good afternoon\";\n} else {\n  timeGreeting = \"Good evening\";\n}\n\n// Create VAPI assistant configuration with timezone awareness\nconst vapiConfig = {\n  model: {\n    provider: \"openai\",\n    model: \"gpt-3.5-turbo\",\n    temperature: 0.3,\n    systemMessage: `You are Alex, a friendly customer service representative from ${orderData.store_name || 'TechMart'}. You are calling to confirm an e-commerce order during appropriate business hours (10 AM - 3 PM customer local time).\n\nCustomer Information:\n- Name: ${orderData.customer_name}\n- Local time: ${customerLocalTime}\n- Timezone: ${orderData.customer_timezone}\n\nOrder Details:\n${orderSummary}\n\nYour goals:\n1. Use appropriate time-based greeting (it's currently ${timeGreeting.toLowerCase()} for the customer)\n2. Confirm you're speaking with ${orderData.customer_name}\n3. Verify the order details are correct\n4. Confirm the shipping address\n5. Handle any customer concerns professionally\n6. Get final confirmation to proceed with shipping\n\nBe natural, friendly, and concise. Acknowledge that you're calling during their business hours. If the customer wants to make changes, collect the details and confirm you'll process the update.\n\nCall Script Flow:\n1. \"${timeGreeting}, this is Alex from ${orderData.store_name || 'TechMart'}. Am I speaking with ${orderData.customer_name}?\"\n2. \"I hope I'm calling at a convenient time. I'm calling to confirm your recent order placed on ${orderData.order_date}.\"\n3. \"Let me go over the details: [read order items and total]\"\n4. \"Does everything sound correct?\"\n5. \"Great! Your order will be shipped to [read address]. Is this address still correct?\"\n6. \"Perfect! Your order is confirmed and will be processed today. You'll receive tracking info via email within 24 hours.\"`\n  },\n  voice: {\n    provider: \"11labs\",\n    voiceId: \"rachel\",\n    stability: 0.5,\n    similarityBoost: 0.5,\n    style: 0.2,\n    useSpeakerBoost: true\n  },\n  firstMessage: `${timeGreeting}! This is Alex from ${orderData.store_name || 'TechMart'} calling to confirm your recent order. Am I speaking with ${orderData.customer_name}? I hope I'm calling at a convenient time.`,\n  endCallMessage: \"Thank you for confirming your order! Have a wonderful rest of your day!\",\n  recordingEnabled: true,\n  endCallPhrases: [\"goodbye\", \"thank you\", \"that's all\", \"end call\", \"have a good day\"],\n  maxDurationSeconds: 300,\n  backgroundSound: \"office\",\n  backchannelingEnabled: true,\n  backgroundDenoisingEnabled: true,\n  modelOutputInMessagesEnabled: true,\n  silenceTimeoutSeconds: 10,\n  responseDelaySeconds: 1\n};\n\nreturn {\n  json: {\n    order_id: orderData.order_id,\n    customer_name: orderData.customer_name,\n    customer_phone: orderData.customer_phone,\n    customer_email: orderData.customer_email,\n    customer_timezone: orderData.customer_timezone,\n    order_summary: orderSummary,\n    vapi_config: vapiConfig,\n    call_context: {\n      order_total: orderData.total,\n      items_count: orderData.items.length,\n      shipping_address: orderData.shipping_address,\n      order_date: orderData.order_date,\n      customer_local_time: customerLocalTime,\n      time_greeting: timeGreeting\n    }\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "initiate-vapi-call",
      "name": "VAPI 호출 시작",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1340,
        200
      ],
      "parameters": {
        "url": "https://api.vapi.ai/call/phone",
        "options": {},
        "jsonBody": "={\n  \"phoneNumberId\": \"{{ $vars.VAPI_PHONE_NUMBER_ID }}\",\n  \"customer\": {\n    \"number\": \"{{ $json.customer_phone }}\",\n    \"name\": \"{{ $json.customer_name }}\"\n  },\n  \"assistant\": {{ JSON.stringify($json.vapi_config) }},\n  \"metadata\": {\n    \"order_id\": \"{{ $json.order_id }}\",\n    \"type\": \"order_confirmation\",\n    \"timestamp\": \"{{ new Date().toISOString() }}\",\n    \"customer_timezone\": \"{{ $json.customer_timezone }}\"\n  }\n}",
        "sendBody": true,
        "contentType": "json",
        "sendHeaders": true,
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer {{ $vars.VAPI_API_KEY }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "httpHeaderAuth"
      },
      "typeVersion": 4.2
    },
    {
      "id": "check-call-status",
      "name": "통화 상태 확인",
      "type": "n8n-nodes-base.if",
      "position": [
        1560,
        200
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "call-successful",
              "operator": {
                "type": "string",
                "operation": "isNotEmpty"
              },
              "leftValue": "={{ $json.id }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "update-order-status",
      "name": "주문 상태 업데이트",
      "type": "n8n-nodes-base.airtable",
      "position": [
        1780,
        100
      ],
      "parameters": {
        "tableId": "orders",
        "resource": "database",
        "columnsUi": {
          "columns": [
            {
              "value": "initiated",
              "column": "call_status"
            },
            {
              "value": "={{ $json.id }}",
              "column": "call_id"
            },
            {
              "value": "={{ new Date().toISOString() }}",
              "column": "call_timestamp"
            },
            {
              "value": "calling",
              "column": "confirmation_status"
            },
            {
              "value": "={{ $('Format Order Data').item.json.customer_timezone }}",
              "column": "customer_timezone"
            },
            {
              "value": "={{ $('Format Order Data').item.json.call_context.customer_local_time }}",
              "column": "customer_local_time"
            }
          ]
        },
        "operation": "update",
        "updateKey": "order_id",
        "dataToSend": "defineBelow"
      },
      "credentials": {
        "airtableTokenApi": {
          "id": "airtable-credentials",
          "name": "Airtable Orders Database"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "schedule-call-later",
      "name": "나중에 통화 예약",
      "type": "n8n-nodes-base.airtable",
      "position": [
        1120,
        400
      ],
      "parameters": {
        "tableId": "orders",
        "resource": "database",
        "columnsUi": {
          "columns": [
            {
              "value": "scheduled",
              "column": "confirmation_status"
            },
            {
              "value": "={{ $json.customer_timezone }}",
              "column": "customer_timezone"
            },
            {
              "value": "={{ $json.next_call_time }}",
              "column": "next_call_time"
            },
            {
              "value": "={{ $json.scheduling_info.customer_local_time }}",
              "column": "customer_local_time"
            },
            {
              "value": "Outside calling hours (10 AM - 3 PM local time)",
              "column": "scheduled_reason"
            },
            {
              "value": "0",
              "column": "call_attempts"
            },
            {
              "value": "={{ new Date().toISOString() }}",
              "column": "last_updated"
            }
          ]
        },
        "operation": "update",
        "updateKey": "order_id",
        "dataToSend": "defineBelow"
      },
      "credentials": {
        "airtableTokenApi": {
          "id": "airtable-credentials",
          "name": "Airtable Orders Database"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "scheduled-caller",
      "name": "예약된 통화 확인기",
      "type": "n8n-nodes-base.schedule",
      "position": [
        240,
        800
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 */15 * * * *"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "get-scheduled-calls",
      "name": "예약된 통화 가져오기",
      "type": "n8n-nodes-base.airtable",
      "position": [
        460,
        800
      ],
      "parameters": {
        "options": {
          "filterByFormula": "AND({confirmation_status} = 'scheduled', {next_call_time} <= NOW(), {call_attempts} < 3)"
        },
        "tableId": "orders",
        "resource": "database",
        "operation": "list"
      },
      "credentials": {
        "airtableTokenApi": {
          "id": "airtable-credentials",
          "name": "Airtable Orders Database"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "vapi-webhook",
      "name": "VAPI 웹훅 핸들러",
      "type": "n8n-nodes-base.webhook",
      "position": [
        240,
        600
      ],
      "webhookId": "vapi-webhook-handler",
      "parameters": {
        "path": "vapi-webhook",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 1
    },
    {
      "id": "check-webhook-type",
      "name": "웹훅 유형 확인",
      "type": "n8n-nodes-base.if",
      "position": [
        460,
        600
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "call-ended",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.message?.type }}",
              "rightValue": "call-end"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "process-call-results",
      "name": "통화 결과 처리",
      "type": "n8n-nodes-base.code",
      "position": [
        680,
        500
      ],
      "parameters": {
        "jsCode": "// Process VAPI call results with enhanced analysis\nconst webhookData = $input.first().json;\nconst callData = webhookData.message;\n\n// Extract call information\nconst callId = callData.call?.id;\nconst callDuration = callData.call?.endedAt ? \n  new Date(callData.call.endedAt) - new Date(callData.call.startedAt) : 0;\n\n// Get transcript and messages\nconst transcript = callData.transcript || '';\nconst messages = callData.messages || [];\n\n// Enhanced confirmation detection\nlet confirmationStatus = 'unknown';\nlet customerResponse = 'no_response';\nlet confidence = 0;\n\nconst transcriptLower = transcript.toLowerCase();\n\n// Positive confirmation keywords\nconst positiveKeywords = ['yes', 'correct', 'right', 'confirm', 'good', 'perfect', 'sounds good', 'that\\'s right'];\nconst negativeKeywords = ['no', 'wrong', 'incorrect', 'change', 'different', 'not right', 'cancel'];\nconst issueKeywords = ['problem', 'issue', 'concern', 'question', 'but', 'however', 'wait'];\n\n// Calculate confidence scores\nlet positiveScore = 0;\nlet negativeScore = 0;\nlet issueScore = 0;\n\npositiveKeywords.forEach(keyword => {\n  if (transcriptLower.includes(keyword)) {\n    positiveScore += 1;\n  }\n});\n\nnegativeKeywords.forEach(keyword => {\n  if (transcriptLower.includes(keyword)) {\n    negativeScore += 2; // Weight negative more heavily\n  }\n});\n\nissueKeywords.forEach(keyword => {\n  if (transcriptLower.includes(keyword)) {\n    issueScore += 1;\n  }\n});\n\n// Determine status based on scores and call duration\nif (callDuration < 10000) { // Less than 10 seconds\n  confirmationStatus = 'no_answer';\n  customerResponse = 'no_answer';\n  confidence = 0.9;\n} else if (negativeScore > 0 || issueScore > 1) {\n  confirmationStatus = 'needs_review';\n  customerResponse = 'requested_changes';\n  confidence = 0.8;\n} else if (positiveScore >= 2) {\n  confirmationStatus = 'confirmed';\n  customerResponse = 'confirmed';\n  confidence = 0.9;\n} else if (positiveScore >= 1 && negativeScore === 0) {\n  confirmationStatus = 'likely_confirmed';\n  customerResponse = 'likely_confirmed';\n  confidence = 0.7;\n} else {\n  confirmationStatus = 'unclear';\n  customerResponse = 'unclear';\n  confidence = 0.3;\n}\n\n// Extract specific issues mentioned\nconst issues = [];\nif (transcriptLower.includes('address') || transcriptLower.includes('shipping')) {\n  issues.push('shipping_address');\n}\nif (transcriptLower.includes('item') || transcriptLower.includes('product') || transcriptLower.includes('order')) {\n  issues.push('order_items');\n}\nif (transcriptLower.includes('cancel') || transcriptLower.includes('don\\'t want')) {\n  issues.push('cancellation_request');\n}\nif (transcriptLower.includes('payment') || transcriptLower.includes('card') || transcriptLower.includes('billing')) {\n  issues.push('payment_issue');\n}\nif (transcriptLower.includes('time') || transcriptLower.includes('date') || transcriptLower.includes('when')) {\n  issues.push('delivery_timing');\n}\n\n// Determine if followup is needed\nconst requiresFollowup = [\n  'needs_review', \n  'no_answer', \n  'unclear'\n].includes(confirmationStatus) || issues.length > 0;\n\nreturn {\n  json: {\n    call_id: callId,\n    order_id: callData.metadata?.order_id,\n    confirmation_status: confirmationStatus,\n    customer_response: customerResponse,\n    confidence_score: confidence,\n    call_duration: Math.round(callDuration / 1000), // in seconds\n    transcript: transcript,\n    issues: issues,\n    call_cost: callData.cost || 0,\n    processed_at: new Date().toISOString(),\n    requires_followup: requiresFollowup,\n    analysis_scores: {\n      positive: positiveScore,\n      negative: negativeScore,\n      issues: issueScore\n    },\n    call_quality: callDuration > 30000 ? 'good' : callDuration > 10000 ? 'fair' : 'poor'\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "update-final-status",
      "name": "최종 상태 업데이트",
      "type": "n8n-nodes-base.airtable",
      "position": [
        900,
        500
      ],
      "parameters": {
        "tableId": "orders",
        "resource": "database",
        "columnsUi": {
          "columns": [
            {
              "value": "={{ $json.confirmation_status }}",
              "column": "confirmation_status"
            },
            {
              "value": "={{ $json.customer_response }}",
              "column": "customer_response"
            },
            {
              "value": "={{ $json.confidence_score }}",
              "column": "confidence_score"
            },
            {
              "value": "={{ $json.call_duration }}",
              "column": "call_duration"
            },
            {
              "value": "={{ $json.transcript }}",
              "column": "call_transcript"
            },
            {
              "value": "={{ $json.issues.join(', ') }}",
              "column": "issues_reported"
            },
            {
              "value": "={{ $json.requires_followup }}",
              "column": "requires_followup"
            },
            {
              "value": "={{ $json.call_quality }}",
              "column": "call_quality"
            },
            {
              "value": "={{ $json.call_cost }}",
              "column": "call_cost"
            },
            {
              "value": "={{ $json.processed_at }}",
              "column": "last_updated"
            }
          ]
        },
        "operation": "update",
        "updateKey": "order_id",
        "dataToSend": "defineBelow"
      },
      "credentials": {
        "airtableTokenApi": {
          "id": "airtable-credentials",
          "name": "Airtable Orders Database"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "check-followup-needed",
      "name": "후속 조치 필요 여부 확인",
      "type": "n8n-nodes-base.if",
      "position": [
        1120,
        500
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "needs-followup",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.requires_followup }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "send-followup-alert",
      "name": "후속 조치 알림 전송",
      "type": "n8n-nodes-base.emailSend",
      "position": [
        1340,
        400
      ],
      "parameters": {
        "message": "<!DOCTYPE html>\n<html>\n<head>\n    <style>\n        body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }\n        .header { background: #f8f9fa; padding: 15px; border-radius: 5px; margin-bottom: 20px; }\n        .status { padding: 10px; border-radius: 5px; margin: 10px 0; }\n        .needs-review { background: #fff3cd; border-left: 4px solid #ffc107; }\n        .no-answer { background: #f8d7da; border-left: 4px solid #dc3545; }\n        .unclear { background: #d1ecf1; border-left: 4px solid #17a2b8; }\n        .transcript { background: #f5f5f5; padding: 15px; border-radius: 5px; font-family: monospace; }\n        .metrics { display: flex; gap: 20px; margin: 15px 0; }\n        .metric { text-align: center; padding: 10px; background: #e9ecef; border-radius: 5px; }\n    </style>\n</head>\n<body>\n    <div class=\"header\">\n        <h2>🔔 Order Confirmation Follow-up Required</h2>\n        <p><strong>Order ID:</strong> {{ $('Process Call Results').item.json.order_id }}</p>\n        <p><strong>Call ID:</strong> {{ $('Process Call Results').item.json.call_id }}</p>\n        <p><strong>Processed:</strong> {{ $('Process Call Results').item.json.processed_at }}</p>\n    </div>\n\n    <div class=\"status {{ $('Process Call Results').item.json.confirmation_status.replace('_', '-') }}\">\n        <h3>Status: {{ $('Process Call Results').item.json.confirmation_status.toUpperCase().replace('_', ' ') }}</h3>\n        <p><strong>Customer Response:</strong> {{ $('Process Call Results').item.json.customer_response }}</p>\n        <p><strong>Confidence Score:</strong> {{ Math.round($('Process Call Results').item.json.confidence_score * 100) }}%</p>\n    </div>\n\n    <div class=\"metrics\">\n        <div class=\"metric\">\n            <strong>{{ $('Process Call Results').item.json.call_duration }}s</strong><br>\n            Call Duration\n        </div>\n        <div class=\"metric\">\n            <strong>${{ $('Process Call Results').item.json.call_cost }}</strong><br>\n            Call Cost\n        </div>\n        <div class=\"metric\">\n            <strong>{{ $('Process Call Results').item.json.call_quality.toUpperCase() }}</strong><br>\n            Call Quality\n        </div>\n    </div>\n\n    <div>\n        <h4>Issues Reported:</h4>\n        <p>{{ $('Process Call Results').item.json.issues.length > 0 ? $('Process Call Results').item.json.issues.join(', ') : 'None specific' }}</p>\n    </div>\n\n    <div>\n        <h4>Call Transcript:</h4>\n        <div class=\"transcript\">{{ $('Process Call Results').item.json.transcript || 'No transcript available' }}</div>\n    </div>\n\n    <div style=\"margin-top: 20px; padding: 15px; background: #d4edda; border-radius: 5px;\">\n        <h4>📞 Next Action Required:</h4>\n        <p>Please follow up with the customer manually to resolve any concerns and complete the order confirmation.</p>\n    </div>\n</body>\n</html>",
        "options": {},
        "subject": "🔔 Order Confirmation Follow-up Required - {{ $('Process Call Results').item.json.order_id }}",
        "toEmail": "support@yourstore.com",
        "emailType": "html",
        "fromEmail": "orders@yourstore.com"
      },
      "credentials": {
        "smtp": {
          "id": "smtp-credentials",
          "name": "Company SMTP"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "send-confirmation-email",
      "name": "확인 이메일 전송",
      "type": "n8n-nodes-base.emailSend",
      "position": [
        1340,
        600
      ],
      "parameters": {
        "message": "<!DOCTYPE html>\n<html>\n<head>\n    <style>\n        body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; }\n        .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; }\n        .content { padding: 30px; }\n        .order-box { background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; }\n        .success { background: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 15px; border-radius: 5px; margin: 20px 0; }\n        .footer { background: #f8f9fa; padding: 20px; text-align: center; font-size: 14px; color: #666; }\n    </style>\n</head>\n<body>\n    <div class=\"header\">\n        <h1>✅ Order Confirmed!</h1>\n        <p>Thank you for confirming your order with us</p>\n    </div>\n    \n    <div class=\"content\">\n        <p>Hi {{ $('Format Order Data').item.json.customer_name }},</p>\n        \n        <div class=\"success\">\n            <strong>Great news!</strong> We've successfully confirmed your order during our call today.\n        </div>\n        \n        <div class=\"order-box\">\n            <h3>Order Details:</h3>\n            <p><strong>Order ID:</strong> {{ $('Process Call Results').item.json.order_id }}</p>\n            <p><strong>Order Summary:</strong><br>{{ $('Format Order Data').item.json.order_summary }}</p>\n        </div>\n        \n        <h3>What happens next?</h3>\n        <ul>\n            <li>📦 Your order is now being processed</li>\n            <li>🚚 You'll receive tracking information within 24 hours</li>\n            <li>📧 All updates will be sent to this email address</li>\n            <li>🎯 Expected delivery: 3-5 business days</li>\n        </ul>\n        \n        <p>If you have any questions or concerns, please don't hesitate to contact our customer service team.</p>\n        \n        <p>Thank you for your business!</p>\n    </div>\n    \n    <div class=\"footer\">\n        <p>{{ $('Format Order Data').item.json.store_name || 'TechMart' }} Customer Service<br>\n        This email was sent because you recently confirmed an order with us.</p>\n    </div>\n</body>\n</html>",
        "options": {},
        "subject": "✅ Your Order is Confirmed - {{ $('Process Call Results').item.json.order_id }}",
        "toEmail": "={{ $('Format Order Data').item.json.customer_email || 'customer@example.com' }}",
        "emailType": "html",
        "fromEmail": "orders@yourstore.com"
      },
      "credentials": {
        "smtp": {
          "id": "smtp-credentials",
          "name": "Company SMTP"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "validation-error-response",
      "name": "검증 오류 응답",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        680,
        500
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { \"success\": false, \"error\": \"Invalid order data or missing phone number\" } }}"
      },
      "typeVersion": 1
    },
    {
      "id": "call-error-response",
      "name": "통화 오류 응답",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1560,
        300
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { \"success\": false, \"error\": \"Failed to initiate call\", \"details\": $json } }}"
      },
      "typeVersion": 1
    },
    {
      "id": "success-response",
      "name": "성공 응답",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1780,
        200
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { \"success\": true, \"call_id\": $json.id, \"message\": \"Call initiated successfully\", \"customer_local_time\": $('Format Order Data').item.json.call_context.customer_local_time, \"greeting_used\": $('Format Order Data').item.json.call_context.time_greeting } }}"
      },
      "typeVersion": 1
    },
    {
      "id": "scheduled-response",
      "name": "예약된 응답",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1340,
        400
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { \"success\": true, \"message\": \"Call scheduled for later\", \"next_call_time\": $json.next_call_time, \"customer_timezone\": $json.customer_timezone, \"customer_local_time\": $json.scheduling_info.customer_local_time, \"reason\": \"Outside calling hours (10 AM - 3 PM weekdays local time)\" } }}"
      },
      "typeVersion": 1
    },
    {
      "id": "webhook-response",
      "name": "웹훅 응답",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1560,
        700
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { \"success\": true, \"message\": \"Webhook received and processed\", \"order_id\": $('Process Call Results').item.json.order_id, \"status\": $('Process Call Results').item.json.confirmation_status } }}"
      },
      "typeVersion": 1
    },
    {
      "id": "increment-call-attempts",
      "name": "통화 시도 횟수 증가",
      "type": "n8n-nodes-base.airtable",
      "position": [
        680,
        800
      ],
      "parameters": {
        "tableId": "orders",
        "resource": "database",
        "columnsUi": {
          "columns": [
            {
              "value": "={{ ($json.call_attempts || 0) + 1 }}",
              "column": "call_attempts"
            },
            {
              "value": "={{ new Date().toISOString() }}",
              "column": "last_attempt"
            },
            {
              "value": "calling",
              "column": "confirmation_status"
            }
          ]
        },
        "operation": "update",
        "updateKey": "order_id",
        "dataToSend": "defineBelow"
      },
      "credentials": {
        "airtableTokenApi": {
          "id": "airtable-credentials",
          "name": "Airtable Orders Database"
        }
      },
      "typeVersion": 2
    }
  ],
  "pinData": {},
  "settings": {
    "callerPolicy": "workflowsFromSameOwner",
    "executionOrder": "v1",
    "saveManualExecutions": true
  },
  "updatedAt": "2024-08-13T12:00:00.000Z",
  "versionId": "2.0",
  "staticData": null,
  "connections": {
    "check-calling-hours": {
      "main": [
        [
          {
            "node": "format-order-data",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "schedule-call-later",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "webhook-trigger": {
      "main": [
        [
          {
            "node": "validation-check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "check-call-status": {
      "main": [
        [
          {
            "node": "update-order-status",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "call-error-response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "format-order-data": {
      "main": [
        [
          {
            "node": "initiate-vapi-call",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "check-webhook-type": {
      "main": [
        [
          {
            "node": "process-call-results",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "webhook-response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "initiate-vapi-call": {
      "main": [
        [
          {
            "node": "check-call-status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "get-scheduled-calls": {
      "main": [
        [
          {
            "node": "increment-call-attempts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "update-final-status": {
      "main": [
        [
          {
            "node": "check-followup-needed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "update-order-status": {
      "main": [
        [
          {
            "node": "success-response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "validation-check": {
      "main": [
        [
          {
            "node": "timezone-checker",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "validation-error-response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "process-call-results": {
      "main": [
        [
          {
            "node": "update-final-status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "send-followup-alert": {
      "main": [
        [
          {
            "node": "webhook-response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "vapi-webhook": {
      "main": [
        [
          {
            "node": "check-webhook-type",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "scheduled-caller": {
      "main": [
        [
          {
            "node": "get-scheduled-calls",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "increment-call-attempts": {
      "main": [
        [
          {
            "node": "timezone-checker",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "schedule-call-later": {
      "main": [
        [
          {
            "node": "scheduled-response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "send-confirmation-email": {
      "main": [
        [
          {
            "node": "webhook-response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "check-followup-needed": {
      "main": [
        [
          {
            "node": "send-followup-alert",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "send-confirmation-email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "timezone-checker": {
      "main": [
        [
          {
            "node": "check-calling-hours",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "triggerCount": 2
}
자주 묻는 질문

이 워크플로우를 어떻게 사용하나요?

위의 JSON 구성 코드를 복사하여 n8n 인스턴스에서 새 워크플로우를 생성하고 "JSON에서 가져오기"를 선택한 후, 구성을 붙여넣고 필요에 따라 인증 설정을 수정하세요.

이 워크플로우는 어떤 시나리오에 적합한가요?

고급 - 지원 챗봇, 멀티모달 AI

유료인가요?

이 워크플로우는 완전히 무료이며 직접 가져와 사용할 수 있습니다. 다만, 워크플로우에서 사용하는 타사 서비스(예: OpenAI API)는 사용자 직접 비용을 지불해야 할 수 있습니다.

워크플로우 정보
난이도
고급
노드 수24
카테고리2
노드 유형8
난이도 설명

고급 사용자를 위한 16+개 노드의 복잡한 워크플로우

외부 링크
n8n.io에서 보기

이 워크플로우 공유

카테고리

카테고리: 34