How to Automate Sales Follow-ups with n8n

By The LightCube Labs team

The Problem

Deals rarely die from a clear no. They die from silence. A prospect reaches the qualified stage, the rep gets busy with new demos, and two weeks later nobody remembers who was supposed to write back. Manual reminders in the CRM pile up and get ignored, and generic bulk sequences feel robotic. This n8n workflow checks HubSpot every morning, finds deals that have sat in a stage too long, picks a message that fits how overdue the deal is, sends it as the deal owner, and writes the touch back to the CRM so the record stays honest.

What You Will Need

Step-by-Step Workflow Setup

1 Schedule the daily run

In n8n the Schedule Trigger needs the workflow to be active, so remember to switch the n8n workflow on after testing.

Start with a Schedule Trigger node. A morning run means reps see the logged activity before their first call, and prospects receive email during their working day.

Add a Schedule Trigger node and configure:

Node: Schedule Trigger
Trigger Interval: Days
Days Between Triggers: 1
Trigger at Hour: 9am
Trigger at Minute: 0
Timezone: set in workflow settings (America/New_York)

Set the timezone in the workflow settings, not only on the instance, because a self-hosted n8n server often runs in UTC. To skip weekends, switch the interval to Cron and use this expression:

Cron Expression: 0 9 * * 1-5
Sample output:
{
  "timestamp": "2026-11-04T09:00:00.000-05:00",
  "Readable date": "November 4th 2026, 9:00:00 am",
  "Day of week": "Wednesday"
}

2 Search stalled deals in HubSpot

The n8n HubSpot node wraps the CRM search API, which means n8n handles paging and rate limits when Return All is enabled.

The HubSpot node can search deals with filter groups. Filters inside one group are combined with AND, and separate groups are combined with OR. That is exactly what two stage rules need: qualified deals older than 7 days, or scheduled presentations older than 14 days.

Node: HubSpot
Authentication: App Token
Resource: Deal
Operation: Search
Return All: on
Filter Groups:
  Group 1:
    dealstage EQ qualifiedtobuy
    days_in_stage GT 7
  Group 2:
    dealstage EQ presentationscheduled
    days_in_stage GT 14
Additional Fields > Properties: dealname, dealstage, days_in_stage, hubspot_owner_id, notes_last_updated
Associations: contacts

If your portal does not have a days_in_stage property, create a calculation property in HubSpot, or filter on the built-in date-entered property for the stage and compare it to a timestamp built with this expression:

Expression: {{ $now.minus({ days: 7 }).toMillis() }}
Sample output:
{
  "id": "5120934410",
  "properties": {
    "dealname": "Northwind annual plan",
    "dealstage": "qualifiedtobuy",
    "days_in_stage": "15",
    "hubspot_owner_id": "88213",
    "notes_last_updated": "2026-10-19T14:02:00Z"
  },
  "associations": { "contacts": { "results": [{ "id": "20311" }] } }
}

Add a second HubSpot node set to Contact, Get, if you need the contact email and first name, since the search returns only the association id.

3 Select a template by stage and days overdue

Putting this rule in an n8n Code node keeps the escalation ladder visible in one place, and any n8n user on your team can adjust the day counts.

A Code node chooses the message tone. The rule is time-based escalation: a gentle nudge at 7 days, a value reminder at 14, and a last chance note at 21 or more. Presentation stage deals use the same ladder but start one week later because the prospect already has material to review.

Node: Code
Mode: Run Once for Each Item
Language: JavaScript

const stage = $json.properties.dealstage;
const days = Number($json.properties.days_in_stage);
let template = null;
if (days >= 21) template = 'last_chance';
else if (days >= 14) template = 'value_reminder';
else if (days >= 7 && stage === 'qualifiedtobuy') template = 'gentle_nudge';
return { json: { ...$json, template, days } };

Add an IF node after it that checks {{ $json.template }} is not empty, so deals that match no rule are dropped instead of emailed.

Sample output:
{
  "id": "5120934410",
  "template": "value_reminder",
  "days": 15
}

4 Personalize the message

You can preview the personalized text for each item in the n8n output panel before any email is sent from n8n.

The next Code node holds the template text as an object and replaces tokens with real values. Keep the copy short and plain. Merge fields should read as natural sentences, not as a form letter.

Node: Code
Mode: Run Once for Each Item

const bodies = {
  gentle_nudge: 'Hi {first}, I wanted to check in on {deal}. Is there anything I can clear up on {company} side?',
  value_reminder: 'Hi {first}, since we last spoke on {last}, here is how teams like {company} usually get value from {deal}. Happy to walk through it.',
  last_chance: 'Hi {first}, I have not heard back about {deal}. If the timing is wrong, tell me and I will close the loop on my side.'
};
const c = $('HubSpot Contact').item.json.properties;
const text = bodies[$json.template]
  .replace('{first}', c.firstname || 'there')
  .replace(/{company}/g, c.company || 'your')
  .replace(/{deal}/g, $json.properties.dealname)
  .replace('{last}', new Date($json.properties.notes_last_updated).toDateString());
