DEV Community

Cover image for How to send Automated Personalized Cold Emails with n8n to Boost Response Rates
Ukagha Nzubechukwu
Ukagha Nzubechukwu

Posted on

How to send Automated Personalized Cold Emails with n8n to Boost Response Rates

Generic cold emails get deleted faster than spam these days. Yet, personalizing each email for hundreds of prospects is a productivity nightmare no individual or sales team can afford.

Many email marketing campaigns fail to achieve their desired impact primarily because they lack personalization and specificity. When email providers like Gmail perceive your email as "generic", they are more likely to be categorized as promotional content or, worse yet, as spam. This misclassification prevents their delivery to the intended individuals.

Even if your emails bypass spam filters and land in the inbox, they risk being ignored or dismissed. Potential clients will skim over them, viewing your message as just another faceless message that takes up important space in their email storage.

Personalizing your message signals authenticity and shows you’ve taken the time to understand the person and their interests. Mention a specific trigger, for example, “I noticed your team is hiring a new sales manager.” This shows awareness of their situation and opens a timely, relevant conversation. That kind of specificity makes recipients far more likely to stop, read, and respond.

In this guide, I will show you how to build an n8n workflow that generates personalized email content and automates the sending process, all while ensuring compliance and deliverability.

Important — scope of this guide
This is an introductory walkthrough for setting up an n8n workflow that personalizes and sends emails. I don't cover list scraping or acquisition techniques; the guide assumes you already have a lawful, consented email list.

Prerequisites

To complete this guide, make sure you have the following items ready;

  • An n8n account.
  • A prepared email list (Google Sheet recommended). I've provided a template for you. Open this spreadsheet and make a copy for your use. This Google Sheet already includes the necessary headers, such as First Name, Email, Title, Company, LinkedIn, Website, Summary, Industry, and Location. Additionally, you can add extra columns for further personalization.

Note: Don’t worry if fields like Subject, icebreaker, elevatorPitch, callToAction, or postscript are empty, the workflow will fill those automatically.

  • No n8n experience required, this guide walks you step-by-step through the entire process.

Why Traditional Cold Email Doesn’t Work Anymore

The old strategy of mass “spray and pray” emails has lost its effectiveness. These impersonal communications are quickly flagged as spam or deleted, often before they even get a chance to be read.

While high-touch outreach from Sales Development Representatives (SDRs) can yield better replies, this method is not scalable. A single SDR may only be able to send a limited number of meticulously crafted emails each day, resulting in a slow and costly process that can burn teams out. Manual research produces quality, but not the volume most teams need.

A more effective strategy is to adopt a hybrid approach that combines the benefits of automation with genuine contextual personalization. n8n allows you to personalize messages with warm triggers and craft introductory lines that feel human. This method ensures that your outreach feels thoughtfully composed and tailored to each individual, all while helping you to reach a much larger audience. By doing this, you increase your inbox hit rate and also significantly improve response rates, transforming your outreach from mere noise into meaningful conversations.

How to Send Automated Personalized Cold Emails with n8n

You will sign up for n8n to create a workflow that automatically pulls your email list, generates personalized outreach messages with ChatGPT, and sends your first targeted campaign.

Step 1 — Create an n8n account

To begin, visit n8n’s website and create a free account, no credit card required. You get a 14-day free trial, so there’s no need to worry about being charged while you try the platform.

Step 2 — Add your trigger

After login, you’ll land on your n8n dashboard.
n8n homepage

Click “Start from scratch” to open the workflow editor, and then, hit the “Add first step” button to add your trigger node, the starting point of everything your workflow will do.

n8n workflow editor

Quick note: A trigger node is what starts your automation. Think of it as the “on switch.” It can be almost anything: a button you click, someone submitting a form, a new row appearing in your spreadsheet, or even a schedule (like “every morning at 9 am”).

For this guide, choose Trigger manually.

Trigger options for n8n

