Qualify and call back inbound leads with OpenAI, Bland AI, Airtable and SendGrid
Pull contacts, verify each address with BillionVerify, and continue to Bland AI — only deliverable addresses get through.
Why verify before the send
Sending to invalid, risky, catch-all, or disposable addresses spikes your bounce rate and erodes sender reputation. A verification gate before the Bland AI step removes that risk automatically — only deliverable addresses continue, the rest are flagged.
The workflow
BillionVerify — verification sits right before the send.
Node by node
- 1WebhookSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 2Call Outcome WebhookSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 3Validate & Clean DataSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 4Parse Call OutcomeSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 5Respond to Webhook1Source· n8n
Provides or transforms the contact data flowing through the workflow.
- 6AI Lead QualificationSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 7CodeSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 8Check Validation StatusLogic· n8n
Branches on the verification result: only deliverable addresses continue to the send; the rest are skipped and flagged.
- 9Check Call SuccessLogic· n8n
Branches on the verification result: only deliverable addresses continue to the send; the rest are skipped and flagged.
- 10Create a recordSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 11Respond to WebhookSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 12Code1Source· n8n
Provides or transforms the contact data flowing through the workflow.
- 13Verify Email (BillionVerify) 4Verify· billionverify
The BillionVerify node verifies the address — status (valid / invalid / risky / catch-all / role / disposable), is_deliverable, and a confidence score — before anything is sent.
- 14Check for AI CallingLogic· n8n
Branches on the verification result: only deliverable addresses continue to the send; the rest are skipped and flagged.
- 15Create Calendar eventSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 16IF deliverable 4Logic· n8n
Branches on the verification result: only deliverable addresses continue to the send; the rest are skipped and flagged.
- 17Get availability in a calendarSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 18Verify Email (BillionVerify) 2Verify· billionverify
The BillionVerify node verifies the address — status (valid / invalid / risky / catch-all / role / disposable), is_deliverable, and a confidence score — before anything is sent.
- 19Add Successful Call RecordSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 20Send Priority Email for Failed CallsSend· n8n
Sends only to verified, deliverable addresses. Swap in your own provider node if you send elsewhere.
- 21Code in JavaScriptSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 22IF deliverable 2Logic· n8n
Branches on the verification result: only deliverable addresses continue to the send; the rest are skipped and flagged.
- 23Verify Email (BillionVerify) 3Verify· billionverify
The BillionVerify node verifies the address — status (valid / invalid / risky / catch-all / role / disposable), is_deliverable, and a confidence score — before anything is sent.
- 24Update Failed Call RecordSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 25Trigger AI Phone CallSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 26Send Priority EmailSend· n8n
Sends only to verified, deliverable addresses. Swap in your own provider node if you send elsewhere.
- 27IF deliverable 3Logic· n8n
Branches on the verification result: only deliverable addresses continue to the send; the rest are skipped and flagged.
- 28Update Call RecordSource· n8n
Provides or transforms the contact data flowing through the workflow.
- 29Send Booking ConfirmationEmailSend· n8n
Sends only to verified, deliverable addresses. Swap in your own provider node if you send elsewhere.
- 30Check for Follow-Up SequenceLogic· n8n
Branches on the verification result: only deliverable addresses continue to the send; the rest are skipped and flagged.
- 31Verify Email (BillionVerify)Verify· billionverify
The BillionVerify node verifies the address — status (valid / invalid / risky / catch-all / role / disposable), is_deliverable, and a confidence score — before anything is sent.
- 32IF deliverableLogic· n8n
Branches on the verification result: only deliverable addresses continue to the send; the rest are skipped and flagged.
- 33Send Nurture EmailSend· n8n
Sends only to verified, deliverable addresses. Swap in your own provider node if you send elsewhere.
Workflow JSON
Copy or download this workflow, then import it in n8n (Workflows → Import from File / Paste). Install the BillionVerify community node first, then add your API key credential.
{
"name": "Qualify and call back inbound leads with OpenAI, Bland AI, Airtable and SendGrid + BillionVerify",
"nodes": [
{
"id": "6d51b2a6-28ab-416b-b489-dfbccb3a002c",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
960,
640
],
"webhookId": "LEAD_INTAKE_WEBHOOK_ID",
"parameters": {
"path": "=LEAD_INTAKE_WEBHOOK_ID",
"options": {
"allowedOrigins": "*",
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2.1
},
{
"id": "3288c234-0c1b-42fe-8025-8d38f2858963",
"name": "Validate & Clean Data",
"type": "n8n-nodes-base.code",
"position": [
1184,
640
],
"parameters": {
"jsCode": "// Fixed Tally webhook validation using actual structure\nconst webhookData = $input.first().json;\n\nconsole.log(\"=== PROCESSING TALLY WEBHOOK ===\");\n\nlet leadData = {};\n\ntry {\n // Access fields from the correct location: body.data.fields\n const fields = webhookData.body?.data?.fields;\n \n if (!fields || !Array.isArray(fields)) {\n throw new Error(\"No fields array found in webhook.body.data.fields\");\n }\n \n console.log(\"Processing\", fields.length, \"fields from Tally\");\n \n // Process each field\n fields.forEach((field, index) => {\n console.log(`Field ${index}: \"${field.label}\" (${field.type})`);\n \n switch(field.label) {\n case 'Name':\n leadData.name = field.value;\n console.log(`✓ Name: ${field.value}`);\n break;\n \n case 'Email':\n leadData.email = field.value;\n console.log(`✓ Email: ${field.value}`);\n break;\n \n case 'Phone Number':\n leadData.phone = field.value;\n console.log(`✓ Phone: ${field.value}`);\n break;\n \n case 'Company Name':\n leadData.company = field.value;\n console.log(`✓ Company: ${field.value}`);\n break;\n \n case 'Message':\n leadData.message = field.value;\n console.log(`✓ Message: ${field.value}`);\n break;\n \n case 'Company Size':\n // For dropdown fields, map the selected ID to text using options\n if (field.type === 'DROPDOWN' && field.options && field.value) {\n const selectedId = Array.isArray(field.value) ? field.value[0] : field.value;\n const selectedOption = field.options.find(opt => opt.id === selectedId);\n leadData.company_size = selectedOption ? selectedOption.text : 'Not specified';\n console.log(`✓ Company Size: ${selectedId} → ${leadData.company_size}`);\n } else {\n leadData.company_size = field.value || 'Not specified';\n }\n break;\n \n case 'When do you need a solution?':\n if (field.type === 'DROPDOWN' && field.options && field.value) {\n const selectedId = Array.isArray(field.value) ? field.value[0] : field.value;\n const selectedOption = field.options.find(opt => opt.id === selectedId);\n leadData.timeline = selectedOption ? selectedOption.text : 'Not specified';\n console.log(`✓ Timeline: ${selectedId} → ${leadData.timeline}`);\n } else {\n leadData.timeline = field.value || 'Not specified';\n }\n break;\n \n case \"What's your role in the company?\":\n if (field.type === 'DROPDOWN' && field.options && field.value) {\n const selectedId = Array.isArray(field.value) ? field.value[0] : field.value;\n const selectedOption = field.options.find(opt => opt.id === selectedId);\n leadData.role = selectedOption ? selectedOption.text : 'Not specified';\n console.log(`✓ Role: ${selectedId} → ${leadData.role}`);\n } else {\n leadData.role = field.value || 'Not specified';\n }\n break;\n \n default:\n console.log(`⚠️ Unmapped field: \"${field.label}\"`);\n }\n });\n \n console.log(\"Final extracted data:\", leadData);\n \n} catch (error) {\n console.log(\"❌ ERROR:\", error.message);\n return [{\n json: {\n status: \"error\",\n errors: [`Processing failed: ${error.message}`],\n debug: {\n webhookStructure: typeof webhookData,\n hasBody: !!webhookData.body,\n hasBodyData: !!(webhookData.body && webhookData.body.data),\n hasFields: !!(webhookData.body && webhookData.body.data && webhookData.body.data.fields)\n }\n }\n }];\n}\n\n// Validation\nconst errors = [];\nif (!leadData.name || leadData.name.trim() === '') {\n errors.push(\"Name is required\");\n}\nif (!leadData.email || !leadData.email.includes('@')) {\n errors.push(\"Valid email is required\");\n}\nif (!leadData.phone || leadData.phone.trim() === '') {\n errors.push(\"Phone is required\");\n}\n\nif (errors.length > 0) {\n console.log(\"❌ Validation failed:\", errors);\n return [{\n json: {\n status: \"error\",\n errors: errors,\n debug: { extractedData: leadData }\n }\n }];\n}\n\n// Create final structured lead object\nconst cleanedData = {\n name: leadData.name.trim(),\n email: leadData.email.toLowerCase().trim(),\n phone: leadData.phone.replace(/[^\\d+]/g, ''),\n company: leadData.company?.trim() || \"\",\n message: leadData.message?.trim() || \"\",\n company_size: leadData.company_size,\n timeline: leadData.timeline,\n role: leadData.role,\n timestamp: new Date().toISOString(),\n leadId: `lead_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`\n};\n\nconsole.log(\"✅ SUCCESS - Final lead data:\");\nconsole.log(` Company Size: ${cleanedData.company_size}`);\nconsole.log(` Timeline: ${cleanedData.timeline}`);\nconsole.log(` Role: ${cleanedData.role}`);\n\nreturn [{\n json: {\n status: \"success\",\n lead: cleanedData\n }\n}];"
},
"typeVersion": 2
},
{
"id": "31df8d4b-9c41-4d90-8734-bb12512539a4",
"name": "Check Validation Status",
"type": "n8n-nodes-base.if",
"position": [
1760,
640
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "9b310b34-90c0-4f6c-84c5-c28413168ef5",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $('Validate & Clean Data').item.json.status }}",
"rightValue": "success"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "10d016e3-2314-4c8c-a839-9dde3d2e6d84",
"name": "Create a record",
"type": "n8n-nodes-base.airtable",
"position": [
1984,
544
],
"parameters": {
"base": {
"__rl": true,
"mode": "list",
"value": "appXXXXXXXXXXXXXX",
"cachedResultUrl": "https://airtable.com/appXXXXXXXXXXXXXX",
"cachedResultName": "Your Lead Base"
},
"table": {
"__rl": true,
"mode": "list",
"value": "tblXXXXXXXXXXXXXX",
"cachedResultUrl": "https://airtable.com/appXXXXXXXXXXXXXX/tblXXXXXXXXXXXXXX",
"cachedResultName": "Leads"
},
"columns": {
"value": {
"Name": "={{ $('Validate & Clean Data').item.json.lead.name }}",
"Email": "={{ $('Validate & Clean Data').item.json.lead.email }}",
"Phone": "={{ $('Validate & Clean Data').item.json.lead.phone }}",
"Status": "New",
"Company": "={{ $('Validate & Clean Data').item.json.lead.company }}",
"Message": "={{ $('Validate & Clean Data').item.json.lead.message }}",
"Lead Score": "={{ $('AI Lead Qualification').item.json.message.content.score }}",
"Company Size": "={{ $('Validate & Clean Data').item.json.lead.company_size }}",
"When do you need a solution?": "={{ $('Validate & Clean Data').item.json.lead.timeline }}",
"What's your role in the company?": "={{ $('Validate & Clean Data').item.json.lead.role }}"
},
"schema": [
{
"id": "Name",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Email",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Email",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Phone",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Phone",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Company",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Company",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Company Size",
"type": "options",
"display": true,
"options": [
{
"name": "1-10 people",
"value": "1-10 people"
},
{
"name": "11-50 people",
"value": "11-50 people"
},
{
"name": "51-200 people",
"value": "51-200 people"
},
{
"name": "201+ people",
"value": "201+ people"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Company Size",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "When do you need a solution?",
"type": "options",
"display": true,
"options": [
{
"name": "Immediately (within 2 weeks)",
"value": "Immediately (within 2 weeks)"
},
{
"name": "Soon (1-3 months)",
"value": "Soon (1-3 months)"
},
{
"name": "This year (3-12 months)",
"value": "This year (3-12 months)"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "When do you need a solution?",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "What's your role in the company?",
"type": "options",
"display": true,
"options": [
{
"name": "Owner/CEO/Founder",
"value": "Owner/CEO/Founder"
},
{
"name": "Manager/Director",
"value": "Manager/Director"
},
{
"name": "Employee/Team Member",
"value": "Employee/Team Member"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "What's your role in the company?",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Message",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Message",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Lead Score",
"type": "number",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Lead Score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "options",
"display": true,
"options": [
{
"name": "New",
"value": "New"
},
{
"name": "Contacted",
"value": "Contacted"
},
{
"name": "Qualified",
"value": "Qualified"
},
{
"name": "Unqualified",
"value": "Unqualified"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Meeting Scheduled",
"type": "options",
"display": true,
"options": [
{
"name": "Yes",
"value": "Yes"
},
{
"name": "No",
"value": "No"
},
{
"name": "Pending",
"value": "Pending"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Meeting Scheduled",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Meeting Date",
"type": "dateTime",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Meeting Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Notes",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Notes",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Created",
"type": "string",
"display": true,
"removed": true,
"readOnly": true,
"required": false,
"displayName": "Created",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Lead ID",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Lead ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Record ID",
"type": "string",
"display": true,
"removed": false,
"readOnly": true,
"required": false,
"displayName": "Record ID",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "create"
},
"credentials": {
"airtableTokenApi": {
"id": "credential-id",
"name": "Airtable Personal Access Token account 4"
}
},
"typeVersion": 2.1
},
{
"id": "00266787-e974-4f60-a81a-d762a959fc13",
"name": "Respond to Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
1984,
736
],
"parameters": {
"options": {},
"respondWith": "json",
"responseBody": "={\n \"success\": false,\n \"message\": \"Validation failed\",\n \"errors\": \"Invalid data provided\"\n}"
},
"typeVersion": 1.4
},
{
"id": "d71fa900-789d-40b3-b3fa-f8fc752ee2ed",
"name": "AI Lead Qualification",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
1408,
640
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-3.5-turbo",
"cachedResultName": "GPT-3.5-TURBO"
},
"options": {},
"messages": {
"values": [
{
"role": "system",
"content": "=You are an expert B2B lead qualification specialist for AI automation services. You now have structured qualification data that is MORE RELIABLE than message analysis. Score leads 1-10 using this weighted scoring system:\n\n**PRIMARY SCORING FACTORS (70% of score):**\n\n**COMPANY SIZE SCORING:**\n- \"201+ people\" = 4 points (Enterprise - high budget, complex processes)\n- \"51-200 people\" = 3 points (Mid-market - growth stage, automation needs)\n- \"11-50 people\" = 2 points (SMB - emerging automation needs)\n- \"1-10 people\" = 1 point (Startup/small - limited budget)\n- Not specified = 0 points\n\n**TIMELINE SCORING:**\n- \"Immediately (within 2 weeks)\" = 3 points (Hot lead - urgent need)\n- \"Soon (1-3 months)\" = 2 points (Warm lead - active buying process)\n- \"This year (3-12 months)\" = 1 point (Planning phase)\n- \"Just exploring\" or not specified = 0 points\n\n**ROLE SCORING:**\n- \"Owner/CEO/Founder\" = 2 points (Decision maker)\n- \"Manager/Director\" = 1.5 points (Strong influence)\n- \"Employee/Team Member\" = 0.5 points (Limited authority)\n- Not specified = 0 points\n\n**SECONDARY FACTORS (30% of score):**\n- Professional email domain (+0.5 points)\n- Complete contact info (+0.5 points)\n- Detailed message with specific pain points (+1 point)\n- Business email vs personal (-0.5 for gmail/yahoo unless startup)\n\n**TOTAL SCORING SYSTEM:**\n- 8.5-10 points = SCORE 9-10 (HOT - Immediate call)\n- 6.5-8.4 points = SCORE 7-8 (WARM - Priority email + call within 24h)\n- 4.5-6.4 points = SCORE 5-6 (LUKEWARM - Nurture sequence)\n- Below 4.5 points = SCORE 1-4 (COLD - Minimal follow-up)\n\n**EXAMPLES:**\n- 201+ people + Immediately + Owner/CEO = 9 points = HOT LEAD\n- 51-200 people + Soon + Manager + good message = 7.5 points = WARM LEAD\n- 11-50 people + This year + Employee = 4 points = LUKEWARM\n- 1-10 people + Just exploring = 2 points = COLD\n\nIMPORTANT: Even if someone writes a brief message like 'Tell me more', if they selected '201+ people' + 'Immediately' + 'Owner/CEO', that's a 9-point HOT LEAD. The structured data is more reliable than message analysis.\n\nReturn JSON: {\"score\": X, \"priority\": \"hot/warm/lukewarm/cold\", \"reasoning\": \"specific point breakdown\", \"next_action\": \"immediate_call/priority_email/nurture_sequence/minimal_followup\", \"deal_size_estimate\": \"enterprise/mid-market/small/micro\"}"
},
{
"content": "=LEAD QUALIFICATION ANALYSIS\n\n**STRUCTURED DATA:**\n- Company Size: {{ $node['Validate & Clean Data'].json.lead.company_size }}\n- Timeline: {{ $node['Validate & Clean Data'].json.lead.timeline }}\n- Role: {{ $node['Validate & Clean Data'].json.lead.role }}\n\n**CONTACT DETAILS:**\n- Name: {{ $node['Validate & Clean Data'].json.lead.name }}\n- Email: {{ $node['Validate & Clean Data'].json.lead.email }}\n- Company: {{ $node['Validate & Clean Data'].json.lead.company }}\n- Phone: {{ $node['Validate & Clean Data'].json.lead.phone }}\n\n**MESSAGE:**\n{{ $node['Validate & Clean Data'].json.lead.message }}\n\n**SCORING INSTRUCTIONS:**\nUse the point system above. Calculate total points from company size + timeline + role + secondary factors, then map to 1-10 score. Provide detailed reasoning showing your point calculations."
}
]
},
"jsonOutput": true
},
"credentials": {
"openAiApi": {
"id": "credential-id",
"name": "OpenAi account"
}
},
"typeVersion": 1.8
},
{
"id": "1c325fd0-be68-4a0c-b4dd-df0cdb4c9fa7",
"name": "Check for Follow-Up Sequence",
"type": "n8n-nodes-base.if",
"position": [
3248,
640
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "70420fe4-f926-4afd-9d95-bbaa859f2ff5",
"operator": {
"type": "number",
"operation": "lte"
},
"leftValue": "={{ $('AI Lead Qualification').item.json.message.content.score }}",
"rightValue": 6
}
]
}
},
"typeVersion": 2.2
},
{
"id": "6f7725c3-6fe5-41f8-b60a-29a7e465da4a",
"name": "Send Nurture Email",
"type": "n8n-nodes-base.sendGrid",
"position": [
3472,
640
],
"parameters": {
"subject": "={{ $node[\"Validate & Clean Data\"].json.lead.name }}, here's how AI automation can help {{ $node[\"Validate & Clean Data\"].json.lead.company }}",
"toEmail": "={{ $node[\"Validate & Clean Data\"].json.lead.email }}",
"fromName": "Your Name - AI Automation",
"resource": "mail",
"fromEmail": "user@example.com",
"contentType": "text/html",
"contentValue": "=<p>Hi {{ $node[\"Validate & Clean Data\"].json.lead.name }},</p> <p>Thanks for your interest in AI automation! I wanted to share some insights that might help {{ $node[\"Validate & Clean Data\"].json.lead.company }}.</p> <p><strong>Companies like yours typically see results in:</strong></p> <ul> <li>40-60% reduction in manual data processing</li> <li>Automated lead qualification and routing</li> <li>Improved customer response times</li> <li>Enhanced data accuracy and insights</li> </ul> <p><strong>Free Resources:</strong></p> <p>I've prepared a case study showing how a similar company automated their lead process and increased conversion by 35%.</p> <p>Would you like me to send it along or would you like a brief call to discuss your specific automation opportunities?</p> <p>Best regards,<br>Abi</p>",
"additionalFields": {}
},
"credentials": {
"sendGridApi": {
"id": "credential-id",
"name": "SendGrid account"
}
},
"typeVersion": 1
},
{
"id": "48cf115e-b84a-4b36-97fe-2235045e3c4e",
"name": "Check for AI Calling",
"type": "n8n-nodes-base.if",
"position": [
2208,
544
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "a468a91b-7678-4609-8546-6ec3741b5728",
"operator": {
"type": "number",
"operation": "gte"
},
"leftValue": "={{ $('AI Lead Qualification').item.json.message.content.score }}",
"rightValue": 8
}
]
}
},
"typeVersion": 2.2
},
{
"id": "9aafc4e1-88d5-4d2e-807b-e90fc8fcab2b",
"name": "Trigger AI Phone Call",
"type": "n8n-nodes-base.httpRequest",
"position": [
3248,
448
],
"parameters": {
"url": "https://api.bland.ai/v1/calls",
"method": "POST",
"options": {},
"jsonBody": "= {\n \"phone_number\": \"{{ $node['Validate & Clean Data'].json.lead.phone }}\",\n \"task\": \"You are Sarah, an AI automation consultant. You are calling {{ $node['Validate & Clean Data'].json.lead.name }} from {{ $node['Validate & Clean Data'].json.lead.company }}. Start by saying: 'Hi, is this {{ $node['Validate & Clean Data'].json.lead.name }}?' Wait for confirmation. Then introduce yourself: 'Hi {{ $node['Validate & Clean Data'].json.lead.name }}, this is Sarah from [YOUR_COMPANY]. I'm calling about the AI automation inquiry you submitted for {{ $node['Validate & Clean Data'].json.lead.company }}. Do you have a quick minute to chat?' If they're available, ask: 'What prompted you to look into AI automation for your business?' Listen to their response, then ask about their current challenges. If they show genuine interest in learning more, say: 'I'd love to schedule a brief 15-minute consultation to discuss how we could help {{ $node['Validate & Clean Data'].json.lead.company }} specifically. {{ $node['Code in JavaScript'].json.availabilityDescription }}. What day and time would work best for you?' Once they give a preferred time, repeat it back: 'Perfect, so that's [repeat their preferred day and time]. I'll have someone send you the calendar invite for [repeat exact day/time]. Thanks so much for your time today!'\",\n \"voice\": \"maya\",\n \"max_duration\": 300,\n \"webhook\": \"https://YOUR-N8N-INSTANCE.com/webhook/CALL_OUTCOME_WEBHOOK_ID\",\n \"metadata\": {\n\"record_id\": \"{{ $node['Create a record'].json.id }}\",\n \"lead_name\": \"{{ $node['Validate & Clean Data'].json.lead.name }}\",\n \"lead_email\": \"{{ $node['Validate & Clean Data'].json.lead.email }}\",\n \"lead_id\": \"{{ $node['Validate & Clean Data'].json.lead.leadId }}\",\n \"lead_company\": \"{{ $node['Validate & Clean Data'].json.lead.company }}\",\n\"message\": \"{{ $node['Validate & Clean Data'].json.lead.message }}\"\n }\n }\n",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "Bearer YOUR_TOKEN_HERE"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "d508fb3f-5407-46d2-b402-f48207349a09",
"name": "Send Priority Email",
"type": "n8n-nodes-base.sendGrid",
"position": [
2432,
640
],
"parameters": {
"subject": "=Hi {{ $node[\"Validate & Clean Data\"].json.lead.name }}, let's discuss your AI automation needs",
"toEmail": "={{ $('Validate & Clean Data').item.json.lead.email }}",
"fromName": "Your Name - AI Automation",
"resource": "mail",
"fromEmail": "user@example.com",
"contentType": "text/html",
"contentValue": "=<p>Hi {{ $node[\"Validate & Clean Data\"].json.lead.name }},</p>\n\n<p>Thanks for your interest in AI automation! Based on your message, I can see {{ $node[\"Validate & Clean Data\"].json.lead.company }} has strong automation potential.</p>\n\n<p> I wanted to reach out personally and I'd love to schedule a 15-minute call to discuss:</p>\n<ul>\n<li>Your specific automation challenges</li>\n<li>How AI can streamline your processes</li>\n<li>ROI potential for your business</li>\n</ul>\n\n<p>When would be a good time this week?</p>\n\n<p>Best regards,<br>\nAbi<br>\nAI Automation Specialist</p>",
"additionalFields": {}
},
"credentials": {
"sendGridApi": {
"id": "credential-id",
"name": "SendGrid account"
}
},
"typeVersion": 1
},
{
"id": "4b33e334-7e42-4c99-9026-899e8125f0f9",
"name": "Call Outcome Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
960,
1408
],
"webhookId": "CALL_OUTCOME_WEBHOOK_ID",
"parameters": {
"path": "CALL_OUTCOME_WEBHOOK_ID",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2.1
},
{
"id": "40962417-a8de-49f4-bee7-29c33eb7a266",
"name": "Get availability in a calendar",
"type": "n8n-nodes-base.googleCalendar",
"position": [
2432,
448
],
"parameters": {
"options": {},
"timeMax": "={{ $now.plus(7, 'day') }}",
"calendar": {
"__rl": true,
"mode": "list",
"value": "user@example.com",
"cachedResultName": "user@example.com"
},
"resource": "calendar"
},
"credentials": {
"googleCalendarOAuth2Api": {
"id": "credential-id",
"name": "Google Calendar account"
}
},
"typeVersion": 1.3
},
{
"id": "843172bc-73f7-451d-ba31-796f7c7d99da",
"name": "Code in JavaScript",
"type": "n8n-nodes-base.code",
"position": [
3024,
448
],
"parameters": {
"jsCode": "// Get calendar events - handle different data structures\nlet events = [];\nconst inputData = $input.all()[0];\n\n// Handle different possible data structures from Google Calendar\nif (inputData && inputData.json) {\n if (Array.isArray(inputData.json)) {\n events = inputData.json;\n } else if (inputData.json.items && Array.isArray(inputData.json.items)) {\n events = inputData.json.items;\n } else if (inputData.json.events && Array.isArray(inputData.json.events)) {\n events = inputData.json.events;\n }\n}\n\nconsole.log(\"Events data:\", events);\n\nconst now = new Date();\n\n// Your working hours\nconst workingHours = {\n start: 9, // 9 AM\n end: 17, // 5 PM\n days: [1, 2, 3, 4, 5], // Monday-Friday\n duration: 15 // 15-minute meetings\n};\n\n// Find all available days and general time ranges\nconst availableDays = [];\nconst specificSlots = [];\n\nfor (let day = 1; day <= 7; day++) {\n const checkDate = new Date(now.getTime() + day * 24 * 60 * 60 * 1000);\n \n if (workingHours.days.includes(checkDate.getDay())) {\n let dayHasAvailability = false;\n let availableHours = [];\n \n for (let hour = workingHours.start; hour < workingHours.end; hour++) {\n const slotTime = new Date(checkDate);\n slotTime.setHours(hour, 0, 0, 0);\n \n // Check if slot conflicts with existing events\n const hasConflict = events.length > 0 && events.some(event => {\n if (!event.start || !event.end) return false;\n const eventStart = new Date(event.start.dateTime || event.start.date);\n const eventEnd = new Date(event.end.dateTime || event.end.date);\n return slotTime >= eventStart && slotTime < eventEnd;\n });\n \n if (!hasConflict && slotTime > now) {\n dayHasAvailability = true;\n availableHours.push(hour);\n \n // Keep some specific slots for calendar booking later\n if (specificSlots.length < 5) {\n specificSlots.push(slotTime.toISOString());\n }\n }\n }\n \n if (dayHasAvailability) {\n const dayName = checkDate.toLocaleDateString('en-US', { weekday: 'long' });\n const formattedDate = checkDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });\n \n // Create time ranges instead of specific times\n const timeRanges = [];\n if (availableHours.includes(9) || availableHours.includes(10) || availableHours.includes(11)) {\n timeRanges.push('morning (9am-12pm)');\n }\n if (availableHours.includes(13) || availableHours.includes(14) || availableHours.includes(15)) {\n timeRanges.push('afternoon (1pm-4pm)');\n }\n if (availableHours.includes(16)) {\n timeRanges.push('late afternoon (4pm-5pm)');\n }\n \n if (timeRanges.length > 0) {\n const dayDescription = dayName + ' ' + formattedDate + ' - ' + timeRanges.join(' or ');\n availableDays.push(dayDescription);\n }\n }\n }\n \n if (availableDays.length >= 4) break; // Limit to 4 days of availability\n}\n\n// Create flexible availability description\nconst availabilityDescription = availableDays.length > 0 \n ? 'I have good availability: ' + availableDays.join(', ')\n : 'I have flexibility this week between 9am-5pm Monday through Friday';\n\nreturn [{ \n json: {\n availabilityDescription: availabilityDescription, // For AI script\n availableDateTime: specificSlots[0], // First specific slot for calendar booking\n allAvailableSlots: specificSlots, // All specific slots available\n debug: { totalEvents: events.length, availableDays: availableDays.length }\n }\n}];"
},
"typeVersion": 2
},
{
"id": "71f8e9d7-72f7-4570-9058-8071839b89ab",
"name": "Parse Call Outcome",
"type": "n8n-nodes-base.code",
"position": [
1184,
1312
],
"parameters": {
"jsCode": "/**\n * Parse Bland.ai webhook → normalize call outcome + surface Airtable record id\n * Works whether Bland posts `variables.metadata` or `metadata` at root.\n */\n\n//// 1) Load payload robustly\nconst fromWebhook = $items('Call Outcome Webhook', 0, 0)?.json;\nconst webhookData = (fromWebhook && Object.keys(fromWebhook).length)\n ? fromWebhook\n : ($input.first()?.json ?? {});\n\nif (!webhookData || Object.keys(webhookData).length === 0) {\n console.log('Parse Call Outcome: empty payload (no webhook item, no input). Skipping.');\n return $input.all().length ? $input.all() : [{ json: { skipped: true } }];\n}\n\nconst body = webhookData.body ?? webhookData;\n\n//// 2) Transcript (array or single field)\nlet transcriptText = '';\nif (Array.isArray(body.transcripts)) {\n transcriptText = body.transcripts\n .map(t => (t && t.text ? String(t.text) : ''))\n .filter(Boolean)\n .join(' ');\n} else if (body.transcript) {\n transcriptText = String(body.transcript);\n} else if (body.summary) {\n transcriptText = String(body.summary);\n}\n\n//// 3) Duration & disposition\nconst disposition = String(body.disposition_tag ?? '').toUpperCase();\nconst durationSec = Number(\n body.corrected_duration ??\n body.duration ??\n body.call_length ??\n webhookData.corrected_duration ??\n webhookData.duration ??\n webhookData.call_length ??\n 0\n);\n\n// “Connected” = actual human exchange: >5s OR any transcript captured\nconst connected = (durationSec > 5) || (transcriptText.trim().length > 0);\n\n//// 4) Lead basics\nconst vars = body.variables ?? {};\nconst meta = vars.metadata ?? body.metadata ?? {};\nconst leadData = {\n name: body.lead_name ?? meta.lead_name ?? webhookData.lead_name ?? 'Lead',\n email: body.lead_email ?? meta.lead_email ?? webhookData.lead_email ?? '',\n leadId: body.lead_id ?? meta.lead_id ?? webhookData.lead_id ?? '',\n company: body.lead_company ?? meta.lead_company ?? body.company ?? webhookData.lead_company ?? ''\n};\n\n// >>> 5) **Airtable Record ID** from metadata (this is what you’ll match on later)\nconst recordId = meta.record_id || webhookData.record_id || '';\n\n//// 6) Interest heuristic\nconst tLower = transcriptText.toLowerCase();\nlet interested =\n ['yes','sure','schedule','book','sounds good',\"let's do\",'lets do','ok','okay','that works']\n .some(k => tLower.includes(k));\n\nif (!interested && ['CALL_BACK_SCHEDULED','MEETING_SCHEDULED','FOLLOW_UP'].includes(disposition)) {\n interested = true;\n}\n\n//// 7) Extract an agreed time phrase (very light NLP)\nlet agreedTime = null;\nlet agreedDateTime = null;\n\nconst ampm = '(?:am|a\\\\.m\\\\.|pm|p\\\\.m\\\\.)';\nconst patterns = [\n // friday at 11am\n new RegExp(`(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday)\\\\s+at\\\\s+(\\\\d{1,2}(?::\\\\d{2})?\\\\s*${ampm})`, 'gi'),\n // 11am on friday\n new RegExp(`(\\\\d{1,2}(?::\\\\d{2})?\\\\s*${ampm})\\\\s+(?:on\\\\s+)?(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday)`, 'gi'),\n // tomorrow at 3pm / next week at 2pm\n new RegExp(`(?:tomorrow|next\\\\s+\\\\w+)\\\\s+at\\\\s+(\\\\d{1,2}(?::\\\\d{2})?\\\\s*${ampm})`, 'gi'),\n // friday at eleven a.m.\n new RegExp(`(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday)\\\\s+at\\\\s+([a-z\\\\-]+\\\\s*${ampm})`, 'gi')\n];\n\nfor (const rx of patterns) {\n const m = tLower.match(rx);\n if (m) { agreedTime = m[0]; break; }\n}\n\n//// 8) Coarse normalization to an ISO datetime (fallbacks if needed)\nif (agreedTime) {\n const now = new Date();\n const scheduled = new Date(now.getTime() + 24 * 60 * 60 * 1000); // default = tomorrow\n\n const norm = agreedTime.replace(/\\./g, ''); // normalize a.m./p.m. dots away\n const slots = [\n { key: /\\b10\\s?:?00?\\s?am\\b/, h: 10 },\n { key: /\\beleven\\s?am\\b|\\b11\\s?:?00?\\s?am\\b/, h: 11 },\n { key: /\\b2\\s?:?00?\\s?pm\\b|\\btwo\\s?pm\\b/, h: 14 },\n { key: /\\b3\\s?:?00?\\s?pm\\b|\\bthree\\s?pm\\b/, h: 15 },\n ];\n let setHour = false;\n for (const s of slots) {\n if (s.key.test(norm)) { scheduled.setHours(s.h, 0, 0, 0); setHour = true; break; }\n }\n if (!setHour) {\n if (/\\bpm\\b/.test(norm)) scheduled.setHours(14, 0, 0, 0);\n else scheduled.setHours(10, 0, 0, 0);\n }\n agreedDateTime = scheduled.toISOString();\n}\n\n//// 9) Return normalized payload (includes record_id)\nreturn [{\n json: {\n call_connected: connected,\n call_duration: durationSec || 0,\n lead_interested: interested,\n agreed_time_text: agreedTime,\n agreed_datetime: agreedDateTime,\n transcript: transcriptText,\n\n lead_name: leadData.name,\n lead_email: leadData.email,\n lead_id: leadData.leadId, // optional, unused if you only match by record id\n lead_company: leadData.company,\n\n record_id: recordId, // <<< use this in Airtable updates\n disposition_tag: disposition,\n webhook_data: webhookData\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "47073473-14ff-419b-a41d-6e892ed444ca",
"name": "Check Call Success",
"type": "n8n-nodes-base.if",
"position": [
1632,
1312
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "89d7bb5c-55a5-42a1-bfb7-bc9783256c0f",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $json.call_success }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.2
},
{
"id": "6c33f06d-642a-4302-966b-0d90272debfe",
"name": "Create Calendar event",
"type": "n8n-nodes-base.googleCalendar",
"position": [
2080,
1216
],
"parameters": {
"end": "={{ $json.end_iso }}",
"start": "={{ $json.start_iso }}",
"calendar": {
"__rl": true,
"mode": "list",
"value": "user@example.com",
"cachedResultName": "user@example.com"
},
"additionalFields": {
"summary": "=AI Automation Consultation - {{ $('Call Outcome Webhook').item.json.lead_name || 'Lead' }}"
}
},
"credentials": {
"googleCalendarOAuth2Api": {
"id": "credential-id",
"name": "Google Calendar account"
}
},
"typeVersion": 1.3
},
{
"id": "810ec7c9-77d6-4452-8e90-dc9464a25ede",
"name": "Add Successful Call Record",
"type": "n8n-nodes-base.airtable",
"position": [
2304,
1216
],
"parameters": {
"base": {
"__rl": true,
"mode": "list",
"value": "appXXXXXXXXXXXXXX",
"cachedResultUrl": "https://airtable.com/appXXXXXXXXXXXXXX",
"cachedResultName": "Your Lead Base"
},
"table": {
"__rl": true,
"mode": "list",
"value": "tblXXXXXXXXXXXXXX",
"cachedResultUrl": "https://airtable.com/appXXXXXXXXXXXXXX/tblXXXXXXXXXXXXXX",
"cachedResultName": "Leads"
},
"columns": {
"value": {
"Notes": "Call successful - Meeting scheduled for {{ $('Parse Call Outcome').item.json.agreed_time_text }}",
"Status": "Contacted",
"Message": "={{ $json.lead_message || $json.message_or_summary || '' }}\n",
"Meeting Date": "={{\n (() => {\n const fromCal = $items('Create Calendar event', 0, 0).json.start?.dateTime;\n const fromParse = $('Parse Call Outcome').item.json.agreed_datetime;\n const d = fromCal || fromParse;\n return d ? new Date(d).toISOString().slice(0,10) : null; // YYYY-MM-DD\n })()\n}}\n",
"Meeting Scheduled": "Yes"
},
"schema": [
{
"id": "id",
"type": "string",
"display": true,
"removed": true,
"readOnly": true,
"required": false,
"displayName": "id",
"defaultMatch": true
},
{
"id": "Name",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Email",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Email",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Phone",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Phone",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Company",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Company",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Company Size",
"type": "options",
"display": true,
"options": [
{
"name": "1-10 people",
"value": "1-10 people"
},
{
"name": "11-50 people",
"value": "11-50 people"
},
{
"name": "51-200 people",
"value": "51-200 people"
},
{
"name": "201+ people",
"value": "201+ people"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Company Size",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "When do you need a solution?",
"type": "options",
"display": true,
"options": [
{
"name": "Immediately (within 2 weeks)",
"value": "Immediately (within 2 weeks)"
},
{
"name": "Soon (1-3 months)",
"value": "Soon (1-3 months)"
},
{
"name": "This year (3-12 months)",
"value": "This year (3-12 months)"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "When do you need a solution?",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "What's your role in the company?",
"type": "options",
"display": true,
"options": [
{
"name": "Owner/CEO/Founder",
"value": "Owner/CEO/Founder"
},
{
"name": "Manager/Director",
"value": "Manager/Director"
},
{
"name": "Employee/Team Member",
"value": "Employee/Team Member"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "What's your role in the company?",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Message",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Message",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Lead Score",
"type": "number",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Lead Score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "options",
"display": true,
"options": [
{
"name": "New",
"value": "New"
},
{
"name": "Contacted",
"value": "Contacted"
},
{
"name": "Qualified",
"value": "Qualified"
},
{
"name": "Unqualified",
"value": "Unqualified"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Meeting Scheduled",
"type": "options",
"display": true,
"options": [
{
"name": "Yes",
"value": "Yes"
},
{
"name": "No",
"value": "No"
},
{
"name": "Pending",
"value": "Pending"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Meeting Scheduled",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Meeting Date",
"type": "dateTime",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Meeting Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Notes",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Notes",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Created",
"type": "string",
"display": true,
"removed": true,
"readOnly": true,
"required": false,
"displayName": "Created",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Lead ID",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Lead ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Record ID",
"type": "string",
"display": true,
"removed": false,
"readOnly": true,
"required": false,
"displayName": "Record ID",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {
"typecast": true
},
"operation": "update"
},
"credentials": {
"airtableTokenApi": {
"id": "credential-id",
"name": "Airtable Personal Access Token account 4"
}
},
"typeVersion": 2.1
},
{
"id": "9aecb6e7-0af0-41a0-9be6-98c4c8379228",
"name": "Send Booking ConfirmationEmail",
"type": "n8n-nodes-base.sendGrid",
"position": [
2528,
1216
],
"parameters": {
"subject": "Your AI automation consultation is confirmed!",
"toEmail": "={{ \n String(\n $json.fields?.Email \n || $('Parse Call Outcome').item.json.lead_email \n || $items('Call Outcome Webhook',0,0).json.body?.variables?.metadata?.lead_email \n || ''\n ).trim()\n}}\n",
"fromName": "Your Name - AI Automation",
"resource": "mail",
"fromEmail": "user@example.com",
"contentType": "text/html",
"contentValue": "=Hi {{ \n $json.fields?.Name \n || $('Parse Call Outcome').item.json.lead_name \n || 'there'\n}}, Great talking with you today! Your 15-minute AI automation consultation is confirmed for {{ $('Parse Call Outcome').item.json.agreed_time_text }}\n{{ (() => {\n const ev = $('Create Calendar event').item.json;\n if (!ev?.start?.dateTime) return ''; // nothing to add\n const tz = ev.start.timeZone || 'UTC';\n const pretty = new Date(ev.start.dateTime).toLocaleString('en-GB', {\n weekday: 'long', day: 'numeric', month: 'short', year: 'numeric',\n hour: 'numeric', minute: '2-digit', timeZone: tz\n });\n return `, ${pretty}`; // prepend the comma only if we have a value\n})() }}\n. I've sent you a calendar invite. Looking forward to our conversation!",
"additionalFields": {}
},
"credentials": {
"sendGridApi": {
"id": "credential-id",
"name": "SendGrid account"
}
},
"typeVersion": 1
},
{
"id": "0e48d014-b418-4040-a4a6-17b567a2906f",
"name": "Update Call Record",
"type": "n8n-nodes-base.airtable",
"position": [
3024,
640
],
"parameters": {
"base": {
"__rl": true,
"mode": "list",
"value": "appXXXXXXXXXXXXXX",
"cachedResultUrl": "https://airtable.com/appXXXXXXXXXXXXXX",
"cachedResultName": "Your Lead Base"
},
"table": {
"__rl": true,
"mode": "list",
"value": "tblXXXXXXXXXXXXXX",
"cachedResultUrl": "https://airtable.com/appXXXXXXXXXXXXXX/tblXXXXXXXXXXXXXX",
"cachedResultName": "Leads"
},
"columns": {
"value": {
"Notes": "Call attempted - Email sent as follow-up",
"Status": "Contacted",
"Message": "={{ $('Create a record').item.json.fields.Message }}",
"Record ID": "={{ $('Create a record').item.json.fields['Record ID'] }}",
"Lead Score": 0,
"Meeting Scheduled": "No"
},
"schema": [
{
"id": "id",
"type": "string",
"display": true,
"removed": true,
"readOnly": true,
"required": false,
"displayName": "id",
"defaultMatch": true
},
{
"id": "Name",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Email",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Email",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Phone",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Phone",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Company",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Company",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Company Size",
"type": "options",
"display": true,
"options": [
{
"name": "1-10 people",
"value": "1-10 people"
},
{
"name": "11-50 people",
"value": "11-50 people"
},
{
"name": "51-200 people",
"value": "51-200 people"
},
{
"name": "201+ people",
"value": "201+ people"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Company Size",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "When do you need a solution?",
"type": "options",
"display": true,
"options": [
{
"name": "Immediately (within 2 weeks)",
"value": "Immediately (within 2 weeks)"
},
{
"name": "Soon (1-3 months)",
"value": "Soon (1-3 months)"
},
{
"name": "This year (3-12 months)",
"value": "This year (3-12 months)"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "When do you need a solution?",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "What's your role in the company?",
"type": "options",
"display": true,
"options": [
{
"name": "Owner/CEO/Founder",
"value": "Owner/CEO/Founder"
},
{
"name": "Manager/Director",
"value": "Manager/Director"
},
{
"name": "Employee/Team Member",
"value": "Employee/Team Member"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "What's your role in the company?",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Message",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Message",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Lead Score",
"type": "number",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Lead Score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "options",
"display": true,
"options": [
{
"name": "New",
"value": "New"
},
{
"name": "Contacted",
"value": "Contacted"
},
{
"name": "Qualified",
"value": "Qualified"
},
{
"name": "Unqualified",
"value": "Unqualified"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Meeting Scheduled",
"type": "options",
"display": true,
"options": [
{
"name": "Yes",
"value": "Yes"
},
{
"name": "No",
"value": "No"
},
{
"name": "Pending",
"value": "Pending"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Meeting Scheduled",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Meeting Date",
"type": "dateTime",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Meeting Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Notes",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Notes",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Created",
"type": "string",
"display": true,
"removed": true,
"readOnly": true,
"required": false,
"displayName": "Created",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Lead ID",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Lead ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Record ID",
"type": "string",
"display": true,
"removed": false,
"readOnly": true,
"required": false,
"displayName": "Record ID",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"Record ID"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "update"
},
"credentials": {
"airtableTokenApi": {
"id": "credential-id",
"name": "Airtable Personal Access Token account 4"
}
},
"typeVersion": 2.1
},
{
"id": "5afcd2d8-fa53-45da-b66f-b990cfca279e",
"name": "Send Priority Email for Failed Calls",
"type": "n8n-nodes-base.sendGrid",
"position": [
1856,
1408
],
"parameters": {
"subject": "={{ $('Parse Call Outcome').item.json.lead_name }}, I tried calling about your AI automation inquiry",
"toEmail": "={{ \n String(\n $json.fields?.Email \n || $('Parse Call Outcome').item.json.lead_email \n || $items('Call Outcome Webhook',0,0).json.body?.variables?.metadata?.lead_email \n || ''\n ).trim()\n}}\n",
"fromName": "Your Name - AI Automation",
"resource": "mail",
"fromEmail": "user@example.com",
"contentType": "text/html",
"contentValue": "={{ \n $json.fields?.Name \n || $('Parse Call Outcome').item.json.lead_name \n || 'there'\n}},</p>\n\n<p>I just tried calling you about the AI automation inquiry you submitted for {{ $('Parse Call Outcome').item.json.lead_name }}. Unfortunately, I wasn't able to reach you.</p>\n\n<p>Based on your inquiry details, I can see {{ $('Parse Call Outcome').item.json.lead_name }} has strong automation potential, so I wanted to follow up personally.</p>\n\n<p><strong>I'd love to schedule a quick 15-minute call to discuss:</strong></p>\n<ul>\n<li>Your specific automation challenges</li>\n<li>How AI can streamline your processes</li>\n<li>ROI potential for your business size</li>\n</ul>\n\n<p>You can reply to this email with a few times that work for you this week, or feel free to call me back at your convenience.</p>\n\n<p>Best regards,<br>\nAbi<br>\nAI Automation Specialist<br>\nyour-email@example.com</p>",
"additionalFields": {}
},
"credentials": {
"sendGridApi": {
"id": "credential-id",
"name": "SendGrid account"
}
},
"typeVersion": 1
},
{
"id": "1da629d5-aa5e-45cf-abae-ba9d0b00fffe",
"name": "Update Failed Call Record",
"type": "n8n-nodes-base.airtable",
"position": [
2080,
1408
],
"parameters": {
"base": {
"__rl": true,
"mode": "list",
"value": "appXXXXXXXXXXXXXX",
"cachedResultUrl": "https://airtable.com/appXXXXXXXXXXXXXX",
"cachedResultName": "Your Lead Base"
},
"table": {
"__rl": true,
"mode": "list",
"value": "tblXXXXXXXXXXXXXX",
"cachedResultUrl": "https://airtable.com/appXXXXXXXXXXXXXX/tblXXXXXXXXXXXXXX",
"cachedResultName": "Leads"
},
"columns": {
"value": {
"Notes": "Call attempted - Email sent as follow-up",
"Status": "Contacted",
"Record ID": "{{ $json.record_id }}",
"Lead Score": 0,
"Meeting Scheduled": "No"
},
"schema": [
{
"id": "id",
"type": "string",
"display": true,
"removed": true,
"readOnly": true,
"required": false,
"displayName": "id",
"defaultMatch": true
},
{
"id": "Name",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Email",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Email",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Phone",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Phone",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Company",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Company",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Company Size",
"type": "options",
"display": true,
"options": [
{
"name": "1-10 people",
"value": "1-10 people"
},
{
"name": "11-50 people",
"value": "11-50 people"
},
{
"name": "51-200 people",
"value": "51-200 people"
},
{
"name": "201+ people",
"value": "201+ people"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Company Size",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "When do you need a solution?",
"type": "options",
"display": true,
"options": [
{
"name": "Immediately (within 2 weeks)",
"value": "Immediately (within 2 weeks)"
},
{
"name": "Soon (1-3 months)",
"value": "Soon (1-3 months)"
},
{
"name": "This year (3-12 months)",
"value": "This year (3-12 months)"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "When do you need a solution?",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "What's your role in the company?",
"type": "options",
"display": true,
"options": [
{
"name": "Owner/CEO/Founder",
"value": "Owner/CEO/Founder"
},
{
"name": "Manager/Director",
"value": "Manager/Director"
},
{
"name": "Employee/Team Member",
"value": "Employee/Team Member"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "What's your role in the company?",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Message",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Message",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Lead Score",
"type": "number",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Lead Score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "options",
"display": true,
"options": [
{
"name": "New",
"value": "New"
},
{
"name": "Contacted",
"value": "Contacted"
},
{
"name": "Qualified",
"value": "Qualified"
},
{
"name": "Unqualified",
"value": "Unqualified"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Meeting Scheduled",
"type": "options",
"display": true,
"options": [
{
"name": "Yes",
"value": "Yes"
},
{
"name": "No",
"value": "No"
},
{
"name": "Pending",
"value": "Pending"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Meeting Scheduled",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Meeting Date",
"type": "dateTime",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Meeting Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Notes",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Notes",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Created",
"type": "string",
"display": true,
"removed": true,
"readOnly": true,
"required": false,
"displayName": "Created",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Lead ID",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Lead ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Record ID",
"type": "string",
"display": true,
"removed": false,
"readOnly": true,
"required": false,
"displayName": "Record ID",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "update"
},
"credentials": {
"airtableTokenApi": {
"id": "credential-id",
"name": "Airtable Personal Access Token account 4"
}
},
"typeVersion": 2.1
},
{
"id": "119586d0-7a75-4193-90cb-2cf211424d96",
"name": "Respond to Webhook1",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
1184,
1504
],
"parameters": {
"options": {
"responseCode": 200
},
"respondWith": "json",
"responseBody": "={\n \"success\": true\n}"
},
"typeVersion": 1.4
},
{
"id": "383cf3b1-a428-4130-8d8a-57e4d06c8ce4",
"name": "Code",
"type": "n8n-nodes-base.code",
"position": [
1408,
1312
],
"parameters": {
"jsCode": "// Build a single truthy flag the IF node can use\nconst ok =\n $json.call_connected === true &&\n ($json.lead_interested === true || ($json.agreed_datetime ?? null) !== null);\n\n// Optional debug (shows in execution logs)\nconsole.log({\n call_connected: $json.call_connected,\n lead_interested: $json.lead_interested,\n agreed_datetime: $json.agreed_datetime,\n call_success: ok,\n});\n\nreturn [{ json: { ...$json, call_success: ok } }];\n"
},
"typeVersion": 2
},
{
"id": "848f4b8b-7c9b-4298-b2c8-21255f9d07fe",
"name": "Code1",
"type": "n8n-nodes-base.code",
"position": [
1856,
1216
],
"parameters": {
"jsCode": "// Build RFC3339 start/end datetimes for Google Calendar\n\n// Use agreed_datetime if present; else default to tomorrow 14:00 UTC\nlet start = $json.agreed_datetime\n ? new Date($json.agreed_datetime)\n : new Date(Date.now() + 24 * 60 * 60 * 1000);\n\nif (!$json.agreed_datetime) {\n // set default to 14:00 UTC\n start.setUTCHours(14, 0, 0, 0);\n}\n\n// 15-minute meeting by default\nconst end = new Date(start.getTime() + 15 * 60 * 1000);\n\n// Attach ISO strings for the Calendar node\nreturn [{\n json: {\n ...$json,\n start_iso: start.toISOString(), // e.g., 2025-09-26T13:00:00.000Z\n end_iso: end.toISOString(), // e.g., 2025-09-26T13:15:00.000Z\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "a7a4446b-716f-4fc4-a0cf-51a77d7b9ca9",
"name": "Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
160,
144
],
"parameters": {
"color": 4,
"width": 720,
"height": 1512,
"content": "### Qualify and Call Back Inbound Leads with OpenAI, Bland AI, Airtable & SendGrid\n\nThis n8n template demonstrates how to automatically capture, qualify, and follow up with inbound leads using AI — including an outbound voice agent that calls hot leads, books a meeting on Google Calendar, and confirms by email — reducing speed-to-lead from hours to seconds.\n\n### Use Cases\n\n- Founders and agencies losing inbound leads because no one follows up fast enough\n- Sales teams that want voice follow-up on hot leads without hiring an SDR\n- Businesses running paid ads to a form/quiz and needing instant qualification + booking\n\n### How It Works\n\n1. **Trigger**: Webhook receives the lead from your form, quiz, or landing page (expects `name`, `email`, `phone`, `company`, and any qualification fields).\n2. **Validate & Log**:\n - Code node normalises and cleans the payload\n - IF node rejects submissions missing required fields\n - Lead is written to Airtable\n3. **AI Qualification**: OpenAI scores the lead and returns one of three next actions:\n - `nurture_email`\n - `priority_email`\n - `ai_call`\n4. **Routing**:\n - If low-intent:\n - Sends a nurture email via SendGrid\n - If medium-intent:\n - Sends a priority email via SendGrid\n - If high-intent:\n - Pulls free slots from Google Calendar\n - Formats them into a natural-speech sentence\n - Triggers a Bland AI voice agent to call the lead with a qualification + booking script\n5. **Call Outcome Handling**: A second webhook in the same workflow receives Bland AI's post-call payload:\n - If a slot was booked:\n - Creates the Google Calendar event\n - Updates the Airtable record with call outcome and meeting time\n - Sends a booking confirmation email via SendGrid\n - If the call failed (no answer, declined, error):\n - Updates the Airtable record with the failure reason\n - Sends a priority follow-up email so a human can step in\n\n### Customisation Options\n\n- Swap Bland AI for Vapi, Retell, or any voice agent with HTTP + callback support\n- Replace SendGrid with Gmail, Postmark, or Resend\n- Replace Airtable with Notion, Google Sheets, or HubSpot\n- Add a Slack node to ping the team in parallel when a hot lead comes in\n- Tune the qualification prompt and voice script to match your ICP and offer\n- Add a priority threshold so only deals over a certain size trigger the voice call\n\n### Prerequisites/Credential Setup\n\nTo use this workflow securely, you'll need the following credentials set up in n8n:\n\n- **OpenAI API** – for the lead qualification node\n- **Airtable Personal Access Token** – to write and update the `Leads` table\n- **SendGrid API** – to send nurture, priority, and confirmation emails\n- **Google Calendar OAuth2** – to read availability and create booked events\n- **Bland AI API key** – via an HTTP Header Auth credential in n8n; used by the **Trigger AI Phone Call** node\n\n### Secure Configuration\n\n- All credential fields use **n8n Credential Types**\n- No API keys, base IDs, or webhook URLs are hardcoded\n- The Bland AI callback URL must be set to your production **Call Outcome Webhook** URL before going live\n\n### Why This Helps\n\n- Cuts speed-to-lead from hours to under a minute on hot leads\n- Removes the manual \"review the form submission\" step entirely\n- Books meetings without a human ever picking up the phone\n- Maintains a clean Airtable audit trail of every lead, qualification decision, and call outcome\n\n---\n\nWith this template, founders and small sales teams can qualify and follow up with every inbound lead automatically, including voice outreach on the highest-intent ones — without adding headcount.\n"
},
"typeVersion": 1
},
{
"id": "8b434062-0fcc-4040-81df-f8c899086468",
"name": "Section — Intake & validation",
"type": "n8n-nodes-base.stickyNote",
"position": [
912,
320
],
"parameters": {
"color": 5,
"width": 1188,
"height": 568,
"content": "### 1. Intake & validation\n- **Webhook** receives the form payload (expects `name`, `email`, `phone`, `company`, etc.)\n- **Validate & Clean Data** normalises fields and flags missing required values\n- **Check Validation Status** routes invalid leads back to the form\n- **Create a record** writes the validated lead to Airtable\n- **Respond to Webhook** returns a 200 to the form so the user sees the thank-you state"
},
"typeVersion": 1
},
{
"id": "5ddd4200-adfd-415e-8e00-293492dbeed6",
"name": "Section — AI qualification & routing",
"type": "n8n-nodes-base.stickyNote",
"position": [
2128,
288
],
"parameters": {
"color": 6,
"width": 736,
"height": 552,
"content": "### 2. AI qualification\n- **AI Lead Qualification** (OpenAI) scores the lead and returns a recommended next action: `nurture_email`, `priority_email`, or `ai_call`.\n- **Check for Follow-Up Sequence** branches cold leads to the nurture path.\n- **Check for AI Calling** branches hot leads to the Bland AI voice path.\n\nTune the prompt inside the OpenAI node to match your qualification criteria."
},
"typeVersion": 1
},
{
"id": "e5b59b02-d371-43f1-a342-f65eeed5065d",
"name": "Section — AI voice call",
"type": "n8n-nodes-base.stickyNote",
"position": [
2896,
288
],
"parameters": {
"color": 3,
"width": 848,
"height": 552,
"content": "### 3. AI voice call (Bland AI)\n- **Get availability in a calendar** pulls free slots from Google Calendar.\n- **Code in JavaScript** formats those slots into a human sentence for the voice agent.\n- **Trigger AI Phone Call** posts to Bland AI with the script, the lead's number, and the n8n callback URL (the second webhook in this workflow).\n\nReplace the `webhook` URL in the Bland AI body with your live Call Outcome webhook URL before going live."
},
"typeVersion": 1
},
{
"id": "f9365c7e-4772-46d3-ba2a-637caf2548b6",
"name": "Section — Call outcome & booking",
"type": "n8n-nodes-base.stickyNote",
"position": [
912,
1056
],
"parameters": {
"color": 7,
"width": 1864,
"height": 572,
"content": "### 4. Call outcome → booking\n- **Call Outcome Webhook** receives Bland AI's post-call payload.\n- **Parse Call Outcome** extracts the booked slot, lead email, and call status.\n- **Check Call Success** branches the booked path vs. the failed path.\n\n**Success path:** create the Google Calendar event → log the call in Airtable → send the booking confirmation email via SendGrid.\n\n**Failure path:** update the Airtable record and send a priority follow-up email so a human can step in."
},
"typeVersion": 1
},
{
"parameters": {
"operation": "verify",
"email": "={{ $node[\"Validate & Clean Data\"].json.lead.email }}",
"additionalOptions": {}
},
"type": "n8n-nodes-billionverify.billionVerify",
"typeVersion": 1,
"position": [
3112,
640
],
"name": "Verify Email (BillionVerify)",
"credentials": {
"billionVerifyApi": {
"id": "",
"name": "BillionVerify account"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "is-deliverable",
"leftValue": "={{ $json.is_deliverable }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
]
}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
3292,
640
],
"name": "IF deliverable"
},
{
"parameters": {
"operation": "verify",
"email": "={{ $('Validate & Clean Data').item.json.lead.email }}",
"additionalOptions": {}
},
"type": "n8n-nodes-billionverify.billionVerify",
"typeVersion": 1,
"position": [
2072,
640
],
"name": "Verify Email (BillionVerify) 2",
"credentials": {
"billionVerifyApi": {
"id": "",
"name": "BillionVerify account"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "is-deliverable",
"leftValue": "={{ $json.is_deliverable }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
]
}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
2252,
640
],
"name": "IF deliverable 2"
},
{
"parameters": {
"operation": "verify",
"email": "={{ \n String(\n $json.fields?.Email \n || $('Parse Call Outcome').item.json.lead_email \n || $items('Call Outcome Webhook',0,0).json.body?.variables?.metadata?.lead_email \n || ''\n ).trim()\n}}\n",
"additionalOptions": {}
},
"type": "n8n-nodes-billionverify.billionVerify",
"typeVersion": 1,
"position": [
2168,
1216
],
"name": "Verify Email (BillionVerify) 3",
"credentials": {
"billionVerifyApi": {
"id": "",
"name": "BillionVerify account"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "is-deliverable",
"leftValue": "={{ $json.is_deliverable }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
]
}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
2348,
1216
],
"name": "IF deliverable 3"
},
{
"parameters": {
"operation": "verify",
"email": "={{ \n String(\n $json.fields?.Email \n || $('Parse Call Outcome').item.json.lead_email \n || $items('Call Outcome Webhook',0,0).json.body?.variables?.metadata?.lead_email \n || ''\n ).trim()\n}}\n",
"additionalOptions": {}
},
"type": "n8n-nodes-billionverify.billionVerify",
"typeVersion": 1,
"position": [
1496,
1408
],
"name": "Verify Email (BillionVerify) 4",
"credentials": {
"billionVerifyApi": {
"id": "",
"name": "BillionVerify account"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "is-deliverable",
"leftValue": "={{ $json.is_deliverable }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
]
}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1676,
1408
],
"name": "IF deliverable 4"
}
],
"connections": {
"Code": {
"main": [
[
{
"node": "Check Call Success",
"type": "main",
"index": 0
}
]
]
},
"Code1": {
"main": [
[
{
"node": "Create Calendar event",
"type": "main",
"index": 0
}
]
]
},
"Webhook": {
"main": [
[
{
"node": "Validate & Clean Data",
"type": "main",
"index": 0
}
]
]
},
"Create a record": {
"main": [
[
{
"node": "Check for AI Calling",
"type": "main",
"index": 0
}
]
]
},
"Check Call Success": {
"main": [
[
{
"node": "Code1",
"type": "main",
"index": 0
}
],
[
{
"node": "Verify Email (BillionVerify) 4",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript": {
"main": [
[
{
"node": "Trigger AI Phone Call",
"type": "main",
"index": 0
}
]
]
},
"Parse Call Outcome": {
"main": [
[
{
"node": "Code",
"type": "main",
"index": 0
}
]
]
},
"Update Call Record": {
"main": [
[
{
"node": "Check for Follow-Up Sequence",
"type": "main",
"index": 0
}
]
]
},
"Send Priority Email": {
"main": [
[
{
"node": "Update Call Record",
"type": "main",
"index": 0
}
]
]
},
"Call Outcome Webhook": {
"main": [
[
{
"node": "Parse Call Outcome",
"type": "main",
"index": 0
},
{
"node": "Respond to Webhook1",
"type": "main",
"index": 0
}
]
]
},
"Check for AI Calling": {
"main": [
[
{
"node": "Get availability in a calendar",
"type": "main",
"index": 0
}
],
[
{
"node": "Verify Email (BillionVerify) 2",
"type": "main",
"index": 0
}
]
]
},
"AI Lead Qualification": {
"main": [
[
{
"node": "Check Validation Status",
"type": "main",
"index": 0
}
]
]
},
"Create Calendar event": {
"main": [
[
{
"node": "Add Successful Call Record",
"type": "main",
"index": 0
}
]
]
},
"Validate & Clean Data": {
"main": [
[
{
"node": "AI Lead Qualification",
"type": "main",
"index": 0
}
]
]
},
"Check Validation Status": {
"main": [
[
{
"node": "Create a record",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
},
"Add Successful Call Record": {
"main": [
[
{
"node": "Verify Email (BillionVerify) 3",
"type": "main",
"index": 0
}
]
]
},
"Check for Follow-Up Sequence": {
"main": [
[
{
"node": "Verify Email (BillionVerify)",
"type": "main",
"index": 0
}
]
]
},
"Get availability in a calendar": {
"main": [
[
{
"node": "Code in JavaScript",
"type": "main",
"index": 0
}
]
]
},
"Send Priority Email for Failed Calls": {
"main": [
[
{
"node": "Update Failed Call Record",
"type": "main",
"index": 0
}
]
]
},
"Verify Email (BillionVerify)": {
"main": [
[
{
"node": "IF deliverable",
"type": "main",
"index": 0
}
]
]
},
"IF deliverable": {
"main": [
[
{
"node": "Send Nurture Email",
"type": "main",
"index": 0
}
],
[]
]
},
"Verify Email (BillionVerify) 2": {
"main": [
[
{
"node": "IF deliverable 2",
"type": "main",
"index": 0
}
]
]
},
"IF deliverable 2": {
"main": [
[
{
"node": "Send Priority Email",
"type": "main",
"index": 0
}
],
[]
]
},
"Verify Email (BillionVerify) 3": {
"main": [
[
{
"node": "IF deliverable 3",
"type": "main",
"index": 0
}
]
]
},
"IF deliverable 3": {
"main": [
[
{
"node": "Send Booking ConfirmationEmail",
"type": "main",
"index": 0
}
],
[]
]
},
"Verify Email (BillionVerify) 4": {
"main": [
[
{
"node": "IF deliverable 4",
"type": "main",
"index": 0
}
]
]
},
"IF deliverable 4": {
"main": [
[
{
"node": "Send Priority Email for Failed Calls",
"type": "main",
"index": 0
}
],
[]
]
}
},
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
}
}When to use this
- Cleaning a list before a Bland AI send or sync.
- Protecting Bland AI deliverability and sender reputation.
- Keeping bounce rates low so your sending is never throttled.
FAQ
Why verify before sending in Bland AI?
Verifying first keeps your bounce rate low, which protects your sender reputation and your results.
How do I import this workflow?
Download the JSON, then in n8n go to Workflows → Import from File (or paste it). Install the BillionVerify community node and add your API key credential.
What happens to risky or catch-all addresses?
They are routed to the false branch and excluded from the send. You decide whether to retry, review, or drop them.
Add verification to your workflow
Create a free account, grab your API key, and stop bounces before they happen.
Get started free