AutomationFlowsWeb Scraping › VAPI to Claude AI Chatbot Workflow

VAPI to Claude AI Chatbot Workflow

Original n8n title: My Workflow 20 - Fixed_complete_v3

My workflow 20 - FIXED_COMPLETE_V3. Uses httpRequest. Webhook trigger; 10 nodes.

Webhook trigger★★★★☆ complexity10 nodesHTTP Request
Web Scraping Trigger: Webhook Nodes: 10 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": "My workflow 20 - FIXED_COMPLETE_V3",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "/v1/chat/completions",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "9a0e58cb-1376-40cd-b4ce-65f391cd4bd8",
      "name": "Webhook - VAPI Endpoint",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1.1,
      "position": [
        -1568,
        192
      ]
    },
    {
      "parameters": {
        "jsCode": "// Zaktualizowany kod do ekstrakcji danych z VAPI (obs\u0142uga conversation-update)\n\nconst body = $input.first().json.body || $input.first().json;\n\n// Inicjalizacja zmiennych\nlet messages = [];\nlet userText = '';\nlet callId = 'unknown';\nlet messageType = 'unknown';\n\n// Sprawdzenie, czy mamy struktur\u0119 message (Server Webhook)\nif (body && body.message) {\n  const message = body.message;\n  messageType = message.type || 'unknown';\n\n  // Obs\u0142uga formatu conversation-update\n  if (messageType === 'conversation-update' && message.conversation) {\n    messages = message.conversation;\n\n    // Wyci\u0105gni\u0119cie ostatniej wiadomo\u015bci u\u017cytkownika\n    const userMessages = messages.filter(m => m.role === 'user');\n    if (userMessages.length > 0) {\n      userText = userMessages[userMessages.length - 1].content || '';\n    }\n  }\n\n  // Ekstrakcja Call ID\n  if (message.call && message.call.id) {\n    callId = message.call.id;\n  }\n} \n// Fallback dla innych format\u00f3w (np. testy bezpo\u015brednie lub stary Custom LLM)\nelse if (body && body.messages && Array.isArray(body.messages)) {\n  messageType = 'custom-llm-fallback';\n  messages = body.messages;\n  const userMessages = messages.filter(m => m.role === 'user');\n  if (userMessages.length > 0) {\n    userText = userMessages[userMessages.length - 1].content || '';\n  }\n  callId = (body.call && body.call.id) || `call_${Date.now()}`;\n}\n\n\n// System prompt (bez zmian)\nconst systemPrompt = `Jeste\u015b Guardian Angel - empatycznym wsparciem emocjonalnym. \n\nZASADY:\n1. Odpowiadaj TYLKO po polsku\n2. Maksymalnie 2-3 zdania na odpowied\u017a\n3. B\u0105d\u017a ciep\u0142y, empatyczny i wspieraj\u0105cy\n4. Zadawaj otwarte pytania pomagaj\u0105ce osobie otworzy\u0107 si\u0119\n5. Unikaj dawania rad - raczej pomagaj znale\u017a\u0107 w\u0142asne rozwi\u0105zania\n6. Nie u\u017cywaj znak\u00f3w specjalnych (%, #, @) dla lepszego TTS\n7. Pisz liczby s\u0142ownie\n8. U\u017cywaj kropek dla naturalnych pauz, nie wielokropk\u00f3w`;\n\nreturn {\n  messages: messages,\n  user_text: userText,\n  call_id: callId,\n  system_prompt: systemPrompt,\n  message_type: messageType,\n  has_context: messages.length > 1,\n  message_count: messages.length,\n  timestamp: new Date().toISOString()\n};"
      },
      "id": "69b73786-cc38-40ea-89c7-883c7aa117da",
      "name": "Extract Context from VAPI",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1344,
        192
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{$vars.SUPABASE_URL}}/rest/v1/rpc/upsert_session",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "={{$vars.SUPABASE_SERVICE_ROLE}}"
            },
            {
              "name": "Authorization",
              "value": "Bearer {{$vars.SUPABASE_SERVICE_ROLE}}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Prefer",
              "value": "return=representation"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "{\n  \"p_call_id\": \"{{$node['Extract Context from VAPI'].json.call_id}}\",\n  \"p_channel\": \"voice\"\n}",
        "options": {
          "timeout": 10000
        }
      },
      "id": "90f5a035-be65-4178-8175-6c0e006f23b3",
      "name": "Upsert Session in Supabase",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [
        -1120,
        192
      ],
      "continueOnFail": true
    },
    {
      "parameters": {
        "url": "={{$vars.SUPABASE_URL}}/rest/v1/transcripts",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "session_id",
              "value": "=eq.{{$node['Upsert Session in Supabase'].json[0].id}}"
            },
            {
              "name": "order",
              "value": "timestamp.asc"
            },
            {
              "name": "limit",
              "value": "20"
            }
          ]
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "={{$vars.SUPABASE_SERVICE_ROLE}}"
            },
            {
              "name": "Authorization",
              "value": "Bearer {{$vars.SUPABASE_SERVICE_ROLE}}"
            }
          ]
        },
        "options": {
          "timeout": 5000
        }
      },
      "id": "b1f70c8a-3d8a-4204-ad24-2171829f894e",
      "name": "Load Session History",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [
        -896,
        192
      ],
      "continueOnFail": true
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Build conversation context for Claude with extensive error handling\nconst extractedData = $node['Extract Context from VAPI'].json;\nconst sessionResponse = $node['Upsert Session in Supabase'].json;\nconst historyData = $node['Load Session History'].json || [];\n\n// Handle both single object and array response from Supabase RPC\nlet sessionId = null;\nif (sessionResponse) {\n  if (Array.isArray(sessionResponse) && sessionResponse.length > 0) {\n    sessionId = sessionResponse[0]?.id || null;\n  } else if (typeof sessionResponse === 'object' && sessionResponse !== null) {\n    // Check for nested result structure\n    sessionId = sessionResponse.id || sessionResponse.data?.id || null;\n  }\n}\n\n// Fallback if session ID still not found (e.g. Supabase connection failed)\nif (!sessionId) {\n  console.log('Warning: Session ID not found or Upsert failed, generating temporary one');\n  // U\u017cywamy call_id jako fallback, je\u015bli Supabase zawiedzie\n  sessionId = extractedData.call_id || 'temp_' + Date.now();\n}\n\n// Build messages array with history\nlet conversationMessages = [];\n\n// Add historical messages if they exist\nif (Array.isArray(historyData) && historyData.length > 0) {\n  historyData.forEach(transcript => {\n    if (transcript.text && transcript.speaker) {\n      conversationMessages.push({\n        role: transcript.speaker === 'user' ? 'user' : 'assistant',\n        content: transcript.text\n      });\n    }\n  });\n}\n\n// Add current message from VAPI (if not already in history)\nif (extractedData.user_text) {\n  // Check if this message is already in history (deduplication)\n  const lastHistoryMessage = historyData.length > 0 ? historyData[historyData.length - 1] : null;\n  const isDuplicate = lastHistoryMessage && \n                      lastHistoryMessage.text === extractedData.user_text &&\n                      lastHistoryMessage.speaker === 'user';\n  \n  if (!isDuplicate) {\n    conversationMessages.push({\n      role: 'user',\n      content: extractedData.user_text\n    });\n  }\n}\n\n// Ensure we always have at least one message if user text exists\nif (conversationMessages.length === 0 && extractedData.user_text) {\n  conversationMessages.push({\n    role: 'user',\n    content: extractedData.user_text\n  });\n}\n\n// Limit context to last 20 messages to avoid token limits\nif (conversationMessages.length > 20) {\n  conversationMessages = conversationMessages.slice(-20);\n}\n\nreturn {\n  messages: conversationMessages,\n  session_id: sessionId,\n  call_id: extractedData.call_id,\n  user_text: extractedData.user_text || '',\n  system_prompt: extractedData.system_prompt,\n  has_history: historyData.length > 0,\n  history_count: historyData.length\n};"
      },
      "id": "c01eaf32-7f82-4fc4-ac60-8a4f44141160",
      "name": "Build Claude Context",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -672,
        192
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "x-api-key",
              "value": "={{$vars.ANTHROPIC_API_KEY}}"
            },
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ {\n  \"model\":\"claude-3-5-sonnet-20240620\",\n  \"max_tokens\": 250,\n  \"temperature\": 0.7,\n  \"messages\": $json.messages || [],\n  \"system\": $json.system_prompt || \"\"\n} }}",
        "options": {
          "timeout": "={{ 30000 }}"
        }
      },
      "id": "df752409-c605-4c52-aa07-1b1c549294f9",
      "name": "Call Claude API",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [
        -448,
        192
      ],
      "retryOnFail": false,
      "maxTries": 3,
      "waitBetweenTries": 2000
    },
    {
      "parameters": {
        "jsCode": "// Parse Claude response and prepare for VAPI (OpenAI compatible format)\nconst claudeResponse = $input.first().json;\nconst contextData = $node['Build Claude Context'].json;\n\n// Initialize parsed response with safe defaults\nlet assistantReply = \"Przepraszam, mam chwilowe trudno\u015bci techniczne. Czy mo\u017cesz powt\u00f3rzy\u0107?\";\nlet riskLevel = \"none\";\n\ntry {\n  // Anthropic Messages API returns content blocks; prefer the first text block\n  if (claudeResponse && Array.isArray(claudeResponse.content)) {\n    const textBlock = claudeResponse.content.find(b => b && b.type === 'text');\n    if (textBlock && typeof textBlock.text === 'string' && textBlock.text.trim()) {\n      assistantReply = textBlock.text.trim();\n    }\n  } else if (claudeResponse && claudeResponse.content && claudeResponse.content[0] && claudeResponse.content[0].text) {\n    assistantReply = String(claudeResponse.content[0].text || assistantReply).trim();\n  }\n\n  // Basic risk detection (suicide / self-harm keywords in Polish)\n  const riskKeywords = [\n    'samob\u00f3jstw', 'zabi\u0107 si\u0119', 'sko\u0144czy\u0107 ze sob\u0105',\n    'nie chc\u0119 \u017cy\u0107', 'lepiej b\u0119dzie bez mnie',\n    'skrzywdzi\u0107 si\u0119', 'zrani\u0107 si\u0119'\n  ];\n  const userText = String(contextData.user_text || '').toLowerCase();\n\n  if (userText && riskKeywords.some(keyword => userText.includes(keyword))) {\n    riskLevel = \"high\";\n    assistantReply += \" Je\u015bli my\u015blisz o skrzywdzeniu siebie, prosz\u0119 skontaktuj si\u0119 natychmiast z lokalnymi s\u0142u\u017cbami ratunkowymi lub zaufan\u0105 lini\u0105 wsparcia kryzysowego.\";\n  }\n\n} catch (error) {\n  console.error('Error parsing Claude response:', error);\n}\nconst createdTs = Math.floor(Date.now() / 1000);\n// Ensure the model ID is correctly reflected \nconst modelId = claudeResponse?.model || \"claude-3-5-sonnet-20240620\"; \nconst completionId = (claudeResponse?.id ? `chatcmpl_${claudeResponse.id}` : `chatcmpl_${createdTs}_${Math.random().toString(36).slice(2,8)}`);\n\nconst promptTokens = claudeResponse?.usage?.input_tokens ?? undefined;\nconst completionTokens = claudeResponse?.usage?.output_tokens ?? undefined;\nconst totalTokens = (promptTokens || 0) + (completionTokens || 0);\n\n// VAPI expects OpenAI compatible format when the endpoint is /v1/chat/completions\nconst vapiResponse = {\n  id: completionId,\n  object: \"chat.completion\",\n  created: createdTs,\n  model: modelId,\n  choices: [{\n    index: 0,\n    message: {\n      role: \"assistant\",\n      content: assistantReply\n    },\n    finish_reason: \"stop\"\n  }],\n  usage: {\n    prompt_tokens: promptTokens,\n    completion_tokens: completionTokens,\n    total_tokens: totalTokens || undefined\n  }\n};\nconst validSessionId = contextData.session_id;\n\nreturn {\n  vapi_response: vapiResponse,\n  assistant_text: assistantReply,\n  risk_level: riskLevel,\n  session_id: validSessionId,\n  call_id: contextData.call_id || 'unknown',\n  user_text: contextData.user_text || '',\n  timestamp: new Date().toISOString()\n};"
      },
      "id": "04b90bb1-077c-4375-8e78-f66ae9b302de",
      "name": "Parse Claude Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -224,
        192
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{$vars.SUPABASE_URL}}/rest/v1/transcripts",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "={{$vars.SUPABASE_SERVICE_ROLE}}"
            },
            {
              "name": "Authorization",
              "value": "Bearer {{$vars.SUPABASE_SERVICE_ROLE}}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Prefer",
              "value": "return=minimal"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "{\n  \"session_id\": \"{{$node['Parse Claude Response'].json.session_id}}\",\n  \"speaker\": \"user\",\n  \"text\": \"{{$node['Parse Claude Response'].json.user_text}}\",\n  \"risk_level\": null,\n  \"timestamp\": \"{{$node['Parse Claude Response'].json.timestamp}}\"\n}",
        "options": {
          "timeout": 5000
        }
      },
      "id": "e395097f-dcdb-4115-9fa2-35d84d5971aa",
      "name": "Save User Transcript (Background)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [
        0,
        256
      ],
      "continueOnFail": true,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{$vars.SUPABASE_URL}}/rest/v1/transcripts",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "={{$vars.SUPABASE_SERVICE_ROLE}}"
            },
            {
              "name": "Authorization",
              "value": "Bearer {{$vars.SUPABASE_SERVICE_ROLE}}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Prefer",
              "value": "return=minimal"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "{\n  \"session_id\": \"{{$node['Parse Claude Response'].json.session_id}}\",\n  \"speaker\": \"assistant\",\n  \"text\": \"{{$node['Parse Claude Response'].json.assistant_text}}\",\n  \"risk_level\": \"{{$node['Parse Claude Response'].json.risk_level}}\",\n  \"timestamp\": \"{{$node['Parse Claude Response'].json.timestamp}}\"\n}",
        "options": {
          "timeout": 5000
        }
      },
      "id": "a8e2f5e9-d192-44aa-b6a7-e28178c4d32a",
      "name": "Save Assistant Transcript (Background)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [
        0,
        448
      ],
      "continueOnFail": true,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{$node['Parse Claude Response'].json.vapi_response}}",
        "options": {
          "responseCode": 200,
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          }
        }
      },
      "id": "4296e94a-fe74-4b30-a3d3-a4e25152cff2",
      "name": "Respond to VAPI (Immediate)",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        0,
        64
      ]
    }
  ],
  "connections": {
    "Webhook - VAPI Endpoint": {
      "main": [
        [
          {
            "node": "Extract Context from VAPI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert Session in Supabase": {
      "main": [
        [
          {
            "node": "Load Session History",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Session History": {
      "main": [
        [
          {
            "node": "Build Claude Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Claude Context": {
      "main": [
        [
          {
            "node": "Call Claude API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Claude API": {
      "main": [
        [
          {
            "node": "Parse Claude Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Claude Response": {
      "main": [
        [
          {
            "node": "Save User Transcript (Background)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Save Assistant Transcript (Background)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Respond to VAPI (Immediate)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Context from VAPI": {
      "main": [
        [
          {
            "node": "Upsert Session in Supabase",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "8556e397-74d8-45f4-8230-d41a113f5fca",
  "id": "BfXStvCtRzloo6V4",
  "tags": []
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

My workflow 20 - FIXED_COMPLETE_V3. Uses httpRequest. Webhook trigger; 10 nodes.

Source: https://github.com/BGMLAI/IORS-Master-Project/blob/cdb8eb76cf0dc98d46ab9ab05397e964ec3fe1da/workflows/iors.json — original creator credit. Request a take-down →

More Web Scraping workflows → · Browse all categories →

Related workflows

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

Web Scraping

This n8n template provides enterprise-level version control for your workflows using GitHub integration. Stop losing hours to broken workflows and manual exports – get proper commit history, visual di

n8n, Execute Workflow Trigger, HTTP Request +1
Web Scraping

This flow creates dummy files for every item added in your *Arrs (Radarr/Sonarr) with the tag .

HTTP Request, Ssh
Web Scraping

This workflow acts as a central API gateway for all technical indicator agents in the Binance Spot Market Quant AI system. It listens for incoming webhook requests and dynamically routes them to the c

HTTP Request
Web Scraping

Sign PDF documents with legally-compliant digital signatures using X.509 certificates. Supports multiple PAdES signature levels (B, T, LT, LTA) with optional visible stamps.

Execute Command, HTTP Request, Read Write File +1
Web Scraping

📡 This workflow serves as the central Alpha Vantage API fetcher for Tesla trading indicators, delivering cleaned 20-point JSON outputs for three timeframes: , , and . It is required by the following a

HTTP Request