This starts the workflow only when you click the play button. It’s the easiest way to test things while you’re just getting started.

n8n manual trigger

Step 3 — Connect your Google Sheet to the workflow

Now that your workflow has a trigger, it’s time to bring in your spreadsheet. This is where your email list lives, so you want n8n to grab that data and pass it along to the rest of your automation.

Click the "+" button after your trigger node, search for Google Sheets, and select it. From the list of actions, choose "Get Row(s) in Sheet".

n8n editing canva

It’s okay if your “Credential to connect with” looks different. That just means I’ve already set one up; I’ll guide you through the process anyway.

In the Credentials field, open the dropdown and select "Create new credential". Then, click "Sign in with Google", and sign in with your account. Once connected, you will see a message that says "Account connected."

n8n editing canva

In the Resource field, select “Sheet within Document.

n8n resource field

For Operation, choose “Get Row(s).

n8n Operation field

Under Document, keep “From list” and select the spreadsheet that contains your email list.

n8n Document description

In Sheet, keep “From list” and choose the sheet with your contacts.

n8n sheet

You can ignore the filter options for now; they’re only useful if you want to pull specific rows.

Finally, test your spreadsheet node by clicking Execute step. If everything is working, you’ll see your spreadsheet data appear in the output tab, ready to be used in the next step of your workflow.

n8n output

Make sure to click the pin icon in the top-right corner of the output panel so the data is visible while you configure the next node. This way, you don’t have to rerun this node every time.

Step 4 — Personalize your emails with ChatGPT

In this step, you’ll pass each row from your Google Sheet into an OpenAI model, which will generate a personalized email for you.

Click the “+” button after the Google Sheets node, search for OpenAI, and choose "Message a model".

You’ll see the Google Sheets data in the left panel. This is what the model will use for personalization.

n8n input panel

In the "Credential to connect with" input, open the dropdown and click "Create new credential". A pop-up will appear where you can enter your OpenAI API key.

OPEN AI pop up

To retrieve your API key, visit the OpenAI platform. Then, return to the n8n popup, paste the secret into the API key field, and save. If everything is done correctly, you will see a message saying the connection was tested successfully.

Keep the Resource input set to its default option, which is "Text".

Resource input

For the Operation input, leave it at its default setting as "Message a model".

Operation input

Select a model from the dropdown menu: if you're using n8n's free credits or a free OpenAI account, choose a smaller model; if you have a paid OpenAI account, opt for a more powerful one.

model input

For the message field, you have two sub-fields: "prompt" and "role". In the prompt sub-field, enter the following text:

You are a helpful assistant whose role is to personalize emails and make them more human.
Enter fullscreen mode Exit fullscreen mode

In the role sub-field, select "System."

message field

To proceed, click on "Add message" to add another sub-field. In the prompt input, enter the following block of text below and set the role to User:

Full prompt — Click to expand!
You are a concise outbound email writer. For each spreadsheet row, you are given the following input fields:
Civility, First Name, Last Name, Full Name, Title Description, Email, Profile (URL), Title, Company, Company Legal, Company Phone, Company Website, Company LinkedIn, Company Summary, Industry, Company Location.

Goal: produce a short, human-feeling outreach email broken into 5 parts:
{"subjectLine":"", "iceBreaker":"", "elevatorPitch":"", "callToAction":"", "ps":""}

Task steps (must follow in order):
1. Quick research (web): using the Profile URL, Company Website, Company LinkedIn and public sources (news, Crunchbase/AngelList, product pages, recent posts), gather 3 one-line facts:
   a. What the person actually does / focus (paraphrase — do NOT copy LinkedIn text verbatim).
   b. One recent signal or behavior you can mention (product launch, press, hiring, content they posted, conference talk).
   c. A concise, credible gap or opportunity for the company (one sentence) — e.g., lead-gen funnel unclear, demo scheduling low/no automation, pricing page confusing, low content-to-conversion flow, no obvious appointment pipeline. When unsure, default to a safe, general, plausible gap framed as an *opportunity* rather than a failure.

