AutomationFlowsEmail & Gmail › Send Google Review Request and Reminder Emails with Gmail and Google Sheets

Send Google Review Request and Reminder Emails with Gmail and Google Sheets

By43 Sunsets @the43sunsets on n8n.io

This workflow runs daily and reads a Google Sheets job ledger to send Google review request and reminder emails via Gmail, then updates the sheet with sent dates to prevent duplicate messages. Runs every day at 9am on a schedule. Loads your email templates and timing settings,…

Cron / scheduled trigger★★★★☆ complexity11 nodesGoogle SheetsGmail
Email & Gmail Trigger: Cron / scheduled Nodes: 11 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #17059 — we link there as the canonical source.

This workflow follows the Gmail → Google Sheets recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "name": "Send Google review request emails to customers with Gmail and Google Sheets",
  "nodes": [
    {
      "id": "f4b0s001-0000-4000-8000-000000000001",
      "name": "Sticky Note \u2014 read me first",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -800,
        -80
      ],
      "parameters": {
        "width": 700,
        "height": 840,
        "content": "## Google reviews, asked for at the right moment \u2014 and never twice\n\n### How it works\nYou track finished jobs in one Google Sheet. Once a day, this workflow reads every open row and sends a friendly review request 2 days after the job, while the experience is still fresh. If the row is still open 7 days after the ask, it sends one gentle reminder, then stops for good. Mark a row `done` (review arrived) or `skip` and nothing more is ever sent. Every send is written back to the sheet, so a customer can never get the same email twice, even on manual re-runs.\n\nNo AI key needed \u2014 both templates are plain text with placeholders, so this runs 100% free.\n\n### Setup\n1. Connect **Gmail** and **Google Sheets** (one-click Google sign-in on n8n Cloud).\n2. Make a sheet with headers: `job_id, customer_name, customer_email, job_description, job_date, status, request_sent, reminder_sent` \u2014 set `status` to `open` and `job_date` as YYYY-MM-DD.\n3. Get your review link: Google Business Profile \u2192 **Ask for reviews** \u2192 copy the short link into **\u2699\ufe0f Your settings**.\n4. Select your sheet in both Sheets nodes, edit the two templates to sound like you, and activate. It checks once a day at 9am.\n\n### Honest notes\nThis sends for real \u2014 run one test row with your own email first. It cannot tell whether a review was left: marking a row `done` is on you. Ask everyone and never offer rewards \u2014 selective asking (review gating) and incentives break Google's review policy. Built with retries and an idempotent write-back for the day something breaks mid-run."
      },
      "typeVersion": 1
    },
    {
      "id": "f4b0s002-0000-4000-8000-000000000002",
      "name": "Sticky Note \u2014 section 1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -60,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 560,
        "height": 840,
        "content": "## 1. Read the customer ledger\nRuns daily at 9am. Pulls every row from your jobs sheet \u2014 the sheet stays the single source of truth that you and the workflow both edit."
      },
      "typeVersion": 1
    },
    {
      "id": "f4b0s003-0000-4000-8000-000000000003",
      "name": "Sticky Note \u2014 section 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        520,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 460,
        "height": 840,
        "content": "## 2. Decide who to ask today\nKeeps only rows with status `open`. The first ask goes out once the job is 2+ days old; the single reminder only ever follows an ask that was actually sent, 7+ days later. This double guard (status + sent-flags) is what makes re-runs safe: no customer ever gets a duplicate, and nobody gets a reminder for an email they never received."
      },
      "typeVersion": 1
    },
    {
      "id": "f4b0s004-0000-4000-8000-000000000004",
      "name": "Sticky Note \u2014 section 3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1000,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 500,
        "height": 840,
        "content": "## 3. Send + write back\nFills the template with the customer's name, the job and your review link, sends it from your Gmail, then immediately stamps the sheet (`request_sent` / `reminder_sent` = date). Write-back happens right after each send, so even a crash mid-run cannot cause a double email tomorrow."
      },
      "typeVersion": 1
    },
    {
      "id": "f4b00001-0000-4000-8000-00000000000a",
      "name": "Every day at 9am",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        0,
        320
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "triggerAtHour": 9
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "f4b00002-0000-4000-8000-00000000000b",
      "name": "\u2699\ufe0f Your settings",
      "type": "n8n-nodes-base.set",
      "position": [
        180,
        320
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "b1",
              "name": "businessName",
              "type": "string",
              "value": "YOUR BUSINESS NAME"
            },
            {
              "id": "b2",
              "name": "yourName",
              "type": "string",
              "value": "YOUR NAME"
            },
            {
              "id": "b3",
              "name": "reviewLink",
              "type": "string",
              "value": "https://g.page/r/YOUR-REVIEW-LINK/review"
            },
            {
              "id": "b4",
              "name": "requestOffset",
              "type": "number",
              "value": 2
            },
            {
              "id": "b5",
              "name": "reminderGap",
              "type": "number",
              "value": 7
            },
            {
              "id": "b6",
              "name": "subject1",
              "type": "string",
              "value": "How did we do, {customer_name}?"
            },
            {
              "id": "b7",
              "name": "body1",
              "type": "string",
              "value": "Hi {customer_name},\n\nThanks again for choosing {business_name} for {job_description}. If you have a spare minute, a Google review would mean a lot \u2014 it's how a small business like ours gets found:\n\n{review_link}\n\nAnd if anything about the job wasn't right, just reply to this email instead \u2014 I'd rather hear it from you and fix it.\n\n{your_name}\n{business_name}"
            },
            {
              "id": "b8",
              "name": "subject2",
              "type": "string",
              "value": "One small favor, {customer_name}?"
            },
            {
              "id": "b9",
              "name": "body2",
              "type": "string",
              "value": "Hi {customer_name},\n\nJust a gentle nudge \u2014 if you've already left a review, thank you, and please ignore this. If not, it takes about a minute:\n\n{review_link}\n\nEither way, thanks for your business. And if anything still needs attention, just reply \u2014 happy to help.\n\n{your_name}\n{business_name}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "f4b00003-0000-4000-8000-00000000000c",
      "name": "Read all customers",
      "type": "n8n-nodes-base.googleSheets",
      "maxTries": 3,
      "position": [
        360,
        320
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.5,
      "waitBetweenTries": 5000
    },
    {
      "id": "f4b00004-0000-4000-8000-00000000000d",
      "name": "Pick who to ask today",
      "type": "n8n-nodes-base.code",
      "position": [
        540,
        320
      ],
      "parameters": {
        "jsCode": "// Decide who gets the review request (or the single reminder) today.\n// Double idempotency guard: status must be 'open' AND the step's flag must be empty.\n// The reminder is anchored to request_sent, so it can never fire before the ask itself.\nconst cfg = $('\u2699\ufe0f Your settings').first().json;\nconst today = new Date();\ntoday.setHours(0, 0, 0, 0);\nconst daysSince = (dateStr) => {\n  const d = new Date(dateStr);\n  if (isNaN(d.getTime())) return null;\n  d.setHours(0, 0, 0, 0);\n  return Math.floor((today - d) / 86400000);\n};\nconst due = [];\n\nfor (const item of $('Read all customers').all()) {\n  const row = item.json;\n  if (String(row.status || '').trim().toLowerCase() !== 'open') continue;\n  if (!row.job_date || !row.customer_email) continue;\n\n  let step = null;\n  if (!row.request_sent) {\n    const days = daysSince(row.job_date);\n    if (days !== null && days >= cfg.requestOffset) step = 1;\n  } else if (!row.reminder_sent) {\n    const days = daysSince(row.request_sent);\n    if (days !== null && days >= cfg.reminderGap) step = 2;\n  }\n  if (!step) continue;\n\n  const fill = (t) => String(t)\n    .replaceAll('{customer_name}', row.customer_name || 'there')\n    .replaceAll('{job_description}', row.job_description || 'your project')\n    .replaceAll('{review_link}', cfg.reviewLink)\n    .replaceAll('{your_name}', cfg.yourName)\n    .replaceAll('{business_name}', cfg.businessName);\n\n  due.push({\n    json: {\n      job_id: row.job_id,\n      customer_email: row.customer_email,\n      step,\n      subject: fill(cfg['subject' + step]),\n      body: fill(cfg['body' + step]),\n      flag_column: step === 1 ? 'request_sent' : 'reminder_sent',\n    },\n  });\n}\n\nreturn due;"
      },
      "typeVersion": 2
    },
    {
      "id": "f4b00005-0000-4000-8000-00000000000e",
      "name": "One customer at a time",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        720,
        320
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "f4b00006-0000-4000-8000-00000000000f",
      "name": "Send the review request",
      "type": "n8n-nodes-base.gmail",
      "maxTries": 3,
      "position": [
        1060,
        336
      ],
      "parameters": {
        "sendTo": "={{ $json.customer_email }}",
        "message": "={{ $json.body }}",
        "options": {},
        "subject": "={{ $json.subject }}",
        "emailType": "text"
      },
      "retryOnFail": true,
      "typeVersion": 2.1,
      "waitBetweenTries": 5000
    },
    {
      "id": "f4b00007-0000-4000-8000-000000000010",
      "name": "Stamp the sheet (sent)",
      "type": "n8n-nodes-base.googleSheets",
      "maxTries": 3,
      "position": [
        1260,
        336
      ],
      "parameters": {
        "columns": {
          "value": {
            "job_id": "={{ $('One customer at a time').item.json.job_id }}",
            "={{ $('One customer at a time').item.json.flag_column }}": "={{ $now.toFormat('yyyy-MM-dd') }}"
          },
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "job_id"
          ]
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.5,
      "waitBetweenTries": 5000
    }
  ],
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Every day at 9am": {
      "main": [
        [
          {
            "node": "\u2699\ufe0f Your settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read all customers": {
      "main": [
        [
          {
            "node": "Pick who to ask today",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u2699\ufe0f Your settings": {
      "main": [
        [
          {
            "node": "Read all customers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick who to ask today": {
      "main": [
        [
          {
            "node": "One customer at a time",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "One customer at a time": {
      "main": [
        [],
        [
          {
            "node": "Send the review request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Stamp the sheet (sent)": {
      "main": [
        [
          {
            "node": "One customer at a time",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send the review request": {
      "main": [
        [
          {
            "node": "Stamp the sheet (sent)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow runs daily and reads a Google Sheets job ledger to send Google review request and reminder emails via Gmail, then updates the sheet with sent dates to prevent duplicate messages. Runs every day at 9am on a schedule. Loads your email templates and timing settings,…

Source: https://n8n.io/workflows/17059/ — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Email & Gmail

Automatically extract structured information from emails using AI-powered document analysis. This workflow processes emails from specified domains, classifies them by type, and extracts structured dat

Gmail, HTTP Request, AWS S3 +1
Email & Gmail

This weekly workflow helps you stay on top of SEO visibility losses by automatically detecting when your previously strong keywords fall out of Google’s top 10 results.

N8N Nodes Dataforseo, Google Sheets, Gmail
Email & Gmail

What This Flow Does

Gmail, Google Sheets, HTTP Request +1
Email & Gmail

This n8n workflow sends personalized outreach emails automatically while enforcing strict safety rules such as email validation, spam checks, daily limits, and human-like delays.

Google Drive, Google Sheets, Gmail
Email & Gmail

This workflow automates a 3-step cold email sequence from Gmail using leads in Google Sheets, generates personalized copy with Anthropic Claude, enforces a gradual daily sending cap, schedules follow-

Google Sheets, HTTP Request, Gmail