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": "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": []
}
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 →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
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
This flow creates dummy files for every item added in your *Arrs (Radarr/Sonarr) with the tag .
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
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.
📡 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