2. Third-party/company comparison: identify one relevant peer/competitor/adjacent company and note a single, tactful difference or shortcoming you can use as a contrast to frame an opportunity for your prospect. Keep it high-level and non-judgmental.

3. Build `thingAboutThem` and `relatedThing`:
   - `thingAboutThem`: a short phrase (3–6 words) summarizing the person’s focus or recent initiative (e.g., "lead gen for SMBs", "product-led growth", "enterprise onboarding").
   - `relatedThing`: a related phrase you can reference in the icebreaker (e.g., "your recent blog on demo conversions", "the webinar you did on onboarding").

4. Compose outputs with these constraints:
   - Tone: "casual bar conversation", spartan, plain language, short sentences.
   - No fancy jargon. Use one human-sounding sentence for each field where possible.
   - Subject line: lowercase, ≤60 characters, include First Name and `thingAboutThem` if natural (format: `hey {FirstName}, quick thought re: {thingAboutThem}`).
   - Icebreaker: 10–25 words. Specific detail from research, paraphrased (do not paste profile copy).
   - Elevator pitch: one short sentence (≤18 words) with a plausible, quantifiable result (use ranges like "~$X/mo" or "% lift" and hedge with "can", "likely", "typically").
   - Call to action: 1–2 sentences. Offer a low-friction next step (15–30 min call / free audit) and a risk-minimizing guarantee (e.g., "I'll guarantee X or you don't pay", or "I'll prove value in 2 weeks").
   - PS: 5–12 words, curious & conversational.

5. Safety & accuracy rules:
   - Do NOT invent awards, numbers, or proprietary customer names. If no public evidence exists, say something plausible but hedged ("looks like", "appears", "seems").
   - Never state private data you can’t verify. If profile or company pages are unavailable, use neutral, general icebreakers referencing industry trends.
   - Keep claims defensible and small. Avoid legal, medical, political claims.

6. Output: only return one JSON object exactly in this format (no extra text):
{"subjectLine":"", "iceBreaker":"", "elevatorPitch":"", "callToAction":"", "ps":""}

7. If a required field from the spreadsheet is missing, fall back:
   - Missing Profile URL: use Company Website / Company LinkedIn first. If none, use industry-level icebreaker.
   - Missing CompanySummary: summarize the Company Website homepage (1 short phrase).
Enter fullscreen mode Exit fullscreen mode

Add yet another sub-field. In the prompt input, enter the following block of text below and set the role to User.

This block represents the JSON data from your spreadsheet, and it’s important because it passes the right fields (like name, company, and profile) into the prompt so each email can be personalized correctly

Full prompt — Click to expand!
Civility: {{ $json.Civility }}
First Name:  {{ $json['First Name'] }} 
Last Name: {{ $json['Last Name'] }} 
Full Name: {{ $json['Full Name'] }}
Title Description: {{ $json.Title }} 
Email: {{ $json.Email }} 
Profile (URL): {{ $json.Profile }} 
Title: {{ $json.Title }}
Company:{{ $json.Company }} 
Company Legal: {{ $json['Company Legal Name'] }} 
Company Phone: {{ $json['Company Phone'] }} 
Company Website: {{ $json.Website }}
Company LinkedIn:{{ $json.Linkedin }} 
Company Summary: {{ $json.Summary }} 
Industry: {{ $json.Industry }} 
Company Location: {{ $json['Company Location'] }}
Enter fullscreen mode Exit fullscreen mode

Finally, toggle "Output Content as JSON" to ON.

We are ready to execute this step; click the "Execute step" button. Your AI will return a JSON object containing fields like subject line, icebreaker, and call-to-action.

Step 5 - Update Google Spreadsheet

Now that your OpenAI node is generating personalized emails, the next step is to send those results back to your spreadsheet.

