AutomationFlowsAI & RAG › Ua House - Call Handler (main)

Ua House - Call Handler (main)

UA House - Call Handler (Main). Uses httpRequest. Webhook trigger; 19 nodes.

Webhook trigger★★★★☆ complexity19 nodesHTTP Request
AI & RAG Trigger: Webhook Nodes: 19 Complexity: ★★★★☆ Added:

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 →

Download .json
{
  "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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\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"
    }
  ]
}
Pro

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 →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

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

HTTP Request, Postgres, Stop And Error +2
AI & RAG

Whatsapp Multi Agent System optimized copy 2.0. Uses airtable, httpRequest, errorTrigger. Webhook trigger; 44 nodes.

Airtable, HTTP Request, Error Trigger
AI & RAG

Invoice Agent. Uses httpRequest, emailSend. Webhook trigger; 29 nodes.

HTTP Request, Email Send
AI & RAG

Reputation Engine — SEO QA Agent. Uses httpRequest. Webhook trigger; 28 nodes.

HTTP Request
AI & RAG

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

HTTP Request