return { json: { ...$json, email: c.email, text } };
Sample output:
{
  "email": "dana@northwind.example",
  "text": "Hi Dana, since we last spoke on Mon Oct 19 2026, here is how teams like Northwind usually get value from Northwind annual plan. Happy to walk through it."
}

The fallback value in firstname prevents an empty greeting when the contact record is incomplete.

5 Send the email with the owner as reply-to

The n8n Send Email node supports a Reply To option, so n8n can send from one mailbox while replies reach the deal owner.

The Send Email node sends through SMTP. Send from a shared address and set the reply-to header to the deal owner, so replies land in the right inbox. Look up the owner address with a HubSpot owners request or a small mapping table keyed by hubspot_owner_id.

Node: Send Email
Credential: SMTP account
From Email: sales@yourcompany.example
To Email: {{ $json.email }}
Subject: Quick check on {{ $json.properties.dealname }}
Email Format: Text
Text: {{ $json.text }}
Options > Reply To: {{ $json.ownerEmail }}

Under Settings on this node, enable Continue On Fail so one bounced address does not stop the rest of the batch.

6 Log the activity in HubSpot

Logging from n8n back to HubSpot closes the loop, and the n8n execution history doubles as an audit trail of what was sent.

Finish with a HubSpot node on the Engagement resource. Logging the email keeps reps from double contacting the same person and lets the next run measure the days since the last touch.

Node: HubSpot
Resource: Engagement
Operation: Create
Type: Email
Metadata:
  subject: {{ $('Send Email').item.json.subject }}
  text: {{ $json.text }}
  Note: Template used: {{ $json.template }}
Associations:
  Contact IDs: {{ $json.associations.contacts.results[0].id }}
  Deal IDs: {{ $json.id }}
  Owner ID: {{ $json.properties.hubspot_owner_id }}
Sample response:
{
  "engagement": { "id": 771204, "type": "EMAIL", "ownerId": 88213 },
  "associations": { "contactIds": [20311], "dealIds": [5120934410] }
}

Setting the owner id means the activity appears under the rep who owns the deal, not under the integration user.

Before you activate the n8n workflow, point the HubSpot node at a sandbox pipeline and swap the recipient for your own address. Run the n8n workflow manually and read each n8n node output until the template, the merge fields, and the logged activity all look right. Then activate it and let n8n run on schedule.

Troubleshooting

The same deal is emailed every morning

The search still matches because days_in_stage keeps growing. Add a filter on the last contacted date so deals touched in the past 6 days are excluded, or write the sent template to a deal property and skip a deal when that value already equals the new template. In n8n, open the execution and compare the search filters with the HubSpot UI.

The HubSpot search returns zero deals

Stage filters need the internal stage value, not the label shown in the pipeline. Open the pipeline settings and copy the internal id. Custom pipelines use numeric ids rather than qualifiedtobuy. Use the n8n pinned data feature to test the stage values without hitting the API.

Replies go to the shared mailbox

Some SMTP providers overwrite the reply-to header unless the sender domain is verified. Verify the domain, and test by sending one message to yourself and checking the raw headers. Send a test from n8n to your own address first.

Free n8n Workflow Resources

Get started with the free workflow pack on the LightCube Labs templates page.

Production-Ready Workflow Packs

Get this workflow and more, with setup guides, editable configurations, and a shared failure-alert system.

View the Starter ($29) and Pro ($99) Packs

Frequently Asked Questions

How many follow-ups will one deal receive from this n8n workflow?

One per run at most, and the template depends on days in stage. A deal stuck for 30 days would receive the nudge, reminder, and last chance only if each earlier email also reset or logged its contact date. Use the logged activity date to space messages at least a week apart. The n8n workflow reads the deal stage again on every run.

Can the emails be sent from each rep instead of a shared address?

Yes. Store one SMTP credential per rep and pick it with a Switch node on the owner id. The reply-to approach is simpler to maintain, while per-rep credentials give better deliverability and a personal sender name. In n8n you can route each owner to a different SMTP credential.

What happens when a prospect replies or the deal moves forward?

The next run re-reads the deal stage. A deal that advanced no longer matches either filter group, so follow-ups stop on their own. For replies, add a rule that skips deals with a recent inbound email logged. Because n8n re-reads HubSpot daily, no extra state is needed.

Is it safe to email contacts who unsubscribed?

Not automatically. Add a filter for the contact opt-out property in the contact lookup step and drop any contact marked as unsubscribed before the Send Email node. You can add this check as one more n8n node.