To begin, click the “+” button after your OpenAI node, search for Google Sheets, and select “Update row in sheet”. You’ve already worked with this node earlier, so most of the setup will feel familiar. Let’s walk through the key parts quickly:

In the “Credential to connect with” field, keep the default option. n8n will automatically reuse the same Google Sheets credential you created earlier.

Leave Resource set to “Sheet within document” and Operation as “Update Row”.

In the Document field, choose “FROM ID”. Next, go to your Google Sheet and copy the ID from its URL. A typical spreadsheet URL looks like this:

https://docs.google.com/spreadsheets/d/<1YjJeRUDXAKGEc-xYv_GfSj3E8s7YDPQwhgDrx9fYsCQ>/edit#gid=784785913
Enter fullscreen mode Exit fullscreen mode

The spreadsheet URL does not include angle brackets (< >). I added them to indicate where to copy.

Copy the Google Sheet ID from the URL and paste it into the input field next to “FROM ID”.

For the Sheet field, leave “From list” selected, then choose the specific sheet that holds your email data.

Keep Mapping Column Mode as it is. In the Column to match on, select "Email". This ensures that each row is matched to the correct contact, preventing updates to the wrong row.

Now, fill in the values to match the fields with the data from your previous steps.
You can paste the expressions below or drag and drop the fields from the left panel.

Full prompt — Click to expand!
Email (using to match): {{ $('Get row(s) in sheet').item.json.Email }}
Civility: {{ $('Get row(s) in sheet').item.json.Civility }}
First Name: {{ $('Get row(s) in sheet').item.json['First Name'] }}
Last Name: {{ $('Get row(s) in sheet').item.json['Last Name'] }}
Full Name: {{ $('Get row(s) in sheet').item.json['Full Name'] }}
Title: {{ $('Get row(s) in sheet').item.json.Title }}
Profile: {{ $('Get row(s) in sheet').item.json.Profile }}
Company Name: {{ $('Get row(s) in sheet').item.json['Company Name'] }}
Company Legal Name: {{ $('Get row(s) in sheet').item.json['Company Legal Name'] }}
Company Phone: {{ $('Get row(s) in sheet').item.json['Company Phone'] }}
Company Website: {{ $('Get row(s) in sheet').item.json['Company Website'] }}
Company LinkedIn: {{ $('Get row(s) in sheet').item.json['Company LinkedIn'] }}
Company Summary: {{ $('Get row(s) in sheet').item.json['Company Summary'] }}
Industry: {{ $('Get row(s) in sheet').item.json.Industry }}
Company Location: {{ $('Get row(s) in sheet').item.json['Company Location'] }}
Title Description: {{ $('Get row(s) in sheet').item.json['Title Description'] }}
Subject: {{ $json.message.content.subjectLine }}
Icebreaker: {{ $json.message.content.iceBreaker }}
Elevator Pitch: {{ $json.message.content.elevatorPitch }}
Call To Action: {{ $json.message.content.callToAction }}
Postscript: {{ $json.message.content.ps }}
Enter fullscreen mode Exit fullscreen mode

Make sure every field has a value; otherwise, n8n might skip empty ones.

You can also drag-and-drop values directly into each input if you prefer. Here is a video to show you how it's done.

And finally, click "Execute Step" to append your personalized email content to your spreadsheet.

Step 6 — Send your personalized emails automatically

Now that your spreadsheet has all the personalized fields (subject line, icebreaker, pitch, call-to-action, and postscript), you can automatically send your emails using the "Send a message" Gmail node.

In this step, you can either use your Gmail account to send emails or choose the SMTP option if you prefer to connect your own mail server or a service like SendGrid. For this example, use the Gmail option; the SMTP method is straightforward and similar.

To begin, click the “+” button after your last Google Sheets node, search for Gmail, and select "Send a message".

In Credential to connect with, click "Create new credential" and sign in with the Gmail account you’ll use to send.

