If your team sends follow-up emails manually after checking a spreadsheet, this tutorial replaces that process. You will build an n8n workflow that reads a Google Sheet on a schedule, identifies rows that need an email, sends the email automatically, and marks the row so the same email is never sent twice.

The most common version of this workflow: a tracking sheet where items age past a deadline and someone needs to be notified. Quote follow-ups, overdue tasks, pending approvals, shipment confirmations. All follow the same pattern.

No code beyond a short JavaScript snippet. No ERP. Just Google Sheets and Gmail.

What You Are Building

An n8n workflow that:

  1. Runs on a schedule
  2. Reads every row in a Google Sheet
  3. Identifies rows where a condition is met (date passed, status set to a specific value, a flag column is populated)
  4. Sends a personalized email for each matching row using Gmail
  5. Updates the row in the sheet to record that the email was sent

The result: emails go out automatically, the sheet stays updated, and no one manually tracks what has or has not been sent.

Prerequisites

A Google Sheet with at least four columns:

  • A recipient field (email address or a name you can use in the subject line)
  • A condition field (a date, a status, or any value you want to test against)
  • An item field (what the email is about: invoice number, PO number, task name)
  • A sent flag column (blank by default; n8n will write "Sent" and a timestamp here after the email goes out)

The column names can be anything. You will map them inside n8n.

n8n instance. n8n Cloud or self-hosted, version 1.0 or later.

Google Sheets credentials in n8n. Go to Credentials in n8n, add Google Sheets OAuth2, and authorize against the account that owns the sheet.

Gmail credentials in n8n. Go to Credentials, add Gmail OAuth2, and authorize against the account you want to send from. This is the same OAuth flow as Google Sheets; just select Gmail instead.

Workflow Overview

Six nodes, single purpose each:

Schedule Trigger
    → Google Sheets (read all rows)
    → Code node (filter rows that meet the condition)
    → IF node (skip if nothing matches)
    → Gmail (send email per row)
    → Google Sheets (update row, mark sent)

The Code node does the filtering. The IF node handles the empty case. The final Google Sheets node writes the sent record. Keeping filtering separate from the send node means you can adjust the condition without touching the email logic.

Step-by-Step Build

Step 1: Use n8n AI to Scaffold the Workflow

Before adding any nodes manually, use n8n's built-in AI to generate the starting structure. Open a new workflow, click the AI button in the editor, and describe what you want to build:

"Create a workflow that runs on a schedule, reads all rows from a Google Sheet, filters rows where a date column is in the past and a sent flag column is empty, sends a Gmail to each matching row, and updates the row to mark it as sent."

n8n AI will generate the core nodes and wire them together. It may not know your exact column names yet, but it will produce the right node types in the right sequence. You will configure credentials, column names, and filter conditions in the steps below.

Once the AI has laid out the workflow, locate the Schedule Trigger node it created. Set the interval based on the urgency of the emails:

  • Daily follow-ups or reminders: once daily, early morning (7am–8am)
  • Time-sensitive alerts (overdue invoices, SLA windows): every 4–6 hours
  • Near-real-time responses: every 15–30 minutes

For most operations use cases, once daily is the right default. You can always tighten the interval once the workflow is confirmed working.

Step 2: Add the Google Sheets Node (Read All Rows)

Add a Google Sheets node.

  • Operation: Get Many Rows
  • Credential: Select your Google Sheets credential
  • Spreadsheet: Select or paste the URL
  • Sheet: Select the tab
  • Return All Rows: Enable

Run the node manually. Confirm that the output panel shows each row as a separate item with your column names as JSON keys. Note the exact key names; you will use them in the Code node.

Step 3: Add a Code Node to Filter Matching Rows

Add a Code node after the Google Sheets node. This node's job is to return only the rows that should receive an email.

The condition depends on your use case. Three common patterns:

Pattern A: Rows where a date column is in the past and the sent flag is empty:

const items = $input.all();
const today = new Date();
today.setHours(0, 0, 0, 0);

return items.filter(item => {
  const followUpDate = new Date(item.json['Follow Up Date']);
  const alreadySent = item.json['Email Sent'];
  return followUpDate <= today && !alreadySent;
}).map(item => ({ json: item.json }));

Pattern B: Rows where Status equals a specific value and sent flag is empty:

const items = $input.all();

return items.filter(item => {
  return item.json['Status'] === 'Pending' && !item.json['Email Sent'];
}).map(item => ({ json: item.json }));

Pattern C: Rows where a numeric value exceeds a threshold:

const items = $input.all();

return items.filter(item => {
  const daysOpen = Number(item.json['Days Open']);
  return daysOpen > 7 && !item.json['Email Sent'];
}).map(item => ({ json: item.json }));

Replace the column names in single quotes with the exact key names from your sheet. Run the node with test data and confirm the output panel shows only the rows you expect.

Step 4: Add an IF Node to Handle the Empty Case

