The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "UA House - Call Handler (Main)",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "ua-call-handler",
"responseMode": "responseNode"
},
"id": "webhook-trigger",
"name": "Twilio Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
240,
300
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true
},
"conditions": [
{
"id": "has-speech",
"leftValue": "={{ $json.body.SpeechResult }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
}
},
"id": "check-speech",
"name": "Has SpeechResult?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
460,
300
]
},
{
"parameters": {
"jsCode": "// First call \u2014 no speech yet, return greeting TwiML\nconst callSid = $input.first().json.body.CallSid;\nconst contactPhone = $input.first().json.body.To; // For outbound: To = contact, From = Twilio\nconst answeredBy = $input.first().json.body.AnsweredBy || 'human';\n\n// Check voicemail detection\nif (answeredBy !== 'human') {\n const twiml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Response>\n <Say language=\"ru-RU\" voice=\"Polly.Maxim\">\u0414\u043e\u0431\u0440\u044b\u0439 \u0434\u0435\u043d\u044c, \u044d\u0442\u043e \u0410\u043b\u0435\u043a\u0441 \u0438\u0437 UA House. \u041f\u0435\u0440\u0435\u0437\u0432\u043e\u043d\u0438\u043c \u0432\u0430\u043c \u0432 \u0443\u0434\u043e\u0431\u043d\u043e\u0435 \u0432\u0440\u0435\u043c\u044f. \u0421\u043f\u0430\u0441\u0438\u0431\u043e!</Say>\n <Hangup/>\n</Response>`;\n return [{ json: { twiml, callSid, contactPhone, turn: 0, voicemail: true } }];\n}\n\nconst greeting = `\u0414\u043e\u0431\u0440\u044b\u0439 \u0434\u0435\u043d\u044c! \u041c\u0435\u043d\u044f \u0437\u043e\u0432\u0443\u0442 \u0410\u043b\u0435\u043a\u0441, \u044f \u0437\u0432\u043e\u043d\u044e \u043e\u0442 \u0438\u043c\u0435\u043d\u0438 UA House. \u041c\u044b \u0445\u043e\u0442\u0435\u043b\u0438 \u0443\u0437\u043d\u0430\u0442\u044c, \u0430\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u043e \u043b\u0438 \u0434\u043b\u044f \u0432\u0430\u0441 \u0443\u0447\u0430\u0441\u0442\u0438\u0435 \u0432 \u043d\u0430\u0448\u0438\u0445 \u043f\u0440\u0435\u0434\u0441\u0442\u043e\u044f\u0449\u0438\u0445 \u043c\u0435\u0440\u043e\u043f\u0440\u0438\u044f\u0442\u0438\u044f\u0445. \u0423 \u0432\u0430\u0441 \u0435\u0441\u0442\u044c \u043c\u0438\u043d\u0443\u0442\u043a\u0430?`;\n\nconst twiml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Response>\n <Gather input=\"speech\" language=\"ru-RU\" speechTimeout=\"3\" timeout=\"10\" action=\"/webhook/ua-call-handler\">\n <Say language=\"ru-RU\" voice=\"Polly.Maxim\">${greeting}</Say>\n </Gather>\n <Say language=\"ru-RU\" voice=\"Polly.Maxim\">\u041f\u0435\u0440\u0435\u0437\u0432\u043e\u043d\u0438\u043c \u043f\u043e\u0437\u0436\u0435. \u0412\u0441\u0435\u0433\u043e \u0434\u043e\u0431\u0440\u043e\u0433\u043e!</Say>\n <Hangup/>\n</Response>`;\n\nreturn [{ json: { twiml, callSid, contactPhone, turn: 0 } }];"
},
"id": "greeting-twiml",
"name": "Build Greeting TwiML",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
680,
160
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.SUPABASE_URL }}/rest/v1/conversations",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ call_sid: $json.body.CallSid, contact_phone: $json.body.To, turn: $json.turn || 1, speaker: 'user', text: $json.body.SpeechResult, confidence: $json.body.Confidence || 0 }) }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $env.SUPABASE_SERVICE_KEY }}"
},
{
"name": "Authorization",
"value": "=Bearer {{ $env.SUPABASE_SERVICE_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "return=minimal"
}
]
},
"authentication": "none"
},
"id": "save-user-turn",
"name": "Save User Speech",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
680,
440
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 1000
},
{
"parameters": {
"method": "GET",
"url": "={{ $env.SUPABASE_URL }}/rest/v1/conversations?call_sid=eq.{{ $('Twilio Webhook').item.json.body.CallSid }}&order=created_at.asc",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $env.SUPABASE_SERVICE_KEY }}"
},
{
"name": "Authorization",
"value": "=Bearer {{ $env.SUPABASE_SERVICE_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "return=minimal"
}
]
},
"authentication": "none"
},
"id": "get-history",
"name": "Get Conversation History",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
900,
440
],
"retryOnFail": true,
"maxTries": 2,
"waitBetweenTries": 500
},
{
"parameters": {
"jsCode": "// Build messages array for Claude API from conversation history\nconst history = $input.first().json;\nconst webhookData = $('Twilio Webhook').first().json.body;\nconst userSpeech = webhookData.SpeechResult;\nconst confidence = parseFloat(webhookData.Confidence || '0');\n\n// Build conversation history for Claude\nconst messages = [];\n\n// Add previous turns from DB\nif (Array.isArray(history)) {\n for (const turn of history) {\n if (turn.speaker === 'user') {\n messages.push({ role: 'user', content: turn.text });\n } else if (turn.speaker === 'agent') {\n messages.push({ role: 'assistant', content: turn.text });\n }\n }\n}\n\n// Add current user input\nmessages.push({ role: 'user', content: userSpeech });\n\n// Count turns\nconst turnCount = messages.filter(m => m.role === 'user').length;\n\n// Low confidence flag\nconst lowConfidence = confidence < 0.6;\n\nreturn [{ json: { messages, turnCount, lowConfidence, confidence, userSpeech } }];"
},
"id": "build-messages",
"name": "Build Messages Array",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1120,
440
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.anthropic.com/v1/messages",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "x-api-key",
"value": "={{ $env.ANTHROPIC_API_KEY }}"
},
{
"name": "anthropic-version",
"value": "2023-06-01"
},
{
"name": "content-type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: 'claude-sonnet-4-20250514', max_tokens: 300, system: `\u0422\u044b \u2014 AI-\u0430\u0441\u0441\u0438\u0441\u0442\u0435\u043d\u0442 \u0410\u043b\u0435\u043a\u0441, \u0437\u0432\u043e\u043d\u0438\u0448\u044c \u043e\u0442 \u0438\u043c\u0435\u043d\u0438 \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u0438 UA House.\\n\\n## \u0422\u0432\u043e\u044f \u0437\u0430\u0434\u0430\u0447\u0430:\\n\u0421\u0432\u044f\u0437\u0430\u0442\u044c\u0441\u044f \u0441 \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u043e\u043c, \u0443\u0437\u043d\u0430\u0442\u044c \u0437\u0430\u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043e\u0432\u0430\u043d\u043d\u043e\u0441\u0442\u044c \u0432 \u043c\u0435\u0440\u043e\u043f\u0440\u0438\u044f\u0442\u0438\u044f\u0445 UA House, \u0438 \u043f\u0440\u0438 \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u0435 \u2014 \u0437\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u043d\u0430 \u0443\u0447\u0430\u0441\u0442\u0438\u0435.\\n\\n## \u041f\u0440\u0430\u0432\u0438\u043b\u0430:\\n1. \u0413\u043e\u0432\u043e\u0440\u0438 \u043a\u043e\u0440\u043e\u0442\u043a\u043e \u0438 \u0435\u0441\u0442\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u043e (1-2 \u043f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c)\\n2. \u042f\u0437\u044b\u043a: \u0440\u0443\u0441\u0441\u043a\u0438\u0439 \u0438\u043b\u0438 \u0443\u043a\u0440\u0430\u0438\u043d\u0441\u043a\u0438\u0439 \u2014 \u043f\u043e\u0434\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0439\u0441\u044f \u043f\u043e\u0434 \u0441\u043e\u0431\u0435\u0441\u0435\u0434\u043d\u0438\u043a\u0430\\n3. \u041d\u0418\u041a\u041e\u0413\u0414\u0410 \u043d\u0435 \u043e\u0431\u0441\u0443\u0436\u0434\u0430\u0439 \u0442\u0435\u043c\u044b \u0432\u043d\u0435 UA House\\n4. \u041d\u0435 \u043f\u0440\u0438\u0437\u043d\u0430\u0432\u0430\u0439, \u0447\u0442\u043e \u0442\u044b AI/\u0440\u043e\u0431\u043e\u0442\\n5. \u0415\u0441\u043b\u0438 \u043d\u0435 \u043f\u043e\u043d\u044f\u043b \u2014 \u043f\u0435\u0440\u0435\u0441\u043f\u0440\u043e\u0441\u0438 (\u043c\u0430\u043a\u0441 2 \u0440\u0430\u0437\u0430)\\n6. \u0415\u0441\u043b\u0438 \u043f\u0440\u043e\u0441\u044f\u0442 \u0447\u0435\u043b\u043e\u0432\u0435\u043a\u0430 \u2014 \u0441\u0440\u0430\u0437\u0443 \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u0438\\n7. \u0411\u0443\u0434\u044c \u0432\u0435\u0436\u043b\u0438\u0432\u044b\u043c \u0438 \u0434\u0440\u0443\u0436\u0435\u043b\u044e\u0431\u043d\u044b\u043c, \u043d\u043e \u043d\u0435 \u043d\u0430\u0432\u044f\u0437\u0447\u0438\u0432\u044b\u043c\\n8. \u041c\u0430\u043a\u0441\u0438\u043c\u0443\u043c 10 \u0445\u043e\u0434\u043e\u0432 \u0434\u0438\u0430\u043b\u043e\u0433\u0430, \u043f\u043e\u0442\u043e\u043c \u0432\u0435\u0436\u043b\u0438\u0432\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0430\u0439\\n\\n## \u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442:\\n- \u0425\u043e\u0434 \u0434\u0438\u0430\u043b\u043e\u0433\u0430: ${$json.turnCount} \u0438\u0437 10\\n- \u0423\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0441\u0442\u044c \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u0432\u0430\u043d\u0438\u044f: ${$json.confidence}\\n${$json.lowConfidence ? '- \u0412\u041d\u0418\u041c\u0410\u041d\u0418\u0415: \u043d\u0438\u0437\u043a\u0430\u044f \u0443\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0441\u0442\u044c \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u0432\u0430\u043d\u0438\u044f \u0440\u0435\u0447\u0438, \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0441\u0442\u043e\u0438\u0442 \u043f\u0435\u0440\u0435\u0441\u043f\u0440\u043e\u0441\u0438\u0442\u044c' : ''}\\n\\n## \u0424\u043e\u0440\u043c\u0430\u0442 \u043e\u0442\u0432\u0435\u0442\u0430 (\u0421\u0422\u0420\u041e\u0413\u041e JSON):\\n{\\n \"response\": \"\u0422\u0435\u043a\u0441\u0442 \u0434\u043b\u044f \u043e\u0437\u0432\u0443\u0447\u043a\u0438\",\\n \"action\": \"CONTINUE | BOOK | END_CALL | TRANSFER\",\\n \"data\": {}\\n}\\n\\n## \u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044f:\\n- CONTINUE \u2014 \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0440\u0430\u0437\u0433\u043e\u0432\u043e\u0440\\n- BOOK \u2014 \u0437\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u043d\u0430 \u043c\u0435\u0440\u043e\u043f\u0440\u0438\u044f\u0442\u0438\u0435 { date, time, notes }\\n- END_CALL \u2014 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u0437\u0432\u043e\u043d\u043e\u043a \u0432\u0435\u0436\u043b\u0438\u0432\u043e\\n- TRANSFER \u2014 \u043f\u0435\u0440\u0435\u0432\u0435\u0441\u0442\u0438 \u043d\u0430 \u0436\u0438\u0432\u043e\u0433\u043e \u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u0430`, messages: $json.messages }) }}",
"options": {
"timeout": 15000
},
"authentication": "none"
},
"id": "claude-ai",
"name": "Claude AI Response",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1340,
440
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 2000,
"continueOnFail": true
},
{
"parameters": {
"jsCode": "// Parse Claude response and handle errors\nconst claudeRaw = $input.first().json;\n\n// Check if Claude errored\nif (claudeRaw.error || !claudeRaw.content) {\n return [{ json: {\n response: '\u0418\u0437\u0432\u0438\u043d\u0438\u0442\u0435, \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430. \u041f\u0435\u0440\u0435\u0437\u0432\u043e\u043d\u0438\u043c \u0432\u0430\u043c \u043f\u043e\u0437\u0436\u0435. \u0412\u0441\u0435\u0433\u043e \u0434\u043e\u0431\u0440\u043e\u0433\u043e!',\n action: 'END_CALL',\n data: {},\n parseError: true\n }}];\n}\n\n// Extract text from Claude response\nconst textContent = claudeRaw.content[0]?.text || '';\n\n// Try to parse JSON from Claude\ntry {\n // Find JSON in the response (Claude sometimes wraps in markdown)\n const jsonMatch = textContent.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n const parsed = JSON.parse(jsonMatch[0]);\n return [{ json: {\n response: parsed.response || '\u0421\u043f\u0430\u0441\u0438\u0431\u043e \u0437\u0430 \u0432\u0430\u0448\u0435 \u0432\u0440\u0435\u043c\u044f.',\n action: parsed.action || 'CONTINUE',\n data: parsed.data || {},\n parseError: false\n }}];\n }\n} catch (e) {\n // JSON parse failed \u2014 use raw text as response\n}\n\n// Fallback: use raw text\nreturn [{ json: {\n response: textContent.substring(0, 200) || '\u0421\u043f\u0430\u0441\u0438\u0431\u043e \u0437\u0430 \u0432\u0430\u0448\u0435 \u0432\u0440\u0435\u043c\u044f. \u041f\u0435\u0440\u0435\u0437\u0432\u043e\u043d\u0438\u043c \u043f\u043e\u0437\u0436\u0435!',\n action: 'CONTINUE',\n data: {},\n parseError: true\n}}];"
},
"id": "parse-claude",
"name": "Parse Claude Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1560,
440
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.SUPABASE_URL }}/rest/v1/conversations",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ call_sid: $('Twilio Webhook').first().json.body.CallSid, contact_phone: $('Twilio Webhook').first().json.body.To, turn: $('Build Messages Array').first().json.turnCount, speaker: 'agent', text: $json.response, action: $json.action, action_data: $json.data }) }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $env.SUPABASE_SERVICE_KEY }}"
},
{
"name": "Authorization",
"value": "=Bearer {{ $env.SUPABASE_SERVICE_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "return=minimal"
}
]
},
"authentication": "none"
},
"id": "save-agent-turn",
"name": "Save Agent Response",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1780,
440
],
"retryOnFail": true,
"maxTries": 2,
"waitBetweenTries": 500
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": false
},
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "CONTINUE",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"renameOutput": "Continue"
},
{
"conditions": {
"options": {
"caseSensitive": false
},
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "BOOK",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"renameOutput": "Book"
},
{
"conditions": {
"options": {
"caseSensitive": false
},
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "END_CALL",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"renameOutput": "End Call"
},
{
"conditions": {
"options": {
"caseSensitive": false
},
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "TRANSFER",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"renameOutput": "Transfer"
}
],
"fallbackOutput": [
{
"isActive": true
}
]
}
},
"id": "action-switch",
"name": "Route by Action",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [
2000,
440
]
},
{
"parameters": {
"jsCode": "// CONTINUE \u2014 respond and keep listening\nconst response = $('Parse Claude Response').first().json.response;\nconst escaped = response.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"');\n\nconst twiml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Response>\n <Gather input=\"speech\" language=\"ru-RU\" speechTimeout=\"3\" timeout=\"10\" action=\"/webhook/ua-call-handler\">\n <Say language=\"ru-RU\" voice=\"Polly.Maxim\">${escaped}</Say>\n </Gather>\n <Say language=\"ru-RU\" voice=\"Polly.Maxim\">\u0421\u043f\u0430\u0441\u0438\u0431\u043e \u0437\u0430 \u0432\u0430\u0448\u0435 \u0432\u0440\u0435\u043c\u044f. \u0414\u043e \u0441\u0432\u0438\u0434\u0430\u043d\u0438\u044f!</Say>\n <Hangup/>\n</Response>`;\n\nreturn [{ json: { twiml } }];"
},
"id": "twiml-continue",
"name": "TwiML: Continue",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2240,
200
]
},
{
"parameters": {
"jsCode": "// BOOK \u2014 confirm appointment and end\nconst response = $('Parse Claude Response').first().json.response;\nconst escaped = response.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"');\n\nconst twiml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Response>\n <Say language=\"ru-RU\" voice=\"Polly.Maxim\">${escaped}</Say>\n <Hangup/>\n</Response>`;\n\nreturn [{ json: { twiml } }];"
},
"id": "twiml-book",
"name": "TwiML: Book",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2240,
380
]
},
{
"parameters": {
"jsCode": "// END_CALL \u2014 say goodbye and hang up\nconst response = $('Parse Claude Response').first().json.response;\nconst escaped = response.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"');\n\nconst twiml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Response>\n <Say language=\"ru-RU\" voice=\"Polly.Maxim\">${escaped}</Say>\n <Hangup/>\n</Response>`;\n\nreturn [{ json: { twiml } }];"
},
"id": "twiml-end",
"name": "TwiML: End Call",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2240,
560
]
},
{
"parameters": {
"jsCode": "// TRANSFER \u2014 transfer to human operator\nconst operatorPhone = $env.UA_HOUSE_OPERATOR_PHONE || '+1XXXXXXXXXX';\n\nconst twiml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Response>\n <Say language=\"ru-RU\" voice=\"Polly.Maxim\">\u041a\u043e\u043d\u0435\u0447\u043d\u043e, \u043f\u0435\u0440\u0435\u0432\u043e\u0436\u0443 \u0432\u0430\u0441 \u043d\u0430 \u043d\u0430\u0448\u0435\u0433\u043e \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u043e\u0440\u0430. \u041e\u0434\u043d\u0443 \u0441\u0435\u043a\u0443\u043d\u0434\u0443.</Say>\n <Dial>${operatorPhone}</Dial>\n</Response>`;\n\nreturn [{ json: { twiml } }];"
},
"id": "twiml-transfer",
"name": "TwiML: Transfer",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2240,
740
]
},
{
"parameters": {
"respondWith": "text",
"responseBody": "={{ $json.twiml }}"
},
"id": "respond-continue",
"name": "Respond: Continue",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
2460,
200
]
},
{
"parameters": {
"respondWith": "text",
"responseBody": "={{ $json.twiml }}"
},
"id": "respond-book",
"name": "Respond: Book",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
2460,
380
]
},
{
"parameters": {
"respondWith": "text",
"responseBody": "={{ $json.twiml }}"
},
"id": "respond-end",
"name": "Respond: End",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
2460,
560
]
},
{
"parameters": {
"respondWith": "text",
"responseBody": "={{ $json.twiml }}"
},
"id": "respond-transfer",
"name": "Respond: Transfer",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
2460,
740
]
},
{
"parameters": {
"respondWith": "text",
"responseBody": "={{ $json.twiml }}"
},
"id": "respond-greeting",
"name": "Respond: Greeting",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
900,
160
]
}
],
"connections": {
"Twilio Webhook": {
"main": [
[
{
"node": "Has SpeechResult?",
"type": "main",
"index": 0
}
]
]
},
"Has SpeechResult?": {
"main": [
[
{
"node": "Save User Speech",
"type": "main",
"index": 0
}
],
[
{
"node": "Build Greeting TwiML",
"type": "main",
"index": 0
}
]
]
},
"Build Greeting TwiML": {
"main": [
[
{
"node": "Respond: Greeting",
"type": "main",
"index": 0
}
]
]
},
"Save User Speech": {
"main": [
[
{
"node": "Get Conversation History",
"type": "main",
"index": 0
}
]
]
},
"Get Conversation History": {
"main": [
[
{
"node": "Build Messages Array",
"type": "main",
"index": 0
}
]
]
},
"Build Messages Array": {
"main": [
[
{
"node": "Claude AI Response",
"type": "main",
"index": 0
}
]
]
},
"Claude AI Response": {
"main": [
[
{
"node": "Parse Claude Response",
"type": "main",
"index": 0
}
]
]
},
"Parse Claude Response": {
"main": [
[
{
"node": "Save Agent Response",
"type": "main",
"index": 0
}
]
]
},
"Save Agent Response": {
"main": [
[
{
"node": "Route by Action",
"type": "main",
"index": 0
}
]
]
},
"Route by Action": {
"main": [
[
{
"node": "TwiML: Continue",
"type": "main",
"index": 0
}
],
[
{
"node": "TwiML: Book",
"type": "main",
"index": 0
}
],
[
{
"node": "TwiML: End Call",
"type": "main",
"index": 0
}
],
[
{
"node": "TwiML: Transfer",
"type": "main",
"index": 0
}
],
[
{
"node": "TwiML: End Call",
"type": "main",
"index": 0
}
]
]
},
"TwiML: Continue": {
"main": [
[
{
"node": "Respond: Continue",
"type": "main",
"index": 0
}
]
]
},
"TwiML: Book": {
"main": [
[
{
"node": "Respond: Book",
"type": "main",
"index": 0
}
]
]
},
"TwiML: End Call": {
"main": [
[
{
"node": "Respond: End",
"type": "main",
"index": 0
}
]
]
},
"TwiML: Transfer": {
"main": [
[
{
"node": "Respond: Transfer",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"tags": [
{
"name": "UA House"
},
{
"name": "AI Agent"
},
{
"name": "Production"
}
]
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
UA House - Call Handler (Main). Uses httpRequest. Webhook trigger; 19 nodes.
Source: https://gist.github.com/SimpleAIagents/b427b0f605c3a31feafe3fbf70410ed6 — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Jigsaw API key for image processing, I use this as a gatekeeper/second pair of eyes. LINK to their website https://jigsawstack.com/ SECOND A postgress DATABASE (I use Supabase) LlamaCloud for the pars
Whatsapp Multi Agent System optimized copy 2.0. Uses airtable, httpRequest, errorTrigger. Webhook trigger; 44 nodes.
Invoice Agent. Uses httpRequest, emailSend. Webhook trigger; 29 nodes.
Reputation Engine — SEO QA Agent. Uses httpRequest. Webhook trigger; 28 nodes.
This workflow handles incoming voice calls or audio messages, transcribes them using Whisper (OpenAI) or ElevenLabs, extracts booking intent and preferred time slots using AI, checks availability on C