diff --git a/fern/assistants/examples/support-escalation.mdx b/fern/assistants/examples/support-escalation.mdx new file mode 100644 index 00000000..0e25524c --- /dev/null +++ b/fern/assistants/examples/support-escalation.mdx @@ -0,0 +1,987 @@ +--- +title: Customer support escalation system +subtitle: Build intelligent support routing using assistants that escalate calls based on customer tier, issue complexity, and agent expertise. +slug: assistants/examples/support-escalation +description: Build a voice AI customer support system with dynamic escalation that routes calls based on customer data, issue type, and real-time agent availability using transfer tools and webhooks. +--- + +## Overview + +Build an intelligent customer support escalation system that determines transfer destinations dynamically using customer tier analysis, issue complexity assessment, and real-time agent availability. This approach uses transfer tools with empty destinations and webhook servers for maximum escalation flexibility. + +**Agent Capabilities:** +* Customer tier-based prioritization and routing +* Issue complexity analysis for specialist routing +* Real-time agent availability and expertise matching +* Intelligent escalation with context preservation + +**What You'll Build:** +* Transfer tool with dynamic escalation logic +* Assistant with intelligent support conversation flow +* Webhook server for escalation destination logic +* CRM integration for customer tier-based routing + +## Prerequisites + +* A [Vapi account](https://dashboard.vapi.ai/) +* Node.js or Python server environment +* (Optional) CRM or customer database for tier lookup + +## Scenario + +We will build a customer support escalation system for TechCorp that intelligently routes support calls based on customer tier, issue complexity, and agent expertise in real-time. + +--- + +## 1. Create a Dynamic Escalation Tool + + + + + + In your Vapi dashboard, click **Tools** in the left sidebar. + + + - Click **Create Tool** + - Select **Transfer Call** as the tool type + - Set tool name: `Smart Support Escalation` + - **Important**: Leave the destinations array empty - this creates a dynamic transfer tool + - Set function name: `escalateToSupport` + - Add description: `Escalate calls to appropriate support specialists based on customer tier and issue complexity` + + + Add these parameters to help the assistant provide context: + - `issue_category` (string): Category of customer issue (technical, billing, account, product) + - `complexity_level` (string): Issue complexity (basic, intermediate, advanced, critical) + - `customer_context` (string): Relevant customer information for routing + - `escalation_reason` (string): Why this needs escalation vs self-service + + + + + ```typescript + import { VapiClient } from "@vapi-ai/server-sdk"; + + const vapi = new VapiClient({ token: process.env.VAPI_API_KEY }); + + async function createSupportEscalationTool() { + try { + const tool = await vapi.tools.create({ + type: "transferCall", + // Empty destinations array makes this a dynamic transfer tool + destinations: [], + function: { + name: "escalateToSupport", + description: "Escalate calls to appropriate support specialists based on customer tier and issue complexity", + parameters: { + type: "object", + properties: { + issue_category: { + type: "string", + description: "Category of customer issue", + enum: ["technical", "billing", "account", "product"] + }, + complexity_level: { + type: "string", + description: "Issue complexity level", + enum: ["basic", "intermediate", "advanced", "critical"] + }, + customer_context: { + type: "string", + description: "Relevant customer information for routing" + }, + escalation_reason: { + type: "string", + description: "Why this needs escalation vs self-service" + } + }, + required: ["issue_category", "complexity_level"] + } + } + }); + + console.log(`Support escalation tool created: ${tool.id}`); + return tool; + } catch (error) { + console.error('Error creating support escalation tool:', error); + throw error; + } + } + + // Create the support escalation tool + const escalationTool = await createSupportEscalationTool(); + ``` + + + ```python + import os + import requests + + def create_support_escalation_tool(): + """Create a dynamic support escalation tool with empty destinations""" + url = "https://api.vapi.ai/tool" + headers = { + "Authorization": f"Bearer {os.getenv('VAPI_API_KEY')}", + "Content-Type": "application/json" + } + + data = { + "type": "transferCall", + # Empty destinations array makes this a dynamic transfer tool + "destinations": [], + "function": { + "name": "escalateToSupport", + "description": "Escalate calls to appropriate support specialists based on customer tier and issue complexity", + "parameters": { + "type": "object", + "properties": { + "issue_category": { + "type": "string", + "description": "Category of customer issue", + "enum": ["technical", "billing", "account", "product"] + }, + "complexity_level": { + "type": "string", + "description": "Issue complexity level", + "enum": ["basic", "intermediate", "advanced", "critical"] + }, + "customer_context": { + "type": "string", + "description": "Relevant customer information for routing" + }, + "escalation_reason": { + "type": "string", + "description": "Why this needs escalation vs self-service" + } + }, + "required": ["issue_category", "complexity_level"] + } + } + } + + try: + response = requests.post(url, headers=headers, json=data) + response.raise_for_status() + tool = response.json() + print(f"Support escalation tool created: {tool['id']}") + return tool + except requests.exceptions.RequestException as error: + print(f"Error creating support escalation tool: {error}") + raise + + # Create the support escalation tool + escalation_tool = create_support_escalation_tool() + ``` + + + ```bash + curl -X POST https://api.vapi.ai/tool \ + -H "Authorization: Bearer $VAPI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "type": "transferCall", + "destinations": [], + "function": { + "name": "escalateToSupport", + "description": "Escalate calls to appropriate support specialists based on customer tier and issue complexity", + "parameters": { + "type": "object", + "properties": { + "issue_category": { + "type": "string", + "description": "Category of customer issue", + "enum": ["technical", "billing", "account", "product"] + }, + "complexity_level": { + "type": "string", + "description": "Issue complexity level", + "enum": ["basic", "intermediate", "advanced", "critical"] + }, + "customer_context": { + "type": "string", + "description": "Relevant customer information for routing" + }, + "escalation_reason": { + "type": "string", + "description": "Why this needs escalation vs self-service" + } + }, + "required": ["issue_category", "complexity_level"] + } + } + }' + ``` + + + +--- + +## 2. Create an Assistant with Smart Escalation + + + + + + - Navigate to **Assistants** in your dashboard + - Click **Create Assistant** + - Name: `TechCorp Support Assistant` + - Add your dynamic escalation tool to the assistant's tools + + + ```txt title="System Prompt" maxLines=15 + You are TechCorp's intelligent customer support assistant. Your job is to: + + 1. Help customers resolve issues when possible + 2. Assess issue complexity and customer needs + 3. Escalate to human specialists when appropriate using the escalateToSupport function + + Try to resolve simple issues first. For complex issues or when customers request human help, escalate intelligently based on: + - Issue category (technical, billing, account, product) + - Complexity level (basic, intermediate, advanced, critical) + - Customer context and history + + Always be professional and efficient in your support. + ``` + + + In assistant settings, enable the **transfer-destination-request** server event. This sends webhooks to your server when escalations are triggered. + + + Configure your server URL to handle escalation requests (e.g., `https://your-app.com/webhook/escalation`) + + + + + ```typescript + import { VapiClient } from "@vapi-ai/server-sdk"; + + const vapi = new VapiClient({ token: process.env.VAPI_API_KEY }); + + async function createSupportAssistant(escalationToolId: string) { + try { + const assistant = await vapi.assistants.create({ + name: "TechCorp Support Assistant", + firstMessage: "Hello! I'm here to help with your TechCorp support needs. I can assist with account questions, technical issues, billing inquiries, and more. What can I help you with today?", + model: { + provider: "openai", + model: "gpt-4o", + messages: [ + { + role: "system", + content: `You are TechCorp's intelligent customer support assistant. Your job is to: + +1. Help customers resolve issues when possible +2. Assess issue complexity and customer needs +3. Escalate to human specialists when appropriate using the escalateToSupport function + +Try to resolve simple issues first. For complex issues or when customers request human help, escalate intelligently based on: +- Issue category (technical, billing, account, product) +- Complexity level (basic, intermediate, advanced, critical) +- Customer context and history + +Always be professional and efficient in your support.` + } + ], + toolIds: [escalationToolId] + }, + voice: { + provider: "11labs", + voiceId: "burt" + }, + serverUrl: "https://your-app.com/webhook/escalation", + serverUrlSecret: process.env.WEBHOOK_SECRET + }); + + console.log(`Support assistant created: ${assistant.id}`); + return assistant; + } catch (error) { + console.error('Error creating support assistant:', error); + throw error; + } + } + + // Create assistant with escalation capabilities + const supportAssistant = await createSupportAssistant("YOUR_ESCALATION_TOOL_ID"); + ``` + + + ```python + import os + import requests + + def create_support_assistant(escalation_tool_id): + """Create assistant with dynamic escalation capabilities""" + url = "https://api.vapi.ai/assistant" + headers = { + "Authorization": f"Bearer {os.getenv('VAPI_API_KEY')}", + "Content-Type": "application/json" + } + + data = { + "name": "TechCorp Support Assistant", + "firstMessage": "Hello! I'\''m here to help with your TechCorp support needs. I can assist with account questions, technical issues, billing inquiries, and more. What can I help you with today?", + "model": { + "provider": "openai", + "model": "gpt-4o", + "messages": [ + { + "role": "system", + "content": """You are TechCorp's intelligent customer support assistant. Your job is to: + +1. Help customers resolve issues when possible +2. Assess issue complexity and customer needs +3. Escalate to human specialists when appropriate using the escalateToSupport function + +Try to resolve simple issues first. For complex issues or when customers request human help, escalate intelligently based on: +- Issue category (technical, billing, account, product) +- Complexity level (basic, intermediate, advanced, critical) +- Customer context and history + +Always be professional and efficient in your support.""" + } + ], + "toolIds": [escalation_tool_id] + }, + "voice": { + "provider": "11labs", + "voiceId": "burt" + }, + "serverUrl": "https://your-app.com/webhook/escalation", + "serverUrlSecret": os.getenv("WEBHOOK_SECRET") + } + + try: + response = requests.post(url, headers=headers, json=data) + response.raise_for_status() + assistant = response.json() + print(f"Support assistant created: {assistant['id']}") + return assistant + except requests.exceptions.RequestException as error: + print(f"Error creating support assistant: {error}") + raise + + # Create assistant with escalation capabilities + support_assistant = create_support_assistant("YOUR_ESCALATION_TOOL_ID") + ``` + + + ```bash + curl -X POST https://api.vapi.ai/assistant \ + -H "Authorization: Bearer $VAPI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "TechCorp Support Assistant", + "firstMessage": "Hello! I'\''m here to help with your TechCorp support needs. I can assist with account questions, technical issues, billing inquiries, and more. What can I help you with today?", + "model": { + "provider": "openai", + "model": "gpt-4o", + "messages": [ + { + "role": "system", + "content": "You are TechCorp'\''s intelligent customer support assistant. Your job is to:\n\n1. Help customers resolve issues when possible\n2. Assess issue complexity and customer needs\n3. Escalate to human specialists when appropriate using the escalateToSupport function\n\nTry to resolve simple issues first. For complex issues or when customers request human help, escalate intelligently based on:\n- Issue category (technical, billing, account, product)\n- Complexity level (basic, intermediate, advanced, critical)\n- Customer context and history\n\nAlways be professional and efficient in your support." + } + ], + "toolIds": ["YOUR_ESCALATION_TOOL_ID"] + }, + "voice": { + "provider": "11labs", + "voiceId": "burt" + }, + "serverUrl": "https://your-app.com/webhook/escalation", + "serverUrlSecret": "your-webhook-secret" + }' + ``` + + + +--- + +## 3. Build Escalation Logic Server + + + + ```typescript + import express from 'express'; + import crypto from 'crypto'; + + const app = express(); + app.use(express.json()); + + // Webhook secret verification + function verifyWebhookSignature(payload: string, signature: string) { + const expectedSignature = crypto + .createHmac('sha256', process.env.WEBHOOK_SECRET!) + .update(payload) + .digest('hex'); + return crypto.timingSafeEqual( + Buffer.from(signature), + Buffer.from(expectedSignature) + ); + } + + // Support escalation logic + function determineSupportDestination(request: any) { + const { functionCall, call, customer } = request; + const { issue_category, complexity_level, customer_context, escalation_reason } = functionCall.parameters; + + // Simulate customer tier lookup + const customerData = lookupCustomerTier(customer.number); + + // Enterprise customer escalation + if (customerData?.tier === 'enterprise' || complexity_level === 'critical') { + return { + type: "number", + number: "+1-555-ENTERPRISE-SUPPORT", + message: "Connecting you to our enterprise support specialist.", + transferPlan: { + mode: "warm-transfer-say-summary", + summaryPlan: { + enabled: true, + messages: [ + { + role: "system", + content: "Provide a summary for the enterprise support specialist." + }, + { + role: "user", + content: `Enterprise customer with ${issue_category} issue. Complexity: ${complexity_level}. Reason: ${escalation_reason}. Context: ${customer_context}` + } + ] + } + } + }; + } + + // Advanced technical issues + if (issue_category === 'technical' && (complexity_level === 'advanced' || complexity_level === 'intermediate')) { + return { + type: "number", + number: "+1-555-TECH-SPECIALISTS", + message: "Transferring you to our technical support specialists.", + transferPlan: { + mode: "warm-transfer-say-message", + message: `Technical ${complexity_level} issue. Customer context: ${customer_context}. Escalation reason: ${escalation_reason}` + } + }; + } + + // Billing and account specialists + if (issue_category === 'billing' || issue_category === 'account') { + return { + type: "number", + number: "+1-555-BILLING-TEAM", + message: "Connecting you with our billing and account specialists.", + transferPlan: { + mode: "warm-transfer-say-message", + message: `${issue_category} issue, complexity ${complexity_level}. Context: ${customer_context}` + } + }; + } + + // Product and feature questions + if (issue_category === 'product') { + return { + type: "number", + number: "+1-555-PRODUCT-SUPPORT", + message: "Transferring you to our product specialists.", + transferPlan: { + mode: "warm-transfer-say-message", + message: `Product ${complexity_level} inquiry. Context: ${customer_context}` + } + }; + } + + // Default to general support + return { + type: "number", + number: "+1-555-GENERAL-SUPPORT", + message: "Connecting you with our support team.", + transferPlan: { + mode: "warm-transfer-say-message", + message: `General ${issue_category} support needed. Level: ${complexity_level}` + } + }; + } + + // Simulate customer tier lookup + function lookupCustomerTier(phoneNumber: string) { + // In production, integrate with your actual CRM + const mockCustomerData = { + "+1234567890": { tier: "enterprise", account: "TechCorp Enterprise" }, + "+0987654321": { tier: "standard", account: "Basic Plan" }, + "+1111111111": { tier: "premium", account: "Premium Support" } + }; + return mockCustomerData[phoneNumber]; + } + + // Support escalation webhook + app.post('/webhook/escalation', (req, res) => { + try { + const signature = req.headers['x-vapi-signature'] as string; + const payload = JSON.stringify(req.body); + + // Verify webhook signature + if (!verifyWebhookSignature(payload, signature)) { + return res.status(401).json({ error: 'Invalid signature' }); + } + + const request = req.body; + + // Only handle transfer destination requests + if (request.type !== 'transfer-destination-request') { + return res.status(200).json({ received: true }); + } + + // Determine destination based on escalation context + const destination = determineSupportDestination(request); + + res.json({ destination }); + } catch (error) { + console.error('Escalation webhook error:', error); + res.status(500).json({ + error: 'Unable to determine escalation destination. Please try again.' + }); + } + }); + + app.listen(3000, () => { + console.log('Support escalation server running on port 3000'); + }); + ``` + + + ```python + import os + import hmac + import hashlib + from fastapi import FastAPI, HTTPException, Request + from pydantic import BaseModel + from typing import Optional, Dict, Any + + app = FastAPI() + + def verify_webhook_signature(payload: bytes, signature: str) -> bool: + """Verify webhook signature""" + webhook_secret = os.getenv('WEBHOOK_SECRET', '').encode() + expected_signature = hmac.new( + webhook_secret, + payload, + hashlib.sha256 + ).hexdigest() + return hmac.compare_digest(signature, expected_signature) + + def lookup_customer_tier(phone_number: str) -> Optional[Dict[str, Any]]: + """Simulate customer tier lookup""" + mock_customer_data = { + "+1234567890": {"tier": "enterprise", "account": "TechCorp Enterprise"}, + "+0987654321": {"tier": "standard", "account": "Basic Plan"}, + "+1111111111": {"tier": "premium", "account": "Premium Support"} + } + return mock_customer_data.get(phone_number) + + def determine_support_destination(request_data: Dict[str, Any]) -> Dict[str, Any]: + """Determine support escalation destination based on request context""" + function_call = request_data.get('functionCall', {}) + parameters = function_call.get('parameters', {}) + customer = request_data.get('customer', {}) + + issue_category = parameters.get('issue_category', 'general') + complexity_level = parameters.get('complexity_level', 'basic') + customer_context = parameters.get('customer_context', '') + escalation_reason = parameters.get('escalation_reason', '') + + # Simulate customer tier lookup + customer_data = lookup_customer_tier(customer.get('number', '')) + + # Enterprise customer escalation + if (customer_data and customer_data.get('tier') == 'enterprise') or complexity_level == 'critical': + return { + "type": "number", + "number": "+1-555-ENTERPRISE-SUPPORT", + "message": "Connecting you to our enterprise support specialist.", + "transferPlan": { + "mode": "warm-transfer-say-summary", + "summaryPlan": { + "enabled": True, + "messages": [ + { + "role": "system", + "content": "Provide a summary for the enterprise support specialist." + }, + { + "role": "user", + "content": f"Enterprise customer with {issue_category} issue. Complexity: {complexity_level}. Reason: {escalation_reason}. Context: {customer_context}" + } + ] + } + } + } + + # Advanced technical issues + if issue_category == 'technical' and complexity_level in ['advanced', 'intermediate']: + return { + "type": "number", + "number": "+1-555-TECH-SPECIALISTS", + "message": "Transferring you to our technical support specialists.", + "transferPlan": { + "mode": "warm-transfer-say-message", + "message": f"Technical {complexity_level} issue. Customer context: {customer_context}. Escalation reason: {escalation_reason}" + } + } + + # Billing and account specialists + if issue_category in ['billing', 'account']: + return { + "type": "number", + "number": "+1-555-BILLING-TEAM", + "message": "Connecting you with our billing and account specialists.", + "transferPlan": { + "mode": "warm-transfer-say-message", + "message": f"{issue_category} issue, complexity {complexity_level}. Context: {customer_context}" + } + } + + # Product and feature questions + if issue_category == 'product': + return { + "type": "number", + "number": "+1-555-PRODUCT-SUPPORT", + "message": "Transferring you to our product specialists.", + "transferPlan": { + "mode": "warm-transfer-say-message", + "message": f"Product {complexity_level} inquiry. Context: {customer_context}" + } + } + + # Default to general support + return { + "type": "number", + "number": "+1-555-GENERAL-SUPPORT", + "message": "Connecting you with our support team.", + "transferPlan": { + "mode": "warm-transfer-say-message", + "message": f"General {issue_category} support needed. Level: {complexity_level}" + } + } + + @app.post("/webhook/escalation") + async def handle_escalation_webhook(request: Request): + try: + # Get raw body for signature verification + body = await request.body() + signature = request.headers.get('x-vapi-signature', '') + + # Verify webhook signature + if not verify_webhook_signature(body, signature): + raise HTTPException(status_code=401, detail="Invalid signature") + + # Parse request body + request_data = await request.json() + + # Only handle transfer destination requests + if request_data.get('type') != 'transfer-destination-request': + return {"received": True} + + # Determine destination based on escalation context + destination = determine_support_destination(request_data) + + return {"destination": destination} + + except Exception as error: + print(f"Escalation webhook error: {error}") + raise HTTPException( + status_code=500, + detail="Unable to determine escalation destination. Please try again." + ) + + if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=3000) + ``` + + + +--- + +## 4. Test Your Support Escalation System + + + + + + - Navigate to **Phone Numbers** in your dashboard + - Click **Create Phone Number** + - Assign your support assistant to the number + - Configure any additional settings + + + Call your number and test various scenarios: + - Basic technical questions (should try to resolve first) + - Complex billing issues from enterprise customers + - Advanced technical problems requiring specialists + - Critical issues requiring immediate escalation + + + Check your server logs to see: + - Escalation requests received + - Customer tier classifications + - Destination routing decisions + - Any errors or routing issues + + + + + ```typescript + import { VapiClient } from "@vapi-ai/server-sdk"; + + const vapi = new VapiClient({ token: process.env.VAPI_API_KEY }); + + async function testSupportEscalation(assistantId: string) { + try { + // Test enterprise customer with complex issue + const enterpriseCall = await vapi.calls.create({ + assistantId: assistantId, + customer: { + number: "+1234567890", // Enterprise customer in your lookup + name: "Enterprise Customer - Technical Issue" + } + }); + + console.log(`Enterprise test call created: ${enterpriseCall.id}`); + + // Test standard customer with billing question + const standardCall = await vapi.calls.create({ + assistantId: assistantId, + customer: { + number: "+0987654321", // Standard customer in your lookup + name: "Standard Customer - Billing Question" + } + }); + + console.log(`Standard test call created: ${standardCall.id}`); + + return { enterpriseCall, standardCall }; + } catch (error) { + console.error('Error creating test calls:', error); + throw error; + } + } + + // Test the support escalation system + const testCalls = await testSupportEscalation('YOUR_ASSISTANT_ID'); + ``` + + + ```python + import requests + import os + + def test_support_escalation(assistant_id): + """Test support escalation with different customer scenarios""" + url = "https://api.vapi.ai/call" + headers = { + "Authorization": f"Bearer {os.getenv('VAPI_API_KEY')}", + "Content-Type": "application/json" + } + + test_scenarios = [ + { + "name": "Enterprise Technical Issue", + "customer": { + "number": "+1234567890", # Enterprise customer + "name": "Enterprise Customer - Technical Issue" + } + }, + { + "name": "Standard Billing Question", + "customer": { + "number": "+0987654321", # Standard customer + "name": "Standard Customer - Billing Question" + } + } + ] + + results = [] + for scenario in test_scenarios: + try: + data = { + "assistantId": assistant_id, + **scenario + } + + response = requests.post(url, headers=headers, json=data) + response.raise_for_status() + call = response.json() + + print(f"{scenario['name']} call created: {call['id']}") + results.append(call) + + except requests.exceptions.RequestException as error: + print(f"Error creating {scenario['name']}: {error}") + + return results + + # Test the support escalation system + test_calls = test_support_escalation('YOUR_ASSISTANT_ID') + ``` + + + +## Advanced Integration Examples + +### CRM Integration (Salesforce) + +```typescript +// Example: Salesforce CRM integration for customer tier lookup +async function lookupCustomerInSalesforce(phoneNumber: string) { + const salesforce = new SalesforceAPI({ + clientId: process.env.SALESFORCE_CLIENT_ID, + clientSecret: process.env.SALESFORCE_CLIENT_SECRET, + redirectUri: process.env.SALESFORCE_REDIRECT_URI + }); + + try { + const customer = await salesforce.query(` + SELECT Id, Account.Type, Support_Tier__c, Case_Count__c, Contract_Level__c + FROM Contact + WHERE Phone = '${phoneNumber}' + `); + + return customer.records[0]; + } catch (error) { + console.error('Salesforce lookup failed:', error); + return null; + } +} +``` + +### Issue Complexity Assessment + +```typescript +function assessIssueComplexity(issueDescription: string, customerHistory: any) { + const complexKeywords = ['api', 'integration', 'custom', 'enterprise', 'migration']; + const criticalKeywords = ['down', 'outage', 'critical', 'urgent', 'emergency']; + + const hasComplexKeywords = complexKeywords.some(keyword => + issueDescription.toLowerCase().includes(keyword) + ); + + const hasCriticalKeywords = criticalKeywords.some(keyword => + issueDescription.toLowerCase().includes(keyword) + ); + + if (hasCriticalKeywords || customerHistory.previousEscalations > 2) { + return 'critical'; + } + + if (hasComplexKeywords || customerHistory.tier === 'enterprise') { + return 'advanced'; + } + + return 'basic'; +} +``` + +### Agent Availability Checking + +```typescript +function getAvailableSpecialist(category: string, complexity: string) { + const specialists = getSpecialistsByCategory(category); + const qualifiedAgents = specialists.filter(agent => + agent.complexityLevel >= complexity && agent.isAvailable + ); + + if (qualifiedAgents.length === 0) { + return { + type: "number", + number: "+1-555-QUEUE-CALLBACK", + message: "All specialists are busy. You'll be added to our priority queue.", + transferPlan: { + mode: "warm-transfer-say-message", + message: `${category} ${complexity} issue - customer needs callback when specialist available` + } + }; + } + + // Return least busy qualified agent + const bestAgent = qualifiedAgents.sort( + (a, b) => a.activeCallCount - b.activeCallCount + )[0]; + + return { + type: "number", + number: bestAgent.phoneNumber, + message: `Connecting you to ${bestAgent.name}, our ${category} specialist.`, + transferPlan: { + mode: "warm-transfer-say-summary", + summaryPlan: { + enabled: true, + messages: [ + { + role: "system", + content: `Provide a summary for ${bestAgent.name}` + } + ] + } + } + }; +} +``` + +## Error Handling Best Practices + +### Comprehensive Error Handling + +```typescript +function handleEscalationError(error: any, context: any) { + console.error('Support escalation error:', error); + + // Log escalation details for debugging + console.error('Escalation context:', { + phoneNumber: context.customer?.number, + issueCategory: context.functionCall?.parameters?.issue_category, + complexityLevel: context.functionCall?.parameters?.complexity_level, + timestamp: new Date().toISOString() + }); + + // Return fallback destination + return { + type: "number", + number: process.env.FALLBACK_SUPPORT_NUMBER, + message: "I'll connect you with our general support team who can help you.", + transferPlan: { + mode: "warm-transfer-say-message", + message: "Escalation routing error - connecting to general support team" + } + }; +} +``` + +### Queue Management + +```typescript +async function getEscalationWithQueueManagement(context: any) { + try { + const queueStatus = await checkSupportQueueStatus(); + const destination = await determineEscalationDestination(context); + + // Add queue time estimate if available + if (queueStatus.estimatedWaitTime > 5) { + destination.message += ` Current wait time is approximately ${queueStatus.estimatedWaitTime} minutes.`; + } + + return destination; + } catch (error) { + return handleEscalationError(error, context); + } +} +``` + +## Next Steps + +You've built a sophisticated customer support escalation system using assistants! Consider these enhancements: + +* **[Property management call routing](/workflows/examples/property-management)** - Explore the visual workflow approach +* **[Call Analysis](/assistants/call-analysis)** - Analyze escalation patterns and optimize routing +* **[Custom Tools](/tools/custom-tools)** - Build additional tools for advanced support logic +* **[Webhooks](/server-url)** - Learn more about webhook security and advanced event handling diff --git a/fern/calls/call-dynamic-transfers.mdx b/fern/calls/call-dynamic-transfers.mdx index 71c79e1f..81c256ac 100644 --- a/fern/calls/call-dynamic-transfers.mdx +++ b/fern/calls/call-dynamic-transfers.mdx @@ -1,115 +1,472 @@ --- -title: Dynamic Call Transfers +title: Dynamic call transfers +subtitle: Route calls to different destinations based on real-time conversation context and external data. slug: calls/call-dynamic-transfers +description: Learn how Vapi's dynamic call transfers work and explore implementation patterns for intelligent call routing. --- -## Before you start +## Overview -Prerequisites: -- Access to a server or cloud function that can receive requests from Vapi +Dynamic call transfers enable intelligent routing by determining transfer destinations in real-time based on conversation context, customer data, or external system information. Unlike static transfers with predefined destinations, dynamic transfers make routing decisions on-the-fly during the call. -## Overview +**Key capabilities:** +* Real-time destination selection based on conversation analysis +* Integration with CRM systems, databases, and external APIs +* Conditional routing logic for departments, specialists, or geographic regions +* Context-aware transfers with conversation summaries +* Fallback handling for unavailable destinations + +## Prerequisites + +* A [Vapi account](https://dashboard.vapi.ai/) +* A server or cloud function that can receive webhooks from Vapi +* (Optional) CRM system or customer database for enhanced routing logic + +## How It Works + +Dynamic transfers operate by leaving the destination unspecified initially, then using webhooks to determine the appropriate destination when needed. + +**Transfer flow:** +1. **Trigger** - Voice agent determines a transfer is needed based on conversation +2. **Webhook** - Vapi sends `transfer-destination-request` to your server with call context +3. **Decision** - Your server analyzes context and external data to determine routing +4. **Response** - Server returns destination details and transfer configuration +5. **Transfer** - Vapi executes the transfer to the determined destination -Dynamic call transfers let your assistant transfer calls to different destinations (phone numbers, SIP, or other assistants) based on real-time context. This guide shows you how to set up a custom transfer tool, connect it to your assistant, handle transfer requests with your server, and respond with the right destination or error. +**Available context:** Your webhook receives conversation transcript, extracted variables, customer information, function parameters, and call metadata. + +--- + +## Quick Implementation Guide - - Create a transfer tool with an empty `destinations` array. This acts as a placeholder so you can define destinations dynamically at runtime. - - ```bash - curl -X POST https://api.vapi.ai/tool \ - -H "Authorization: Bearer insert-private-key-here" \ - -H "Content-Type: application/json" \ - -d '{ - "type": "transferCall", - "destinations": [], - "function": { - "name": "dynamicDestinationTransferCall" - } - }' - ``` - + + + + - Navigate to **Tools** in your dashboard + - Click **Create Tool** + - Select **Transfer Call** as the tool type + - **Important**: Leave the destinations array empty - this creates a dynamic transfer tool + - Set function name: `dynamicTransfer` + - Add description explaining when this tool should be used + + + ```typescript + import { VapiClient } from "@vapi-ai/server-sdk"; + + const vapi = new VapiClient({ token: process.env.VAPI_API_KEY }); + + const dynamicTool = await vapi.tools.create({ + type: "transferCall", + // Empty destinations array makes this dynamic + destinations: [], + function: { + name: "dynamicTransfer", + description: "Transfer call to appropriate destination based on customer needs", + parameters: { + type: "object", + properties: { + reason: { + type: "string", + description: "Reason for transfer" + }, + urgency: { + type: "string", + enum: ["low", "medium", "high", "critical"] + } + } + } + } + }); - - After creating the tool, link it to your assistant. This enables the assistant to trigger the tool during calls. + console.log(`Dynamic transfer tool created: ${dynamicTool.id}`); + ``` + + + ```python + import requests + import os + + def create_dynamic_transfer_tool(): + url = "https://api.vapi.ai/tool" + headers = { + "Authorization": f"Bearer {os.getenv('VAPI_API_KEY')}", + "Content-Type": "application/json" + } + + data = { + "type": "transferCall", + "destinations": [], # Empty for dynamic routing + "function": { + "name": "dynamicTransfer", + "description": "Transfer call to appropriate destination based on customer needs", + "parameters": { + "type": "object", + "properties": { + "reason": { + "type": "string", + "description": "Reason for transfer" + }, + "urgency": { + "type": "string", + "enum": ["low", "medium", "high", "critical"] + } + } + } + } + } + + response = requests.post(url, headers=headers, json=data) + return response.json() + + tool = create_dynamic_transfer_tool() + print(f"Dynamic transfer tool created: {tool['id']}") + ``` + + + ```bash + curl -X POST https://api.vapi.ai/tool \ + -H "Authorization: Bearer $VAPI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "type": "transferCall", + "destinations": [], + "function": { + "name": "dynamicTransfer", + "description": "Transfer call to appropriate destination based on customer needs", + "parameters": { + "type": "object", + "properties": { + "reason": {"type": "string", "description": "Reason for transfer"}, + "urgency": {"type": "string", "enum": ["low", "medium", "high", "critical"]} + } + } + } + }' + ``` + + - - In your assistant settings, select the `transfer-destination-request` server event. This event sends a webhook to your server whenever a transfer is requested, so you can dynamically determine the destination. + + + + - Navigate to **Assistants** + - Create a new assistant or edit an existing one + - Add your dynamic transfer tool to the assistant + - Enable the **transfer-destination-request** server event + - Set your server URL to handle the webhook + + + ```typescript + const assistant = await vapi.assistants.create({ + name: "Dynamic Transfer Assistant", + firstMessage: "Hello! How can I help you today?", + model: { + provider: "openai", + model: "gpt-4o", + messages: [ + { + role: "system", + content: "You help customers and transfer them when needed using the dynamicTransfer tool. Assess the customer's needs and transfer to the appropriate department." + } + ], + toolIds: ["YOUR_DYNAMIC_TOOL_ID"] + }, + voice: { + provider: "11labs", + voiceId: "burt" + }, + serverUrl: "https://your-server.com/webhook", + serverUrlSecret: process.env.WEBHOOK_SECRET + }); + ``` + + + ```python + def create_assistant_with_dynamic_transfer(tool_id): + url = "https://api.vapi.ai/assistant" + headers = { + "Authorization": f"Bearer {os.getenv('VAPI_API_KEY')}", + "Content-Type": "application/json" + } + + data = { + "name": "Dynamic Transfer Assistant", + "firstMessage": "Hello! How can I help you today?", + "model": { + "provider": "openai", + "model": "gpt-4o", + "messages": [{ + "role": "system", + "content": "You help customers and transfer them when needed using the dynamicTransfer tool. Assess the customer's needs and transfer to the appropriate department." + }], + "toolIds": [tool_id] + }, + "voice": {"provider": "11labs", "voiceId": "burt"}, + "serverUrl": "https://your-server.com/webhook", + "serverUrlSecret": os.getenv("WEBHOOK_SECRET") + } + + response = requests.post(url, headers=headers, json=data) + return response.json() + ``` + + + ```bash + curl -X POST https://api.vapi.ai/assistant \ + -H "Authorization: Bearer $VAPI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Dynamic Transfer Assistant", + "firstMessage": "Hello! How can I help you today?", + "model": { + "provider": "openai", + "model": "gpt-4o", + "messages": [{ + "role": "system", + "content": "You help customers and transfer them when needed using the dynamicTransfer tool." + }], + "toolIds": ["YOUR_DYNAMIC_TOOL_ID"] + }, + "serverUrl": "https://your-server.com/webhook" + }' + ``` + + - - Update your assistant's server URL to point to your server. Your server will receive a webhook with call details whenever a transfer is triggered, and should respond with the appropriate destination or an error. + + + + ```typescript + import express from 'express'; + import crypto from 'crypto'; + + const app = express(); + app.use(express.json()); + + function verifyWebhookSignature(payload: string, signature: string) { + const expectedSignature = crypto + .createHmac('sha256', process.env.WEBHOOK_SECRET!) + .update(payload) + .digest('hex'); + return crypto.timingSafeEqual( + Buffer.from(signature), + Buffer.from(expectedSignature) + ); + } + + app.post('/webhook', (req, res) => { + try { + const signature = req.headers['x-vapi-signature'] as string; + const payload = JSON.stringify(req.body); + + if (!verifyWebhookSignature(payload, signature)) { + return res.status(401).json({ error: 'Invalid signature' }); + } + + const request = req.body; + + if (request.type !== 'transfer-destination-request') { + return res.status(200).json({ received: true }); + } + + // Simple routing logic - customize for your needs + const { functionCall, customer } = request; + const urgency = functionCall.parameters?.urgency || 'medium'; + + let destination; + if (urgency === 'critical') { + destination = { + type: "number", + number: "+1-555-EMERGENCY", + message: "Connecting you to our emergency team." + }; + } else { + destination = { + type: "number", + number: "+1-555-SUPPORT", + message: "Transferring you to our support team." + }; + } + + res.json({ destination }); + } catch (error) { + console.error('Webhook error:', error); + res.status(500).json({ + error: 'Transfer routing failed. Please try again.' + }); + } + }); + + app.listen(3000, () => { + console.log('Webhook server running on port 3000'); + }); + ``` + + + ```python + import os + import hmac + import hashlib + from fastapi import FastAPI, HTTPException, Request + + app = FastAPI() + + def verify_webhook_signature(payload: bytes, signature: str) -> bool: + webhook_secret = os.getenv('WEBHOOK_SECRET', '').encode() + expected_signature = hmac.new( + webhook_secret, payload, hashlib.sha256 + ).hexdigest() + return hmac.compare_digest(signature, expected_signature) + + @app.post("/webhook") + async def handle_webhook(request: Request): + try: + body = await request.body() + signature = request.headers.get('x-vapi-signature', '') + + if not verify_webhook_signature(body, signature): + raise HTTPException(status_code=401, detail="Invalid signature") + + request_data = await request.json() + + if request_data.get('type') != 'transfer-destination-request': + return {"received": True} + + # Simple routing logic - customize for your needs + function_call = request_data.get('functionCall', {}) + urgency = function_call.get('parameters', {}).get('urgency', 'medium') + + if urgency == 'critical': + destination = { + "type": "number", + "number": "+1-555-EMERGENCY", + "message": "Connecting you to our emergency team." + } + else: + destination = { + "type": "number", + "number": "+1-555-SUPPORT", + "message": "Transferring you to our support team." + } + + return {"destination": destination} + + except Exception as error: + print(f"Webhook error: {error}") + raise HTTPException( + status_code=500, + detail="Transfer routing failed. Please try again." + ) + ``` + + - - Use a prompt like this to trigger the transfer tool: - - ``` - [TASK] - trigger the dynamicDestinationTransferCall tool - ``` - - When triggered, the assistant sends a `transfer-destination-request` webhook to your server. The webhook includes call details, transcripts, and messages. - - **Sample request payload:** - ```json - { - "type": "transfer-destination-request", - "artifact": { - "messages": [...], - "transcript": "Hello, how can I help you?", - "messagesOpenAIFormatted": [...] - }, - "assistant": { "id": "assistant123" }, - "phoneNumber": "+14155552671", - "customer": { "id": "customer456" }, - "call": { "id": "call789", "status": "ongoing" } - } - ``` + + + + - Create a phone number and assign your assistant + - Call the number and test different transfer scenarios + - Monitor your webhook server logs to see the routing decisions + - Verify transfers are working to the correct destinations + + + ```typescript + // Test with an outbound call + const testCall = await vapi.calls.create({ + assistantId: "YOUR_ASSISTANT_ID", + customer: { + number: "+1234567890" // Your test number + } + }); + + console.log(`Test call created: ${testCall.id}`); + + // Monitor webhook server logs to see transfer requests + ``` + + + ```python + def test_dynamic_transfers(assistant_id): + url = "https://api.vapi.ai/call" + headers = { + "Authorization": f"Bearer {os.getenv('VAPI_API_KEY')}", + "Content-Type": "application/json" + } + + data = { + "assistantId": assistant_id, + "customer": {"number": "+1234567890"} + } + + response = requests.post(url, headers=headers, json=data) + call = response.json() + print(f"Test call created: {call['id']}") + return call + ``` + + + - - Your server should respond with either a valid `destination` or an `error`. - - #### Number destination example - ```json - { - "destination": { - "type": "number", - "message": "Connecting you to our support line.", - "number": "+14155552671", - "numberE164CheckEnabled": true, - "callerId": "+14155551234", - "extension": "101" - } - } - ``` +--- + +## Implementation Approaches + +**Assistant-based implementation** uses transfer-type tools with conditions interpreted by the assistant through system prompts. The assistant determines when and where to route calls based on clearly defined tool purposes and routing logic in the prompt. Best for quick setup and simpler routing scenarios. + +**Workflow-based implementation** uses conditional logic based on outputs from any workflow node - tools, API requests, conversation variables, or other data sources. Conditions evaluate node outputs to determine routing paths within visual workflows. Best for complex business logic, structured decision trees, and team-friendly configuration. + + + +
+ +
+ **Assistant-based routing** - #### SIP destination example - ```json - { - "destination": { - "type": "sip", - "message": "Connecting your call via SIP.", - "sipUri": "sip:customer-support@domain.com", - "sipHeaders": { - "X-Custom-Header": "value" - } - } - } - ``` + Route customers to appropriate support tiers based on conversation analysis and customer data +
+ +
+ +
+ **Workflow-based routing** - #### Error response example - ```json - { - "error": "Invalid destination specified." - } - ``` - Every response must include either a `destination` or an `error` to indicate the outcome of the transfer request. -
- + Direct tenant calls to the right department with automated verification + + + +## Routing Patterns + +### Common Use Cases + +* **Customer support routing** - Route based on issue type, customer tier, agent availability, and interaction history. Enterprise customers and critical issues get priority routing to specialized teams. + +* **Geographic routing** - Direct calls to regional offices based on customer location and business hours. Automatically handle time zone differences and language preferences. + +* **Load balancing** - Distribute calls across available agents to optimize wait times and agent utilization. Route to the least busy qualified agent. + +* **Escalation management** - Implement intelligent escalation based on conversation tone, issue complexity, and customer history. Automatically route urgent issues to senior agents. + +### Transfer Configuration + +1. **Warm transfers** provide context to receiving agents with AI-generated conversation summaries, ensuring smooth handoffs with full context. + +2. **Cold transfers** route calls immediately with predefined context messages, useful for simple departmental routing. + +3. **Conditional transfers** apply different transfer modes based on routing decisions, such as priority handling for enterprise customers. + +4. **Destination types** include phone numbers for human agents, SIP endpoints for VoIP systems, and Vapi assistants for specialized AI agents. + + +**Security considerations:** Always verify webhook signatures to ensure requests come from Vapi. Never log sensitive customer data, implement proper access controls, and follow privacy regulations like GDPR and CCPA when handling customer information in routing decisions. + -## Conclusion +## Related Documentation -Dynamic call transfers empower your assistant to route calls efficiently based on real-time data. By implementing this flow, you can ensure seamless interactions and provide a better experience for your users. +* **[Call Forwarding](/call-forwarding)** - Static transfer options and transfer plans +* **[Webhooks](/server-url)** - Webhook security and event handling patterns +* **[Custom Tools](/tools/custom-tools)** - Build custom tools for advanced routing logic diff --git a/fern/docs.yml b/fern/docs.yml index eee64da7..f9e6b055 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -231,6 +231,9 @@ navigation: - page: Documentation agent path: assistants/examples/docs-agent.mdx icon: fa-light fa-microphone + - page: Support escalation + path: assistants/examples/support-escalation.mdx + icon: fa-light fa-headset - page: Multilingual agent path: assistants/examples/multilingual-agent.mdx icon: fa-light fa-globe @@ -258,6 +261,9 @@ navigation: - page: Order management path: workflows/examples/ecommerce-order-management.mdx icon: fa-light fa-shopping-cart + - page: Property management + path: workflows/examples/property-management.mdx + icon: fa-light fa-building - page: Multilingual support path: workflows/examples/multilingual-support.mdx icon: fa-light fa-globe diff --git a/fern/guides.mdx b/fern/guides.mdx index 7a03c784..f555f59b 100644 --- a/fern/guides.mdx +++ b/fern/guides.mdx @@ -33,6 +33,14 @@ slug: guides
Build an ecommerce order management assistant that can track orders and process returns + +
+ +
+
Built with Workflows
+
+ Build a call routing workflow that dynamically routes tenant calls based on verification and inquiry type +
@@ -41,7 +49,7 @@ slug: guides
Create an outbound sales agent that can schedule appointments automatically - +
@@ -57,6 +65,14 @@ slug: guides
Build a dynamic agent with automatic language detection and real-time language switching
+ +
+ +
+
Built with Assistants
+
+ Build an intelligent support escalation system with dynamic routing based on customer tier and issue complexity +
diff --git a/fern/static/images/workflows/examples/property-management.png b/fern/static/images/workflows/examples/property-management.png new file mode 100644 index 00000000..7d92b4f6 Binary files /dev/null and b/fern/static/images/workflows/examples/property-management.png differ diff --git a/fern/workflows/examples/property-management.mdx b/fern/workflows/examples/property-management.mdx new file mode 100644 index 00000000..d93b8437 --- /dev/null +++ b/fern/workflows/examples/property-management.mdx @@ -0,0 +1,707 @@ +--- +title: Property management call routing +subtitle: Build a call routing workflow for property management that dynamically routes calls based on tenant status, inquiry type, and agent availability. +slug: workflows/examples/property-management +description: Build a voice AI property management system with dynamic call routing that determines destinations based on tenant verification, inquiry type analysis, and real-time agent availability using workflow API requests. +--- + + + Property Management Workflow + + +## Overview + +Build a property management call routing workflow that determines transfer destinations dynamically using tenant verification, inquiry type analysis, and real-time agent availability. This approach uses visual workflow nodes with API Request nodes for maximum routing flexibility. + +**Workflow Capabilities:** +* Tenant status verification and prioritization +* Inquiry type classification for specialist routing +* Real-time agent availability and queue management +* Emergency routing for urgent maintenance issues + +**What You'll Build:** +* Visual workflow with conditional routing logic +* API Request nodes for dynamic destination logic +* Tenant verification with CRM integration +* Emergency escalation with priority queuing + +## Quick Start: Create the Complete Workflow + +Use this cURL command to create the entire property management workflow in one shot: + + +```bash title="Complete Workflow Creation" +curl -X POST https://api.vapi.ai/workflow \ + -H "Authorization: Bearer $VAPI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Property Management Call Router", + "nodes": [ + { + "type": "conversation", + "name": "Initial Greeting", + "isStart": true, + "prompt": "You are a helpful property management assistant for Riverside Property Management. Start by greeting the caller and asking how you can help them today. Listen to determine: Is this an emergency/urgent maintenance issue? What type of inquiry is this (maintenance, lease, rent, general)? Keep responses under 25 words and be professional.", + "model": { + "provider": "openai", + "model": "gpt-4" + }, + "variableExtractionPlan": { + "schema": { + "type": "object", + "properties": { + "inquiry_type": { + "type": "string", + "description": "Type of inquiry", + "enum": ["emergency", "maintenance", "lease", "rent", "general"] + }, + "caller_phone": { + "type": "string", + "description": "Caller phone number for tenant lookup" + } + } + } + } + }, + { + "type": "conversation", + "name": "Emergency Handling", + "prompt": "This is an emergency maintenance situation. Tell the caller you understand this is an emergency and that you are immediately connecting them with emergency maintenance. Keep the interaction brief and gather only essential details about the emergency.", + "model": { + "provider": "openai", + "model": "gpt-4" + }, + "variableExtractionPlan": { + "schema": { + "type": "object", + "properties": { + "emergency_details": { + "type": "string", + "description": "Brief description of emergency" + } + } + } + } + }, + { + "type": "tool", + "name": "Transfer to General Office", + "tool": { + "type": "transferCall", + "destinations": [ + { + "type": "number", + "number": "+12025551234", + "message": "Connecting you to our office team who will assist you with your inquiry." + } + ] + } + } + ], + "edges": [ + { + "from": "Initial Greeting", + "to": "Emergency Handling", + "condition": { + "type": "ai", + "prompt": "Route to emergency handling if the caller has an emergency or urgent maintenance issue" + } + }, + { + "from": "Initial Greeting", + "to": "Transfer to General Office", + "condition": { + "type": "ai", + "prompt": "Route to transfer for all non-emergency inquiries (maintenance, lease, rent, general)" + } + }, + { + "from": "Emergency Handling", + "to": "Transfer to General Office", + "condition": { + "type": "ai", + "prompt": "After gathering emergency details, transfer to office" + } + } + ] + }' +``` + + + +Replace `$VAPI_API_KEY` with your actual API key from the [Vapi Dashboard](https://dashboard.vapi.ai/). Update the phone number in the `transferCall` destination to your actual office number. + + +Once created, you can retrieve the workflow ID and attach it to a phone number for testing. + +## Test Workflow Creation + +After creating the workflow, you can test it and get the workflow ID: + + +```bash title="Get Your Workflow ID" +# First, create the workflow using the command above, then: +curl -X GET https://api.vapi.ai/workflow \ + -H "Authorization: Bearer $VAPI_API_KEY" \ + | jq '.[] | select(.name == "Property Management Call Router") | {id: .id, name: .name, nodes: (.nodes | length), edges: (.edges | length)}' +``` + +```bash title="Get Specific Workflow Details" +# Replace WORKFLOW_ID with the actual ID from the previous command +curl -X GET https://api.vapi.ai/workflow/WORKFLOW_ID \ + -H "Authorization: Bearer $VAPI_API_KEY" \ + | jq '{id: .id, name: .name, nodes: [.nodes[].name], edges: [.edges[].condition]}' +``` + +```bash title="Attach Workflow to Phone Number" +# Replace PHONE_NUMBER_ID with your actual phone number ID +curl -X PATCH https://api.vapi.ai/phone-number/PHONE_NUMBER_ID \ + -H "Authorization: Bearer $VAPI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "workflowId": "WORKFLOW_ID" + }' +``` + + + +You'll need `jq` installed for JSON parsing. On macOS: `brew install jq`, on Ubuntu: `sudo apt-get install jq` + + +## API Response Structure + +When you create the workflow, you'll receive a response like this: + +```json +{ + "id": "wf_1234567890abcdef", + "name": "Property Management Call Router", + "orgId": "org_1234567890abcdef", + "createdAt": "2024-01-15T10:30:00.000Z", + "updatedAt": "2024-01-15T10:30:00.000Z", + "nodes": [ + { + "id": "greeting", + "type": "conversation", + "name": "Initial Greeting", + "isStart": true, + "firstMessage": "Hello! You've reached Riverside Property Management...", + "systemPrompt": "You are a helpful property management assistant...", + "model": { + "provider": "openai", + "model": "gpt-4" + }, + "extractVariables": [...] + }, + // ... more nodes + ], + "edges": [ + { + "id": "greeting_to_lookup", + "source": "greeting", + "target": "tenant_lookup", + "condition": "Always route to tenant lookup after greeting" + }, + // ... more edges + ] +} +``` + +### Key Fields for Integration + +| Field | Description | Usage | +|-------|-------------|-------| +| `id` | Workflow ID | Use this to attach workflow to phone numbers | +| `name` | Workflow name | For identification in dashboard | +| `nodes` | Array of workflow nodes | Conversation and tool nodes | +| `edges` | Array of connections | Define the flow between nodes | +| `createdAt` | Creation timestamp | For tracking and reference | + +### Node Types in This Workflow + +- **Conversation Nodes**: Handle AI conversations with tenants +- **Tool Nodes**: Execute API calls for tenant lookup and routing +- **Transfer Nodes**: Route calls to appropriate agents + +## Prerequisites + +* A [Vapi account](https://dashboard.vapi.ai/) +* Property management system API or tenant database +* (Optional) Agent availability tracking system + +## Scenario + +We will build a call routing workflow for Riverside Property Management that intelligently routes tenant calls based on their status, inquiry type, and agent availability. + +--- + +## 1. Create a Workflow + + + + + + In your Vapi dashboard, click **Workflows** in the left sidebar. + + + - Click **Create Workflow** + - Name: `Property Management Call Router` + - Select blank template to start with basic call start node + - Click **Create Workflow** + + + Click on the conversation node and configure: + + **Prompt**: + ```txt + You are a helpful property management assistant for Riverside Property Management. Start by greeting the caller and asking how you can help them today. Listen to determine: Is this an emergency/urgent maintenance issue? What type of inquiry is this (maintenance, lease, rent, general)? Keep responses under 25 words and be professional. + ``` + + **Variable Extraction Schema**: + ```json + { + "type": "object", + "properties": { + "inquiry_type": { + "type": "string", + "description": "Type of inquiry", + "enum": ["emergency", "maintenance", "lease", "rent", "general"] + }, + "caller_phone": { + "type": "string", + "description": "Caller phone number for tenant lookup" + } + } + } + ``` + + + + + Use the working cURL command from the Quick Start section above to create the entire workflow programmatically. + + + +--- + +## 2. Add Tenant Verification Node + + + + Add an **API Request** node after the greeting: + + **Node Configuration**: + - Node ID: `tenant_lookup` + - HTTP Method: `POST` + - URL: `https://your-property-system.com/api/tenants/lookup` + - Headers: `Authorization: Bearer YOUR_API_KEY` + - Body: + ```json + { + "phone": "{{caller_phone}}", + "inquiry_type": "{{inquiry_type}}" + } + ``` + + + Map the API response to workflow variables: + - `tenant_status` → Extract from `response.tenant.status` + - `property_address` → Extract from `response.tenant.property` + - `account_standing` → Extract from `response.tenant.account_standing` + - `emergency_contact` → Extract from `response.tenant.emergency_contact` + + + Configure what happens if the API call fails: + - **On Error**: Route to general agent queue + - **Error Message**: "I'll connect you with our general team who can help." + + + +--- + +## 3. Build Emergency Routing Logic + + + + Add a **Conversation** node for emergency handling: + + **Condition**: `inquiry_type == "emergency"` + + **First Message**: + ```txt + I understand this is an emergency. Let me immediately connect you with our emergency maintenance team. Please stay on the line. + ``` + + **System Prompt**: + ```txt + This is an emergency maintenance situation. Confirm the emergency details quickly and route to emergency maintenance immediately. Keep interaction brief. + ``` + + + Add an **API Request** node to get emergency destination: + + **URL**: `https://your-system.com/api/routing/emergency` + **Method**: `POST` + **Body**: + ```json + { + "tenant_id": "{{tenant_id}}", + "property": "{{property_address}}", + "inquiry_type": "emergency", + "priority": "high" + } + ``` + + + Add a **Transfer Call** node: + - **Destination**: Use the phone number from the API response + - **Transfer Plan**: Include emergency context and tenant information + - **Priority**: Set to highest priority for immediate routing + + + +--- + +## 4. Create Inquiry-Based Routing + + + + Add **API Request** node for maintenance team routing: + + **Condition**: `inquiry_type == "maintenance"` + **URL**: `https://your-system.com/api/routing/maintenance` + **Body**: + ```json + { + "tenant_id": "{{tenant_id}}", + "property": "{{property_address}}", + "inquiry_type": "maintenance", + "tenant_status": "{{tenant_status}}" + } + ``` + + Response should include: + - Available maintenance coordinator phone + - Estimated wait time + - Work order creation capability + + + Add **API Request** node for leasing inquiries: + + **Condition**: `inquiry_type == "lease"` + **URL**: `https://your-system.com/api/routing/leasing` + **Body**: + ```json + { + "tenant_id": "{{tenant_id}}", + "property": "{{property_address}}", + "inquiry_type": "lease", + "account_standing": "{{account_standing}}" + } + ``` + + + Add **API Request** node for billing department: + + **Condition**: `inquiry_type == "rent"` + **URL**: `https://your-system.com/api/routing/billing` + **Body**: + ```json + { + "tenant_id": "{{tenant_id}}", + "account_standing": "{{account_standing}}", + "inquiry_type": "rent" + } + ``` + + + +--- + +## 5. Add Agent Availability Logic + + + + Before each transfer, add an **API Request** to check agent availability: + + **URL**: `https://your-system.com/api/agents/availability` + **Method**: `GET` + **Query Parameters**: `department={{department}}&priority={{priority}}` + + Response includes: + - Available agents with phone numbers + - Current queue length + - Estimated wait times + + + Add conditional routing based on availability: + + **If agents available**: Direct transfer to agent + **If queue exists**: Inform caller of wait time and offer callback + **If all busy**: Route to voicemail or priority callback system + + + Add **API Request** node for callback scheduling: + + **URL**: `https://your-system.com/api/callbacks/schedule` + **Body**: + ```json + { + "tenant_id": "{{tenant_id}}", + "phone": "{{caller_phone}}", + "inquiry_type": "{{inquiry_type}}", + "priority": "{{priority}}", + "requested_time": "{{preferred_callback_time}}" + } + ``` + + + +--- + +## 6. Build Transfer Nodes with Context + + + + Use the API response data to populate transfer nodes: + + **Maintenance Transfer**: + - **Destination**: `{{maintenance_agent_phone}}` + - **Message**: "Connecting you to {{agent_name}} from our maintenance team." + - **Transfer Plan**: Include tenant property address and issue details + + **Leasing Transfer**: + - **Destination**: `{{leasing_agent_phone}}` + - **Message**: "Transferring you to our leasing office." + - **Transfer Plan**: Include tenant status and lease information + + **Billing Transfer**: + - **Destination**: `{{billing_agent_phone}}` + - **Message**: "Connecting you with our billing department." + - **Transfer Plan**: Include account standing and payment history + + + Each transfer node should include rich context: + + ```txt title="Transfer Plan Summary" + Tenant: {{tenant_name}} at {{property_address}} + Account Status: {{account_standing}} + Inquiry Type: {{inquiry_type}} + Previous Context: {{conversation_summary}} + Priority: {{priority_level}} + ``` + + + +--- + +## 7. Add Error Handling and Fallbacks + + + + Add **API Request** node for fallback scenarios: + + **Triggers**: + - API lookup failures + - No available agents + - Unknown inquiry types + - System errors + + **URL**: `https://your-system.com/api/routing/fallback` + **Body**: + ```json + { + "phone": "{{caller_phone}}", + "error_type": "{{error_reason}}", + "original_inquiry": "{{inquiry_type}}" + } + ``` + + + Create **Transfer Call** node for general queue: + + **Destination**: Main office line + **Message**: "Let me connect you with our general team who can assist you." + **Transfer Plan**: "Call requires general assistance - routing details unavailable" + + + Add **End Call** node with voicemail message: + + **Condition**: All agents busy and caller declines callback + **Message**: "Please leave a detailed voicemail including your name, property address, and the nature of your request. We'll call you back within 4 hours." + + + +--- + +## 8. Test Your Property Routing Workflow + + + + + + - Navigate to **Phone Numbers** in your dashboard + - Click **Create Phone Number** + - Assign your property management workflow + - Configure any additional settings + + + Call your number and test various scenarios: + - Emergency maintenance calls + - Regular maintenance requests from verified tenants + - Leasing inquiries from prospective tenants + - Billing questions from current tenants + - Calls from unrecognized phone numbers + + + Check your workflow analytics to verify: + - Call routing patterns + - Emergency response times + - Variable extraction accuracy + - Transfer success rates + + + + + Test your workflow using the API: + + ```bash + # Create a test call + curl -X POST https://api.vapi.ai/call \ + -H "Authorization: Bearer $VAPI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "workflowId": "YOUR_WORKFLOW_ID", + "customer": { + "number": "+1234567890", + "name": "Test Caller" + } + }' + ``` + + + +## API Integration Examples + +Your property management system can integrate with the workflow using these API endpoints: + +### Tenant Lookup Endpoint + +```http +POST /api/tenants/lookup +Content-Type: application/json + +{ + "phone": "+1234567890", + "inquiry_type": "maintenance" +} +``` + +**Response:** +```json +{ + "tenant": { + "id": "tenant_123", + "name": "John Smith", + "status": "active", + "property": "123 Main St, Apt 4B", + "account_standing": "good" + }, + "routing_suggestion": "maintenance_team" +} +``` + +### Agent Availability Check + +```http +GET /api/agents/availability?department=maintenance&priority=normal +``` + +**Response:** +```json +{ + "available_agents": [ + { + "id": "agent_456", + "name": "Mike Johnson", + "phone": "+15551234567", + "department": "maintenance" + } + ], + "queue_length": 2, + "estimated_wait_minutes": 5, + "department_status": "available" +} +``` + +### Emergency Routing + +```http +POST /api/routing/emergency +Content-Type: application/json + +{ + "tenant_id": "tenant_123", + "property": "123 Main St, Apt 4B", + "inquiry_type": "emergency" +} +``` + +**Response:** +```json +{ + "destination": "+15559876543", + "agent_name": "Emergency Maintenance", + "ticket_id": "EM_789", + "priority": "critical" +} +``` + +## Advanced Workflow Features + +### Queue Management with Priorities + +Configure priority-based routing in your tenant lookup API: + +```json +{ + "tenant": { + "tier": "commercial", + "account_standing": "good", + "inquiry_type": "emergency" + }, + "routing_priority": "critical" +} +``` + +Priority levels: +- **Critical**: Emergency situations, commercial tenants +- **High**: Good standing tenants with urgent issues +- **Normal**: Standard maintenance and lease inquiries +- **Low**: Delinquent accounts with non-urgent matters + +### Business Hours Routing + +Configure time-based routing logic: + +```json +{ + "business_hours": { + "weekdays": "9:00-17:00", + "weekends": "10:00-15:00" + }, + "after_hours_routing": { + "emergency": "+15559876543", + "general": "voicemail" + } +} +``` + +## Next Steps + +You've built a sophisticated property management call routing workflow! Consider these enhancements: + +* **[Customer support escalation system](/assistants/examples/support-escalation)** - Explore the assistant-based approach +* **[Workflow Analytics](/workflows/analytics)** - Track routing patterns and optimize decision trees +* **[Integration Templates](/workflows/integrations)** - Connect with popular property management systems +* **[Advanced Routing](/workflows/advanced-routing)** - Implement complex routing logic with multiple conditions