Lead Intake & Booking Automation by mr SHADOWLead Intake & Booking Automation by mr SHADOW

Lead Intake & Booking Automation

Portfolio project
mr SHADOW
main

Lead Intake & Booking Automation.json

BlameMore file actions

Latest commit

Oct 13, 2026
1884d7e · Oct 13, 2026

History

Open commit details
History
3688 lines (3688 loc) · 149 KB
Top
Copy path

Lead Intake & Booking Automation.json

File metadata and controls

Code
Blame
3688 lines (3688 loc) · 149 KB
Open symbols panel
Edit and raw actions
{ "name": "Lead Intake & Booking Automation", "nodes": [ { "parameters": { "formTitle": "Book a Service / Request a Callback", "formDescription": "Tell us how we can help and we will get back to you shortly.", "formFields": { "values": [ { "fieldLabel": "Full Name", "requiredField": true }, { "fieldLabel": "Email", "fieldType": "email", "requiredField": true }, { "fieldLabel": "Phone", "requiredField": true }, { "fieldLabel": "Service Type", "fieldType": "dropdown", "fieldOptions": { "values": [ { "option": "General Inquiry" }, { "option": "Booking" }, { "option": "Support" }, { "option": "Emergency" } ] }, "requiredField": true }, { "fieldLabel": "Urgency", "fieldType": "dropdown", "fieldOptions": { "values": [ { "option": "Low" }, { "option": "Normal" }, { "option": "High" }, { "option": "Urgent" } ] } }, { "fieldLabel": "Preferred Date", "fieldType": "date" }, { "fieldLabel": "Message", "fieldType": "textarea" }, { "fieldType": "hiddenField", "fieldName": "source" } ] }, "options": { "buttonLabel": "Submit request", "path": "lead-intake", "respondWithOptions": { "values": { "formSubmittedText": "Thank you! Your request has been received. Check your inbox for a confirmation email with your reference number." } }, "ignoreBots": true } }, "id": "c9c8b1d2-2084-4730-949b-21f7fab753ff", "name": "Lead Intake Form", "type": "n8n-nodes-base.formTrigger", "typeVersion": 2.6, "position": [ 0, 96 ], "webhookId": "b3d2ca8a-a27c-40b7-9337-13fe0514263c" }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "const j = $json;\nconst raw = (k) => (j[k] === undefined || j[k] === null) ? '' : String(j[k]);\n// Safety: strip HTML tags, angle brackets and links from customer text so it cannot be used to send spam or phishing through our emails\nconst stripUnsafe = (v) => String(v).replace(/<[^>]*>/g, ' ').replace(/[<>]/g, ' ').replace(/\b(?:https?:\/\/|www\.)\S+/gi, '[link removed]');\nconst clean = (k) => stripUnsafe(raw(k)).replace(/\s+/g, ' ').trim();\nconst fullName = clean('Full Name');\nconst email = clean('Email').replace(/\s/g, '').toLowerCase();\n// Phone normalized to digits only (e.g. +961 70-123 456 -> 96170123456) so it stores and matches consistently\nconst phone = clean('Phone').replace(/[^0-9]/g, '');\nconst serviceType = clean('Service Type');\nconst urgency = clean('Urgency') || 'Normal';\nconst preferredDate = clean('Preferred Date');\nconst message = stripUnsafe(raw('Message')).replace(/[ \t]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim();\n\n// Source tracking: ?source=Facebook on the form URL, defaults to Website\nconst SOURCES = ['Website', 'Facebook', 'Instagram', 'WhatsApp', 'Google', 'Manual', 'Other'];\nconst srcRaw = clean('source');\nconst source = srcRaw ? (SOURCES.find(s => s.toLowerCase() === srcRaw.toLowerCase()) || 'Other') : 'Website';\n\nconst missing = [];\nif (!fullName) missing.push('Full Name');\nif (!email) missing.push('Email');\nif (!phone) missing.push('Phone');\nif (!serviceType) missing.push('Service Type');\nconst emailValid = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email);\nif (email && !emailValid) missing.push('Valid Email');\nif (phone && phone.replace(/[^0-9]/g, '').length < 7) missing.push('Valid Phone');\n// Length limits (spam / abuse protection)\nif (fullName.length > 100) missing.push('Full Name too long (max 100)');\nif (email.length > 254) missing.push('Email too long');\nif (phone.length > 20) missing.push('Phone too long (max 20 digits)');\nif (message.length > 2000) missing.push('Message too long (max 2000)');\nif (preferredDate.length > 40) missing.push('Preferred Date invalid');\nconst isValid = missing.length === 0;\nreturn {\n json: {\n fullName, email, phone, serviceType, urgency, preferredDate, message, source,\n isValid,\n missingFields: missing.join(', '),\n validationError: isValid ? '' : ('Missing or invalid fields: ' + missing.join(', ')),\n submittedAt: new Date().toISOString(),\n },\n};" }, "id": "01a511ac-7ab2-4b87-9729-259317ffc0e2", "name": "Validate & Normalize", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -112, 128 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose", "version": 2 }, "combinator": "and", "conditions": [ { "id": "is-valid", "leftValue": "={{ $json.isValid }}", "rightValue": true, "operator": { "type": "boolean", "operation": "equals" } } ] }, "options": {} }, "id": "93d1335d-5061-4969-952f-7e2c5d9f8c10", "name": "Valid?", "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ 112, 128 ] }, { "parameters": { "assignments": { "assignments": [ { "id": "lead-id", "name": "leadId", "type": "string", "value": "={{ "LEAD-" + $now.toFormat("yyyyLLdd") + "-" + $execution.id + "-" + Math.floor(Math.random() * 9000 + 1000) }}" } ] }, "includeOtherFields": true, "options": {} }, "id": "fcd239a6-7e35-42d4-9bc6-c6ad8abf9b73", "name": "Generate Lead ID", "type": "n8n-nodes-base.set", "typeVersion": 3.5, "position": [ 416, 224 ] }, { "parameters": { "documentId": { "__rl": true, "value": "https://docs.google.com/spreadsheets/d/1zbH0BDvjm8bD7n1bGoqwQ0OejbRan4Fi6WPTI8Hy4yE/edit?usp=drivesdk", "mode": "url" }, "sheetName": { "__rl": true, "mode": "name", "value": "Leads" }, "options": {} }, "id": "84d14dd8-8fbe-4812-b4a2-939617973ad2", "name": "Check Duplicate", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 640, 224 ], "executeOnce": true, "alwaysOutputData": true, "credentials": { "googleSheetsOAuth2Api": { "id": "mBDySotAeocOEtec", "name": "Google Sheets account" } }, "onError": "continueRegularOutput" }, { "parameters": { "jsCode": "// Compare email (case-insensitive) and phone (digits only, leading zeros ignored) against every existing lead\nconst lead = $('Generate Lead ID').first().json;\nconst normEmail = (v) => String(v || '').replace(/\s/g, '').toLowerCase();\nconst normPhone = (v) => String(v || '').replace(/[^0-9]/g, '').replace(/^0+/, '');\nconst email = normEmail(lead.email);\nconst phone = normPhone(lead.phone);\nconst rows = $('Check Duplicate').all().filter(i => {\n const r = i.json || {};\n if (!r.LeadID && !r.Email && !r.Phone) return false;\n const emailMatch = email && normEmail(r.Email) === email;\n const phoneMatch = phone.length >= 7 && normPhone(r.Phone) === phone;\n return emailMatch || phoneMatch;\n});\nconst isDuplicate = rows.length > 0;\nconst matchedLeadIds = rows.map(r => r.json.LeadID).filter(Boolean).join(', ');\nreturn [{ json: Object.assign({}, lead, { isDuplicate, duplicateCount: rows.length, matchedLeadIds }) }];" }, "id": "d6520b5f-d1dd-4dae-80c2-2d40e97052c6", "name": "Evaluate Duplicate", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 864, 224 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose", "version": 2 }, "combinator": "and", "conditions": [ { "id": "is-dup", "leftValue": "={{ $json.isDuplicate }}", "rightValue": true, "operator": { "type": "boolean", "operation": "equals" } } ] }, "options": {} }, "id": "0d8c0961-9cbc-4857-9c41-74e749862edd", "name": "Duplicate?", "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ 1088, 224 ] }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "https://docs.google.com/spreadsheets/d/1zbH0BDvjm8bD7n1bGoqwQ0OejbRan4Fi6WPTI8Hy4yE/edit?usp=drivesdk", "mode": "url" }, "sheetName": { "__rl": true, "mode": "name", "value": "Log" }, "columns": { "mappingMode": "defineBelow", "value": { "Timestamp": "={{ $now.toISO() }}", "LeadID": "={{ $json.leadId }}", "Event": "Duplicate", "Status": "Skipped", "Priority": "", "Email": "={{ $json.email }}", "Phone": "={{ $json.phone }}", "Details": "={{ "Duplicate lead detected (" + $json.duplicateCount + " existing match(es): " + $json.matchedLeadIds + ") - not saved again" }}", "ErrorMessage": "", "ExecutionId": "={{ $execution.id }}" }, "schema": [ { "id": "Timestamp", "displayName": "Timestamp", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "LeadID", "displayName": "LeadID", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Event", "displayName": "Event", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Status", "displayName": "Status", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Priority", "displayName": "Priority", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Email", "displayName": "Email", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Phone", "displayName": "Phone", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Details", "displayName": "Details", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "ErrorMessage", "displayName": "ErrorMessage", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "ExecutionId", "displayName": "ExecutionId", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false } ] }, "options": {} }, "id": "1ace1af3-48aa-4592-94ed-3c622eb2a795", "name": "Log Duplicate", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 944, 128 ], "credentials": { "googleSheetsOAuth2Api": { "id": "mBDySotAeocOEtec", "name": "Google Sheets account" } }, "onError": "continueRegularOutput" }, { "parameters": { "sendTo": "fazeshadow001@gmail.com", "subject": "=Duplicate lead skipped: {{ $("Evaluate Duplicate").item.json.fullName }}", "message": "=<div style="margin:0;padding:24px 12px;background:#f3f4f6;font-family:'Segoe UI',Helvetica,Arial,sans-serif">\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:620px;margin:0 auto;background:#ffffff;border-radius:10px;overflow:hidden;border:1px solid #e5e7eb">\n<tr><td style="background:#475569;padding:22px 28px;color:#ffffff">\n<div style="font-size:11px;letter-spacing:1.5px;text-transform:uppercase;opacity:.85">Duplicate submission</div>\n<div style="font-size:21px;font-weight:600;margin-top:6px;line-height:1.3">Lead already in your CRM</div>\n<div style="font-size:14px;margin-top:6px;opacity:.92">{{ $("Evaluate Duplicate").item.json.fullName }} submitted the form again</div>\n</td></tr>\n<tr><td style="padding:26px 28px;color:#1f2937;font-size:15px;line-height:1.6">\n<div style="background:#f8fafc;border-left:4px solid #475569;padding:12px 16px;border-radius:4px;font-size:14px;margin:0 0 16px">This submission matches an existing lead, so it was <b>not saved again</b> and no new confirmation was sent.</div>\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;margin:4px 0 18px">\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Name</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top"><b>{{ $("Evaluate Duplicate").item.json.fullName }}</b></td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Email</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Evaluate Duplicate").item.json.email }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Phone</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Evaluate Duplicate").item.json.phone }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Service</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Evaluate Duplicate").item.json.serviceType }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Matches</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top"><b>{{ $("Evaluate Duplicate").item.json.matchedLeadIds }}</b></td></tr>\n</table>\n<p style="margin:0"><b>Suggested action:</b> open the existing record and reach out if the customer is still waiting &mdash; a repeat submission often means they want a faster reply.</p>\n</td></tr>\n<tr><td style="padding:16px 28px;background:#f9fafb;border-top:1px solid #e5e7eb;color:#6b7280;font-size:12px;line-height:1.5">Lead-to-Booking Automation &middot; Execution {{ $execution.id }}<br>Track progress in the Leads sheet: New &rarr; Contacted &rarr; Qualified &rarr; Appointment Booked &rarr; Won / Lost.</td></tr>\n</table></div>", "options": { "appendAttribution": false } }, "id": "5f5371db-63d3-4736-8127-a9bc40f2c824", "name": "Notify Owner: Duplicate", "type": "n8n-nodes-base.gmail", "typeVersion": 2.2, "position": [ 1168, 128 ], "webhookId": "82a86a4b-afe8-4d2c-85a2-10906c9bf49f", "credentials": { "gmailOAuth2": { "id": "CgKFdJBbgVLSVRI0", "name": "Gmail account 2" } }, "onError": "continueRegularOutput" }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "// Lead scoring (0-100) - adjust the weights here\nconst j = $json;\nconst urg = String(j.urgency || '').toLowerCase();\nconst svc = String(j.serviceType || '').toLowerCase();\nlet score = 0;\nif (urg.includes('urgent')) score += 40;\nelse if (urg.includes('high')) score += 30;\nelse if (urg.includes('normal')) score += 15;\nelse if (urg.includes('low')) score += 5;\nelse score += 10;\nif (svc.includes('emergency')) score += 30;\nelse if (svc.includes('booking')) score += 25;\nelse if (svc.includes('support')) score += 15;\nelse score += 10;\nif (j.preferredDate) {\n score += 10;\n const d = new Date(j.preferredDate);\n if (!isNaN(d.getTime())) {\n const days = (d.getTime() - Date.now()) / 86400000;\n if (days >= -1 && days <= 3) score += 10;\n }\n}\nif (String(j.message || '').length >= 30) score += 10;\nscore = Math.min(score, 100);\n\n// Priority: explicit urgency / service rules first, then score thresholds\nlet priority = 'Normal';\nif (urg.includes('urgent') || svc.includes('emergency') || score >= 85) priority = 'Urgent';\nelse if (urg.includes('high') || svc.includes('booking') || score >= 55) priority = 'High';\n\nconst actions = {\n Urgent: 'Call the customer within 1 hour, then set Status to Contacted.',\n High: 'Contact the customer today to confirm details and propose an appointment slot.',\n Normal: 'Reply within 1 business day. An automatic follow-up is sent if Status is still New.',\n};\nconst responseTimes = { Urgent: 'within the next hour', High: 'later today', Normal: 'within 1 business day' };\n\nreturn { json: Object.assign({}, j, {\n priority,\n leadScore: score,\n status: 'New',\n duplicate: 'No',\n recommendedAction: actions[priority],\n responseTime: responseTimes[priority],\n}) };" }, "id": "bce20d45-cdb1-4fe7-9f19-d76a03b2e33a", "name": "Classify Lead", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 944, 704 ] }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "https://docs.google.com/spreadsheets/d/1zbH0BDvjm8bD7n1bGoqwQ0OejbRan4Fi6WPTI8Hy4yE/edit?usp=drivesdk", "mode": "url" }, "sheetName": { "__rl": true, "mode": "name", "value": "Leads" }, "columns": { "mappingMode": "defineBelow", "value": { "LeadID": "={{ $json.leadId }}", "Timestamp": "={{ $json.submittedAt }}", "FullName": "={{ $json.fullName }}", "Email": "={{ $json.email }}", "Phone": "={{ $json.phone }}", "ServiceType": "={{ $json.serviceType }}", "Urgency": "={{ $json.urgency }}", "PreferredDate": "={{ $json.preferredDate }}", "Message": "={{ $json.message }}", "Priority": "={{ $json.priority }}", "Status": "={{ $json.status }}", "LeadScore": "={{ $json.leadScore }}", "Source": "={{ $json.source }}", "Duplicate": "={{ $json.duplicate }}", "FollowUpSentAt": "", "AppointmentAt": "", "LastUpdated": "={{ $now.toISO() }}" }, "schema": [ { "id": "LeadID", "displayName": "LeadID", "type": "string", "canBeUsedToMatch": true, "display": true, "required": false }, { "id": "Timestamp", "displayName": "Timestamp", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "FullName", "displayName": "FullName", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Email", "displayName": "Email", "type": "string", "canBeUsedToMatch": true, "display": true, "required": false }, { "id": "Phone", "displayName": "Phone", "type": "string", "canBeUsedToMatch": true, "display": true, "required": false }, { "id": "ServiceType", "displayName": "ServiceType", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Urgency", "displayName": "Urgency", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "PreferredDate", "displayName": "PreferredDate", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Message", "displayName": "Message", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Priority", "displayName": "Priority", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Status", "displayName": "Status", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "LeadScore", "displayName": "LeadScore", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Source", "displayName": "Source", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Duplicate", "displayName": "Duplicate", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "FollowUpSentAt", "displayName": "FollowUpSentAt", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "AppointmentAt", "displayName": "AppointmentAt", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "LastUpdated", "displayName": "LastUpdated", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false } ] }, "options": { "cellFormat": "RAW" } }, "id": "e4d3c39f-8b2b-4ec9-86c1-2c795bdbc235", "name": "Save Lead", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 2048, 208 ], "credentials": { "googleSheetsOAuth2Api": { "id": "mBDySotAeocOEtec", "name": "Google Sheets account" } }, "onError": "continueErrorOutput" }, { "parameters": { "rules": { "values": [ { "conditions": { "options": { "caseSensitive": false, "leftValue": "", "typeValidation": "loose", "version": 1 }, "combinator": "and", "conditions": [ { "leftValue": "={{ $("Classify Lead").item.json.priority }}", "rightValue": "Urgent", "operator": { "type": "string", "operation": "equals" } } ] }, "renameOutput": true, "outputKey": "Urgent" }, { "conditions": { "options": { "caseSensitive": false, "leftValue": "", "typeValidation": "loose", "version": 1 }, "combinator": "and", "conditions": [ { "leftValue": "={{ $("Classify Lead").item.json.priority }}", "rightValue": "High", "operator": { "type": "string", "operation": "equals" } } ] }, "renameOutput": true, "outputKey": "High" }, { "conditions": { "options": { "caseSensitive": false, "leftValue": "", "typeValidation": "loose", "version": 1 }, "combinator": "and", "conditions": [ { "leftValue": "={{ $("Classify Lead").item.json.priority }}", "rightValue": "Normal", "operator": { "type": "string", "operation": "equals" } } ] }, "renameOutput": true, "outputKey": "Normal" } ] }, "options": { "fallbackOutput": "extra", "renameFallbackOutput": "Normal (fallback)" } }, "id": "0ed4a7c6-880d-46c1-9150-1373b2c79dba", "name": "Route by Priority", "type": "n8n-nodes-base.switch", "typeVersion": 3.4, "position": [ 2272, -16 ] }, { "parameters": { "sendTo": "fazeshadow001@gmail.com", "subject": "=[URGENT] New lead: {{ $("Classify Lead").item.json.fullName }} - {{ $("Classify Lead").item.json.serviceType }} ({{ $("Classify Lead").item.json.leadId }})", "message": "=<div style="margin:0;padding:24px 12px;background:#f3f4f6;font-family:'Segoe UI',Helvetica,Arial,sans-serif">\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:620px;margin:0 auto;background:#ffffff;border-radius:10px;overflow:hidden;border:1px solid #e5e7eb">\n<tr><td style="background:#b91c1c;padding:22px 28px;color:#ffffff">\n<div style="font-size:11px;letter-spacing:1.5px;text-transform:uppercase;opacity:.85">Urgent lead</div>\n<div style="font-size:21px;font-weight:600;margin-top:6px;line-height:1.3">Respond within 1 hour</div>\n<div style="font-size:14px;margin-top:6px;opacity:.92">{{ $("Classify Lead").item.json.fullName }} &middot; {{ $("Classify Lead").item.json.serviceType }}</div>\n</td></tr>\n<tr><td style="padding:26px 28px;color:#1f2937;font-size:15px;line-height:1.6">\n<div style="background:#f9fafb;border-left:4px solid #b91c1c;padding:12px 16px;border-radius:4px;font-size:14px;margin:0 0 16px"><b>Recommended next action:</b> {{ $("Classify Lead").item.json.recommendedAction }}</div>\n<div style="font-size:12px;letter-spacing:1px;text-transform:uppercase;color:#9ca3af;margin-bottom:4px">Customer</div>\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;margin:4px 0 18px">\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Name</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top"><b>{{ $("Classify Lead").item.json.fullName }}</b></td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Phone</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top"><a href="tel:{{ $("Classify Lead").item.json.phone }}" style="color:#2563eb;text-decoration:none">{{ $("Classify Lead").item.json.phone }}</a></td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Email</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top"><a href="mailto:{{ $("Classify Lead").item.json.email }}" style="color:#2563eb;text-decoration:none">{{ $("Classify Lead").item.json.email }}</a></td></tr>\n</table>\n<div style="font-size:12px;letter-spacing:1px;text-transform:uppercase;color:#9ca3af;margin-bottom:4px">Request</div>\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;margin:4px 0 18px">\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Lead ID</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Classify Lead").item.json.leadId }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Service</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Classify Lead").item.json.serviceType }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Priority</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top"><b>{{ $("Classify Lead").item.json.priority }}</b> &middot; score {{ $("Classify Lead").item.json.leadScore }}/100</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Preferred date</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Classify Lead").item.json.preferredDate || "Not specified" }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Source</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Classify Lead").item.json.source }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Message</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Classify Lead").item.json.message || "No message provided" }}</td></tr>\n</table>\n<p style="font-size:13px;color:#6b7280;margin:0">To book this lead, set Status to <b>Qualified</b> and fill <b>AppointmentAt</b> (YYYY-MM-DD HH:mm) in the Leads sheet.</p>\n</td></tr>\n<tr><td style="padding:16px 28px;background:#f9fafb;border-top:1px solid #e5e7eb;color:#6b7280;font-size:12px;line-height:1.5">Lead-to-Booking Automation &middot; {{ $("Classify Lead").item.json.leadId }} &middot; Execution {{ $execution.id }}<br>Track progress in the Leads sheet: New &rarr; Contacted &rarr; Qualified &rarr; Appointment Booked &rarr; Won / Lost.</td></tr>\n</table></div>", "options": { "appendAttribution": false } }, "id": "ae54e1e1-136b-4b89-beec-f5a2e4a406b6", "name": "Notify Owner: Urgent", "type": "n8n-nodes-base.gmail", "typeVersion": 2.2, "position": [ 2496, -176 ], "webhookId": "793334cd-ba35-4ca8-a2db-93f8e9aac22c", "credentials": { "gmailOAuth2": { "id": "CgKFdJBbgVLSVRI0", "name": "Gmail account 2" } }, "onError": "continueErrorOutput" }, { "parameters": { "sendTo": "={{ $("Classify Lead").item.json.email }}", "subject": "=We received your request - {{ $("Classify Lead").item.json.serviceType }} (Ref {{ $("Classify Lead").item.json.leadId }})", "message": "=<div style="margin:0;padding:24px 12px;background:#f3f4f6;font-family:'Segoe UI',Helvetica,Arial,sans-serif">\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:620px;margin:0 auto;background:#ffffff;border-radius:10px;overflow:hidden;border:1px solid #e5e7eb">\n<tr><td style="background:#1e3a5f;padding:22px 28px;color:#ffffff">\n<div style="font-size:11px;letter-spacing:1.5px;text-transform:uppercase;opacity:.85">Request received</div>\n<div style="font-size:21px;font-weight:600;margin-top:6px;line-height:1.3">Thank you, {{ $("Classify Lead").item.json.fullName }}</div>\n<div style="font-size:14px;margin-top:6px;opacity:.92">We have received your request and will be in touch soon.</div>\n</td></tr>\n<tr><td style="padding:26px 28px;color:#1f2937;font-size:15px;line-height:1.6">\n<div style="text-align:center;margin:0 0 20px"><div style="display:inline-block;background:#eef2f7;border-radius:6px;padding:10px 18px"><span style="font-size:12px;color:#6b7280;text-transform:uppercase;letter-spacing:1px">Your reference</span><br><span style="font-size:18px;font-weight:600;color:#1e3a5f;letter-spacing:.5px">{{ $("Classify Lead").item.json.leadId }}</span></div></div>\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;margin:4px 0 18px">\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Service requested</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Classify Lead").item.json.serviceType }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Preferred date</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Classify Lead").item.json.preferredDate || "Not specified" }}</td></tr>\n</table>\n<div style="font-size:15px;font-weight:600;color:#111827;margin:0 0 10px">What happens next</div>\n<table role="presentation" cellpadding="0" cellspacing="0" style="margin:0 0 6px;font-size:14px">\n<tr><td style="vertical-align:top;padding:0 12px 10px 0"><span style="display:inline-block;width:24px;height:24px;line-height:24px;border-radius:12px;background:#1e3a5f;color:#fff;text-align:center;font-size:12px;font-weight:600">1</span></td><td style="padding:2px 0 10px">Our team reviews your request.</td></tr>\n<tr><td style="vertical-align:top;padding:0 12px 10px 0"><span style="display:inline-block;width:24px;height:24px;line-height:24px;border-radius:12px;background:#1e3a5f;color:#fff;text-align:center;font-size:12px;font-weight:600">2</span></td><td style="padding:2px 0 10px">We contact you {{ $("Classify Lead").item.json.responseTime }} by phone or email.</td></tr>\n<tr><td style="vertical-align:top;padding:0 12px 10px 0"><span style="display:inline-block;width:24px;height:24px;line-height:24px;border-radius:12px;background:#1e3a5f;color:#fff;text-align:center;font-size:12px;font-weight:600">3</span></td><td style="padding:2px 0 10px">Together we confirm a time that suits you.</td></tr>\n</table>\n<p style="margin:10px 0 0">If anything changes, just reply to this email and quote your reference number.</p>\n<p style="margin:22px 0 0">Kind regards,<br><b>The team</b></p>\n</td></tr>\n<tr><td style="padding:16px 28px;background:#f9fafb;border-top:1px solid #e5e7eb;color:#6b7280;font-size:12px;line-height:1.5">You are receiving this email because you submitted a request through our website. Simply reply to this email if you have any questions.</td></tr>\n</table></div>", "options": { "appendAttribution": false } }, "id": "ed697389-8eb0-4b75-a210-7fc0b5e1b6e6", "name": "Send Confirmation Email", "type": "n8n-nodes-base.gmail", "typeVersion": 2.2, "position": [ 2720, 0 ], "webhookId": "7c39773d-d305-4ac0-b4fe-d79824e663ad", "credentials": { "gmailOAuth2": { "id": "CgKFdJBbgVLSVRI0", "name": "Gmail account 2" } }, "onError": "continueErrorOutput" }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "https://docs.google.com/spreadsheets/d/1zbH0BDvjm8bD7n1bGoqwQ0OejbRan4Fi6WPTI8Hy4yE/edit?usp=drivesdk", "mode": "url" }, "sheetName": { "__rl": true, "mode": "name", "value": "Log" }, "columns": { "mappingMode": "defineBelow", "value": { "Timestamp": "={{ $now.toISO() }}", "LeadID": "={{ $("Classify Lead").item.json.leadId }}", "Event": "OwnerEmailFailed", "Status": "Failed", "Priority": "={{ $("Classify Lead").item.json.priority }}", "Email": "={{ $("Classify Lead").item.json.email }}", "Phone": "={{ $("Classify Lead").item.json.phone }}", "Details": "={{ "Owner notification failed at step: " + $prevNode.name + ". Customer confirmation is still attempted - check this lead manually." }}", "ErrorMessage": "={{ (typeof $json.error === "string" ? $json.error : ($json.error && $json.error.message)) || $json.message || "Unknown error" }}", "ExecutionId": "={{ $execution.id }}" }, "schema": [ { "id": "Timestamp", "displayName": "Timestamp", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "LeadID", "displayName": "LeadID", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Event", "displayName": "Event", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Status", "displayName": "Status", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Priority", "displayName": "Priority", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Email", "displayName": "Email", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Phone", "displayName": "Phone", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Details", "displayName": "Details", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "ErrorMessage", "displayName": "ErrorMessage", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "ExecutionId", "displayName": "ExecutionId", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false } ] }, "options": {} }, "id": "e4e30297-81c2-493d-8cf1-c158f11798fe", "name": "Log Owner Email Error", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 2048, 592 ], "credentials": { "googleSheetsOAuth2Api": { "id": "mBDySotAeocOEtec", "name": "Google Sheets account" } }, "onError": "continueRegularOutput" }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "https://docs.google.com/spreadsheets/d/1zbH0BDvjm8bD7n1bGoqwQ0OejbRan4Fi6WPTI8Hy4yE/edit?usp=drivesdk", "mode": "url" }, "sheetName": { "__rl": true, "mode": "name", "value": "Log" }, "columns": { "mappingMode": "defineBelow", "value": { "Timestamp": "={{ $now.toISO() }}", "LeadID": "={{ $("Classify Lead").item.json.leadId }}", "Event": "LeadProcessed", "Status": "={{ (() => { try { return $("Log Owner Email Error").all().length > 0; } catch (e) { return false; } })() ? "Partial" : "Success" }}", "Priority": "={{ $("Classify Lead").item.json.priority }}", "Email": "={{ $("Classify Lead").item.json.email }}", "Phone": "={{ $("Classify Lead").item.json.phone }}", "Details": "={{ "Lead saved (score " + $("Classify Lead").item.json.leadScore + ", source " + $("Classify Lead").item.json.source + "), " + ((() => { try { return $("Log Owner Email Error").all().length > 0; } catch (e) { return false; } })() ? "confirmation email sent, but OWNER NOTIFICATION FAILED - check this lead manually" : "owner notified and confirmation email sent") }}", "ErrorMessage": "", "ExecutionId": "={{ $execution.id }}" }, "schema": [ { "id": "Timestamp", "displayName": "Timestamp", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "LeadID", "displayName": "LeadID", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Event", "displayName": "Event", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Status", "displayName": "Status", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Priority", "displayName": "Priority", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Email", "displayName": "Email", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Phone", "displayName": "Phone", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Details", "displayName": "Details", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "ErrorMessage", "displayName": "ErrorMessage", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "ExecutionId", "displayName": "ExecutionId", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false } ] }, "options": {} }, "id": "3fa1674e-9481-4f7a-9914-245cc53e3e48", "name": "Log Success", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 2944, 0 ], "credentials": { "googleSheetsOAuth2Api": { "id": "mBDySotAeocOEtec", "name": "Google Sheets account" } }, "onError": "continueRegularOutput" }, { "parameters": { "jsCode": "const item = $input.first();\nconst j = (item && item.json) || {};\nconst err = j.error || {};\nlet lead = {};\ntry { lead = $('Classify Lead').first().json || {}; } catch (e) {}\nif (!lead.leadId) { try { lead = $('Generate Lead ID').first().json || {}; } catch (e) {} }\nlet failedStep = 'Unknown step';\ntry { failedStep = $prevNode.name; } catch (e) {}\nconst errorMessage = (typeof err === 'string' ? err : (err.message || j.message)) || 'Unknown error';\nconst area = /Save Lead/.test(failedStep) ? 'Google Sheets' : (/Notify|Confirmation/.test(failedStep) ? 'Email' : 'API');\nreturn [{ json: {\n leadId: lead.leadId || '',\n email: lead.email || '',\n phone: lead.phone || '',\n priority: lead.priority || '',\n failedStep,\n area,\n errorMessage,\n} }];" }, "id": "85db9a52-0962-4f37-9bb6-1073e7d2edcf", "name": "Format Error", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 2272, 400 ] }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "https://docs.google.com/spreadsheets/d/1zbH0BDvjm8bD7n1bGoqwQ0OejbRan4Fi6WPTI8Hy4yE/edit?usp=drivesdk", "mode": "url" }, "sheetName": { "__rl": true, "mode": "name", "value": "Log" }, "columns": { "mappingMode": "defineBelow", "value": { "Timestamp": "={{ $now.toISO() }}", "LeadID": "={{ $json.leadId }}", "Event": "Error", "Status": "Failed", "Priority": "={{ $json.priority }}", "Email": "={{ $json.email }}", "Phone": "={{ $json.phone }}", "Details": "={{ $json.area + " failure at step: " + $json.failedStep }}", "ErrorMessage": "={{ $json.errorMessage }}", "ExecutionId": "={{ $execution.id }}" }, "schema": [ { "id": "Timestamp", "displayName": "Timestamp", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "LeadID", "displayName": "LeadID", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Event", "displayName": "Event", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Status", "displayName": "Status", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Priority", "displayName": "Priority", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Email", "displayName": "Email", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Phone", "displayName": "Phone", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Details", "displayName": "Details", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "ErrorMessage", "displayName": "ErrorMessage", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "ExecutionId", "displayName": "ExecutionId", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false } ] }, "options": {} }, "id": "657aa905-d577-4608-847d-7dedaf6d90a0", "name": "Log Error", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 2496, 400 ], "credentials": { "googleSheetsOAuth2Api": { "id": "mBDySotAeocOEtec", "name": "Google Sheets account" } }, "onError": "continueRegularOutput" }, { "parameters": { "sendTo": "fazeshadow001@gmail.com", "subject": "=Lead workflow error ({{ $("Format Error").item.json.area }}) - action needed", "message": "=<div style="margin:0;padding:24px 12px;background:#f3f4f6;font-family:'Segoe UI',Helvetica,Arial,sans-serif">\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:620px;margin:0 auto;background:#ffffff;border-radius:10px;overflow:hidden;border:1px solid #e5e7eb">\n<tr><td style="background:#991b1b;padding:22px 28px;color:#ffffff">\n<div style="font-size:11px;letter-spacing:1.5px;text-transform:uppercase;opacity:.85">Action needed</div>\n<div style="font-size:21px;font-weight:600;margin-top:6px;line-height:1.3">A lead could not be processed</div>\n<div style="font-size:14px;margin-top:6px;opacity:.92">Failed in: {{ $("Format Error").item.json.area }}</div>\n</td></tr>\n<tr><td style="padding:26px 28px;color:#1f2937;font-size:15px;line-height:1.6">\n<div style="background:#fef2f2;border-left:4px solid #991b1b;padding:12px 16px;border-radius:4px;font-size:14px;margin:0 0 16px"><b>Error:</b> {{ $("Format Error").item.json.errorMessage }}</div>\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;margin:4px 0 18px">\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Lead ID</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Format Error").item.json.leadId }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Email</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Format Error").item.json.email }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Phone</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Format Error").item.json.phone }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Failed step</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Format Error").item.json.failedStep }}</td></tr>\n</table>\n<p style="margin:0"><b>What to do:</b> check the Log tab for this lead and contact the customer manually so the request is not lost.</p>\n</td></tr>\n<tr><td style="padding:16px 28px;background:#f9fafb;border-top:1px solid #e5e7eb;color:#6b7280;font-size:12px;line-height:1.5">Lead-to-Booking Automation &middot; Execution {{ $execution.id }}<br>Track progress in the Leads sheet: New &rarr; Contacted &rarr; Qualified &rarr; Appointment Booked &rarr; Won / Lost.</td></tr>\n</table></div>", "options": { "appendAttribution": false } }, "id": "17495574-67b2-44cb-9777-10bd23385724", "name": "Notify Owner: System Error", "type": "n8n-nodes-base.gmail", "typeVersion": 2.2, "position": [ 2720, 400 ], "webhookId": "ce2723c6-7a46-4bca-88eb-0e41fc510d76", "credentials": { "gmailOAuth2": { "id": "CgKFdJBbgVLSVRI0", "name": "Gmail account 2" } }, "onError": "continueRegularOutput" }, { "parameters": { "sendTo": "fazeshadow001@gmail.com", "subject": "=[HIGH] New lead: {{ $("Classify Lead").item.json.fullName }} - {{ $("Classify Lead").item.json.serviceType }} ({{ $("Classify Lead").item.json.leadId }})", "message": "=<div style="margin:0;padding:24px 12px;background:#f3f4f6;font-family:'Segoe UI',Helvetica,Arial,sans-serif">\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:620px;margin:0 auto;background:#ffffff;border-radius:10px;overflow:hidden;border:1px solid #e5e7eb">\n<tr><td style="background:#c2410c;padding:22px 28px;color:#ffffff">\n<div style="font-size:11px;letter-spacing:1.5px;text-transform:uppercase;opacity:.85">High-priority lead</div>\n<div style="font-size:21px;font-weight:600;margin-top:6px;line-height:1.3">Contact this lead today</div>\n<div style="font-size:14px;margin-top:6px;opacity:.92">{{ $("Classify Lead").item.json.fullName }} &middot; {{ $("Classify Lead").item.json.serviceType }}</div>\n</td></tr>\n<tr><td style="padding:26px 28px;color:#1f2937;font-size:15px;line-height:1.6">\n<div style="background:#f9fafb;border-left:4px solid #c2410c;padding:12px 16px;border-radius:4px;font-size:14px;margin:0 0 16px"><b>Recommended next action:</b> {{ $("Classify Lead").item.json.recommendedAction }}</div>\n<div style="font-size:12px;letter-spacing:1px;text-transform:uppercase;color:#9ca3af;margin-bottom:4px">Customer</div>\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;margin:4px 0 18px">\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Name</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top"><b>{{ $("Classify Lead").item.json.fullName }}</b></td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Phone</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top"><a href="tel:{{ $("Classify Lead").item.json.phone }}" style="color:#2563eb;text-decoration:none">{{ $("Classify Lead").item.json.phone }}</a></td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Email</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top"><a href="mailto:{{ $("Classify Lead").item.json.email }}" style="color:#2563eb;text-decoration:none">{{ $("Classify Lead").item.json.email }}</a></td></tr>\n</table>\n<div style="font-size:12px;letter-spacing:1px;text-transform:uppercase;color:#9ca3af;margin-bottom:4px">Request</div>\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;margin:4px 0 18px">\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Lead ID</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Classify Lead").item.json.leadId }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Service</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Classify Lead").item.json.serviceType }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Priority</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top"><b>{{ $("Classify Lead").item.json.priority }}</b> &middot; score {{ $("Classify Lead").item.json.leadScore }}/100</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Preferred date</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Classify Lead").item.json.preferredDate || "Not specified" }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Source</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Classify Lead").item.json.source }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Message</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Classify Lead").item.json.message || "No message provided" }}</td></tr>\n</table>\n<p style="font-size:13px;color:#6b7280;margin:0">To book this lead, set Status to <b>Qualified</b> and fill <b>AppointmentAt</b> (YYYY-MM-DD HH:mm) in the Leads sheet.</p>\n</td></tr>\n<tr><td style="padding:16px 28px;background:#f9fafb;border-top:1px solid #e5e7eb;color:#6b7280;font-size:12px;line-height:1.5">Lead-to-Booking Automation &middot; {{ $("Classify Lead").item.json.leadId }} &middot; Execution {{ $execution.id }}<br>Track progress in the Leads sheet: New &rarr; Contacted &rarr; Qualified &rarr; Appointment Booked &rarr; Won / Lost.</td></tr>\n</table></div>", "options": { "appendAttribution": false } }, "id": "89268e77-6102-4781-8619-31127ed4abe5", "name": "Notify Owner: High", "type": "n8n-nodes-base.gmail", "typeVersion": 2.2, "position": [ 2496, 16 ], "webhookId": "a127a883-7c50-401e-9083-96c513593ce2", "credentials": { "gmailOAuth2": { "id": "CgKFdJBbgVLSVRI0", "name": "Gmail account 2" } }, "onError": "continueErrorOutput" }, { "parameters": { "sendTo": "fazeshadow001@gmail.com", "subject": "=[NEW] New lead: {{ $("Classify Lead").item.json.fullName }} - {{ $("Classify Lead").item.json.serviceType }} ({{ $("Classify Lead").item.json.leadId }})", "message": "=<div style="margin:0;padding:24px 12px;background:#f3f4f6;font-family:'Segoe UI',Helvetica,Arial,sans-serif">\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:620px;margin:0 auto;background:#ffffff;border-radius:10px;overflow:hidden;border:1px solid #e5e7eb">\n<tr><td style="background:#1d4ed8;padding:22px 28px;color:#ffffff">\n<div style="font-size:11px;letter-spacing:1.5px;text-transform:uppercase;opacity:.85">New lead</div>\n<div style="font-size:21px;font-weight:600;margin-top:6px;line-height:1.3">A new lead has arrived</div>\n<div style="font-size:14px;margin-top:6px;opacity:.92">{{ $("Classify Lead").item.json.fullName }} &middot; {{ $("Classify Lead").item.json.serviceType }}</div>\n</td></tr>\n<tr><td style="padding:26px 28px;color:#1f2937;font-size:15px;line-height:1.6">\n<div style="background:#f9fafb;border-left:4px solid #1d4ed8;padding:12px 16px;border-radius:4px;font-size:14px;margin:0 0 16px"><b>Recommended next action:</b> {{ $("Classify Lead").item.json.recommendedAction }}</div>\n<div style="font-size:12px;letter-spacing:1px;text-transform:uppercase;color:#9ca3af;margin-bottom:4px">Customer</div>\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;margin:4px 0 18px">\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Name</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top"><b>{{ $("Classify Lead").item.json.fullName }}</b></td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Phone</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top"><a href="tel:{{ $("Classify Lead").item.json.phone }}" style="color:#2563eb;text-decoration:none">{{ $("Classify Lead").item.json.phone }}</a></td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Email</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top"><a href="mailto:{{ $("Classify Lead").item.json.email }}" style="color:#2563eb;text-decoration:none">{{ $("Classify Lead").item.json.email }}</a></td></tr>\n</table>\n<div style="font-size:12px;letter-spacing:1px;text-transform:uppercase;color:#9ca3af;margin-bottom:4px">Request</div>\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;margin:4px 0 18px">\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Lead ID</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Classify Lead").item.json.leadId }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Service</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Classify Lead").item.json.serviceType }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Priority</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top"><b>{{ $("Classify Lead").item.json.priority }}</b> &middot; score {{ $("Classify Lead").item.json.leadScore }}/100</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Preferred date</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Classify Lead").item.json.preferredDate || "Not specified" }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Source</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Classify Lead").item.json.source }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Message</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Classify Lead").item.json.message || "No message provided" }}</td></tr>\n</table>\n<p style="font-size:13px;color:#6b7280;margin:0">To book this lead, set Status to <b>Qualified</b> and fill <b>AppointmentAt</b> (YYYY-MM-DD HH:mm) in the Leads sheet.</p>\n</td></tr>\n<tr><td style="padding:16px 28px;background:#f9fafb;border-top:1px solid #e5e7eb;color:#6b7280;font-size:12px;line-height:1.5">Lead-to-Booking Automation &middot; {{ $("Classify Lead").item.json.leadId }} &middot; Execution {{ $execution.id }}<br>Track progress in the Leads sheet: New &rarr; Contacted &rarr; Qualified &rarr; Appointment Booked &rarr; Won / Lost.</td></tr>\n</table></div>", "options": { "appendAttribution": false } }, "id": "2415a2f9-989d-4a05-b1d9-1601b3b243ac", "name": "Notify Owner: Normal", "type": "n8n-nodes-base.gmail", "typeVersion": 2.2, "position": [ 2496, 208 ], "webhookId": "3fc1161d-308c-4b22-8cdd-e64784c73f1c", "credentials": { "gmailOAuth2": { "id": "CgKFdJBbgVLSVRI0", "name": "Gmail account 2" } }, "onError": "continueErrorOutput" }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "https://docs.google.com/spreadsheets/d/1zbH0BDvjm8bD7n1bGoqwQ0OejbRan4Fi6WPTI8Hy4yE/edit?usp=drivesdk", "mode": "url" }, "sheetName": { "__rl": true, "mode": "name", "value": "Log" }, "columns": { "mappingMode": "defineBelow", "value": { "Timestamp": "={{ $now.toISO() }}", "LeadID": "", "Event": "ValidationError", "Status": "Rejected", "Priority": "", "Email": "={{ $json.email }}", "Phone": "={{ $json.phone }}", "Details": "={{ "Invalid submission from " + $json.source }}", "ErrorMessage": "={{ $json.validationError }}", "ExecutionId": "={{ $execution.id }}" }, "schema": [ { "id": "Timestamp", "displayName": "Timestamp", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "LeadID", "displayName": "LeadID", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Event", "displayName": "Event", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Status", "displayName": "Status", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Priority", "displayName": "Priority", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Email", "displayName": "Email", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Phone", "displayName": "Phone", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Details", "displayName": "Details", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "ErrorMessage", "displayName": "ErrorMessage", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "ExecutionId", "displayName": "ExecutionId", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false } ] }, "options": {} }, "id": "7c9caf95-066c-4ed4-8b37-6908627ce712", "name": "Log Validation Error", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 416, 32 ], "credentials": { "googleSheetsOAuth2Api": { "id": "mBDySotAeocOEtec", "name": "Google Sheets account" } }, "onError": "continueRegularOutput" }, { "parameters": { "sendTo": "fazeshadow001@gmail.com", "subject": "Submission rejected: incomplete lead form", "message": "=<div style="margin:0;padding:24px 12px;background:#f3f4f6;font-family:'Segoe UI',Helvetica,Arial,sans-serif">\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:620px;margin:0 auto;background:#ffffff;border-radius:10px;overflow:hidden;border:1px solid #e5e7eb">\n<tr><td style="background:#a16207;padding:22px 28px;color:#ffffff">\n<div style="font-size:11px;letter-spacing:1.5px;text-transform:uppercase;opacity:.85">Submission rejected</div>\n<div style="font-size:21px;font-weight:600;margin-top:6px;line-height:1.3">An incomplete form was received</div>\n<div style="font-size:14px;margin-top:6px;opacity:.92">Nothing was saved and no customer email was sent</div>\n</td></tr>\n<tr><td style="padding:26px 28px;color:#1f2937;font-size:15px;line-height:1.6">\n<div style="background:#fefce8;border-left:4px solid #a16207;padding:12px 16px;border-radius:4px;font-size:14px;margin:0 0 16px"><b>Reason:</b> {{ $("Validate & Normalize").item.json.validationError }}</div>\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;margin:4px 0 18px">\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Name</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Validate & Normalize").item.json.fullName || "Not provided" }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Email</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Validate & Normalize").item.json.email || "Not provided" }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Phone</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Validate & Normalize").item.json.phone || "Not provided" }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Source</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Validate & Normalize").item.json.source }}</td></tr>\n</table>\n<p style="margin:0">If this looks like a real customer, contact them directly using the details above.</p>\n</td></tr>\n<tr><td style="padding:16px 28px;background:#f9fafb;border-top:1px solid #e5e7eb;color:#6b7280;font-size:12px;line-height:1.5">Lead-to-Booking Automation &middot; Execution {{ $execution.id }}<br>Track progress in the Leads sheet: New &rarr; Contacted &rarr; Qualified &rarr; Appointment Booked &rarr; Won / Lost.</td></tr>\n</table></div>", "options": { "appendAttribution": false } }, "id": "e20a3115-0b8a-402d-976f-c6afed2e9549", "name": "Notify Owner: Invalid Submission", "type": "n8n-nodes-base.gmail", "typeVersion": 2.2, "position": [ 640, 32 ], "webhookId": "61b1e6dc-c68f-40e2-a2d8-343ac4ef98a8", "credentials": { "gmailOAuth2": { "id": "CgKFdJBbgVLSVRI0", "name": "Gmail account 2" } }, "onError": "continueRegularOutput" }, { "parameters": { "rule": { "interval": [ { "field": "hours" } ] } }, "id": "938ccb94-d7f0-4a93-a360-ce06fe35d41f", "name": "Hourly Follow-up & Booking Check", "type": "n8n-nodes-base.scheduleTrigger", "typeVersion": 1.4, "position": [ 80, 416 ] }, { "parameters": { "assignments": { "assignments": [ { "id": "fu-delay", "name": "followUpDelayHours", "type": "number", "value": 24 }, { "id": "fu-start", "name": "followUpStartDate", "type": "string", "value": "2026-10-01" }, { "id": "appt-dur", "name": "appointmentDurationMinutes", "type": "number", "value": 60 }, { "id": "tz", "name": "timezone", "type": "string", "value": "Asia/Beirut" }, { "id": "fu-max", "name": "maxFollowUpAttempts", "type": "number", "value": 3 }, { "id": "fu-retry", "name": "followUpRetryDelayHours", "type": "number", "value": 2 }, { "id": "fu-stuck", "name": "stuckFollowUpHours", "type": "number", "value": 3 }, { "id": "bk-stuck", "name": "stuckBookingHours", "type": "number", "value": 3 } ] }, "options": {} }, "id": "fe4c6d59-61e0-47cb-b0fd-4904dcef166d", "name": "Automation Settings", "type": "n8n-nodes-base.set", "typeVersion": 3.5, "position": [ 352, 1456 ] }, { "parameters": { "documentId": { "__rl": true, "value": "https://docs.google.com/spreadsheets/d/1zbH0BDvjm8bD7n1bGoqwQ0OejbRan4Fi6WPTI8Hy4yE/edit?usp=drivesdk", "mode": "url" }, "sheetName": { "__rl": true, "mode": "name", "value": "Leads" }, "options": {} }, "id": "416c50de-7011-42b2-926f-33dd8f442d81", "name": "Read Open Leads", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 576, 1456 ], "credentials": { "googleSheetsOAuth2Api": { "id": "mBDySotAeocOEtec", "name": "Google Sheets account" } } }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose", "version": 2 }, "combinator": "and", "conditions": [ { "id": "is-stuck", "leftValue": "={{ (() => { const s = $("Automation Settings").first().json; const num = (v, d) => isNaN(parseFloat(v)) ? d : Number(v); if (String($json.FollowUpStatus || "").trim() === "Sending") { const last = DateTime.fromISO(String($json.FollowUpLastAttemptAt || "")); if (!last.isValid || last <= $now.minus({ hours: num(s.stuckFollowUpHours, 3) })) return true; } if (String($json.Status || "").trim() === "Booking Pending") { const upd = DateTime.fromISO(String($json.LastUpdated || "")); if (!upd.isValid || upd <= $now.minus({ hours: num(s.stuckBookingHours, 3) })) return true; } return false; })() }}", "rightValue": true, "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ] }, "looseTypeValidation": true, "options": {} }, "id": "c8516982-d67b-420c-af24-20ca51b617b4", "name": "Stuck Follow-up or Booking?", "type": "n8n-nodes-base.filter", "typeVersion": 2.3, "position": [ 1984, 448 ] }, { "parameters": { "operation": "update", "documentId": { "__rl": true, "value": "https://docs.google.com/spreadsheets/d/1zbH0BDvjm8bD7n1bGoqwQ0OejbRan4Fi6WPTI8Hy4yE/edit?usp=drivesdk", "mode": "url" }, "sheetName": { "__rl": true, "mode": "name", "value": "Leads" }, "columns": { "mappingMode": "defineBelow", "value": { "LeadID": "={{ $json.LeadID }}", "FollowUpStatus": "={{ String($json.FollowUpStatus || "").trim() === "Sending" ? "Needs Review" : String($json.FollowUpStatus || "") }}", "Status": "={{ String($json.Status || "").trim() === "Booking Pending" ? "Booking Needs Review" : String($json.Status || "") }}", "LastUpdated": "={{ $now.toISO() }}" }, "matchingColumns": [ "LeadID" ], "schema": [ { "id": "LeadID", "displayName": "LeadID", "type": "string", "canBeUsedToMatch": true, "display": true, "required": false }, { "id": "FollowUpStatus", "displayName": "FollowUpStatus", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Status", "displayName": "Status", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "LastUpdated", "displayName": "LastUpdated", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false } ] }, "options": {} }, "id": "76fba85e-206d-4063-9e02-412ff6b3430d", "name": "Mark Needs Review", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 2208, 448 ], "credentials": { "googleSheetsOAuth2Api": { "id": "mBDySotAeocOEtec", "name": "Google Sheets account" } }, "onError": "continueRegularOutput" }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "https://docs.google.com/spreadsheets/d/1zbH0BDvjm8bD7n1bGoqwQ0OejbRan4Fi6WPTI8Hy4yE/edit?usp=drivesdk", "mode": "url" }, "sheetName": { "__rl": true, "mode": "name", "value": "Log" }, "columns": { "mappingMode": "defineBelow", "value": { "Timestamp": "={{ $now.toISO() }}", "LeadID": "={{ $("Stuck Follow-up or Booking?").item.json.LeadID }}", "Event": "={{ String($("Stuck Follow-up or Booking?").item.json.Status || "").trim() === "Booking Pending" ? "BookingStuck" : "FollowUpStuck" }}", "Status": "Warning", "Priority": "={{ $("Stuck Follow-up or Booking?").item.json.Priority }}", "Email": "={{ $("Stuck Follow-up or Booking?").item.json.Email }}", "Phone": "={{ $("Stuck Follow-up or Booking?").item.json.Phone }}", "Details": "={{ String($("Stuck Follow-up or Booking?").item.json.Status || "").trim() === "Booking Pending" ? ("Booking was left at Booking Pending since " + $("Stuck Follow-up or Booking?").item.json.LastUpdated + " (appointment " + $("Stuck Follow-up or Booking?").item.json.AppointmentAt + "). The confirmation may or may not have been sent, so it will NOT be retried automatically. Check Gmail Sent, then set Status to Appointment Booked or back to Qualified. Status set to Booking Needs Review.") : ("Follow-up was left at Sending since " + $("Stuck Follow-up or Booking?").item.json.FollowUpLastAttemptAt + " (attempt " + $("Stuck Follow-up or Booking?").item.json.FollowUpAttempts + "). It may or may not have been delivered, so it will NOT be re-sent automatically. Check Gmail Sent, then contact the lead manually. Status set to Needs Review.") }}", "ErrorMessage": "", "ExecutionId": "={{ $execution.id }}" }, "schema": [ { "id": "Timestamp", "displayName": "Timestamp", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "LeadID", "displayName": "LeadID", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Event", "displayName": "Event", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Status", "displayName": "Status", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Priority", "displayName": "Priority", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Email", "displayName": "Email", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Phone", "displayName": "Phone", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Details", "displayName": "Details", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "ErrorMessage", "displayName": "ErrorMessage", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "ExecutionId", "displayName": "ExecutionId", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false } ] }, "options": {} }, "id": "784c15d8-e7be-4861-aadd-c3c440e33689", "name": "Log Stuck Item", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 2432, 688 ], "credentials": { "googleSheetsOAuth2Api": { "id": "mBDySotAeocOEtec", "name": "Google Sheets account" } }, "onError": "continueRegularOutput" }, { "parameters": { "sendTo": "fazeshadow001@gmail.com", "subject": "=Leads need review (interrupted follow-up or booking): {{ $("Stuck Follow-up or Booking?").all().length }} lead(s)", "message": "=<div style="margin:0;padding:24px 12px;background:#f3f4f6;font-family:'Segoe UI',Helvetica,Arial,sans-serif">\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:620px;margin:0 auto;background:#ffffff;border-radius:10px;overflow:hidden;border:1px solid #e5e7eb">\n<tr><td style="background:#b45309;padding:22px 28px;color:#ffffff">\n<div style="font-size:11px;letter-spacing:1.5px;text-transform:uppercase;opacity:.85">Review required</div>\n<div style="font-size:21px;font-weight:600;margin-top:6px;line-height:1.3">Some leads need your attention</div>\n<div style="font-size:14px;margin-top:6px;opacity:.92">{{ $("Stuck Follow-up or Booking?").all().length }} interrupted follow-up(s) or booking(s)</div>\n</td></tr>\n<tr><td style="padding:26px 28px;color:#1f2937;font-size:15px;line-height:1.6">\n<p style="margin:0 0 14px">These items were interrupted mid-send, so the customer email may or may not have been delivered. To avoid duplicate emails they will <b>not</b> be retried automatically.</p>\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;margin:0 0 18px;font-size:14px">\n<tr><td style="padding:8px 10px;background:#f9fafb;color:#6b7280;font-size:12px;text-transform:uppercase;letter-spacing:1px">Lead</td><td style="padding:8px 10px;background:#f9fafb;color:#6b7280;font-size:12px;text-transform:uppercase;letter-spacing:1px">Type</td><td style="padding:8px 10px;background:#f9fafb;color:#6b7280;font-size:12px;text-transform:uppercase;letter-spacing:1px">Customer</td></tr>\n{{ $("Stuck Follow-up or Booking?").all().map(i => "<tr><td style='padding:9px 10px;border-bottom:1px solid #f0f1f3'><b>" + i.json.LeadID + "</b></td><td style='padding:9px 10px;border-bottom:1px solid #f0f1f3'>" + (String(i.json.Status || "").trim() === "Booking Pending" ? "Booking (" + i.json.AppointmentAt + ")" : "Follow-up") + "</td><td style='padding:9px 10px;border-bottom:1px solid #f0f1f3'>" + i.json.FullName + "<br><span style='color:#6b7280;font-size:12px'>" + i.json.Email + "</span></td></tr>").join("") }}\n</table>\n<p style="margin:0"><b>Next step:</b> check Gmail Sent for each lead, then contact them manually if needed. Follow-ups are marked <b>Needs Review</b>; bookings are set to <b>Booking Needs Review</b>.</p>\n</td></tr>\n<tr><td style="padding:16px 28px;background:#f9fafb;border-top:1px solid #e5e7eb;color:#6b7280;font-size:12px;line-height:1.5">Lead-to-Booking Automation &middot; Execution {{ $execution.id }}<br>Track progress in the Leads sheet: New &rarr; Contacted &rarr; Qualified &rarr; Appointment Booked &rarr; Won / Lost.</td></tr>\n</table></div>", "options": { "appendAttribution": false } }, "id": "8f2609eb-bb58-4342-b0b5-348409a91e33", "name": "Notify Owner: Needs Review", "type": "n8n-nodes-base.gmail", "typeVersion": 2.2, "position": [ 2656, 1568 ], "executeOnce": true, "webhookId": "47b67294-9e88-4aff-8d15-43b1e3ef770f", "credentials": { "gmailOAuth2": { "id": "CgKFdJBbgVLSVRI0", "name": "Gmail account 2" } }, "onError": "continueRegularOutput" }, { "parameters": { "conditions": { "options": { "caseSensitive": false, "leftValue": "", "typeValidation": "loose", "version": 2 }, "combinator": "and", "conditions": [ { "id": "status-new", "leftValue": "={{ String($json.Status || "").trim() }}", "rightValue": "New", "operator": { "type": "string", "operation": "equals" } }, { "id": "not-followed", "leftValue": "={{ String($json.FollowUpSentAt || "") }}", "rightValue": "", "operator": { "type": "string", "operation": "empty", "singleValue": true } }, { "id": "has-email", "leftValue": "={{ String($json.Email || "") }}", "rightValue": "", "operator": { "type": "string", "operation": "notEmpty", "singleValue": true } }, { "id": "old-enough", "leftValue": "={{ (() => { const c = DateTime.fromISO(String($json.Timestamp || "")); const s = $("Automation Settings").first().json; return c.isValid && c <= $now.minus({ hours: Number(s.followUpDelayHours) }) && c >= DateTime.fromISO(s.followUpStartDate); })() }}", "rightValue": true, "operator": { "type": "boolean", "operation": "true", "singleValue": true } }, { "id": "retry-eligible", "leftValue": "={{ (() => { const s = $("Automation Settings").first().json; const st = String($json.FollowUpStatus || "").trim(); const n = Number($json.FollowUpAttempts) || 0; const max = Number(s.maxFollowUpAttempts) || 3; if (n >= max) return false; if (st === "") return true; if (st === "Retry Pending") { const last = DateTime.fromISO(String($json.FollowUpLastAttemptAt || "")); return !last.isValid || last <= $now.minus({ hours: (isNaN(parseFloat(s.followUpRetryDelayHours)) ? 2 : Number(s.followUpRetryDelayHours)) }); } return false; })() }}", "rightValue": true, "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ] }, "looseTypeValidation": true, "options": {} }, "id": "f5867855-7be0-4f72-8068-6b341078a785", "name": "Needs Follow-up?", "type": "n8n-nodes-base.filter", "typeVersion": 2.3, "position": [ 1984, 576 ] }, { "parameters": { "operation": "update", "documentId": { "__rl": true, "value": "https://docs.google.com/spreadsheets/d/1zbH0BDvjm8bD7n1bGoqwQ0OejbRan4Fi6WPTI8Hy4yE/edit?usp=drivesdk", "mode": "url" }, "sheetName": { "__rl": true, "mode": "name", "value": "Leads" }, "columns": { "mappingMode": "defineBelow", "value": { "LeadID": "={{ $json.LeadID }}", "FollowUpStatus": "Sending", "FollowUpAttempts": "={{ (Number($json.FollowUpAttempts) || 0) + 1 }}", "FollowUpLastAttemptAt": "={{ $now.toISO() }}", "LastUpdated": "={{ $now.toISO() }}" }, "matchingColumns": [ "LeadID" ], "schema": [ { "id": "LeadID", "displayName": "LeadID", "type": "string", "canBeUsedToMatch": true, "display": true, "required": false }, { "id": "FollowUpStatus", "displayName": "FollowUpStatus", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "FollowUpAttempts", "displayName": "FollowUpAttempts", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "FollowUpLastAttemptAt", "displayName": "FollowUpLastAttemptAt", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "LastUpdated", "displayName": "LastUpdated", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false } ] }, "options": {} }, "id": "41532fd1-130b-4d45-a0bf-f02982d9fe20", "name": "Claim Follow-up Attempt", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 2208, 768 ], "credentials": { "googleSheetsOAuth2Api": { "id": "mBDySotAeocOEtec", "name": "Google Sheets account" } }, "onError": "continueErrorOutput" }, { "parameters": { "sendTo": "={{ $("Needs Follow-up?").item.json.Email }}", "subject": "=Following up on your {{ $("Needs Follow-up?").item.json.ServiceType }} request (Ref {{ $("Needs Follow-up?").item.json.LeadID }})", "message": "=<div style="margin:0;padding:24px 12px;background:#f3f4f6;font-family:'Segoe UI',Helvetica,Arial,sans-serif">\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:620px;margin:0 auto;background:#ffffff;border-radius:10px;overflow:hidden;border:1px solid #e5e7eb">\n<tr><td style="background:#1e3a5f;padding:22px 28px;color:#ffffff">\n<div style="font-size:11px;letter-spacing:1.5px;text-transform:uppercase;opacity:.85">Following up</div>\n<div style="font-size:21px;font-weight:600;margin-top:6px;line-height:1.3">Still looking for help, {{ $("Needs Follow-up?").item.json.FullName }}?</div>\n<div style="font-size:14px;margin-top:6px;opacity:.92">Your {{ $("Needs Follow-up?").item.json.ServiceType }} request &middot; Ref {{ $("Needs Follow-up?").item.json.LeadID }}</div>\n</td></tr>\n<tr><td style="padding:26px 28px;color:#1f2937;font-size:15px;line-height:1.6">\n<p style="margin:0 0 14px">We wanted to follow up on the <b>{{ $("Needs Follow-up?").item.json.ServiceType }}</b> request you sent us recently. We would be glad to help.</p>\n<p style="margin:0 0 14px">Simply reply to this email with:</p>\n<ul style="margin:0 0 14px;padding-left:20px">\n<li style="margin-bottom:6px">a time that is convenient for a quick call, or</li>\n<li>any extra details about what you need.</li>\n</ul>\n<p style="margin:0">We will get back to you as soon as we hear from you.</p>\n<p style="margin:22px 0 0">Kind regards,<br><b>The team</b></p>\n</td></tr>\n<tr><td style="padding:16px 28px;background:#f9fafb;border-top:1px solid #e5e7eb;color:#6b7280;font-size:12px;line-height:1.5">Reference {{ $("Needs Follow-up?").item.json.LeadID }}. You are receiving this email because you submitted a request through our website. Simply reply to this email if you have any questions.</td></tr>\n</table></div>", "options": { "appendAttribution": false } }, "id": "f83adfcf-5aff-4328-b5c4-c8066f355062", "name": "Send Follow-up Email", "type": "n8n-nodes-base.gmail", "typeVersion": 2.2, "position": [ 2432, 496 ], "webhookId": "9758bead-91a9-4b39-8fd9-f777307a80ba", "credentials": { "gmailOAuth2": { "id": "CgKFdJBbgVLSVRI0", "name": "Gmail account 2" } }, "onError": "continueErrorOutput" }, { "parameters": { "operation": "update", "documentId": { "__rl": true, "value": "https://docs.google.com/spreadsheets/d/1zbH0BDvjm8bD7n1bGoqwQ0OejbRan4Fi6WPTI8Hy4yE/edit?usp=drivesdk", "mode": "url" }, "sheetName": { "__rl": true, "mode": "name", "value": "Leads" }, "columns": { "mappingMode": "defineBelow", "value": { "LeadID": "={{ $("Needs Follow-up?").item.json.LeadID }}", "FollowUpSentAt": "={{ $now.toISO() }}", "FollowUpStatus": "Sent", "LastUpdated": "={{ $now.toISO() }}" }, "matchingColumns": [ "LeadID" ], "schema": [ { "id": "LeadID", "displayName": "LeadID", "type": "string", "canBeUsedToMatch": true, "display": true, "required": false }, { "id": "FollowUpSentAt", "displayName": "FollowUpSentAt", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "FollowUpStatus", "displayName": "FollowUpStatus", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "LastUpdated", "displayName": "LastUpdated", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false } ] }, "options": {} }, "id": "3bc051f3-9cb4-4b25-8240-fad818caf5f7", "name": "Mark Follow-up Sent", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 2208, 576 ], "credentials": { "googleSheetsOAuth2Api": { "id": "mBDySotAeocOEtec", "name": "Google Sheets account" } }, "onError": "continueErrorOutput" }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "https://docs.google.com/spreadsheets/d/1zbH0BDvjm8bD7n1bGoqwQ0OejbRan4Fi6WPTI8Hy4yE/edit?usp=drivesdk", "mode": "url" }, "sheetName": { "__rl": true, "mode": "name", "value": "Log" }, "columns": { "mappingMode": "defineBelow", "value": { "Timestamp": "={{ $now.toISO() }}", "LeadID": "={{ $("Needs Follow-up?").item.json.LeadID }}", "Event": "FollowUpSent", "Status": "Success", "Priority": "={{ $("Needs Follow-up?").item.json.Priority }}", "Email": "={{ $("Needs Follow-up?").item.json.Email }}", "Phone": "={{ $("Needs Follow-up?").item.json.Phone }}", "Details": "={{ "Automatic follow-up sent (attempt " + ((Number($("Needs Follow-up?").item.json.FollowUpAttempts) || 0) + 1) + ", Status still New after " + $("Automation Settings").first().json.followUpDelayHours + "h)" }}", "ErrorMessage": "", "ExecutionId": "={{ $execution.id }}" }, "schema": [ { "id": "Timestamp", "displayName": "Timestamp", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "LeadID", "displayName": "LeadID", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Event", "displayName": "Event", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Status", "displayName": "Status", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Priority", "displayName": "Priority", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Email", "displayName": "Email", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Phone", "displayName": "Phone", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Details", "displayName": "Details", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "ErrorMessage", "displayName": "ErrorMessage", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "ExecutionId", "displayName": "ExecutionId", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false } ] }, "options": {} }, "id": "1325037e-225c-430e-934e-09bfa3dd53d2", "name": "Log Follow-up", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 2656, 448 ], "credentials": { "googleSheetsOAuth2Api": { "id": "mBDySotAeocOEtec", "name": "Google Sheets account" } }, "onError": "continueRegularOutput" }, { "parameters": { "conditions": { "options": { "caseSensitive": false, "leftValue": "", "typeValidation": "loose", "version": 2 }, "combinator": "and", "conditions": [ { "id": "status-qualified", "leftValue": "={{ String($json.Status || "").trim() }}", "rightValue": "Qualified", "operator": { "type": "string", "operation": "equals" } }, { "id": "has-appt", "leftValue": "={{ String($json.AppointmentAt || "") }}", "rightValue": "", "operator": { "type": "string", "operation": "notEmpty", "singleValue": true } }, { "id": "has-email-b", "leftValue": "={{ String($json.Email || "") }}", "rightValue": "", "operator": { "type": "string", "operation": "notEmpty", "singleValue": true } } ] }, "looseTypeValidation": true, "options": {} }, "id": "54e5be98-e3e0-45c9-b381-8748b1b22f9d", "name": "Ready to Book?", "type": "n8n-nodes-base.filter", "typeVersion": 2.3, "position": [ 880, 1824 ] }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "// Reads AppointmentAt from the sheet (text like 2026-10-15 14:00, or a Sheets date) and builds start/end times\nconst settings = $('Automation Settings').first().json;\nconst zone = settings.timezone || 'UTC';\nconst dur = Number(settings.appointmentDurationMinutes) || 60;\nconst raw = $json.AppointmentAt;\nlet start = null;\nif (typeof raw === 'number') {\n start = DateTime.fromMillis(Math.round((raw - 25569) * 86400000), { zone: 'utc' }).setZone(zone, { keepLocalTime: true });\n} else {\n const s = String(raw || '').trim();\n const candidates = [\n DateTime.fromISO(s, { zone }),\n DateTime.fromSQL(s, { zone }),\n DateTime.fromFormat(s, 'yyyy-MM-dd H:mm', { zone }),\n DateTime.fromFormat(s, 'M/d/yyyy H:mm:ss', { zone }),\n DateTime.fromFormat(s, 'M/d/yyyy H:mm', { zone }),\n DateTime.fromFormat(s, 'd/M/yyyy H:mm', { zone }),\n ];\n start = candidates.find(d => d.isValid) || null;\n}\nconst validDate = !!(start && start.isValid);\nreturn { json: Object.assign({}, $json, {\n validDate,\n startISO: validDate ? start.toISO() : '',\n endISO: validDate ? start.plus({ minutes: dur }).toISO() : '',\n appointmentLabel: validDate ? start.toFormat("cccc, d LLLL yyyy 'at' HH:mm") + ' (' + zone + ')' : '',\n bookingError: validDate ? '' : ('Could not read AppointmentAt "' + String(raw) + '". Use the format YYYY-MM-DD HH:mm.'),\n}) };" }, "id": "c11391ad-277d-421a-9f7d-11db942035fc", "name": "Prepare Booking", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1104, 1824 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose", "version": 2 }, "combinator": "and", "conditions": [ { "id": "valid-date", "leftValue": "={{ $json.validDate }}", "rightValue": true, "operator": { "type": "boolean", "operation": "equals" } } ] }, "options": {} }, "id": "64204a65-1ebf-41b0-8810-6a254420be47", "name": "Valid Appointment Time?", "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ 1328, 1824 ] }, { "parameters": { "operation": "update", "documentId": { "__rl": true, "value": "https://docs.google.com/spreadsheets/d/1zbH0BDvjm8bD7n1bGoqwQ0OejbRan4Fi6WPTI8Hy4yE/edit?usp=drivesdk", "mode": "url" }, "sheetName": { "__rl": true, "mode": "name", "value": "Leads" }, "columns": { "mappingMode": "defineBelow", "value": { "LeadID": "={{ $json.LeadID }}", "Status": "Booking Pending", "LastUpdated": "={{ $now.toISO() }}" }, "matchingColumns": [ "LeadID" ], "schema": [ { "id": "LeadID", "displayName": "LeadID", "type": "string", "canBeUsedToMatch": true, "display": true, "required": false }, { "id": "Status", "displayName": "Status", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "LastUpdated", "displayName": "LastUpdated", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false } ] }, "options": {} }, "id": "aeef50a7-b684-4a27-b785-367f5c55b7d4", "name": "Mark Booking Pending", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 1552, 2288 ], "credentials": { "googleSheetsOAuth2Api": { "id": "mBDySotAeocOEtec", "name": "Google Sheets account" } }, "onError": "continueErrorOutput" }, { "parameters": { "calendar": { "__rl": true, "mode": "list", "value": "", "cachedResultName": "Select booking calendar" }, "start": "={{ $("Prepare Booking").item.json.startISO }}", "end": "={{ $("Prepare Booking").item.json.endISO }}", "additionalFields": { "description": "=Lead ID: {{ $("Prepare Booking").item.json.LeadID }}\nPhone: {{ $("Prepare Booking").item.json.Phone }}\nEmail: {{ $("Prepare Booking").item.json.Email }}\nMessage: {{ $("Prepare Booking").item.json.Message }}", "summary": "={{ $("Prepare Booking").item.json.ServiceType }} - {{ $("Prepare Booking").item.json.FullName }} ({{ $("Prepare Booking").item.json.LeadID }})" } }, "id": "e228ce5e-971e-48c0-8e1e-30ecc1faddbc", "name": "Create Calendar Event (optional)", "type": "n8n-nodes-base.googleCalendar", "typeVersion": 1.3, "position": [ 1776, 1680 ], "disabled": true, "onError": "continueErrorOutput" }, { "parameters": { "sendTo": "={{ $("Prepare Booking").item.json.Email }}", "subject": "=Appointment confirmed: {{ $("Prepare Booking").item.json.appointmentLabel }} (Ref {{ $("Prepare Booking").item.json.LeadID }})", "message": "=<div style="margin:0;padding:24px 12px;background:#f3f4f6;font-family:'Segoe UI',Helvetica,Arial,sans-serif">\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:620px;margin:0 auto;background:#ffffff;border-radius:10px;overflow:hidden;border:1px solid #e5e7eb">\n<tr><td style="background:#166534;padding:22px 28px;color:#ffffff">\n<div style="font-size:11px;letter-spacing:1.5px;text-transform:uppercase;opacity:.85">Appointment confirmed</div>\n<div style="font-size:21px;font-weight:600;margin-top:6px;line-height:1.3">You're booked, {{ $("Prepare Booking").item.json.FullName }}</div>\n<div style="font-size:14px;margin-top:6px;opacity:.92">Here are the details of your appointment.</div>\n</td></tr>\n<tr><td style="padding:26px 28px;color:#1f2937;font-size:15px;line-height:1.6">\n<div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;padding:18px 20px;margin:0 0 20px;text-align:center"><div style="font-size:12px;color:#166534;text-transform:uppercase;letter-spacing:1px">Date &amp; time</div><div style="font-size:19px;font-weight:600;color:#14532d;margin-top:4px">{{ $("Prepare Booking").item.json.appointmentLabel }}</div></div>\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;margin:4px 0 18px">\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Service</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Prepare Booking").item.json.ServiceType }}</td></tr>\n<tr><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#6b7280;font-size:13px;width:150px;vertical-align:top">Reference</td><td style="padding:9px 0;border-bottom:1px solid #f0f1f3;color:#111827;font-size:14px;vertical-align:top">{{ $("Prepare Booking").item.json.LeadID }}</td></tr>\n</table>\n<p style="margin:0">Need to reschedule? Just reply to this email and quote your reference number &mdash; we are happy to find another time.</p>\n<p style="margin:22px 0 0">Kind regards,<br><b>The team</b></p>\n</td></tr>\n<tr><td style="padding:16px 28px;background:#f9fafb;border-top:1px solid #e5e7eb;color:#6b7280;font-size:12px;line-height:1.5">You are receiving this email because you submitted a request through our website. Simply reply to this email if you have any questions.</td></tr>\n</table></div>", "options": { "appendAttribution": false } }, "id": "0b49a73c-6299-4593-8341-6d5a628b0630", "name": "Send Booking Confirmation", "type": "n8n-nodes-base.gmail", "typeVersion": 2.2, "position": [ 2000, 2096 ], "webhookId": "86ac8848-ffcf-4c63-adb3-2005f46368b4", "credentials": { "gmailOAuth2": { "id": "CgKFdJBbgVLSVRI0", "name": "Gmail account 2" } }, "onError": "continueErrorOutput" }, { "parameters": { "operation": "update", "documentId": { "__rl": true, "value": "https://docs.google.com/spreadsheets/d/1zbH0BDvjm8bD7n1bGoqwQ0OejbRan4Fi6WPTI8Hy4yE/edit?usp=drivesdk", "mode": "url" }, "sheetName": { "__rl": true, "mode": "name", "value": "Leads" }, "columns": { "mappingMode": "defineBelow", "value": { "LeadID": "={{ $("Prepare Booking").item.json.LeadID }}", "Status": "Appointment Booked", "LastUpdated": "={{ $now.toISO() }}" }, "matchingColumns": [ "LeadID" ], "schema": [ { "id": "LeadID", "displayName": "LeadID", "type": "string", "canBeUsedToMatch": true, "display": true, "required": false }, { "id": "Status", "displayName": "Status", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "LastUpdated", "displayName": "LastUpdated", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false } ] }, "options": {} }, "id": "f987b8ee-dd93-4423-927a-97973985702b", "name": "Mark Appointment Booked", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 1552, 1904 ], "credentials": { "googleSheetsOAuth2Api": { "id": "mBDySotAeocOEtec", "name": "Google Sheets account" } }, "onError": "continueErrorOutput" }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "https://docs.google.com/spreadsheets/d/1zbH0BDvjm8bD7n1bGoqwQ0OejbRan4Fi6WPTI8Hy4yE/edit?usp=drivesdk", "mode": "url" }, "sheetName": { "__rl": true, "mode": "name", "value": "Log" }, "columns": { "mappingMode": "defineBelow", "value": { "Timestamp": "={{ $now.toISO() }}", "LeadID": "={{ $("Prepare Booking").item.json.LeadID }}", "Event": "AppointmentBooked", "Status": "Success", "Priority": "={{ $("Prepare Booking").item.json.Priority }}", "Email": "={{ $("Prepare Booking").item.json.Email }}", "Phone": "={{ $("Prepare Booking").item.json.Phone }}", "Details": "={{ "Appointment booked for " + $("Prepare Booking").item.json.appointmentLabel + " and confirmation sent" }}", "ErrorMessage": "", "ExecutionId": "={{ $execution.id }}" }, "schema": [ { "id": "Timestamp", "displayName": "Timestamp", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "LeadID", "displayName": "LeadID", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Event", "displayName": "Event", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Status", "displayName": "Status", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Priority", "displayName": "Priority", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Email", "displayName": "Email", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Phone", "displayName": "Phone", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Details", "displayName": "Details", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "ErrorMessage", "displayName": "ErrorMessage", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "ExecutionId", "displayName": "ExecutionId", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false } ] }, "options": {} }, "id": "f748c2a9-c5ba-42ff-b69a-8865e70fb235", "name": "Log Booking", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 2224, 1696 ], "credentials": { "googleSheetsOAuth2Api": { "id": "mBDySotAeocOEtec", "name": "Google Sheets account" } }, "onError": "continueRegularOutput" }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "// Decides the lead status after a booking problem\nlet p = {};\ntry { p = $('Prepare Booking').item.json || {}; } catch (e) {}\nlet failedStep = 'Unknown step';\ntry { failedStep = $prevNode.name; } catch (e) {}\nconst j = $json || {};\nconst err = j.error;\nlet errorMessage = (typeof err === 'string' ? err : (err && err.message)) || j.message || 'Unknown error';\nlet newStatus = 'Booking Failed';\nlet note = 'Booking could not be completed - fix the lead record, then set Status back to Qualified to retry.';\nif (failedStep === 'Valid Appointment Time?') {\n errorMessage = p.bookingError || j.bookingError || errorMessage;\n note = 'AppointmentAt could not be read - correct it, then set Status back to Qualified.';\n} else if (failedStep === 'Mark Appointment Booked') {\n // Confirmation WAS sent; only the status write failed. Retry writing the correct status.\n newStatus = 'Appointment Booked';\n note = 'Confirmation email WAS sent, but the status update failed. Retrying the status update.';\n}\nreturn { json: {\n leadId: p.LeadID || '', email: p.Email || '', phone: p.Phone || '', priority: p.Priority || '',\n failedStep, errorMessage, newStatus, note,\n} };" }, "id": "846559d0-2474-46cc-83e1-43bac6f3e18f", "name": "Format Booking Error", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 2448, 1888 ] }, { "parameters": { "operation": "update", "documentId": { "__rl": true, "value": "https://docs.google.com/spreadsheets/d/1zbH0BDvjm8bD7n1bGoqwQ0OejbRan4Fi6WPTI8Hy4yE/edit?usp=drivesdk", "mode": "url" }, "sheetName": { "__rl": true, "mode": "name", "value": "Leads" }, "columns": { "mappingMode": "defineBelow", "value": { "LeadID": "={{ $json.leadId }}", "Status": "={{ $json.newStatus }}", "LastUpdated": "={{ $now.toISO() }}" }, "matchingColumns": [ "LeadID" ], "schema": [ { "id": "LeadID", "displayName": "LeadID", "type": "string", "canBeUsedToMatch": true, "display": true, "required": false }, { "id": "Status", "displayName": "Status", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "LastUpdated", "displayName": "LastUpdated", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false } ] }, "options": {} }, "id": "a25a5cc3-147a-4385-ad1f-0e1ce9f198bc", "name": "Mark Booking Failed", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 2672, 1888 ], "credentials": { "googleSheetsOAuth2Api": { "id": "mBDySotAeocOEtec", "name": "Google Sheets account" } }, "onError": "continueRegularOutput" }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "https://docs.google.com/spreadsheets/d/1zbH0BDvjm8bD7n1bGoqwQ0OejbRan4Fi6WPTI8Hy4yE/edit?usp=drivesdk", "mode": "url" }, "sheetName": { "__rl": true, "mode": "name", "value": "Log" }, "columns": { "mappingMode": "defineBelow", "value": { "Timestamp": "={{ $now.toISO() }}", "LeadID": "={{ $("Format Booking Error").item.json.leadId }}", "Event": "BookingFailed", "Status": "Failed", "Priority": "={{ $("Format Booking Error").item.json.priority }}", "Email": "={{ $("Format Booking Error").item.json.email }}", "Phone": "={{ $("Format Booking Error").item.json.phone }}", "Details": "={{ $("Format Booking Error").item.json.note + " (step: " + $("Format Booking Error").item.json.failedStep + ", lead status set to: " + $("Format Booking Error").item.json.newStatus + ")" }}", "ErrorMessage": "={{ $("Format Booking Error").item.json.errorMessage }}", "ExecutionId": "={{ $execution.id }}" }, "schema": [ { "id": "Timestamp", "displayName": "Timestamp", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "LeadID", "displayName": "LeadID", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Event", "displayName": "Event", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Status", "displayName": "Status", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Priority", "displayName": "Priority", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Email", "displayName": "Email", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Phone", "displayName": "Phone", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Details", "displayName": "Details", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "ErrorMessage", "displayName": "ErrorMessage", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "ExecutionId", "displayName": "ExecutionId", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false } ] }, "options": {} }, "id": "6937383e-45ec-4403-802f-eb61cb2cbe3e", "name": "Log Booking Error", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 2224, 2096 ], "credentials": { "googleSheetsOAuth2Api": { "id": "mBDySotAeocOEtec", "name": "Google Sheets account" } }, "onError": "continueRegularOutput" }, { "parameters": { "mode": "runOnceForEachItem", "jsCode": "// Decides the follow-up state after a failure. Retry limits live in Automation Settings.\nconst s = $('Automation Settings').first().json;\nconst max = Number(s.maxFollowUpAttempts) || 3;\nconst retryHours = (isNaN(parseFloat(s.followUpRetryDelayHours)) ? 2 : Number(s.followUpRetryDelayHours));\nlet lead = {};\ntry { lead = $('Needs Follow-up?').item.json || {}; } catch (e) {}\nconst prevAttempts = Number(lead.FollowUpAttempts) || 0;\nlet failedStep = 'Unknown step';\ntry { failedStep = $prevNode.name; } catch (e) {}\nconst j = $json || {};\nconst err = j.error;\nconst errorMessage = (typeof err === 'string' ? err : (err && err.message)) || j.message || 'Unknown error';\nlet attempts, newStatus, sentAt = '', event, note;\nif (failedStep === 'Claim Follow-up Attempt') {\n // Could not reserve the lead: nothing was sent, leave its state unchanged so it is picked up next hour\n attempts = prevAttempts;\n newStatus = String(lead.FollowUpStatus || '');\n event = 'FollowUpSheetError';\n note = 'Could not reserve the follow-up in the sheet; nothing was sent. It will be tried again next hour.';\n} else if (failedStep === 'Mark Follow-up Sent') {\n // The email WAS sent; only the sheet update failed. Re-write Sent so it is never sent again.\n attempts = prevAttempts + 1;\n newStatus = 'Sent';\n sentAt = new Date().toISOString();\n event = 'FollowUpSheetError';\n note = 'Follow-up email WAS sent, but marking it failed. Retrying the sheet update; no email will be re-sent.';\n} else {\n attempts = prevAttempts + 1;\n if (attempts >= max) {\n newStatus = 'Failed';\n event = 'FollowUpFailed';\n note = 'Attempt ' + attempts + ' of ' + max + ' failed. No more retries - contact the lead manually.';\n } else {\n newStatus = 'Retry Pending';\n event = 'FollowUpRetryScheduled';\n note = 'Attempt ' + attempts + ' of ' + max + ' failed. Will retry in about ' + retryHours + 'h.';\n }\n}\nreturn { json: {\n leadId: lead.LeadID || '', email: lead.Email || '', phone: lead.Phone || '', priority: lead.Priority || '',\n failedStep, errorMessage, attempts, newStatus, sentAt, event, note,\n} };" }, "id": "d170eb21-6aee-4d17-b9f7-ec2d2e44f634", "name": "Format Follow-up Error", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 2880, 1184 ] }, { "parameters": { "operation": "update", "documentId": { "__rl": true, "value": "https://docs.google.com/spreadsheets/d/1zbH0BDvjm8bD7n1bGoqwQ0OejbRan4Fi6WPTI8Hy4yE/edit?usp=drivesdk", "mode": "url" }, "sheetName": { "__rl": true, "mode": "name", "value": "Leads" }, "columns": { "mappingMode": "defineBelow", "value": { "LeadID": "={{ $json.leadId }}", "FollowUpStatus": "={{ $json.newStatus }}", "FollowUpAttempts": "={{ $json.attempts }}", "FollowUpSentAt": "={{ $json.sentAt }}", "LastUpdated": "={{ $now.toISO() }}" }, "matchingColumns": [ "LeadID" ], "schema": [ { "id": "LeadID", "displayName": "LeadID", "type": "string", "canBeUsedToMatch": true, "display": true, "required": false }, { "id": "FollowUpStatus", "displayName": "FollowUpStatus", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "FollowUpAttempts", "displayName": "FollowUpAttempts", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "FollowUpSentAt", "displayName": "FollowUpSentAt", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "LastUpdated", "displayName": "LastUpdated", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false } ] }, "options": {} }, "id": "404d930f-7db7-405d-8f5a-bd84834656ca", "name": "Record Follow-up Outcome", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 3104, 1376 ], "credentials": { "googleSheetsOAuth2Api": { "id": "mBDySotAeocOEtec", "name": "Google Sheets account" } }, "onError": "continueRegularOutput" }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "https://docs.google.com/spreadsheets/d/1zbH0BDvjm8bD7n1bGoqwQ0OejbRan4Fi6WPTI8Hy4yE/edit?usp=drivesdk", "mode": "url" }, "sheetName": { "__rl": true, "mode": "name", "value": "Log" }, "columns": { "mappingMode": "defineBelow", "value": { "Timestamp": "={{ $now.toISO() }}", "LeadID": "={{ $("Format Follow-up Error").item.json.leadId }}", "Event": "={{ $("Format Follow-up Error").item.json.event }}", "Status": "={{ $("Format Follow-up Error").item.json.newStatus === "Failed" ? "Failed" : "Warning" }}", "Priority": "={{ $("Format Follow-up Error").item.json.priority }}", "Email": "={{ $("Format Follow-up Error").item.json.email }}", "Phone": "={{ $("Format Follow-up Error").item.json.phone }}", "Details": "={{ $("Format Follow-up Error").item.json.note + " (step: " + $("Format Follow-up Error").item.json.failedStep + ")" }}", "ErrorMessage": "={{ $("Format Follow-up Error").item.json.errorMessage }}", "ExecutionId": "={{ $execution.id }}" }, "schema": [ { "id": "Timestamp", "displayName": "Timestamp", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "LeadID", "displayName": "LeadID", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Event", "displayName": "Event", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Status", "displayName": "Status", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Priority", "displayName": "Priority", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Email", "displayName": "Email", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Phone", "displayName": "Phone", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "Details", "displayName": "Details", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "ErrorMessage", "displayName": "ErrorMessage", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false }, { "id": "ExecutionId", "displayName": "ExecutionId", "type": "string", "canBeUsedToMatch": false, "display": true, "required": false } ] }, "options": {} }, "id": "2df622d1-8879-438b-8aee-229bc13f5969", "name": "Log Follow-up Error", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.7, "position": [ 2656, 672 ], "credentials": { "googleSheetsOAuth2Api": { "id": "mBDySotAeocOEtec", "name": "Google Sheets account" } }, "onError": "continueRegularOutput" } ], "pinData": {}, "connections": { "Lead Intake Form": { "main": [ [ { "node": "Validate & Normalize", "type": "main", "index": 0 } ] ] }, "Validate & Normalize": { "main": [ [ { "node": "Valid?", "type": "main", "index": 0 } ] ] }, "Valid?": { "main": [ [ { "node": "Generate Lead ID", "type": "main", "index": 0 } ], [ { "node": "Log Validation Error", "type": "main", "index": 0 } ] ] }, "Generate Lead ID": { "main": [ [ { "node": "Check Duplicate", "type": "main", "index": 0 } ] ] }, "Check Duplicate": { "main": [ [ { "node": "Evaluate Duplicate", "type": "main", "index": 0 } ] ] }, "Evaluate Duplicate": { "main": [ [ { "node": "Duplicate?", "type": "main", "index": 0 } ] ] }, "Duplicate?": { "main": [ [ { "node": "Log Duplicate", "type": "main", "index": 0 } ], [ { "node": "Classify Lead", "type": "main", "index": 0 } ] ] }, "Log Duplicate": { "main": [ [ { "node": "Notify Owner: Duplicate", "type": "main", "index": 0 } ] ] }, "Classify Lead": { "main": [ [ { "node": "Save Lead", "type": "main", "index": 0 } ] ] }, "Save Lead": { "main": [ [ { "node": "Route by Priority", "type": "main", "index": 0 } ], [ { "node": "Format Error", "type": "main", "index": 0 } ] ] }, "Route by Priority": { "main": [ [ { "node": "Notify Owner: Urgent", "type": "main", "index": 0 } ], [ { "node": "Notify Owner: High", "type": "main", "index": 0 } ], [ { "node": "Notify Owner: Normal", "type": "main", "index": 0 } ], [ { "node": "Notify Owner: Normal", "type": "main", "index": 0 } ] ] }, "Notify Owner: Urgent": { "main": [ [ { "node": "Send Confirmation Email", "type": "main", "index": 0 } ], [ { "node": "Log Owner Email Error", "type": "main", "index": 0 } ] ] }, "Send Confirmation Email": { "main": [ [ { "node": "Log Success", "type": "main", "index": 0 } ], [ { "node": "Format Error", "type": "main", "index": 0 } ] ] }, "Log Owner Email Error": { "main": [ [ { "node": "Send Confirmation Email", "type": "main", "index": 0 } ] ] }, "Format Error": { "main": [ [ { "node": "Log Error", "type": "main", "index": 0 } ] ] }, "Log Error": { "main": [ [ { "node": "Notify Owner: System Error", "type": "main", "index": 0 } ] ] }, "Notify Owner: High": { "main": [ [ { "node": "Send Confirmation Email", "type": "main", "index": 0 } ], [ { "node": "Log Owner Email Error", "type": "main", "index": 0 } ] ] }, "Notify Owner: Normal": { "main": [ [ { "node": "Send Confirmation Email", "type": "main", "index": 0 } ], [ { "node": "Log Owner Email Error", "type": "main", "index": 0 } ] ] }, "Log Validation Error": { "main": [ [ { "node": "Notify Owner: Invalid Submission", "type": "main", "index": 0 } ] ] }, "Hourly Follow-up & Booking Check": { "main": [ [ { "node": "Automation Settings", "type": "main", "index": 0 } ] ] }, "Automation Settings": { "main": [ [ { "node": "Read Open Leads", "type": "main", "index": 0 } ] ] }, "Read Open Leads": { "main": [ [ { "node": "Stuck Follow-up or Booking?", "type": "main", "index": 0 }, { "node": "Needs Follow-up?", "type": "main", "index": 0 }, { "node": "Ready to Book?", "type": "main", "index": 0 } ] ] }, "Stuck Follow-up or Booking?": { "main": [ [ { "node": "Mark Needs Review", "type": "main", "index": 0 } ] ] }, "Mark Needs Review": { "main": [ [ { "node": "Log Stuck Item", "type": "main", "index": 0 } ] ] }, "Log Stuck Item": { "main": [ [ { "node": "Notify Owner: Needs Review", "type": "main", "index": 0 } ] ] }, "Needs Follow-up?": { "main": [ [ { "node": "Claim Follow-up Attempt", "type": "main", "index": 0 } ] ] }, "Claim Follow-up Attempt": { "main": [ [ { "node": "Send Follow-up Email", "type": "main", "index": 0 } ], [ { "node": "Format Follow-up Error", "type": "main", "index": 0 } ] ] }, "Send Follow-up Email": { "main": [ [ { "node": "Mark Follow-up Sent", "type": "main", "index": 0 } ], [ { "node": "Format Follow-up Error", "type": "main", "index": 0 } ] ] }, "Mark Follow-up Sent": { "main": [ [ { "node": "Log Follow-up", "type": "main", "index": 0 } ], [ { "node": "Format Follow-up Error", "type": "main", "index": 0 } ] ] }, "Ready to Book?": { "main": [ [ { "node": "Prepare Booking", "type": "main", "index": 0 } ] ] }, "Prepare Booking": { "main": [ [ { "node": "Valid Appointment Time?", "type": "main", "index": 0 } ] ] }, "Valid Appointment Time?": { "main": [ [ { "node": "Mark Booking Pending", "type": "main", "index": 0 } ], [ { "node": "Format Booking Error", "type": "main", "index": 0 } ] ] }, "Mark Booking Pending": { "main": [ [ { "node": "Create Calendar Event (optional)", "type": "main", "index": 0 } ], [ { "node": "Format Booking Error", "type": "main", "index": 0 } ] ] }, "Create Calendar Event (optional)": { "main": [ [ { "node": "Send Booking Confirmation", "type": "main", "index": 0 } ], [ { "node": "Format Booking Error", "type": "main", "index": 0 } ] ] }, "Send Booking Confirmation": { "main": [ [ { "node": "Mark Appointment Booked", "type": "main", "index": 0 } ], [ { "node": "Format Booking Error", "type": "main", "index": 0 } ] ] }, "Mark Appointment Booked": { "main": [ [ { "node": "Log Booking", "type": "main", "index": 0 } ], [ { "node": "Format Booking Error", "type": "main", "index": 0 } ] ] }, "Format Booking Error": { "main": [ [ { "node": "Mark Booking Failed", "type": "main", "index": 0 } ] ] }, "Mark Booking Failed": { "main": [ [ { "node": "Log Booking Error", "type": "main", "index": 0 } ] ] }, "Format Follow-up Error": { "main": [ [ { "node": "Record Follow-up Outcome", "type": "main", "index": 0 } ] ] }, "Record Follow-up Outcome": { "main": [ [ { "node": "Log Follow-up Error", "type": "main", "index": 0 } ] ] } }, "active": true, "settings": { "executionOrder": "v1", "binaryMode": "separate", "errorWorkflow": "INmTx9dD7BudPJrh" }, "versionId": "6ff8b2c0-fd2a-4db7-86a5-3f52e89895de", "meta": { "instanceId": "209bef296568f00bdf4084fd2f93d89f7ebda03f6c1ea95833df3f7a361e3b10" }, "nodeGroups": [ { "id": "eccdb857-cb28-4d8c-8083-e5e8d934b5e7", "name": "Screen & qualify lead", "nodeIds": [ "01a511ac-7ab2-4b87-9729-259317ffc0e2", "93d1335d-5061-4969-952f-7e2c5d9f8c10", "7c9caf95-066c-4ed4-8b37-6908627ce712", "e20a3115-0b8a-402d-976f-c6afed2e9549", "fcd239a6-7e35-42d4-9bc6-c6ad8abf9b73", "84d14dd8-8fbe-4812-b4a2-939617973ad2", "d6520b5f-d1dd-4dae-80c2-2d40e97052c6", "0d8c0961-9cbc-4857-9c41-74e749862edd", "1ace1af3-48aa-4592-94ed-3c622eb2a795", "5f5371db-63d3-4736-8127-a9bc40f2c824", "bce20d45-cdb1-4fe7-9f19-d76a03b2e33a" ], "description": "Cleans each submission, rejects invalid ones, assigns a Lead ID, stops duplicates by email/phone, then scores and prioritises the lead." }, { "id": "3354a808-8138-4898-8952-d60c4fddbe5c", "name": "Save, notify & confirm", "nodeIds": [ "e4d3c39f-8b2b-4ec9-86c1-2c795bdbc235", "0ed4a7c6-880d-46c1-9150-1373b2c79dba", "ae54e1e1-136b-4b89-beec-f5a2e4a406b6", "89268e77-6102-4781-8619-31127ed4abe5", "2415a2f9-989d-4a05-b1d9-1601b3b243ac", "e4e30297-81c2-493d-8cf1-c158f11798fe", "ed697389-8eb0-4b75-a210-7fc0b5e1b6e6", "3fa1674e-9481-4f7a-9914-245cc53e3e48", "85db9a52-0962-4f37-9bb6-1073e7d2edcf", "657aa905-d577-4608-847d-7dedaf6d90a0", "17495574-67b2-44cb-9777-10bd23385724" ], "description": "Saves the lead as New in the CRM, alerts the owner by priority, confirms to the customer. Sheet or email failures are logged." }, { "id": "08bd1504-de32-4970-8bb4-a21fa3b3b78b", "name": "Load open leads", "nodeIds": [ "fe4c6d59-61e0-47cb-b0fd-4904dcef166d", "416c50de-7011-42b2-926f-33dd8f442d81" ], "description": "Every hour: loads the editable timing settings and reads the CRM so follow-ups and bookings can be processed." }, { "id": "21724810-dec6-48be-b8f6-9d7a5ac216c9", "name": "Flag interrupted items", "nodeIds": [ "c8516982-d67b-420c-af24-20ca51b617b4", "76fba85e-206d-4063-9e02-412ff6b3430d", "784c15d8-e7be-4861-aadd-c3c440e33689", "8f2609eb-bb58-4342-b0b5-348409a91e33" ], "description": "Follow-ups or bookings interrupted mid-send for over 3h are flagged for review and you are emailed; nothing is re-sent automatically." }, { "id": "90f24d8e-4382-4cf6-8293-9f6777075577", "name": "Follow-up", "nodeIds": [ "f5867855-7be0-4f72-8068-6b341078a785", "41532fd1-130b-4d45-a0bf-f02982d9fe20", "f83adfcf-5aff-4328-b5c4-c8066f355062", "3bc051f3-9cb4-4b25-8240-fad818caf5f7", "1325037e-225c-430e-934e-09bfa3dd53d2", "d170eb21-6aee-4d17-b9f7-ec2d2e44f634", "404d930f-7db7-405d-8f5a-bd84834656ca", "2df622d1-8879-438b-8aee-229bc13f5969" ], "description": "Leads still New after the delay get a follow-up; failed sends retry up to the set limit, and a reserved email is never sent twice." }, { "id": "a5f8ceb4-b930-42c7-8cce-6da9092cd647", "name": "Booking", "nodeIds": [ "54e5be98-e3e0-45c9-b381-8748b1b22f9d", "c11391ad-277d-421a-9f7d-11db942035fc", "64204a65-1ebf-41b0-8810-6a254420be47", "aeef50a7-b684-4a27-b785-367f5c55b7d4", "e228ce5e-971e-48c0-8e1e-30ecc1faddbc", "0b49a73c-6299-4593-8341-6d5a628b0630", "f987b8ee-dd93-4423-927a-97973985702b", "f748c2a9-c5ba-42ff-b69a-8865e70fb235", "846559d0-2474-46cc-83e1-43bac6f3e18f", "a25a5cc3-147a-4385-ad1f-0e1ce9f198bc", "6937383e-45ec-4403-802f-eb61cb2cbe3e" ], "description": "Qualified leads with an AppointmentAt are booked; status becomes Appointment Booked only after the confirmation is sent." } ], "id": "8sOKgyxmNaN4T9kE", "tags": [] }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
3646
3647
3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
3683
3684
3685
3686
3687
3688
"name": "Lead Intake & Booking Automation",
"nodes": [
"parameters": {
"formTitle": "Book a Service / Request a Callback",
"formDescription": "Tell us how we can help and we will get back to you shortly.",
"formFields": {
"values": [
"fieldLabel": "Full Name",
"requiredField": true
"fieldLabel": "Email",
"fieldType": "email",
"requiredField": true
"fieldLabel": "Phone",
"requiredField": true
"fieldLabel": "Service Type",
"fieldType": "dropdown",
"fieldOptions": {
"values": [
"option": "General Inquiry"
"option": "Booking"
"option": "Support"
"option": "Emergency"
"requiredField": true
"fieldLabel": "Urgency",
"fieldType": "dropdown",
"fieldOptions": {
"values": [
"option": "Low"
"option": "Normal"
"option": "High"
"option": "Urgent"
"fieldLabel": "Preferred Date",
"fieldType": "date"
"fieldLabel": "Message",
"fieldType": "textarea"
"fieldType": "hiddenField",
"fieldName": "source"
"options": {
"buttonLabel": "Submit request",
"path": "lead-intake",
"respondWithOptions": {
"values": {
"formSubmittedText": "Thank you! Your request has been received. Check your inbox for a confirmation email with your reference number."
"ignoreBots": true
"id": "c9c8b1d2-2084-4730-949b-21f7fab753ff",
"name": "Lead Intake Form",
"type": "n8n-nodes-base.formTrigger",
"typeVersion": 2.6,
"position": [
0,
96
"webhookId": "b3d2ca8a-a27c-40b7-9337-13fe0514263c"
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const j = $json;\nconst raw = (k) => (j[k] === undefined || j[k] === null) ? '' : String(j[k]);\n// Safety: strip HTML tags, angle brackets and links from customer text so it cannot be used to send spam or phishing through our emails\nconst stripUnsafe = (v) => String(v).replace(/<[^>]*>/g, ' ').replace(/[<>]/g, ' ').replace(/\b(?:https?:\/\/|www\.)\S+/gi, '[link removed]');\nconst clean = (k) => stripUnsafe(raw(k)).replace(/\s+/g, ' ').trim();\nconst fullName = clean('Full Name');\nconst email = clean('Email').replace(/\s/g, '').toLowerCase();\n// Phone normalized to digits only (e.g. +961 70-123 456 -> 96170123456) so it stores and matches consistently\nconst phone = clean('Phone').replace(/[^0-9]/g, '');\nconst serviceType = clean('Service Type');\nconst urgency = clean('Urgency') || 'Normal';\nconst preferredDate = clean('Preferred Date');\nconst message = stripUnsafe(raw('Message')).replace(/[ \t]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim();\n\n// Source tracking: ?source=Facebook on the form URL, defaults to Website\nconst SOURCES = ['Website', 'Facebook', 'Instagram', 'WhatsApp', 'Google', 'Manual', 'Other'];\nconst srcRaw = clean('source');\nconst source = srcRaw ? (SOURCES.find(s => s.toLowerCase() === srcRaw.toLowerCase()) || 'Other') : 'Website';\n\nconst missing = [];\nif (!fullName) missing.push('Full Name');\nif (!email) missing.push('Email');\nif (!phone) missing.push('Phone');\nif (!serviceType) missing.push('Service Type');\nconst emailValid = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email);\nif (email && !emailValid) missing.push('Valid Email');\nif (phone && phone.replace(/[^0-9]/g, '').length < 7) missing.push('Valid Phone');\n// Length limits (spam / abuse protection)\nif (fullName.length > 100) missing.push('Full Name too long (max 100)');\nif (email.length > 254) missing.push('Email too long');\nif (phone.length > 20) missing.push('Phone too long (max 20 digits)');\nif (message.length > 2000) missing.push('Message too long (max 2000)');\nif (preferredDate.length > 40) missing.push('Preferred Date invalid');\nconst isValid = missing.length === 0;\nreturn {\n json: {\n fullName, email, phone, serviceType, urgency, preferredDate, message, source,\n isValid,\n missingFields: missing.join(', '),\n validationError: isValid ? '' : ('Missing or invalid fields: ' + missing.join(', ')),\n submittedAt: new Date().toISOString(),\n },\n};"
"id": "01a511ac-7ab2-4b87-9729-259317ffc0e2",
"name": "Validate & Normalize",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-112,
128
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
"combinator": "and",
"conditions": [
"id": "is-valid",
"leftValue": "={{ $json.isValid }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
"options": {}
"id": "3354a808-8138-4898-8952-d60c4fddbe5c",
"name": "Save, notify & confirm",
"nodeIds": [
"e4d3c39f-8b2b-4ec9-86c1-2c795bdbc235",
"0ed4a7c6-880d-46c1-9150-1373b2c79dba",
"ae54e1e1-136b-4b89-beec-f5a2e4a406b6",
"89268e77-6102-4781-8619-31127ed4abe5",
"2415a2f9-989d-4a05-b1d9-1601b3b243ac",
"e4e30297-81c2-493d-8cf1-c158f11798fe",
"ed697389-8eb0-4b75-a210-7fc0b5e1b6e6",
"3fa1674e-9481-4f7a-9914-245cc53e3e48",
"85db9a52-0962-4f37-9bb6-1073e7d2edcf",
"657aa905-d577-4608-847d-7dedaf6d90a0",
"17495574-67b2-44cb-9777-10bd23385724"
"description": "Saves the lead as New in the CRM, alerts the owner by priority, confirms to the customer. Sheet or email failures are logged."
"id": "08bd1504-de32-4970-8bb4-a21fa3b3b78b",
"name": "Load open leads",
"nodeIds": [
"fe4c6d59-61e0-47cb-b0fd-4904dcef166d",
"416c50de-7011-42b2-926f-33dd8f442d81"
"description": "Every hour: loads the editable timing settings and reads the CRM so follow-ups and bookings can be processed."
"id": "21724810-dec6-48be-b8f6-9d7a5ac216c9",
"name": "Flag interrupted items",
"nodeIds": [
"c8516982-d67b-420c-af24-20ca51b617b4",
"76fba85e-206d-4063-9e02-412ff6b3430d",
"784c15d8-e7be-4861-aadd-c3c440e33689",
"8f2609eb-bb58-4342-b0b5-348409a91e33"
"description": "Follow-ups or bookings interrupted mid-send for over 3h are flagged for review and you are emailed; nothing is re-sent automatically."
"id": "90f24d8e-4382-4cf6-8293-9f6777075577",
"name": "Follow-up",
"nodeIds": [
"f5867855-7be0-4f72-8068-6b341078a785",
"41532fd1-130b-4d45-a0bf-f02982d9fe20",
"f83adfcf-5aff-4328-b5c4-c8066f355062",
"3bc051f3-9cb4-4b25-8240-fad818caf5f7",
"1325037e-225c-430e-934e-09bfa3dd53d2",
"d170eb21-6aee-4d17-b9f7-ec2d2e44f634",
"404d930f-7db7-405d-8f5a-bd84834656ca",
"2df622d1-8879-438b-8aee-229bc13f5969"
"description": "Leads still New after the delay get a follow-up; failed sends retry up to the set limit, and a reserved email is never sent twice."
"id": "a5f8ceb4-b930-42c7-8cce-6da9092cd647",
"name": "Booking",
"nodeIds": [
"54e5be98-e3e0-45c9-b381-8748b1b22f9d",
"c11391ad-277d-421a-9f7d-11db942035fc",
"64204a65-1ebf-41b0-8810-6a254420be47",
"aeef50a7-b684-4a27-b785-367f5c55b7d4",
"e228ce5e-971e-48c0-8e1e-30ecc1faddbc",
"0b49a73c-6299-4593-8341-6d5a628b0630",
"f987b8ee-dd93-4423-927a-97973985702b",
"f748c2a9-c5ba-42ff-b69a-8865e70fb235",
"846559d0-2474-46cc-83e1-43bac6f3e18f",
"a25a5cc3-147a-4385-ad1f-0e1ce9f198bc",
"6937383e-45ec-4403-802f-eb61cb2cbe3e"
"description": "Qualified leads with an AppointmentAt are booked; status becomes Appointment Booked only after the confirmation is sent."
"id": "8sOKgyxmNaN4T9kE",
"tags": []
While the code is focused, press Alt+F1 for a menu of operations.

0 likes · 0 views · Oct 13, 2026

Automation workflow for lead intake and booking: validates/normalizes submissions, saves leads to CRM, confirms bookings, retries failed sends, and schedules follow-ups.

Tags