
Affiliate disclosure: some links in this article may be affiliate links. If you sign up through them, we may earn a commission at no extra cost to you. See our Terms of Service.
You have a list of people who might want what you sell. You write to them one by one, you wait, you check your inbox to see who answered, and a few days later you write again to the ones who stayed silent. It is slow, it is repetitive, and the follow-up is the first thing to be forgotten when the week gets busy. That is the job AI email automation tries to take over, and this free n8n workflow is a small, complete example of outbound email automation: it reads leads from a Google Sheet, asks an AI model to write one outreach email per lead, sends it from your Gmail account, waits 24 hours, looks for a reply, sends a follow-up if there is none, and writes the result back into the sheet.
It is a useful template to learn from, because it shows every moving part of outreach in one place: the list, the writing, the sending, the waiting, the reply check and the record keeping. It is also a template that can embarrass you if you switch it on without reading it. In this guide we explain each of the 22 steps in plain English, show with simulated examples where the logic goes wrong, and tell you exactly what we fixed, what we left alone, and what we could not check.
Read this first: it emails real people, written by an AI, with nobody reviewing
- The email check does nothing. The step meant to catch bad email addresses lets every row through, so invalid addresses are not caught.
- Leads can be emailed again and again. Only rows whose status is exactly BOOKED are skipped. A lead you already emailed, or who declined, gets a new email on the next run.
- Replies are judged by one capital-letter word. A reply counts as a booking only if it contains
Yeswith a capital Y. A friendly “Sure, Tuesday works” is treated as no reply, and the lead then receives a follow-up. - There is no opt-out line, no sending limit and no human review. The AI knows only what is in the sheet and can invent details.
Test with your own email addresses first. Rules on unsolicited email differ by country, and this guide is not legal advice.
Template source: this workflow comes from the n8n Library at n8n.io, the public collection of workflow templates. We did not create it. LeenLabs prepared its JSON for publication and documented it. What we changed is listed in the section What we changed in the original template.
At a glance
- What it does: writes and sends a personalized outreach email to each lead in a Google Sheet, checks for a reply after 24 hours, sends one follow-up, and records BOOKED or Declined.
- Apps used: n8n, Google Sheets, Gmail and Azure OpenAI (GPT-4o).
- Trigger: manual. You click a button; nothing runs on a schedule.
- Setup effort: medium to high. Three accounts, a sheet with exact headers, and an Azure OpenAI deployment.
- Cost to run: the file is free. Each lead uses one AI call, and the AI service, Google and n8n have their own plans and limits.
- Watch out for: a validation step that validates nothing, repeated emails, a fragile reply test, invented facts, no opt-out line and lead data going to other services.
- Test status: the code and the tests in the file were run outside n8n on simulated data; the full file was not run in n8n and no live service was contacted.
In this guide
- What is email automation?
- AI email automation for outbound outreach: what this workflow does
- Words you will see in this guide
- Apps and accounts this workflow talks to
- What to have ready before you import anything
- What the logic really does: worked examples
- The big picture: from lead to result
- Follow the data: what each of the 22 steps does
- Email automation tool or n8n workflow: which should you use?
- Get the workflow JSON
- Install it in twelve steps
- What it will not do: limits, costs and trade-offs
- Access, privacy and responsible use
- Problems you may meet and how to fix them
- Too much of a puzzle? Let us build or adapt it for you
- What we changed in the original template
- What was verified, and what was not
- Five ways to adapt the workflow
- Frequently asked questions
- Tools used in this workflow
- Suggested reading
- Keep exploring
What is email automation?
Email automation means that software sends emails for you, based on rules, instead of you writing and sending each one by hand. The simplest kind is a welcome email that goes out when someone signs up. A more advanced kind watches what happens next: it waits, checks whether the person answered, and decides what to send. Some email automation is built for people who already agreed to hear from you (newsletters, receipts, onboarding). Some is built to reach people who have not (outreach, sometimes called cold email). The difference matters a great deal, legally and for your reputation, and we come back to it in the section on responsible use.
The words you will meet most often:
- Marketing email automation: automated emails to a list of subscribers or customers, such as newsletters, promotions or a sequence after sign-up. It usually runs inside a dedicated platform and relies on people having opted in.
- Outbound email automation: automated emails you start, towards people who have not asked to hear from you. This workflow is of this kind.
- AI email automation: automation where an AI model writes, or helps write, the message. This workflow uses it for the first email only.
- Email automation tool: a ready-made product for these jobs. This workflow is not a product; it is a set of instructions that you run inside n8n, and you are responsible for everything it does.
AI email automation for outbound outreach: what this workflow does
In one run, the workflow does these jobs:
- Reads every row of a Google Sheet that holds your leads.
- Checks that each email address looks valid (this step does not work as written).
- Skips rows whose status is exactly BOOKED.
- Writes a subject and a short HTML email for each remaining lead, with an AI model.
- Sends that email from your Gmail account.
- Waits 24 hours, then reads the email thread to see whether the lead answered.
- Updates the sheet to BOOKED if the answer contains the word Yes.
- Sends a fixed follow-up if not, waits another 24 hours, checks again, and then marks the lead BOOKED or Declined.
It suits a person who wants to learn how an outreach sequence is built, or who will take it as a base and fix its weak points. It does not suit you if you need a scheduled sequence over several weeks, safe handling of replies that are not a plain “Yes”, protection against emailing the same person twice, or a way for people to unsubscribe, unless you add those things.
Words you will see in this guide
- n8n (say “n-eight-n”): a tool that connects apps and makes them follow your instructions automatically.
- Workflow: one set of instructions in n8n, drawn as boxes connected by lines.
- Node: one box in a workflow. Each node does one job.
- Trigger: the node that starts a workflow. Here, you click a button.
- Lead: a person or company you hope to do business with. Here, one row in a sheet.
- Outreach: a first message sent to someone who did not ask for it.
- Follow-up: a second message sent when the first got no answer.
- AI agent node: an n8n node that sends instructions and data to an AI model and receives text back. It needs a separate chat model node connected to it.
- System message: the standing instructions given to the AI model: its role, tone and output format.
- Azure OpenAI: a Microsoft cloud service that hosts OpenAI models such as GPT-4o. In it, the “model” is the name you gave your deployment.
- Prompt: the message sent to the AI model for one job. Here, the lead’s details plus a request.
- JSON: a plain text format for structured data, such as {“subject”: “…”, “body_html”: “…”}.
- Thread: Gmail’s name for a conversation: the first email and the replies to it.
- Snippet: the short preview of an email that Gmail provides. It is not the whole message.
- Code node: an n8n node in which you write a few lines of JavaScript.
- Expression: a small formula inside a field, written between double curly brackets, that reads data from earlier nodes.
- Credential: a saved login that lets n8n use one of your accounts. The JSON file does not contain yours.
Apps and accounts this workflow talks to
- n8n: runs the workflow. Because of the two 24-hour waits, it must stay running for at least two days per run.
- Google Sheets: holds your leads and receives the results.
- Gmail: sends the emails from the account you connect, and is read to look for replies.
- Azure OpenAI: the AI model that writes the first email. The lead’s company, contact name, job title, industry, location, source and status are sent to it. We do not link to it here because we could not verify its home page; you will find it in your Microsoft Azure account.
Costs: the workflow file is free. n8n, Google and the AI service each have their own plans, limits and prices, which change, and we have not verified any of them. Every lead uses one AI call.
What to have ready before you import anything
- A Google Sheet with a tab named
outreach automationand these headers in row 1, spelled exactly like this: Company Name, Contact Person, Job Title, Email, Phone, Industry, Location, Lead Source, Booking Status, ClientMessage, LastUpdated, ThreadID. - A second tab named
Invalid Leadswith the same headers. - Leads you may lawfully email. Start with two or three rows that use your own addresses.
- A Gmail account you are willing to send from. Do not use one that matters more than the experiment.
- An Azure OpenAI deployment of a GPT-4o model, and its credential in n8n.
- Your name and company to replace the two placeholders in the AI instructions and the follow-up email.
What the logic really does: worked examples
We ran the workflow’s own Code nodes outside n8n, and evaluated its tests the way n8n would, on simulated data. Here is what came out.
The email check
The second step is meant to check that the Email cell looks like an address. Its formula is a regular expression test (a pattern check) written inside an expression. But it is written as text around the address, so n8n builds a sentence rather than running the test. A sentence is never empty, so the “is not empty” condition is always true.
| Email cell | What a real check says | What the step does |
|---|---|---|
| (empty) | Invalid | Passes |
| abc | Invalid | Passes |
| jane@example.com | Valid | Passes |
So the Invalid Leads branch is never reached, and the node at its end does nothing in practice.
Who gets emailed
The filter skips a row only when Booking Status is exactly BOOKED, with this spelling and these capitals.
| Booking Status | Result |
|---|---|
| (empty) | Emailed |
| BOOKED | Skipped |
| Booked | Emailed |
| Declined | Emailed |
| Contacted | Emailed |
The workflow never writes a status after the first email, so a lead you have already written to still has an empty status and is emailed again at the next run. A lead who declined, and was marked Declined, is emailed again too.
What the AI answer parser accepts
The AI is asked to answer with JSON. The Code node that reads the answer removes code fence markers and parses it.
| AI answer (simulated) | Result |
|---|---|
| Clean JSON with subject and body_html | Works. |
| The same JSON wrapped in a json code fence | Works. |
| A sentence, then the JSON (“Here is your email: …”) | Fails. Subject becomes No Subject and the body becomes Error parsing AI output. |
| Text that is not JSON | Fails in the same way. |
| No answer at all | Fails in the same way. |
| The answer arrives as an object instead of text | The node stops with an error. |
The failure is the dangerous part: the fallback does not stop the run. The next node sends the fallback to the lead, so a real person can receive an email whose subject is “No Subject” and whose body is “Error parsing AI output.”
How a reply is judged
Both reply checks ask whether the second message in the thread contains the text Yes. The test is case-sensitive.
| Reply (simulated) | Counted as a booking? |
|---|---|
| Yes, let’s book Tuesday | Yes |
| Yes | Yes |
| Sure, Tuesday works | No |
| yes please | No |
| No thanks | No |
| Yesterday I was out of the office | Yes, wrongly |
After the first check, a “No” goes to the follow-up, so someone who wrote “No thanks” or “Sure, call me” is emailed again. After the second check, a “No” is marked Declined, whatever the person actually said.
What gets written to the sheet after a reply
The Code node that prepares the BOOKED update reads the From header of the reply. We tested three forms:
Jane Smith <jane@x.com>gives the email jane@x.com and the name Jane Smith.jane@x.comalone gives the email jane@x.com, and the same text as the contact name.JANE SMITH <JANE@X.COM>keeps the capitals, so the email written is JANE@X.COM. If your sheet has it in lowercase, whether the update finds the row depends on how Google Sheets compares text, which we did not test.
The same node cuts the quoted text from the reply only where the text On Wed appears. A reply whose preview contains On Tue keeps the quoted part. And if the second message does not exist, the node does not fail: it outputs an empty email and the status BOOKED, which would then write a row with an empty email.
The follow-up greeting
The follow-up email says “Hi” followed by the contact person, or “there” if that is empty. Because that field is read from the Gmail thread rather than from your sheet, we expect it to be empty, so the greeting is most likely always “Hi there,”. The paragraph that follows also puts the contact person’s name where you would expect the company name: “to explore how we could support Jane in your upcoming initiatives.” You will want to rewrite it.
The big picture: from lead to result
- Read. The sheet rows are loaded.
- Check and filter. Meant to drop bad addresses and BOOKED rows.
- Write. An AI model writes the first email.
- Send. Gmail sends it.
- Wait and look. After 24 hours, the thread is read.
- Branch. A reply with Yes leads to BOOKED. Otherwise a follow-up goes out.
- Wait and look again. After another 24 hours, the follow-up’s own thread is read.
- Record. BOOKED if the word Yes is found, Declined if not.
Follow the data: what each of the 22 steps does
The names below are the names you will see on the canvas.
When clicking ‘Execute workflow’
The manual trigger. Nothing happens until you click Execute workflow. There is no schedule, so the workflow does not run by itself.
Retrieve Lead Records from Google Sheets
Reads every row of the outreach automation tab. You replace the placeholder YOUR_GOOGLE_SHEET_ID with the identifier of your sheet, the long text in its web address, between /d/ and the next slash. Every row becomes one item.
Validate Lead Data Payload
An If node, meant to separate valid email addresses from invalid ones. As shown above, it always says yes. Its “no” output goes to the Invalid Leads step, which therefore never runs.
Filter for Booked Leads
An If node that checks whether Booking Status equals BOOKED. The rows that are BOOKED go to a dead end on purpose. All other rows continue to the AI. Spelling and capitals matter.
Configure GPT-4o Model
The chat model node, connected under the AI agent. It points to a model named gpt-4o through Azure OpenAI. In Azure, that name must match the name of your deployment, which you chose yourself, so you may need to change it. You must create your own credential. If you prefer another AI service, you can replace this node with another chat model node.
Generate Personalized Outreach Email (AI)
The AI agent node. For each lead it sends the company name, contact person, job title, industry, location, lead source and booking status, and asks for a warm, specific, mobile-friendly HTML email under 150 words, with a call to action, signed with your name and company, returned as JSON with two keys. The prompt says the goal is to “re-engage” the person, which suggests an existing relationship; if your leads have never heard from you, edit that wording. The AI has no other facts, so it can invent details, and it can invent a booking link. You replace YOUR_NAME and YOUR_COMPANY in the instructions.
Parse AI Email Output (JavaScript)
A Code node that removes code fence markers from the AI answer and parses it as JSON. If that fails, it outputs the subject No Subject and the body Error parsing AI output., and the run continues. See the table above.
Send Initial Outreach Email via Gmail
A Gmail node that sends the email to the Email value of the matching row in the first sheet step, using the AI’s subject and body. It sends from the Gmail account you connect, immediately, one lead after another, with no pause or limit. Our simulation could not show how n8n matches the AI’s email to the right lead row after the Code node, so check that the right address receives the right email in your test.
Wait Before Thread Check (24 hr)
A Wait node set to 24. In the file we received no unit was written; we expect hours, which is the default of this node, but we did not confirm it for your version, so open the node and make sure it says hours. While it waits, n8n must keep the execution stored.
Fetch Sent Email Thread from Gmail
A Gmail node that gets the whole conversation, using the thread identifier returned when the email was sent.
Check for Client Reply (If Condition)
An If node. It asks whether the second message of the thread contains the text Yes. If yes, it goes to the booked branch. If not, or if there is no second message, to the follow-up.
Extract Reply Details (JavaScript)
A Code node that turns the reply into a tidy row: email, contact name, thread identifier, subject, the message text (cut at On Wed), the status BOOKED and the current date and time. See the examples above.
Update Lead Record – Booked (Google Sheets)
An “append or update” step. It looks for a row with the same Email. If it finds one, it updates Booking Status, ClientMessage, LastUpdated and ThreadID. If it finds none, it adds a new row.
Send Second Follow-Up Email via Gmail
Sends a fixed message to the address in the To field of the first email, with the subject Still open to a quick chat. It is a new email, so it starts a new thread. You replace YOUR_NAME and YOUR_COMPANY in its signature and rewrite the body.
Wait Before Second Thread Check (24 hr)
A second Wait. In the file we received it was set to 24 seconds, which would have checked for a reply almost at once. LeenLabs changed it to 24 hours to match its name and the note.
Fetch Follow-Up Thread from Gmail
Reads the conversation of the follow-up email. A reply that the lead writes to the first email is in a different thread and is not seen.
Check for Second Reply (If Condition)
The same test as the first: does the second message contain Yes.
Extract Second Reply Details (JavaScript)
The same code as the first extraction, for a Yes found in the follow-up thread.
Update Lead Record – Replied (Google Sheets)
The same append-or-update as the booked step. Despite its name it writes the status BOOKED.
Update Lead Status to Declined (Set Node)
A Set node that builds two fields: Booking Status set to Declined and Email set to the To address of the first message in the follow-up thread.
Update Lead Record – Declined (Google Sheets)
Writes the status Declined to the row with that email, or adds a row if none matches. Every lead whose second check fails ends here, including those who never opened the message.
Log Invalid Leads to Google Sheets
Adds rows to the Invalid Leads tab. In the file we received this node was not set up at all. LeenLabs configured it with a placeholder and the tab name. Because the check before it lets every row pass, it is not reached.
Email automation tool or n8n workflow: which should you use?
A dedicated email automation tool is a product built for these jobs. Such tools usually come with unsubscribe handling, sending limits, delivery tracking, templates and support. An n8n workflow like this one gives you freedom and a low price, and nothing else: you build and maintain every safeguard yourself, as the lists of problems below show. If you only need newsletters or a sequence for people who signed up, a dedicated product is probably the safer route. If you want to understand how outreach automation works, or you need something no product offers, an n8n workflow is a good place to learn and to start. We do not rank products here, because we have not tested any, and the best email automation tools for you depend on your country, your list and your budget.
Get the workflow JSON
Open the box below to copy the JSON, or download the file. It contains the 22 nodes above, connected as described, with placeholders for your sheet, your name and your company, and 21 notes on the canvas, including one called Before you turn it on. It contains none of your logins.
Please read this first: this JSON is free and provided as is. n8n versions and installed nodes differ from one person to another, so it may not run correctly on your side without adjustments. We cannot test it on every setup. That is why this article includes a long section, Problems you may meet and how to fix them. And if you would rather not deal with any of it, you can request a custom workflow and let us handle it.
Click to show or hide the workflow JSON
Click inside the box, select the whole text, copy it, then paste it onto an empty n8n canvas. If selecting is awkward on your device, use the download link above.
{
"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"
}
}
Install it in twelve steps
- Prepare the sheet. Create the
outreach automationtab and theInvalid Leadstab with the twelve headers. - Add two or three test leads that use your own email addresses.
- Get the JSON from the box above or the download link.
- Import it. Open a new empty workflow in n8n, then paste the JSON onto the canvas or import it from a file.
- Check the 22 nodes appear, with 21 notes and no missing-node warning.
- Connect Google Sheets in the four nodes that read or write the leads, and the Invalid Leads node.
- Replace
YOUR_GOOGLE_SHEET_IDin all five Google Sheets nodes. - Connect Gmail in the two Gmail nodes that send and the two that read threads.
- Connect Azure OpenAI in Configure GPT-4o Model and set the model to the name of your deployment.
- Replace
YOUR_NAMEandYOUR_COMPANYin the AI agent’s instructions and in the follow-up email, and rewrite the follow-up body. - Check the two Wait nodes say hours. For testing, you can temporarily set them to a few minutes.
- Run it by hand on your test leads, read every email that arrives, then reply “Yes” to one and not to another, and watch the sheet.
What it will not do: limits, costs and trade-offs
- No real email check, and no duplicate check.
- No memory of who was emailed. It never writes a “contacted” status.
- One follow-up only, sent as a new thread, after a fixed 24 hours.
- A crude reply test. One capital-letter word, no understanding of meaning, and no handling of out-of-office messages, forwards or replies from another address.
- No opt-out, no unsubscribe list and no sending limit.
- No human review of what the AI wrote.
- No error alerts. A failed step stops the run for that lead without telling you.
- Manual start, and a long wait that needs n8n to keep running.
- The AI works from seven fields. It cannot know anything else about the lead.
Access, privacy and responsible use
This workflow handles personal data about people who did not ask to hear from you, and it sends messages in your name.
- Check the rules for unsolicited email where you and your leads are. They differ widely by country and by whether the recipient is a business or a person. In many places they involve a way to opt out, a real sender identity and a lawful basis for contacting someone. We are not lawyers, and nothing here is legal advice or a statement that this workflow complies with any law.
- Add an opt-out line and honor it. The template has none. Keep a list of people who must never be emailed again, and filter against it.
- Be honest about who you are. Do not let the AI imply a relationship, a meeting or a fact that does not exist. Read the “re-engage” wording and the follow-up text as if you were the recipient.
- Review before sending. The AI can invent details, a link or a claim. Read its output for every lead, at least until you trust your prompt.
- Protect your Gmail account. Sending many similar emails quickly can get a mailbox limited or blocked. Send a few at a time, and never from an account you cannot afford to lose.
- Know where lead data goes. Names, titles and companies go to the AI service and to Google. Read their terms, and tell people where their data goes if the rules that apply to you require it.
- Check what the Gmail node adds. Depending on your version, an attribution line about n8n may be appended to the emails.
- Protect the sheet. It holds names, emails and replies. Share it only with those who need it.
Problems you may meet and how to fix them
Every n8n setup is different, so this workflow can behave differently for you than in our checks. Here are the most likely problems. For each one: what you see, why it happens, and what to do. They come from how the nodes are built and from running the Code nodes and the tests on simulated data, not from a live run with real accounts.
1. The workflow imports, but a node shows as unknown or has a warning
What you see: a red or grey node after import.
Why: your n8n version may not have the same node versions as the one the template was made on, especially for the AI nodes.
Fix: update n8n, or replace the node with the same kind of node from your own node menu and copy its settings across.
2. Google Sheets says the document or the tab cannot be found
What you see: an error in one of the five Google Sheets nodes.
Why: YOUR_GOOGLE_SHEET_ID was not replaced in every one of them, or the tab name is not outreach automation or Invalid Leads.
Fix: open all five nodes and check the sheet and the tab. The tab names must match exactly.
3. The columns are empty or in the wrong place
What you see: the update steps write into the wrong column, or create new columns.
Why: the headers in row 1 are spelled differently, such as Booking status instead of Booking Status.
Fix: copy the twelve headers exactly from the section above.
4. The email check lets bad addresses through
What you see: emails go to rows with an empty or invalid Email, and Gmail then fails or the message bounces.
Why: the check always passes, as explained above.
Fix: replace it with a real condition that tests the Email value against the pattern, and test it with an empty cell, a word, and a real address.
5. Everyone is emailed again at the next run
What you see: leads you already wrote to receive another outreach email.
Why: the filter only skips BOOKED, and nothing marks a lead as contacted.
Fix: add a step after the first email that writes a status such as Contacted, and change the filter to skip every status except an empty one.
6. A lead who declined is emailed again
What you see: a lead marked Declined gets a new first email.
Why: Declined is not BOOKED, so the filter lets it through.
Fix: skip Declined too, and keep a separate list of people who asked not to be contacted.
7. The lead receives an email with the subject No Subject
What you see: a lead gets an email whose subject is No Subject and whose body is Error parsing AI output.
Why: the AI answered in a form the parser could not read, for example a sentence before the JSON, and the code sends its fallback instead of stopping.
Fix: add a check after the parser that stops the lead when the subject is No Subject, and tighten the instructions so the AI returns only the JSON.
8. The Code node stops with an error on the AI answer
What you see: the parser node fails and nothing is sent.
Why: the AI node returned something that is not text, so the code cannot clean it.
Fix: check what the AI node outputs in your version. Adjust the field name the code reads, which is output, or turn the value into text first.
9. The AI node says it has no model
What you see: an error saying the agent needs a chat model.
Why: the model node is not connected under the agent, or lost its connection on import.
Fix: connect Configure GPT-4o Model to the model input at the bottom of the agent node.
10. Azure OpenAI refuses the request
What you see: an error about a missing deployment, a wrong key or a wrong endpoint.
Why: the model name must be the name of your deployment, and the credential must point to your own resource.
Fix: check the deployment name in Azure and put it in the model field. Check the key and the endpoint.
11. The emails sound generic, or invent facts
What you see: flattering lines about the company that you never wrote, or a link to a calendar that does not exist.
Why: the AI has only seven fields about each lead and is asked to sound specific.
Fix: add a line to the instructions forbidding invented facts and links, add real facts in new sheet columns, and read every email before it goes out.
12. The email says “re-engage” to someone who never heard from you
What you see: an email that refers to a past conversation that never happened.
Why: the prompt says the goal is to re-engage the lead, and the follow-up refers to a previous email.
Fix: rewrite the goal and the follow-up text so they match your real situation.
13. The wrong lead gets the wrong email
What you see: an email addressed to one person arrives at another address.
Why: the sending step takes the address from the sheet node by matching items, and that matching can break after a Code node. We could not test this in n8n.
Fix: test with at least three distinct test leads. If the pairing is wrong, carry the Email field through the AI and parser steps, and use it directly in the Send step.
14. The sending step fails or the account is limited
What you see: Gmail errors, or Google warns about unusual activity.
Why: many similar emails sent in a burst, or a quota reached.
Fix: work with small batches, space them out, and check Google’s current limits for your type of account.
15. The second wait runs too short, or too long
What you see: a check happens after a few seconds, or never when you expect it.
Why: the first Wait has no unit written in the file, and your n8n may read it differently. The second one we set to hours.
Fix: open both Wait nodes and set the unit yourself. For a test, use minutes.
16. The workflow stops during a wait, or does not resume
What you see: an execution stays “waiting” for much longer than expected, or disappears.
Why: n8n has to keep the execution stored and be running when the wait ends. A restart, a small server or a setting that removes old executions can interfere.
Fix: check how your n8n stores executions, and avoid restarts during the two days. We could not test this.
17. A real “Yes” is not recognized
What you see: a lead who clearly agreed is marked Declined, or receives the follow-up.
Why: the test looks for the capital-letter word Yes in the preview of the second message. “Sure”, “yes please” or “Happy to talk” do not match.
Fix: make the test case-insensitive and add other words, or better, have an AI step classify the reply as agree, decline or unclear, and send unclear ones to a person.
18. A lead who said no is marked BOOKED
What you see: a lead who wrote something like “Yesterday I was away, not interested” is marked BOOKED.
Why: the word Yes appears inside Yesterday.
Fix: use a stricter test, or the classification idea above.
19. The reply is not found at all
What you see: the lead answered, but the lead still gets the follow-up.
Why: the reply was sent from another address, started a new thread, arrived before or after the check, or the second message in the thread is an automatic reply or your own message.
Fix: read the thread output in a test run to see exactly what the second message is, and adjust the check.
20. A reply to the first email is missed after the follow-up
What you see: a lead answers the first email on day three, but is marked Declined.
Why: the follow-up is a new thread, and only that new thread is read the second time.
Fix: read the first thread again at the second check, or send the follow-up as a reply in the first thread.
21. The email field of the reply is empty, or the row is not found
What you see: a new row with an empty email, or the status written in a new row instead of the lead’s row.
Why: the From header is missing or in a form the code does not expect, the reply came from a different address than the lead’s, or the capitals differ from your sheet.
Fix: compare the email from the reply to the one in the sheet in a test, and match on the thread identifier or use a lowercase version of both.
22. The follow-up greets “Hi there,” and names the wrong thing
What you see: every follow-up starts with Hi there, and the second paragraph names the person where the company should be.
Why: the greeting reads a field that the thread does not contain, and the paragraph uses the contact person field.
Fix: read the contact person and company from the sheet node by the lead’s email, and rewrite the paragraph.
23. The Gmail node adds a line you did not write
What you see: a sentence such as one saying the email was sent with n8n at the bottom of the messages.
Why: some versions of the node add an attribution by default.
Fix: open the Gmail node options and check for an attribution setting. Read a test email before you send real ones.
24. The Invalid Leads tab stays empty
What you see: bad addresses are processed and nothing is logged.
Why: the check before it never says no.
Fix: fix the check, then make sure the Invalid Leads tab has the same headers as the leads tab.
25. Everything imports but the JSON does not behave as described
What you see: something that this guide describes does not match what you see.
Why: versions differ, and we did not run this exact file in n8n.
Fix: test each step in your own n8n with the “execute step” button, look at what each node outputs, and if you get stuck, request a custom workflow.
Too much of a puzzle? Let us build or adapt it for you
Some people enjoy tinkering with n8n. Others want an outreach sequence that works with their own list, their own wording and their own safeguards. If you are in the second group, LeenLabs builds and adapts n8n workflows for people like you. We can:
- Add a real email check and a duplicate and opt-out list.
- Mark each lead as contacted, replied, declined or unclear, so nobody is emailed twice.
- Classify replies with an AI step and send unclear ones to you.
- Add a human review step before an email is sent.
- Space out the sending and add a daily limit.
- Read both threads at the second check.
- Connect your own list or CRM instead of a sheet.
- Build a new workflow from scratch for another task you want to automate.
We do not run an email marketing agency, and we do not send emails for you. We build the automation, and you stay in charge of who is contacted and what is said.
Request a Custom Workflow, describe what you want to automate, and we will come back to you with the next steps. Prefer to ask a question first? Write to support@leenlabs.site.
What we changed in the original template
We want you to know exactly how this file differs from the template it comes from. Compared with the file we received, LeenLabs made these changes:
- Removed internal data. We removed the template’s identifier, its setup-status marker, the inactive flag, saved test data, connection identifiers and the references to the author’s own spreadsheet. We also renamed the workflow to this article’s title.
- Replaced the sheet references in the five Google Sheets nodes with
YOUR_GOOGLE_SHEET_IDand the tab names described above. - Configured the Invalid Leads node. It was empty in the file we received.
- Replaced the sender’s name and company in the AI instructions and in the follow-up email with
YOUR_NAMEandYOUR_COMPANY. - Changed the second wait from 24 seconds to 24 hours. It did not match its own name and note.
- Rewrote the 19 canvas notes that sit next to the nodes, and the overview note, so that they describe what each step really does, and added the note Before you turn it on. The original notes contained marketing text and made claims we could not check.
The nodes, their settings, the AI prompt, the conditions, the code and its comments, and the connections are otherwise unchanged. That includes the defects described above, which we documented rather than fixed.
What was verified, and what was not
Here is exactly what we did. We ran the two Code nodes (Parse AI Email Output and Extract Reply Details) outside n8n on simulated data: clean, fenced, prefixed, invalid, empty and non-text AI answers, and replies with three forms of the From header, two quoted-text cuts, and a missing second message. We evaluated the Yes test on six simulated replies, built the text of the email check on three values, and listed how the filter treats five statuses. We also checked that the file’s connections point to existing nodes and that it contains no logins.
We did not import this exact file into n8n and run it end to end. We did not connect Google Sheets, Gmail or Azure OpenAI, so we cannot confirm what the AI returns, which fields the Gmail thread contains (we assumed the names From, To, Subject and snippet that the code expects), how the Gmail and Wait nodes behave in your version, whether items stay matched to the right lead, or whether the sheet update finds a row when the capitals differ. That is why we recommend a full test with your own addresses before you turn it on.
Five ways to adapt the workflow
- Change the wording. Edit the AI instructions and the follow-up text.
- Add a review step. Put the AI email in a sheet or a message for you to approve before it is sent.
- Add a status after sending. Write Contacted to the sheet, and make the filter skip it.
- Change the waits. Edit the two Wait nodes, or add a third follow-up.
- Use another AI service. Replace the Azure model node with another chat model node.
These variations are suggestions based on how n8n works, not changes we tested on this template. If you want one built for you, send us a request.
Frequently asked questions
What is email automation?
Software that sends emails for you based on rules instead of you sending each one by hand. It can be as simple as a welcome email, or as involved as this workflow, which writes, sends, waits and checks for a reply.
What is AI email automation?
Email automation in which an AI model writes or helps write the message. In this workflow, the AI writes the first email for each lead. It does not decide when to send, and it does not read the replies.
What is outbound email automation?
Automated emails that you start towards people who did not ask to hear from you, such as outreach or follow-ups. It carries more legal and reputation risk than emails to people who signed up.
Is this a marketing email automation tool?
No. Marketing email automation usually means newsletters and sequences for subscribers who opted in, run inside a dedicated platform. This is an n8n workflow for one-to-one outreach, and it lacks the safeguards such platforms provide.
What are the best email automation tools?
It depends on your goal, your country and your budget, and we have not tested or compared products, so we do not rank them. For newsletters and sign-up sequences, look at dedicated email marketing platforms. For outreach, check carefully that a tool handles opt-outs and sending limits. This workflow is a learning base, not a replacement.
Is there an email automation agency behind this?
LeenLabs is not an email marketing agency. We build and adapt n8n workflows. If you want this one changed, request a custom workflow.
Can I use it for CRM email automation, for example for a moving company?
Only as a starting point. It reads a Google Sheet, not a CRM, and its logic is built for a simple lead list. A moving company would need to connect its CRM, track quotes and move dates, and avoid emailing customers who already booked. That is custom work, and we can discuss it through a request.
Can it do Shopify email automation?
No. It does not connect to Shopify, and its purpose is outreach to leads, not store emails such as abandoned carts or order updates. A different workflow would be needed.
Does it handle replies well?
No. It looks for the capital-letter word Yes in the preview of the second message, and nothing else. See the examples above.
Where does lead data go?
To Google Sheets, Gmail and the AI service. Read their terms.
Is the workflow free?
The file is free. The services it connects have their own plans, limits and prices.
What if the JSON does not work on my n8n?
It can happen, because setups differ. Go through the problems section above. If it still does not work, or you do not want to spend the time, request a custom workflow and we will handle it.
Who built the original template?
The original template comes from the n8n Library at n8n.io. LeenLabs adjusted and documented it.
Tools used in this workflow
The links below go to each tool’s home page, and some may be affiliate links, as explained in our Terms of Service. We do not link to Azure OpenAI because we could not verify its address.
- n8n (may be an affiliate link): the automation platform that runs the workflow, available hosted or self-hosted.
- Gmail (may be an affiliate link): the email service that sends the messages and holds the replies.
- Google Sheets (may be an affiliate link): the spreadsheet that holds the leads and the results.
Suggested reading
If outreach, leads and clean records matter to you, these LeenLabs articles go well with this one:
- How to automate Zoho CRM data quality audits: a workflow guide on keeping CRM records clean.
- Auto-capture supplier invoices from Gmail with n8n: another Gmail workflow, with a different job.
- Lead Follow-Up System: Recover Revenue From Your Pipeline: a deeper business article on following up with leads.
- CRM Data Cleanup: The Friday 20-Minute Protocol: a routine for keeping your lead data usable.
- AI Agents in Finance: What’s Safe to Automate in 2026: a business article on what to hand to AI and what not to.
- Accounts Payable Automation for Small Business (2026 Plan): a business article on automating a different routine.
Keep exploring
Browse more free n8n workflows from LeenLabs.
Need this adapted to your own setup?
Tell us what you want to automate and we will come back to you with the next steps.
