This workflow follows the Airtable → HTTP Request 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 →
{
"nodes": [
{
"parameters": {
"content": "## When a call wraps up\n- Vapi sends over the call report. The caller goes into GoHighLevel with the AI summary attached.\n- Message taken or missed? Dana gets a text. Booked or handed off? It just goes in the log.",
"height": 200,
"width": 440
},
"id": "sticky-section-a",
"name": "When A Call Wraps Up",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
620,
-40
]
},
{
"parameters": {
"content": "## Booking while still on the phone\n- Mid-call, the Vapi agent hits this endpoint the moment a caller wants an estimate visit.\n- The appointment lands on the GoHighLevel calendar and the agent reads the confirmation back to the caller.",
"height": 200,
"width": 460
},
"id": "sticky-section-b",
"name": "Booking While Still On The Phone",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
0,
620
]
},
{
"parameters": {
"httpMethod": "POST",
"path": "demo/vapi-end-of-call",
"responseMode": "responseNode",
"options": {}
},
"id": "webhook-end-of-call",
"name": "A customer call just ended",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
260
]
},
{
"parameters": {
"jsCode": "// Reads Vapi's end-of-call report. Also copes with flat payloads\n// and Zapier-forwarded versions where the nesting got squashed.\nconst body = $json.body ?? $json;\nconst msg = body.message ?? body;\nconst analysis = msg.analysis ?? body.analysis ?? {};\nconst structured = analysis.structuredData ?? body.structuredData ?? {};\nconst call = msg.call ?? body.call ?? {};\n\nconst name = structured.name ?? structured.callerName ?? body.callerName ?? body.name ?? 'Unknown caller';\nconst rawPhone = structured.phone ?? call.customer?.number ?? body.customerNumber ?? body.phone ?? body.from ?? '';\nconst phone = String(rawPhone).replace(/[^\\d+]/g, '');\nconst summary = analysis.summary ?? body.summary ?? msg.summary ?? body.callSummary ?? '';\n\n// Work out how the call ended.\nconst endedReason = String(msg.endedReason ?? body.endedReason ?? body.status ?? '').toLowerCase();\nconst stated = String(structured.outcome ?? body.outcome ?? '').toLowerCase();\n\nlet outcome = 'missed';\nif (stated.includes('book') || endedReason.includes('book')) {\n outcome = 'booked';\n} else if (stated.includes('transfer') || endedReason.includes('transfer') || endedReason.includes('forward')) {\n outcome = 'transferred';\n} else if (\n stated.includes('message') ||\n (summary && !endedReason.includes('no-answer') && !endedReason.includes('failed') && !endedReason.includes('error'))\n) {\n outcome = 'message';\n}\n\nconst labels = {\n booked: 'Booked on the calendar',\n transferred: 'Handed to a person',\n message: 'Message taken',\n missed: 'Missed or failed'\n};\n\nreturn [{ json: {\n name,\n phone,\n summary,\n outcome,\n outcomeLabel: labels[outcome],\n receivedAt: new Date().toISOString()\n} }];"
},
"id": "code-read-call-report",
"name": "Pull out who called and how it went",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
220,
260
]
},
{
"parameters": {
"method": "POST",
"url": "https://services.leadconnectorhq.com/contacts/upsert",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Version",
"value": "2021-07-28"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ locationId: 'bsHomeSvcDemo01', name: $json.name, phone: $json.phone, source: 'Vapi voice agent', tags: ['voice-agent-call'], customFields: [{ id: 'cfBsCallSummary01', value: $json.summary }] }) }}",
"options": {}
},
"id": "http-save-caller",
"name": "Save the caller in GoHighLevel",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
440,
260
],
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"leftValue": "={{ $('Pull out who called and how it went').item.json.outcome }}",
"rightValue": "message",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "Message taken"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"leftValue": "={{ $('Pull out who called and how it went').item.json.outcome }}",
"rightValue": "missed",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "Missed or failed"
}
]
},
"options": {
"fallbackOutput": "extra",
"renameFallbackOutput": "Booked or handed off"
}
},
"id": "switch-call-outcome",
"name": "How did the call end?",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.4,
"position": [
660,
260
]
},
{
"parameters": {
"resource": "sms",
"operation": "send",
"from": "+15550149000",
"to": "+15550147788",
"message": "=Phone agent took a message. {{ $('Pull out who called and how it went').item.json.name }} at {{ $('Pull out who called and how it went').item.json.phone || 'an unknown number' }}. {{ $('Pull out who called and how it went').item.json.summary }}",
"options": {}
},
"id": "twilio-text-dana-message",
"name": "Text Dana the message",
"type": "n8n-nodes-base.twilio",
"typeVersion": 1,
"position": [
900,
140
],
"credentials": {
"twilioApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"resource": "sms",
"operation": "send",
"from": "+15550149000",
"to": "+15550147788",
"message": "=The phone agent could not finish a call from {{ $('Pull out who called and how it went').item.json.phone || 'an unknown number' }}. Worth ringing them back before it goes cold.",
"options": {}
},
"id": "twilio-text-dana-missed",
"name": "Text Dana to ring them back",
"type": "n8n-nodes-base.twilio",
"typeVersion": 1,
"position": [
900,
380
],
"credentials": {
"twilioApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Every path funnels through here so one row per call lands in the log.\nconst c = $('Pull out who called and how it went').item.json;\nreturn [{ json: {\n 'Caller': c.name + ' ' + (c.phone || 'unknown number'),\n 'Outcome': c.outcomeLabel,\n 'Summary': c.summary || 'No summary came through',\n 'Time': c.receivedAt\n} }];"
},
"id": "code-log-row",
"name": "Get the call ready for the log",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1140,
260
]
},
{
"parameters": {
"resource": "record",
"operation": "create",
"base": {
"__rl": true,
"mode": "id",
"value": "appBlueSummit001"
},
"table": {
"__rl": true,
"mode": "id",
"value": "tblLeads"
},
"columns": {
"mappingMode": "autoMapInputData",
"value": {},
"matchingColumns": [],
"schema": []
},
"options": {}
},
"id": "airtable-log-call",
"name": "Log the call in Airtable",
"type": "n8n-nodes-base.airtable",
"typeVersion": 2.2,
"position": [
1360,
260
],
"credentials": {
"airtableTokenApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify({ ok: true }) }}",
"options": {}
},
"id": "respond-report-received",
"name": "Tell Vapi we got the report",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1580,
260
]
},
{
"parameters": {
"httpMethod": "POST",
"path": "demo/vapi-book",
"responseMode": "responseNode",
"options": {}
},
"id": "webhook-book",
"name": "Caller wants an estimate visit",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
860
]
},
{
"parameters": {
"jsCode": "// The Vapi assistant (asst_frontdesk_bs01) calls this tool mid-call.\n// Arguments can arrive as an object or as a JSON string, and some\n// setups send the fields flat, so all three shapes work here.\nconst body = $json.body ?? $json;\nconst msg = body.message ?? body;\nconst toolCall = (msg.toolCalls ?? msg.toolCallList ?? body.toolCalls ?? [])[0] ?? {};\nlet args = toolCall.function?.arguments ?? toolCall.arguments ?? body;\nif (typeof args === 'string') {\n try { args = JSON.parse(args); } catch (e) { args = {}; }\n}\n\nconst name = args.name ?? body.name ?? 'Caller';\nconst phone = String(args.phone ?? body.phone ?? '').replace(/[^\\d+]/g, '');\nconst startTime = args.startTime ?? args.datetime ?? args.time ?? '';\nconst requestedTime = args.requestedTime ?? args.day ?? args.time ?? startTime;\nconst toolCallId = toolCall.id ?? body.toolCallId ?? 'demo-tool-call';\n\nreturn [{ json: { name, phone, startTime: startTime || requestedTime, requestedTime, toolCallId } }];"
},
"id": "code-read-booking",
"name": "Read the booking request",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
220,
860
]
},
{
"parameters": {
"method": "POST",
"url": "https://services.leadconnectorhq.com/contacts/upsert",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Version",
"value": "2021-07-28"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ locationId: 'bsHomeSvcDemo01', name: $json.name, phone: $json.phone, source: 'Vapi voice agent booking', tags: ['voice-agent-call'] }) }}",
"options": {}
},
"id": "http-save-booking-contact",
"name": "Add the caller to GoHighLevel",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
440,
860
],
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"method": "POST",
"url": "https://services.leadconnectorhq.com/calendars/events/appointments",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Version",
"value": "2021-07-28"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ calendarId: 'calBsEstimates01', locationId: 'bsHomeSvcDemo01', contactId: $json.contact.id, startTime: $('Read the booking request').item.json.startTime, title: 'Estimate visit for ' + $('Read the booking request').item.json.name }) }}",
"options": {}
},
"id": "http-create-appointment",
"name": "Put the visit on the calendar",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
660,
860
],
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify({ results: [{ toolCallId: $('Read the booking request').item.json.toolCallId, result: 'You are on the schedule for ' + $('Read the booking request').item.json.requestedTime + '. We will send a text to confirm.' }] }) }}",
"options": {}
},
"id": "respond-booking-result",
"name": "Read the confirmation back to the caller",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
900,
860
]
}
],
"connections": {
"A customer call just ended": {
"main": [
[
{
"node": "Pull out who called and how it went",
"type": "main",
"index": 0
}
]
]
},
"Pull out who called and how it went": {
"main": [
[
{
"node": "Save the caller in GoHighLevel",
"type": "main",
"index": 0
}
]
]
},
"Save the caller in GoHighLevel": {
"main": [
[
{
"node": "How did the call end?",
"type": "main",
"index": 0
}
]
]
},
"How did the call end?": {
"main": [
[
{
"node": "Text Dana the message",
"type": "main",
"index": 0
}
],
[
{
"node": "Text Dana to ring them back",
"type": "main",
"index": 0
}
],
[
{
"node": "Get the call ready for the log",
"type": "main",
"index": 0
}
]
]
},
"Text Dana the message": {
"main": [
[
{
"node": "Get the call ready for the log",
"type": "main",
"index": 0
}
]
]
},
"Text Dana to ring them back": {
"main": [
[
{
"node": "Get the call ready for the log",
"type": "main",
"index": 0
}
]
]
},
"Get the call ready for the log": {
"main": [
[
{
"node": "Log the call in Airtable",
"type": "main",
"index": 0
}
]
]
},
"Log the call in Airtable": {
"main": [
[
{
"node": "Tell Vapi we got the report",
"type": "main",
"index": 0
}
]
]
},
"Caller wants an estimate visit": {
"main": [
[
{
"node": "Read the booking request",
"type": "main",
"index": 0
}
]
]
},
"Read the booking request": {
"main": [
[
{
"node": "Add the caller to GoHighLevel",
"type": "main",
"index": 0
}
]
]
},
"Add the caller to GoHighLevel": {
"main": [
[
{
"node": "Put the visit on the calendar",
"type": "main",
"index": 0
}
]
]
},
"Put the visit on the calendar": {
"main": [
[
{
"node": "Read the confirmation back to the caller",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
airtableTokenApihttpHeaderAuthtwilioApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Voice-Agent-Backend. Uses httpRequest, twilio, airtable. Webhook trigger; 16 nodes.
Source: https://github.com/mcruz1799/automation-examples/blob/main/claude-code/02-automation-file-factory/skills/n8n/section-templates/voice-agent-backend.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.
Onsite Photos to Jobs (SMS Agent). Uses dataTable, twilio, httpRequest, airtable. Webhook trigger; 62 nodes.
Whatsapp Lead Agent. Uses httpRequest, hunter, @tavily/n8n-nodes-tavily, @mendable/n8n-nodes-firecrawl. Webhook trigger; 35 nodes.
Whatsapp Multi Agent System optimized copy 2.0. Uses airtable, httpRequest, errorTrigger. Webhook trigger; 44 nodes.
OpenAI Assistant for Hubspot Chat. Uses httpRequest, airtable. Webhook trigger; 34 nodes.
Template Overview This n8n workflow provides an intelligent, timezone-aware AI voice calling system for e-commerce businesses to automatically confirm customer orders via phone calls. The system uses