Add an IF node after the Code node.

  • Value 1: {{ $items().length }}
  • Condition: Greater than
  • Value 2: 0

Connect the True branch to the Gmail node. Connect the False branch to a No Operation node or leave it unconnected. When nothing matches, the workflow ends without sending any emails.

Step 5: Add the Gmail Node

Add a Gmail node on the True branch of the IF node.

  • Operation: Send Email
  • Credential: Select your Gmail credential
  • To: {{ $json['Recipient Email'] }}, or a fixed address if you are sending all emails to the same person
  • Subject: Build a subject line using fields from the row: Follow-up needed: {{ $json['Item Name'] }}
  • Message: Write the email body using row fields:
Hi {{ $json['Owner'] }},

This is an automated reminder for {{ $json['Item Name'] }}.

Current status: {{ $json['Status'] }}
Due date: {{ $json['Follow Up Date'] }}

Please update the status at your earliest convenience.

For HTML email, switch the Message Type to HTML and wrap the body in <p> tags with <br> line breaks.

Run the Gmail node against one test row. Confirm the email arrives with the correct subject line and populated fields. If a field appears blank, check that the JSON key in your expression matches the column name exactly, including capitalization.

Step 6: Add the Write-Back Google Sheets Node

Add a second Google Sheets node after the Gmail node.

  • Operation: Update Row
  • Credential: Same as before
  • Spreadsheet and Sheet: Same as the read node
  • Column to Match On: Use _rowNumber, the built-in row number from the read output. Set value to {{ $json._rowNumber }}
  • Fields to Update: Set your sent flag column to Sent and a timestamp column to {{ $now.toISODate() }}

This ensures that the next time the workflow runs, the same rows are not matched by the Code node filter because the sent flag is now populated.

Run the complete workflow from the Schedule Trigger. Open the sheet after it finishes and confirm the sent flag and timestamp columns were updated for the rows that received emails.

Step 7: Activate

Toggle the workflow to Active. Test by setting a row to the matching condition, triggering the workflow manually, and confirming the email arrives and the row updates. Reset the test row and let the scheduled run take over.

What to Watch Out For

Timezone mismatches in date comparisons. The new Date() in the Code node uses the timezone of the n8n server, not the timezone of the spreadsheet or the recipients. If your sheet uses dates without times (e.g., "2026-06-10"), comparison against new Date() will behave differently depending on whether n8n is running in UTC or your local timezone. The safest fix: use toISODate() on both sides and compare strings instead of Date objects.

Email sent to wrong recipient on first test. Always test with your own email address in the recipient column before going live. In the Gmail node, you can hard-code your test address in the To field and switch it to the dynamic $json expression after verifying the output is correct.

Row update overwriting other data. The Update Row operation using _rowNumber only updates the fields you specify. It does not overwrite the whole row. Still, confirm the fields you are writing to are the correct columns; a misconfigured field mapping can overwrite data you did not intend to touch.

Sent flag column needs to be consistent. The filter in the Code node checks !item.json['Email Sent']. This works when the column is blank (falsy) or contains text (truthy). If your sheet uses checkboxes instead of text in this column, the value will be false (not sent) or true (sent). Change the condition to item.json['Email Sent'] !== true to handle the checkbox format.

How to Extend This Workflow

Add a second email at a different interval. Duplicate the workflow with a different schedule (e.g., 14 days instead of 7) and a second sent flag column. This gives you a two-touch follow-up sequence without any additional complexity in the original workflow.

Personalize the email body using a lookup. If your sheet has a customer name column, reference it in the Gmail body: Dear {{ $json['Customer Name'] }},. For longer personalized messages, move the email body assembly into a dedicated Code node before the Gmail node.

Route to different senders based on a column value. Add a Switch node between the IF node and the Gmail node. Route rows by region, owner, or category to different Gmail credentials or email addresses. Each branch has its own Gmail node configured for the appropriate sender.

Log sent emails to a separate audit sheet. After the write-back node, add a third Google Sheets node that appends a row to an audit log sheet: recipient, item, timestamp, and which workflow triggered the email. Useful if you run multiple automated email workflows and want a single history view.

Why This Workflow Matters for Operations Teams

Manual email follow-ups fail in two ways: they happen inconsistently (whoever remembers to check the sheet that day), or they never happen at all when the team is busy.

This workflow makes follow-up deterministic. The sheet is the source of truth. The schedule ensures nothing is missed regardless of how busy the day is. The write-back prevents duplicates. Nothing about this requires an ERP integration, a CRM, or a dedicated email marketing tool; it runs entirely from infrastructure most operations teams already have.

Once this workflow is live, the natural extension is building the same read-filter-send-update pattern for other operations touchpoints: purchase order confirmations, shipment notifications, approval requests. The node structure stays identical; only the sheet columns and email content change.

The Flow Kaizen guide covers how to sequence your first five automation builds, starting with quick wins like this one and building toward more connected systems.