Leave the Resource field set to Message and Operation as Send.

Set the To field to

{{ $json.Email }}
Enter fullscreen mode Exit fullscreen mode

Set the subject field to

{{ $json['Subject'] }}
Enter fullscreen mode Exit fullscreen mode

Set Email type to HTML and paste the HTML template into the Message field:

Full html — Click to expand!
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Email</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="x-preheader" content="{{ ($json['iceBreaker'] || 'Quick thought for you') }}">
    <style>
      :root{
        color-scheme: light dark;
        supported-color-schemes: light dark;
      }
      @media (prefers-color-scheme: dark) {
        .card { background: #111111 !important; }
        .body { background: #0b0b0b !important; }
        .text { color: #f2f2f2 !important; }
        .muted { color: #bbbbbb !important; }
        .btn { background: #4f46e5 !important; color: #ffffff !important; }
        .divider { border-color: #2a2a2a !important; }
        .chip { background: #1c1c1c !important; color: #e5e7eb !important; }
      }
    </style>
  </head>
  <body class="body" style="margin:0;padding:0;background:#f6f7fb;">
    <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f6f7fb;">
      <tr>
        <td align="center" style="padding:24px;">
          <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:640px;background:#ffffff;border-radius:14px;overflow:hidden;box-shadow:0 6px 24px rgba(0,0,0,0.06);" class="card">
            <tr>
              <td style="padding:18px 22px;background:linear-gradient(135deg,#6366f1,#22d3ee);">
                <div style="font:600 14px/1.2 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;color:#ffffff;letter-spacing:.3px;">
                  Quick intro for {{ $json['First Name'] || 'you' }}
                </div>
              </td>
            </tr>
            <tr>
              <td style="padding:28px 24px 8px 24px;" class="text">
                <div style="font:16px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;color:#111827;" class="text">
                  <p style="margin:0 0 14px 0;">
                    Hi {{ ($json['Civility'] ? ($json['Civility'] + ' ') : '') + ($json['First Name'] || $json['Full Name'] || 'there') }},
                  </p>
                  <p style="margin:0 0 14px 0;">
                    {{ $json['iceBreaker'] || 'Saw your recent work and had a quick idea that might help your team.' }}
                  </p>
                  <p style="margin:0 0 14px 0;">
                    {{ $json['elevatorPitch'] || 'We help teams ship faster with fewer rollbacks by tightening CI and monitoring.' }}
                  </p>
                  <table role="presentation" cellpadding="0" cellspacing="0" style="margin:18px 0 8px 0;">
                    <tr>
                      <td>
                        <a href="{{ $json['Company Website'] ? ('https://' + $json['Company Website'].replace(/^https?:\/\//,'')) : '#' }}"
                           style="display:inline-block;background:#111827;color:#ffffff;text-decoration:none;padding:12px 18px;border-radius:10px;font:600 14px/1 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;"
                           class="btn">
                          {{ ($json['First Name'] ? ('Quick 15–20 min chat, ' + $json['First Name']) : 'Quick 15–20 min chat') }}
                        </a>
                      </td>
                    </tr>
                  </table>
                  <p style="margin:6px 0 16px 0;color:#374151;">
                    {{ $json['callToAction'] || 'Open to a quick call? I can do a free audit and prove value in 2 weeks.' }}
                  </p>
                  <p style="margin:0 0 6px 0;color:#6b7280;" class="muted">
                    PS — {{ $json['ps '] || $json['ps'] || 'worth a brief pipeline audit?' }}
                  </p>
                </div>
              </td>
            </tr>
            <tr>
              <td style="padding:0 24px;">
                <hr style="border:none;border-top:1px solid #e5e7eb;margin:8px 0 0 0;" class="divider">
              </td>
            </tr>
            <tr>
              <td style="padding:16px 24px 22px 24px;">
                <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="font:13px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;color:#4b5563;">
                  <tr>
                    <td style="padding:2px 0;">
                      <span style="display:inline-block;padding:6px 10px;border-radius:999px;background:#f3f4f6;" class="chip">
                        {{ $json['Full Name'] || (($json['First Name']||'') + ' ' + ($json['Last Name']||'')) || 'Your contact' }}
                      </span>
                      <span style="display:inline-block;padding:6px 10px;border-radius:999px;background:#f3f4f6;margin-left:6px;" class="chip">
                        {{ $json['Title'] || 'Professional' }} @ {{ $json['Company Name'] || $json['Company Legal Name'] || 'Company' }}
                      </span>
                    </td>
                  </tr>
                  <tr>
                    <td style="padding:2px 0;">
                      <span style="display:inline-block;padding:6px 10px;border-radius:999px;background:#f3f4f6;" class="chip">
                        {{ $json['Industry'] || 'Industry' }}
                      </span>
                      <span style="display:inline-block;padding:6px 10px;border-radius:999px;background:#f3f4f6;margin-left:6px;" class="chip">
                        {{ $json['Company Location'] || 'Location' }}
                      </span>
                    </td>
                  </tr>
                  <tr>
                    <td style="padding:2px 0;">
                      <a href="{{ $json['Company Website'] ? ('https://' + $json['Company Website'].replace(/^https?:\/\//,'')) : '#' }}"
                         style="color:#4f46e5;text-decoration:none;">{{ $json['Company Website'] || 'website' }}</a>
                      &nbsp;&nbsp;
                      <a href="{{ $json['Company LinkedIn'] || $json['Profile'] || '#' }}"
                         style="color:#4f46e5;text-decoration:none;">LinkedIn</a>
                      &nbsp;&nbsp;
                      <span>{{ $json['Company Phone'] || '' }}</span>
                    </td>
                  </tr>
                  <tr>
                    <td style="padding:6px 0;color:#6b7280;">
                      {{ $json['Company Summary'] || 'Helping customers succeed with modern software.' }}
                    </td>
                  </tr>
                </table>
              </td>
            </tr>
          </table>
          <div style="max-width:640px;margin:12px auto 0 auto;text-align:center;font:11px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;color:#9ca3af;">
            You’re getting this because someone reached out 1:1 — no list.
          </div>
        </td>
      </tr>
    </table>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Click "Execute node" to send your first personalized email.

Step 7 — Automate the workflow

Once you've tested the entire workflow and confirmed that everything is functioning properly, you can replace the manual trigger with a scheduled trigger. This allows n8n to automate the process, so you won't need to run the workflow manually in the future. By using a Cron trigger, you can set the workflow to execute on a regular schedule, such as every morning at 8 AM.

Conclusion.

In this guide, I showed you the process of feeding data into a workflow, personalizing each row of information, and outputting it in the provided spreadsheet. This is just a starting point, an introduction to the world of automated personalized cold emails. We can take this workflow a step further by adding several nodes that allow us to get recent posts of our prospects. This will help us identify more trigger words to provoke responses.

As you advance beyond the basics, focus on the technical foundations that keep personalization effective at scale; Clean data practices, robust email authentication, and strict compliance with relevant regulations, such as GDPR and CAN-SPAM, are essential for ensuring that your messages do not get misclassified as spam. When it comes to sending practices, it's advisable to gradually warm up new mailboxes by beginning with small volumes of emails that mimic human-like behavior. This strategy helps prevent your messages from being flagged as junk.

If you’d like help building more sophisticated workflows, perhaps pulling recent LinkedIn posts or other triggers into your outreach, I’m happy to help. Personalization done right is a competitive advantage, and investing in the tools and techniques to do it at scale will set you apart from peers who still do one‑size‑fits‑all blasts and time‑consuming manual research.

Top comments (1)

Collapse
 
oyesina_oyerinde_a profile image
Oyesina Oyerinde

Welldone