This workflow corresponds to n8n.io template #15990 — we link there as the canonical source.
This workflow follows the Gmail → Google Sheets recipe pattern — see all workflows that pair these two integrations.
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 →
{
"meta": {
"templateCredsSetupCompleted": false
},
"name": "AI Inbound Call Logger with Twilio, Whisper, and Claude",
"tags": [],
"nodes": [
{
"id": "2d90c944-9397-486b-a113-5430bb9b5882",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-368,
48
],
"parameters": {
"width": 480,
"height": 896,
"content": "## AI Inbound Call Logger with Twilio, Whisper, and Claude\n\n### How it works\n\nThis workflow receives a Twilio call-recording webhook, extracts the call metadata, downloads the recording, and transcribes it with OpenAI Whisper. It sends the transcript to Claude to infer the caller\u2019s intent and urgency, then logs the enriched call details to Google Sheets. If the call is urgent, it sends an email alert before returning a TwiML/webhook response.\n\n### Setup steps\n\n- Configure Twilio to send completed call recording webhooks to the n8n webhook URL used by the \"When Call Recorded\" node.\n- Add credentials or headers for downloading Twilio recordings if the recording URL requires authentication.\n- Configure OpenAI API credentials for the Whisper transcription HTTP request.\n- Configure Anthropic API credentials for the Claude message request.\n- Connect Google Sheets credentials and select the destination spreadsheet/sheet for call logging.\n- Connect Gmail credentials and set the recipient, subject, and body for urgent call alerts.\n- Verify the final webhook response returns the TwiML or acknowledgement expected by Twilio.\n\n### Customization\n\nAdjust the fields extracted from the Twilio webhook, the Claude prompt and urgency criteria, the Google Sheets column mapping, and the Gmail alert recipients to match the business process."
},
"typeVersion": 1
},
{
"id": "aebe3155-f1f6-41e1-aa6f-4f25299ada94",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
192,
144
],
"parameters": {
"color": 7,
"width": 416,
"height": 336,
"content": "## Receive call webhook\n\nStarts the workflow when Twilio reports a recorded call and normalizes the incoming call metadata, including call SID, caller, recipient, and recording URL."
},
"typeVersion": 1
},
{
"id": "9523aa72-e570-48d0-8107-033a25596276",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
640,
160
],
"parameters": {
"color": 7,
"width": 416,
"height": 320,
"content": "## Fetch and transcribe recording\n\nDownloads the MP3 recording from the extracted URL and sends the audio to Whisper to produce a call transcript."
},
"typeVersion": 1
},
{
"id": "c46d70eb-7a67-42eb-9163-872f0513f6b8",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
1104,
144
],
"parameters": {
"color": 7,
"width": 416,
"height": 336,
"content": "## Analyze call intent\n\nUses Claude to analyze the transcript for intent and urgency, then parses and merges the AI analysis with the original call metadata and transcript."
},
"typeVersion": 1
},
{
"id": "31c6471f-d70c-44af-918d-8dd203f7188d",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
1568,
160
],
"parameters": {
"color": 7,
"width": 416,
"height": 320,
"content": "## Log and evaluate call\n\nAppends the analyzed call information to Google Sheets and checks whether the result should be treated as urgent."
},
"typeVersion": 1
},
{
"id": "850143c3-657c-49a1-a51d-4070f7d00e3e",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
2016,
48
],
"parameters": {
"color": 7,
"width": 416,
"height": 432,
"content": "## Alert and respond\n\nSends a Gmail notification for urgent calls and completes the webhook interaction with a TwiML or acknowledgement response."
},
"typeVersion": 1
},
{
"id": "a1b2c3d4-0003-4000-8000-000000000006",
"name": "Receive Call Recording",
"type": "n8n-nodes-base.webhook",
"position": [
240,
320
],
"parameters": {
"path": "twilio-recording",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "a1b2c3d4-0003-4000-8000-000000000007",
"name": "Set Call Metadata",
"type": "n8n-nodes-base.set",
"position": [
460,
320
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "c1-callsid",
"name": "callSid",
"type": "string",
"value": "={{ $json.body?.CallSid || $json.CallSid || '' }}"
},
{
"id": "c2-from",
"name": "from",
"type": "string",
"value": "={{ $json.body?.From || $json.From || '' }}"
},
{
"id": "c3-to",
"name": "to",
"type": "string",
"value": "={{ $json.body?.To || $json.To || '' }}"
},
{
"id": "c4-recurl",
"name": "recordingUrl",
"type": "string",
"value": "={{ $json.body?.RecordingUrl || $json.RecordingUrl || '' }}"
},
{
"id": "c5-recsid",
"name": "recordingSid",
"type": "string",
"value": "={{ $json.body?.RecordingSid || $json.RecordingSid || '' }}"
},
{
"id": "c6-duration",
"name": "duration",
"type": "string",
"value": "={{ ($json.body?.RecordingDuration || $json.RecordingDuration || '0').toString() }}"
},
{
"id": "c7-timestamp",
"name": "timestamp",
"type": "string",
"value": "={{ $now.toISO() }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "a1b2c3d4-0003-4000-8000-000000000008",
"name": "Download Call Recording",
"type": "n8n-nodes-base.httpRequest",
"position": [
680,
320
],
"parameters": {
"url": "={{ $json.recordingUrl }}.mp3",
"method": "GET",
"options": {
"response": {
"response": {
"responseFormat": "file",
"outputPropertyName": "data"
}
}
},
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth"
},
"typeVersion": 4.2
},
{
"id": "a1b2c3d4-0003-4000-8000-000000000009",
"name": "Transcribe Audio with Whisper",
"type": "n8n-nodes-base.httpRequest",
"position": [
900,
320
],
"parameters": {
"url": "https://api.openai.com/v1/audio/transcriptions",
"method": "POST",
"options": {},
"sendBody": true,
"contentType": "multipart-form-data",
"authentication": "genericCredentialType",
"bodyParameters": {
"parameters": [
{
"name": "file",
"parameterType": "formBinaryData",
"inputDataFieldName": "data"
},
{
"name": "model",
"value": "whisper-1"
}
]
},
"genericAuthType": "httpHeaderAuth"
},
"typeVersion": 4.2
},
{
"id": "a1b2c3d4-0003-4000-8000-000000000010",
"name": "Analyze Intent with AI",
"type": "n8n-nodes-base.httpRequest",
"position": [
1152,
320
],
"parameters": {
"url": "https://api.anthropic.com/v1/messages",
"method": "POST",
"options": {},
"jsonBody": "={\n \"model\": \"claude-sonnet-4-6\",\n \"max_tokens\": 1024,\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"You analyze inbound business phone call transcripts and extract structured information. Reply with valid JSON only, no markdown fences, no commentary. Use this exact schema:\\n\\n{\\n \\\"caller_name\\\": \\\"<name if stated, empty string otherwise>\\\",\\n \\\"intent\\\": \\\"new_customer|existing_customer|complaint|appointment|sales_pitch|wrong_number|spam|other\\\",\\n \\\"summary\\\": \\\"<one to two sentence summary of the call>\\\",\\n \\\"service_or_topic\\\": \\\"<what they want, empty string if unclear>\\\",\\n \\\"urgency\\\": \\\"high|medium|low\\\",\\n \\\"callback_needed\\\": <true or false>,\\n \\\"sentiment\\\": \\\"positive|neutral|negative|hostile\\\"\\n}\\n\\nUrgency rules:\\n- high: emergency, escalation, time sensitive (today or now), angry customer, lost opportunity if not called back fast\\n- medium: standard inquiry, scheduled callback expected\\n- low: informational, voicemail with no clear need, spam, wrong number\\n\\nCallback rules: callback_needed is true unless the caller explicitly said no callback is needed or the call is spam or wrong number.\\n\\nCALL METADATA:\\nFrom: \" + {{ JSON.stringify($('Set Call Metadata').item.json.from) }} + \"\\nTo: \" + {{ JSON.stringify($('Set Call Metadata').item.json.to) }} + \"\\nDuration in seconds: \" + {{ JSON.stringify($('Set Call Metadata').item.json.duration) }} + \"\\n\\nTRANSCRIPT:\\n\" + {{ JSON.stringify($json.text) }}\n }\n ]\n}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"headerParameters": {
"parameters": [
{
"name": "anthropic-version",
"value": "2023-06-01"
},
{
"name": "content-type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "a1b2c3d4-0003-4000-8000-000000000011",
"name": "Merge Analysis with Metadata",
"type": "n8n-nodes-base.code",
"position": [
1376,
320
],
"parameters": {
"jsCode": "// Merge the Claude analysis with the original call metadata and transcript\nconst call = $('Set Call Metadata').item.json;\nconst transcript = $('Transcribe Audio with Whisper').item.json.text || '';\nconst raw = $json.content?.[0]?.text || '{}';\n\nlet parsed = {\n caller_name: '',\n intent: 'other',\n summary: 'Failed to parse AI response',\n service_or_topic: '',\n urgency: 'low',\n callback_needed: false,\n sentiment: 'neutral'\n};\n\ntry {\n const cleaned = raw.replace(/^```(?:json)?\\s*/i, '').replace(/\\s*```\\s*$/i, '').trim();\n parsed = JSON.parse(cleaned);\n} catch (e) {\n parsed.summary = 'AI returned malformed JSON: ' + raw.slice(0, 200);\n}\n\nconst isUrgent = parsed.urgency === 'high' || parsed.intent === 'complaint';\n\nreturn [{\n json: {\n ...call,\n transcript,\n caller_name: parsed.caller_name || '',\n intent: parsed.intent,\n summary: parsed.summary,\n service_or_topic: parsed.service_or_topic || '',\n urgency: parsed.urgency,\n callback_needed: parsed.callback_needed === true,\n sentiment: parsed.sentiment,\n isUrgent\n }\n}];"
},
"typeVersion": 2
},
{
"id": "a1b2c3d4-0003-4000-8000-000000000012",
"name": "Append Call Data to Sheets",
"type": "n8n-nodes-base.googleSheets",
"position": [
1620,
320
],
"parameters": {
"columns": {
"value": {
"to": "={{ $json.to }}",
"from": "={{ $json.from }}",
"intent": "={{ $json.intent }}",
"summary": "={{ $json.summary }}",
"urgency": "={{ $json.urgency }}",
"duration": "={{ $json.duration }}",
"sentiment": "={{ $json.sentiment }}",
"timestamp": "={{ $json.timestamp }}",
"transcript": "={{ $json.transcript }}",
"caller_name": "={{ $json.caller_name }}",
"callback_needed": "={{ $json.callback_needed ? 'yes' : 'no' }}",
"service_or_topic": "={{ $json.service_or_topic }}"
},
"schema": [
{
"id": "timestamp",
"type": "string",
"display": true,
"required": false,
"displayName": "timestamp",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "from",
"type": "string",
"display": true,
"required": false,
"displayName": "from",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "to",
"type": "string",
"display": true,
"required": false,
"displayName": "to",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "duration",
"type": "string",
"display": true,
"required": false,
"displayName": "duration",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "caller_name",
"type": "string",
"display": true,
"required": false,
"displayName": "caller_name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "intent",
"type": "string",
"display": true,
"required": false,
"displayName": "intent",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "urgency",
"type": "string",
"display": true,
"required": false,
"displayName": "urgency",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "callback_needed",
"type": "string",
"display": true,
"required": false,
"displayName": "callback_needed",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "sentiment",
"type": "string",
"display": true,
"required": false,
"displayName": "sentiment",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "service_or_topic",
"type": "string",
"display": true,
"required": false,
"displayName": "service_or_topic",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "summary",
"type": "string",
"display": true,
"required": false,
"displayName": "summary",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "transcript",
"type": "string",
"display": true,
"required": false,
"displayName": "transcript",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": []
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "Inbound Calls"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "REPLACE_WITH_YOUR_SHEET_ID"
}
},
"typeVersion": 4.5
},
{
"id": "a1b2c3d4-0003-4000-8000-000000000013",
"name": "Evaluate Urgent Call Status",
"type": "n8n-nodes-base.if",
"position": [
1840,
320
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "cond-urgent",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.isUrgent }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.2
},
{
"id": "a1b2c3d4-0003-4000-8000-000000000014",
"name": "Dispatch Urgent Email Alert",
"type": "n8n-nodes-base.gmail",
"position": [
2060,
208
],
"parameters": {
"sendTo": "you@example.com",
"message": "=<h2>Urgent call needs your attention</h2>\n<p><b>Intent:</b> {{ $json.intent }}<br>\n<b>Urgency:</b> {{ $json.urgency }}<br>\n<b>Sentiment:</b> {{ $json.sentiment }}<br>\n<b>Callback needed:</b> {{ $json.callback_needed ? 'yes' : 'no' }}</p>\n<p><b>From:</b> {{ $json.from }}<br>\n<b>To:</b> {{ $json.to }}<br>\n<b>Caller name:</b> {{ $json.caller_name || '(not stated)' }}<br>\n<b>Duration:</b> {{ $json.duration }} seconds</p>\n<p><b>What they want:</b> {{ $json.service_or_topic }}</p>\n<p><b>Summary:</b> {{ $json.summary }}</p>\n<hr>\n<p><b>Full transcript:</b></p>\n<blockquote style=\"color:#444;\">{{ $json.transcript }}</blockquote>",
"options": {},
"subject": "=Urgent inbound call: {{ $json.intent }} from {{ $json.from }}",
"emailType": "html"
},
"typeVersion": 2.1
},
{
"id": "a1b2c3d4-0003-4000-8000-000000000015",
"name": "Respond with TwiML Message",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
2280,
320
],
"parameters": {
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/xml"
}
]
}
},
"respondWith": "text",
"responseBody": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response></Response>"
},
"typeVersion": 1.1
}
],
"settings": {
"executionOrder": "v1"
},
"connections": {
"Set Call Metadata": {
"main": [
[
{
"node": "Download Call Recording",
"type": "main",
"index": 0
}
]
]
},
"Analyze Intent with AI": {
"main": [
[
{
"node": "Merge Analysis with Metadata",
"type": "main",
"index": 0
}
]
]
},
"Receive Call Recording": {
"main": [
[
{
"node": "Set Call Metadata",
"type": "main",
"index": 0
}
]
]
},
"Download Call Recording": {
"main": [
[
{
"node": "Transcribe Audio with Whisper",
"type": "main",
"index": 0
}
]
]
},
"Append Call Data to Sheets": {
"main": [
[
{
"node": "Evaluate Urgent Call Status",
"type": "main",
"index": 0
}
]
]
},
"Dispatch Urgent Email Alert": {
"main": [
[
{
"node": "Respond with TwiML Message",
"type": "main",
"index": 0
}
]
]
},
"Evaluate Urgent Call Status": {
"main": [
[
{
"node": "Dispatch Urgent Email Alert",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond with TwiML Message",
"type": "main",
"index": 0
}
]
]
},
"Merge Analysis with Metadata": {
"main": [
[
{
"node": "Append Call Data to Sheets",
"type": "main",
"index": 0
}
]
]
},
"Transcribe Audio with Whisper": {
"main": [
[
{
"node": "Analyze Intent with AI",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow handles Twilio recording-complete webhooks, downloads the call audio, transcribes it with OpenAI Whisper, and uses Anthropic Claude to extract structured call insights. It logs every call to Google Sheets, emails urgent calls via Gmail, and returns an empty TwiML…
Source: https://n8n.io/workflows/15990/ — 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.
Signup Intake → CRM triage. Uses formTrigger, googleSheets, telegram, telegramTrigger. Webhook trigger; 28 nodes.
Automate short-term trading research by generating high-quality trade ideas using MCP (Market Context Protocol) signals and AI-powered analysis. 📈🤖 This workflow evaluates market context, catalysts, m
Automatically extract structured information from emails using AI-powered document analysis. This workflow processes emails from specified domains, classifies them by type, and extracts structured dat
What This Flow Does
Receive booking requests via webhook with automatic validation, duplicate detection, availability checking, confirmation emails, Google Calendar sync, and Slack notifications.