{
  "name": "AI Email Automation with n8n: Outbound Outreach (free JSON)",
  "nodes": [
    {
      "id": "0d069126-2ec1-43ac-8122-8d444f20c43b",
      "name": "When clicking ‘Execute workflow’",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -1232,
        304
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "461208fc-76f3-4882-a743-ca623b2ca29b",
      "name": "Configure GPT-4o Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatAzureOpenAi",
      "position": [
        -16,
        832
      ],
      "parameters": {
        "model": "gpt-4o",
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "a7137b17-4d59-4ce1-857c-03e15e590f70",
      "name": "Retrieve Lead Records from Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -944,
        304
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "outreach automation"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4
    },
    {
      "id": "35a5d947-361f-40a9-9d57-efe5a2d462db",
      "name": "Validate Lead Data Payload",
      "type": "n8n-nodes-base.if",
      "position": [
        -688,
        304
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "e2adb005-2b3c-4d1e-8445-442df1fe925a",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "=/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test({{ $json.Email }})",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "f7aaf899-8eec-4178-bc3e-88985592be72",
      "name": "Filter for Booked Leads",
      "type": "n8n-nodes-base.if",
      "position": [
        -400,
        304
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "b7913fcd-b70c-47fd-a926-d09a0086b40e",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json[\"Booking Status\"] }}",
              "rightValue": "BOOKED"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "35b45b6e-c542-49f7-aead-01c561647444",
      "name": "Generate Personalized Outreach Email (AI)",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        0,
        320
      ],
      "parameters": {
        "text": "=Generate a personalized outreach email in HTML format for this lead:\n\nCompany Name: {{ $json[\"Company Name\"] }}\nContact Person: {{ $json[\"Contact Person\"] }}\nJob Title: {{ $json[\"Job Title\"] }}\nIndustry: {{ $json[\"Industry\"] }}\nLocation: {{ $json[\"Location\"] }}\nLead Source: {{ $json[\"Lead Source\"] }}\nBooking Status: {{ $json[\"Booking Status\"] }}\n\nThe goal is to re-engage them and encourage a meeting booking.\nMention their company or industry naturally, and make it sound warm and specific.\nKeep it clean and mobile-friendly HTML (no styles or colors).\n",
        "options": {
          "systemMessage": "=You are an AI sales assistant that writes short, friendly, and professional B2B outreach emails in HTML format. \nEach email should be under 150 words and formatted using <p>, <b>, and <br> tags. \nAlways output both the Subject and the Email Body separately. \nDo not include <html> or <body> tags. \nThe tone should be polite, conversational, and slightly persuasive. \nEnd with a clear call to action (like booking a meeting) and sign off as “YOUR_NAME, YOUR_COMPANY.”\nReturn your answer in JSON format with two keys:\n{\n  \"subject\": \"...\",\n  \"body_html\": \"...\"\n}\n"
        },
        "promptType": "define"
      },
      "typeVersion": 2.1
    },
    {
      "id": "eb74688d-5d4b-4a8f-ba88-03111715fb52",
      "name": "Parse AI Email Output (JavaScript)",
      "type": "n8n-nodes-base.code",
      "position": [
        448,
        320
      ],
      "parameters": {
        "jsCode": "// Loop through AI results and clean up JSON output\nreturn items.map(item => {\n  let raw = item.json.output || '';\n  \n  // Remove code block markers like ```json or ```\n  raw = raw.replace(/```json|```/g, '').trim();\n\n  // Parse JSON safely\n  let parsed;\n  try {\n    parsed = JSON.parse(raw);\n  } catch (e) {\n    parsed = { subject: \"No Subject\", body_html: \"Error parsing AI output.\" };\n  }\n\n  return { json: parsed };\n});\n"
      },
      "typeVersion": 2
    },
    {
      "id": "5e6c825b-1739-411b-b7b5-291937596a9f",
      "name": "Send Initial Outreach Email via Gmail",
      "type": "n8n-nodes-base.gmail",
      "position": [
        768,
        320
      ],
      "parameters": {
        "sendTo": "={{ $('Retrieve Lead Records from Google Sheets').item.json.Email }}",
        "message": "={{ $json.body_html }}",
        "options": {},
        "subject": "={{ $json.subject }}"
      },
      "typeVersion": 2.1
    },
    {
      "id": "540211f0-f006-4845-8fed-02d41d9fc634",
      "name": "Wait Before Thread Check (24 hr)",
      "type": "n8n-nodes-base.wait",
      "position": [
        1040,
        320
      ],
      "parameters": {
        "amount": 24
      },
      "typeVersion": 1
    },
    {
      "id": "537915a6-88d4-4353-b5f7-d0c2424119ce",
      "name": "Fetch Sent Email Thread from Gmail",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1312,
        320
      ],
      "parameters": {
        "options": {},
        "resource": "thread",
        "threadId": "={{ $('Send Initial Outreach Email via Gmail').item.json.threadId }}",
        "operation": "get"
      },
      "typeVersion": 2.1
    },
    {
      "id": "80bbd250-f9ab-46de-9ad6-b3ab8e7f3233",
      "name": "Check for Client Reply (If Condition)",
      "type": "n8n-nodes-base.if",
      "position": [
        1568,
        320
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "4a6d1310-29be-4559-8bea-afe8fe0f0f30",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{$json[\"messages\"][1][\"snippet\"]}}",
              "rightValue": "Yes"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "5366521f-fc42-4907-9e1e-29994458578c",
      "name": "Extract Reply Details (JavaScript)",
      "type": "n8n-nodes-base.code",
      "position": [
        1920,
        48
      ],
      "parameters": {
        "jsCode": "// Loop through all email threads where reply = true\nreturn items.map(item => {\n  const data = item.json;\n\n  // Extract the reply message (second email in thread)\n  const reply = data.messages?.[1];\n  const original = data.messages?.[0];\n\n  // Clean the reply text (remove quoted text from Gmail)\n  let clientMessage = reply?.snippet || \"\";\n  clientMessage = clientMessage.split(\"On Wed\")[0].trim(); // basic cleanup example\n\n  // Prepare clean structured output\n  return {\n    json: {\n      Email: reply?.From?.match(/<(.+?)>/)?.[1] || reply?.From || \"\",\n      ContactPerson: reply?.From?.split(\"<\")[0].trim() || \"\",\n      ThreadID: reply?.threadId || data.id || \"\",\n      Subject: reply?.Subject || original?.Subject || \"\",\n      ClientMessage: clientMessage,\n      BookingStatus: \"BOOKED\",\n      LastUpdated: new Date().toISOString()\n    }\n  };\n});\n"
      },
      "typeVersion": 2
    },
    {
      "id": "dfc24134-8604-47d3-8932-41df52d14620",
      "name": "Update Lead Record – Booked (Google Sheets)",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2224,
        48
      ],
      "parameters": {
        "columns": {
          "value": {
            "Email": "={{ $json.Email }}",
            "ThreadID": "={{ $json.ThreadID }}",
            "LastUpdated": "={{ $json.LastUpdated }}",
            "ClientMessage": "={{ $json.ClientMessage }}",
            "Booking Status": "={{ $json.BookingStatus }}"
          },
          "schema": [
            {
              "id": "Company Name",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Company Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Contact Person",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Contact Person",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Job Title",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Job Title",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Email",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Email",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Phone",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Industry",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Industry",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Location",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Location",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Lead Source",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Lead Source",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Booking Status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Booking Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ClientMessage",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "ClientMessage",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "LastUpdated",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "LastUpdated",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ThreadID",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "ThreadID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "Email"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "outreach automation"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.6
    },
    {
      "id": "bf3a2ee4-1bbc-445e-8c3f-2119dd16e95d",
      "name": "Wait Before Second Thread Check (24 hr)",
      "type": "n8n-nodes-base.wait",
      "position": [
        2224,
        544
      ],
      "parameters": {
        "unit": "hours",
        "amount": 24
      },
      "typeVersion": 1
    },
    {
      "id": "6ac3e8ce-2dac-4953-97d2-3221e3d5be82",
      "name": "Send Second Follow-Up Email via Gmail",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1888,
        544
      ],
      "parameters": {
        "sendTo": "={{ $json.messages[0].To }}",
        "message": "=<p>Hi {{ $json[\"Contact Person\"] || \"there\" }},</p>\n\n<p>I wanted to quickly follow up on my previous email.  \nI understand things can get busy, so just checking if you had a chance to review my message.</p>\n\n<p>I’d love to schedule a short chat to explore how we could support <b> {{ $('Retrieve Lead Records from Google Sheets').item.json['Contact Person'] }}</b> in your upcoming initiatives.</p>\n\n<p>If now’s not a good time, no worries — just let me know when it might be better for you.</p>\n\n<p>Best regards,<br>\n<b>YOUR_NAME</b><br>\nYOUR_COMPANY</p>\n",
        "options": {},
        "subject": "Still open to a quick chat."
      },
      "typeVersion": 2.1
    },
    {
      "id": "7b8c0b58-0aba-4b63-b690-6efee3c35d52",
      "name": "Fetch Follow-Up Thread from Gmail",
      "type": "n8n-nodes-base.gmail",
      "position": [
        2512,
        544
      ],
      "parameters": {
        "options": {},
        "resource": "thread",
        "threadId": "={{ $json.threadId }}",
        "operation": "get"
      },
      "typeVersion": 2.1
    },
    {
      "id": "9b0df250-edcd-4963-a14d-7c332ca2ba78",
      "name": "Check for Second Reply (If Condition)",
      "type": "n8n-nodes-base.if",
      "position": [
        2784,
        544
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "4a6d1310-29be-4559-8bea-afe8fe0f0f30",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{$json[\"messages\"][1][\"snippet\"]}}",
              "rightValue": "Yes"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "54350e4a-92e1-445e-aa9a-05ca7b2bc015",
      "name": "Update Lead Status to Declined (Set Node)",
      "type": "n8n-nodes-base.set",
      "position": [
        3200,
        800
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "eddf188b-c756-4cd3-b5ac-3bdbce3a3049",
              "name": "Booking Status",
              "type": "string",
              "value": "Declined"
            },
            {
              "id": "df707470-60d7-4024-a998-8b7ba3c93283",
              "name": "Email",
              "type": "string",
              "value": "={{ $json.messages[0].To }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "cb100856-ab8b-42d8-a899-8273a21d2498",
      "name": "Update Lead Record – Declined (Google Sheets)",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        3424,
        800
      ],
      "parameters": {
        "columns": {
          "value": {
            "Email": "={{ $json.Email }}",
            "Booking Status": "={{ $json[\"Booking Status\"] }}"
          },
          "schema": [
            {
              "id": "Company Name",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Company Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Contact Person",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Contact Person",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Job Title",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Job Title",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Email",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Email",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Phone",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Industry",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Industry",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Location",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Location",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Lead Source",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Lead Source",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Booking Status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Booking Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ClientMessage",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "ClientMessage",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "LastUpdated",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "LastUpdated",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ThreadID",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "ThreadID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "Email"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "outreach automation"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.6
    },
    {
      "id": "392fd316-f1e0-4de5-aea6-5fb872b1c767",
      "name": "Update Lead Record – Replied (Google Sheets)",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        3440,
        240
      ],
      "parameters": {
        "columns": {
          "value": {
            "Email": "={{ $json.Email }}",
            "ThreadID": "={{ $json.ThreadID }}",
            "LastUpdated": "={{ $json.LastUpdated }}",
            "ClientMessage": "={{ $json.ClientMessage }}",
            "Booking Status": "={{ $json.BookingStatus }}"
          },
          "schema": [
            {
              "id": "Company Name",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Company Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Contact Person",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Contact Person",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Job Title",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Job Title",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Email",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Email",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Phone",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Industry",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Industry",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Location",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Location",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Lead Source",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Lead Source",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Booking Status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Booking Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ClientMessage",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "ClientMessage",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "LastUpdated",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "LastUpdated",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ThreadID",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "ThreadID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "Email"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "outreach automation"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.6
    },
    {
      "id": "6da18860-30f8-476a-84fc-b10084fc6f4e",
      "name": "Extract Second Reply Details (JavaScript)",
      "type": "n8n-nodes-base.code",
      "position": [
        3168,
        240
      ],
      "parameters": {
        "jsCode": "// Loop through all email threads where reply = true\nreturn items.map(item => {\n  const data = item.json;\n\n  // Extract the reply message (second email in thread)\n  const reply = data.messages?.[1];\n  const original = data.messages?.[0];\n\n  // Clean the reply text (remove quoted text from Gmail)\n  let clientMessage = reply?.snippet || \"\";\n  clientMessage = clientMessage.split(\"On Wed\")[0].trim(); // basic cleanup example\n\n  // Prepare clean structured output\n  return {\n    json: {\n      Email: reply?.From?.match(/<(.+?)>/)?.[1] || reply?.From || \"\",\n      ContactPerson: reply?.From?.split(\"<\")[0].trim() || \"\",\n      ThreadID: reply?.threadId || data.id || \"\",\n      Subject: reply?.Subject || original?.Subject || \"\",\n      ClientMessage: clientMessage,\n      BookingStatus: \"BOOKED\",\n      LastUpdated: new Date().toISOString()\n    }\n  };\n});\n"
      },
      "typeVersion": 2
    },
    {
      "id": "3672ff59-f78b-42c3-b5d4-c311bd12bc09",
      "name": "Log Invalid Leads to Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -352,
        784
      ],
      "parameters": {
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Invalid Leads"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": null,
          "matchingColumns": [],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "typeVersion": 4.6
    },
    {
      "id": "fa1d3fb9-4316-473c-a98d-d86688288bc1",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2176,
        -1000
      ],
      "parameters": {
        "width": 976,
        "height": 1000,
        "content": "## AI email automation for outbound outreach and follow-ups\n\nYou start the workflow by hand. It reads leads from a Google Sheet, asks a GPT-4o model to write one outreach email per lead, sends it from your Gmail account, waits 24 hours, and looks at the email thread. If the second message in the thread contains the word Yes, the lead is marked BOOKED. Otherwise a fixed follow-up is sent, the workflow waits 24 hours again, checks the follow-up's thread, and marks the lead BOOKED or Declined in the sheet.\n\n### How it works\n\n1. Manual start, then Retrieve Lead Records from Google Sheets.\n2. Validate Lead Data Payload: meant to catch bad email addresses.\n3. Filter for Booked Leads: only rows that are exactly BOOKED are held back.\n4. Generate Personalized Outreach Email (AI) with Configure GPT-4o Model, then Parse AI Email Output (JavaScript).\n5. Send Initial Outreach Email via Gmail, wait 24 hours, read the thread, test for a reply.\n6. Reply found: extract it and update the sheet as BOOKED.\n7. No reply found: send the follow-up, wait 24 hours, read its thread, test again, then BOOKED or Declined.\n\n### Setup\n\n1. Connect Google Sheets, Gmail and Azure OpenAI credentials.\n2. Replace YOUR_GOOGLE_SHEET_ID in all five Google Sheets nodes.\n3. Leads tab named outreach automation with these headers in row 1: Company Name, Contact Person, Job Title, Email, Phone, Industry, Location, Lead Source, Booking Status, ClientMessage, LastUpdated, ThreadID.\n4. Create a tab named Invalid Leads with the same headers.\n5. Replace YOUR_NAME and YOUR_COMPANY in the AI instructions and in the follow-up email.\n6. Test with your own addresses first. Read the note Before you turn it on.\n\n### Customize\n\n- The wording: the AI instructions in the email-writing node and the follow-up text in the second Gmail node.\n- The waits: the two Wait nodes.\n\nSource: based on a template from the n8n Library at n8n.io, documented by LeenLabs.\nNot tested on live APIs."
      },
      "typeVersion": 1
    },
    {
      "id": "396639c9-7361-4452-9d84-3d979e7823fb",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1008,
        0
      ],
      "parameters": {
        "color": 7,
        "height": 512,
        "content": "## Retrieve Lead Records from Google Sheets\n\nReads every row of the leads tab. Row 1 needs these headers: Company Name, Contact Person, Job Title, Email, Phone, Industry, Location, Lead Source, Booking Status, ClientMessage, LastUpdated, ThreadID."
      },
      "typeVersion": 1
    },
    {
      "id": "7c0917c7-bbb8-493c-b795-7d0cbef9de56",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -752,
        48
      ],
      "parameters": {
        "color": 7,
        "height": 448,
        "content": "## Validate Lead Data Payload\n\nMeant to check that Email looks like an address. As far as we can tell, the expression builds a text string around the address instead of testing it, so every row continues and the Invalid Leads branch is not reached."
      },
      "typeVersion": 1
    },
    {
      "id": "e4fd669c-4099-4fb2-ac1a-f8b50a0d957c",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -464,
        64
      ],
      "parameters": {
        "color": 7,
        "height": 432,
        "content": "## Filter for Booked Leads\n\nRows whose Booking Status is exactly BOOKED stop here. Every other row continues, including Declined rows and rows already contacted."
      },
      "typeVersion": 1
    },
    {
      "id": "91004249-8604-423c-9173-1510532b4c32",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -80,
        576
      ],
      "parameters": {
        "color": 7,
        "height": 416,
        "content": "## Configure GPT-4o Model\n\nAzure OpenAI chat model for the email-writing step. Needs an Azure OpenAI credential, and the model field is the deployment name in Azure. Another chat model node can replace it."
      },
      "typeVersion": 1
    },
    {
      "id": "36b8dd72-9f4a-4f7a-b42f-16b917b7a7f7",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -32,
        -16
      ],
      "parameters": {
        "color": 7,
        "width": 288,
        "height": 528,
        "content": "## Generate Personalized Outreach Email (AI)\n\nWrites a subject and an HTML body as JSON from the lead fields only. It has no other facts about the lead, so it can invent details. Replace YOUR_NAME and YOUR_COMPANY in the sign-off, here and in the follow-up email."
      },
      "typeVersion": 1
    },
    {
      "id": "48df91f8-b98d-47ca-9ae1-7b18883531ea",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        384,
        64
      ],
      "parameters": {
        "color": 7,
        "height": 432,
        "content": "## Parse AI Email Output (JavaScript)\n\nRemoves code fences and parses the JSON. If parsing fails, it outputs the subject No Subject and the body Error parsing AI output., and that is sent to the lead."
      },
      "typeVersion": 1
    },
    {
      "id": "ea0c948b-46f5-4439-811f-b109d9f13b3a",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        704,
        64
      ],
      "parameters": {
        "color": 7,
        "height": 464,
        "content": "## Send Initial Outreach Email via Gmail\n\nSends the email to the Email value of the lead row, from the connected Gmail account. The thread id is used later to look for a reply."
      },
      "typeVersion": 1
    },
    {
      "id": "0d840272-78c6-4676-9e22-e80ed0c51c85",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        976,
        96
      ],
      "parameters": {
        "color": 7,
        "height": 432,
        "content": "## Wait Before Thread Check\n\nWaits 24 hours, then the thread is read."
      },
      "typeVersion": 1
    },
    {
      "id": "d4508106-1715-400b-a4c4-1ae422647747",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1232,
        144
      ],
      "parameters": {
        "color": 7,
        "width": 512,
        "height": 400,
        "content": "## Check for Client Reply\n\nReads the thread and tests whether the second message contains the word Yes, with a capital Y. Anything else is treated as no reply."
      },
      "typeVersion": 1
    },
    {
      "id": "d4b7826a-4119-4638-989d-7ced0c23cfb9",
      "name": "Sticky Note10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1856,
        -224
      ],
      "parameters": {
        "color": 7,
        "height": 464,
        "content": "## Extract Reply Details\n\nReads the second message and prepares the BOOKED update. Quoted text is cut only where the text On Wed appears."
      },
      "typeVersion": 1
    },
    {
      "id": "cfb1aeb6-0030-45a8-aba9-8473abcbb64b",
      "name": "Sticky Note11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2160,
        -256
      ],
      "parameters": {
        "color": 7,
        "height": 496,
        "content": "## Update Lead Record: Booked\n\nAdds or updates the row that matches the Email value."
      },
      "typeVersion": 1
    },
    {
      "id": "89803f2c-06ef-4ff5-aaab-37a21b4e3069",
      "name": "Sticky Note12",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1824,
        272
      ],
      "parameters": {
        "color": 7,
        "height": 480,
        "content": "## Send Second Follow-Up Email\n\nSends a fixed follow-up as a new email thread, not as a reply in the first one."
      },
      "typeVersion": 1
    },
    {
      "id": "ef74ec33-bd55-4f50-baa8-7b8ee5b28953",
      "name": "Sticky Note13",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2160,
        304
      ],
      "parameters": {
        "color": 7,
        "width": 208,
        "height": 432,
        "content": "## Wait Before Second Thread Check\n\nWaits 24 hours. In the file we received this wait was set to 24 seconds; LeenLabs changed it to hours."
      },
      "typeVersion": 1
    },
    {
      "id": "1fc0aee3-436a-4048-a8c7-1961a47ffec4",
      "name": "Sticky Note14",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2432,
        336
      ],
      "parameters": {
        "color": 7,
        "height": 416,
        "content": "## Fetch Follow-Up Thread\n\nReads the follow-up's own thread. A reply to the first email is not seen here."
      },
      "typeVersion": 1
    },
    {
      "id": "67182b47-ddba-47d2-be7c-ea3ad548d7a1",
      "name": "Sticky Note15",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2720,
        320
      ],
      "parameters": {
        "color": 7,
        "height": 432,
        "content": "## Check for Second Reply\n\nSame test as the first one: the second message contains Yes."
      },
      "typeVersion": 1
    },
    {
      "id": "833511eb-4e42-484c-ad8a-8bf59814b5a2",
      "name": "Sticky Note16",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3088,
        608
      ],
      "parameters": {
        "color": 7,
        "width": 560,
        "height": 384,
        "content": "## Update Lead Record: Declined\n\nEverything that is not a Yes is marked Declined, including replies such as No thanks or Sure, call me."
      },
      "typeVersion": 1
    },
    {
      "id": "99ffdfe2-e395-4004-acf7-3dfc51d5cfe6",
      "name": "Sticky Note17",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3104,
        0
      ],
      "parameters": {
        "color": 7,
        "height": 432,
        "content": "## Extract Second Reply Details\n\nSame extraction as the first one, for a Yes in the follow-up thread."
      },
      "typeVersion": 1
    },
    {
      "id": "6570873f-5445-49cb-951d-9aa2014df10c",
      "name": "Sticky Note18",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3376,
        -32
      ],
      "parameters": {
        "color": 7,
        "height": 496,
        "content": "## Update Lead Record: Replied\n\nAdds or updates the row that matches the Email value."
      },
      "typeVersion": 1
    },
    {
      "id": "22fed90c-0601-421b-9079-3ca3652b1f9a",
      "name": "Sticky Note19",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -416,
        560
      ],
      "parameters": {
        "color": 7,
        "height": 464,
        "content": "## Log Invalid Leads\n\nAppends rows to an Invalid Leads tab. Set the spreadsheet and the tab, and give the tab the same headers as the leads tab. Because the check before it passes every row, this node is not reached."
      },
      "typeVersion": 1
    },
    {
      "id": "5d2a8e47-9b13-4c60-a4f1-7e0c3b96d258",
      "name": "Before you turn it on",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2176,
        -1700
      ],
      "parameters": {
        "color": 3,
        "width": 976,
        "height": 660,
        "content": "## Before you turn it on\n\n1. This sends real emails to real people, written by an AI, with no human review. Use your own addresses first.\n2. The email check does not work as written, so bad addresses are not caught.\n3. Only rows whose status is exactly BOOKED are skipped. Declined leads and leads already contacted are emailed again on the next run.\n4. A reply counts as BOOKED only if the second message in the thread contains Yes with a capital Y. A reply such as No thanks or Sure, call me is treated as no reply.\n5. If the AI answer cannot be read, the lead still receives an email with the subject No Subject.\n6. The follow-up is a new thread. A reply to the first email after the follow-up is not seen, and the lead is marked Declined.\n7. The AI only knows the sheet fields. It can invent facts, and it can invent a booking link.\n8. There is no opt-out line, no sender identity check and no sending limit. Rules on unsolicited email differ by country. Check them.\n9. Lead data is sent to Azure OpenAI and Google. Gmail attribution text may be added by the Gmail node. Check it.\n10. Not tested on live APIs."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Configure GPT-4o Model": {
      "ai_languageModel": [
        [
          {
            "node": "Generate Personalized Outreach Email (AI)",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Filter for Booked Leads": {
      "main": [
        [],
        [
          {
            "node": "Generate Personalized Outreach Email (AI)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Lead Data Payload": {
      "main": [
        [
          {
            "node": "Filter for Booked Leads",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log Invalid Leads to Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait Before Thread Check (24 hr)": {
      "main": [
        [
          {
            "node": "Fetch Sent Email Thread from Gmail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Follow-Up Thread from Gmail": {
      "main": [
        [
          {
            "node": "Check for Second Reply (If Condition)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Reply Details (JavaScript)": {
      "main": [
        [
          {
            "node": "Update Lead Record – Booked (Google Sheets)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Sent Email Thread from Gmail": {
      "main": [
        [
          {
            "node": "Check for Client Reply (If Condition)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse AI Email Output (JavaScript)": {
      "main": [
        [
          {
            "node": "Send Initial Outreach Email via Gmail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking ‘Execute workflow’": {
      "main": [
        [
          {
            "node": "Retrieve Lead Records from Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check for Client Reply (If Condition)": {
      "main": [
        [
          {
            "node": "Extract Reply Details (JavaScript)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Second Follow-Up Email via Gmail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check for Second Reply (If Condition)": {
      "main": [
        [
          {
            "node": "Extract Second Reply Details (JavaScript)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Update Lead Status to Declined (Set Node)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Initial Outreach Email via Gmail": {
      "main": [
        [
          {
            "node": "Wait Before Thread Check (24 hr)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Second Follow-Up Email via Gmail": {
      "main": [
        [
          {
            "node": "Wait Before Second Thread Check (24 hr)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait Before Second Thread Check (24 hr)": {
      "main": [
        [
          {
            "node": "Fetch Follow-Up Thread from Gmail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retrieve Lead Records from Google Sheets": {
      "main": [
        [
          {
            "node": "Validate Lead Data Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Second Reply Details (JavaScript)": {
      "main": [
        [
          {
            "node": "Update Lead Record – Replied (Google Sheets)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Personalized Outreach Email (AI)": {
      "main": [
        [
          {
            "node": "Parse AI Email Output (JavaScript)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Lead Status to Declined (Set Node)": {
      "main": [
        [
          {
            "node": "Update Lead Record – Declined (Google Sheets)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}