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语音呼叫代理 - 时区感知(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": {
"Can Call Now?": {
"main": [
[
{
"node": "Format Order Data",
"type": "main",
"index": 0
}
],
[
{
"node": "Schedule Call for Later",
"type": "main",
"index": 0
}
]
]
},
"Order Webhook": {
"main": [
[
{
"node": "Validate Order Data",
"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 if Followup Needed",
"type": "main",
"index": 0
}
]
]
},
"Update Order Status": {
"main": [
[
{
"node": "Success Response",
"type": "main",
"index": 0
}
]
]
},
"Validate Order Data": {
"main": [
[
{
"node": "Check Timezone & Calling Hours",
"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 Follow-up Alert": {
"main": [
[
{
"node": "Webhook Response",
"type": "main",
"index": 0
}
]
]
},
"VAPI Webhook Handler": {
"main": [
[
{
"node": "Check Webhook Type",
"type": "main",
"index": 0
}
]
]
},
"Scheduled Call Checker": {
"main": [
[
{
"node": "Get Scheduled Calls",
"type": "main",
"index": 0
}
]
]
},
"Increment Call Attempts": {
"main": [
[
{
"node": "Check Timezone & Calling Hours",
"type": "main",
"index": 0
}
]
]
},
"Schedule Call for Later": {
"main": [
[
{
"node": "Scheduled Response",
"type": "main",
"index": 0
}
]
]
},
"Send Confirmation Email": {
"main": [
[
{
"node": "Webhook Response",
"type": "main",
"index": 0
}
]
]
},
"Check if Followup Needed": {
"main": [
[
{
"node": "Send Follow-up Alert",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Confirmation Email",
"type": "main",
"index": 0
}
]
]
},
"Check Timezone & Calling Hours": {
"main": [
[
{
"node": "Can Call Now?",
"type": "main",
"index": 0
}
]
]
}
},
"triggerCount": 2
}常见问题
如何使用这个工作流?
复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。
这个工作流适合什么场景?
高级 - 客服机器人, 多模态 AI
需要付费吗?
本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。
相关工作流推荐
从Notion自动执行客户培育邮件和评价收集
通过Telegram使用Google日历和Notion CRM自动化会议安排
If
Set
Code
+8
19 节点Shelly-Ann Davy
客服机器人
AI症状检查与医生推荐
使用Ollama AI和WhatsApp的医疗症状分析与医生推荐
If
Code
Webhook
+6
15 节点Oneclick AI Squad
客服机器人
自动化订单确认,使用Abacate Pay:首购优惠券奖励,通过邮件和Slack
自动化订单确认,使用Abacate Pay:首购优惠券奖励,通过邮件和Slack
If
Set
Code
+6
13 节点Matheus Pedrosa
客户关系管理
WhatsApp上的AI电影推荐器
WhatsApp上的AI电影推荐器
If
Set
Code
+5
20 节点Oneclick AI Squad
客服机器人
牙科诊所接待员
使用 Google Calendar、AI 助手和邮件通知自动化牙科预约
If
Code
Gmail
+7
14 节点Md Sabirul Islam
客服机器人
AI驱动的图书推荐引擎
基于邮件的图书推荐系统,使用Ollama大语言模型和OpenLibrary API
If
Set
Code
+7
21 节点Oneclick AI Squad
客服机器人