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": "注文Webhook",
"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 Webhookハンドラー",
"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": "Webhookタイプの確認",
"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": "Webhookレスポンス",
"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など)は別途料金が発生する場合があります。
関連ワークフロー
Notionから自動で顧客育成メールと評価収集を実行
Telegramを使ってGoogleカレンダーとNotion CRMによる会議スケジュールの自動化
If
Set
Code
+
If
Set
Code
19 ノードShelly-Ann Davy
サポートチャットボット
AI症状チェックと医師推薦
Ollama AIとWhatsAppで医療症状分析と医師の推奨
If
Code
Webhook
+
If
Code
Webhook
15 ノードOneclick AI Squad
サポートチャットボット
Abacate Payを使った注文確認の自動化:メールとSlack経由での初回購入者向けクーポン獲得
注文確定の自動化、Abacate Payを使用:初回購入者向けクーポン報酬、メールおよびSlack経由
If
Set
Code
+
If
Set
Code
13 ノードMatheus Pedrosa
顧客管理
WhatsApp上のAI映画推薦ツール
WhatsApp上のAI映画推薦アシスタント
If
Set
Code
+
If
Set
Code
20 ノードOneclick AI Squad
サポートチャットボット
歯科医院の受付係
Google Calendar、AI アシスタント、メール通知を使った歯科予約の自動化
If
Code
Gmail
+
If
Code
Gmail
14 ノードMd Sabirul Islam
サポートチャットボット
AIを活用した書籍おすすめエンジン
メールベースの書籍推薦システム(Ollama LLM と OpenLibrary API 使用)
If
Set
Code
+
If
Set
Code
21 ノードOneclick AI Squad
サポートチャットボット