AutomationFlowsEmail & Gmail › Send Scheduled Quote Follow-up Emails with Gmail and Google Sheets

Send Scheduled Quote Follow-up Emails with Gmail and Google Sheets

By43 Sunsets @the43sunsets on n8n.io

This workflow runs daily to read open quotes from Google Sheets, calculates how many days have passed since each quote was sent, and sends the appropriate 3-, 7-, or 14-day follow-up email via Gmail, then writes a sent date back to the sheet to prevent duplicates. Runs every day…

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

This workflow corresponds to n8n.io template #16975 — 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 quote follow-up emails on a 3-7-14 day schedule with Gmail and Google Sheets",
  "nodes": [
    {
      "id": "f2a0s001-0000-4000-8000-000000000001",
      "name": "Sticky Note \u2014 read me first",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -800,
        -80
      ],
      "parameters": {
        "width": 700,
        "height": 760,
        "content": "## Quote follow-ups that never slip \u2014 and stop the moment they should\n\n### How it works\nYou track outgoing quotes in one Google Sheet. Once a day, this workflow reads every open quote, works out how many days have passed since you sent it, and emails the right follow-up at day 3, day 7 and day 14 \u2014 each with its own tone (friendly nudge \u2192 value reminder \u2192 polite close-out). The moment you mark a quote `won` or `lost` in the sheet, follow-ups stop instantly. Every send is written back to the sheet, so a quote can never get the same follow-up twice, even if you re-run the workflow manually.\n\nNo AI key needed \u2014 the three 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: `quote_id, customer_name, customer_email, quote_amount, sent_date, status, followup_1, followup_2, followup_3` \u2014 set `status` to `open` and `sent_date` as YYYY-MM-DD.\n3. Select your sheet in both Sheets nodes, put your business details in **\u2699\ufe0f Your settings**, and edit the three templates to sound like you.\n4. Activate. It checks once a day at 9am.\n\n### Customization tips\nChange the day offsets, subjects and body text in \u2699\ufe0f Your settings. Add a fourth touch by copying a template block."
      },
      "typeVersion": 1
    },
    {
      "id": "f2a0s002-0000-4000-8000-000000000002",
      "name": "Sticky Note \u2014 section 1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -60,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 560,
        "height": 760,
        "content": "## 1. Read the quote ledger\nRuns daily at 9am. Pulls every row from your quotes sheet \u2014 the sheet stays the single source of truth that you and the workflow both edit."
      },
      "typeVersion": 1
    },
    {
      "id": "f2a0s003-0000-4000-8000-000000000003",
      "name": "Sticky Note \u2014 section 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        520,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 460,
        "height": 760,
        "content": "## 2. Decide what is due\nKeeps only quotes with status `open`, computes days since `sent_date`, and picks which follow-up (if any) is due today \u2014 skipping any step already marked as sent. This double guard (status + sent-flags) is what makes re-runs safe: no customer ever gets a duplicate."
      },
      "typeVersion": 1
    },
    {
      "id": "f2a0s004-0000-4000-8000-000000000004",
      "name": "Sticky Note \u2014 section 3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1000,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 500,
        "height": 760,
        "content": "## 3. Send + write back\nFills the right template with the customer's name, amount and your details, sends it from your Gmail, then immediately stamps the sheet (`followup_N = date`). Write-back happens right after each send, so even a crash mid-run cannot cause a double email tomorrow."
      },
      "typeVersion": 1
    },
    {
      "id": "f2a00001-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": "f2a00002-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": "yourPhone",
              "type": "string",
              "value": "(555) 000-0000"
            },
            {
              "id": "b4",
              "name": "day1Offset",
              "type": "number",
              "value": 3
            },
            {
              "id": "b5",
              "name": "day2Offset",
              "type": "number",
              "value": 7
            },
            {
              "id": "b6",
              "name": "day3Offset",
              "type": "number",
              "value": 14
            },
            {
              "id": "b7",
              "name": "subject1",
              "type": "string",
              "value": "Quick question about your quote"
            },
            {
              "id": "b8",
              "name": "body1",
              "type": "string",
              "value": "Hi {customer_name},\n\nJust checking in on the quote for {quote_amount} we sent over a few days ago. Happy to walk through any part of it or adjust the scope if something doesn't fit.\n\nIs there anything you'd like me to clarify?\n\n{your_name}\n{business_name} \u00b7 {your_phone}"
            },
            {
              "id": "b9",
              "name": "subject2",
              "type": "string",
              "value": "Still holding your spot \u2014 quote from {business_name}"
            },
            {
              "id": "b10",
              "name": "body2",
              "type": "string",
              "value": "Hi {customer_name},\n\nWanted to follow up on the quote for {quote_amount}. Our schedule for the coming weeks is starting to fill, and I'd hate for you to lose the slot we penciled in.\n\nIf the price or scope is the sticking point, tell me \u2014 there's usually room to find a version that works.\n\n{your_name}\n{business_name} \u00b7 {your_phone}"
            },
            {
              "id": "b11",
              "name": "subject3",
              "type": "string",
              "value": "Closing the file on your quote?"
            },
            {
              "id": "b12",
              "name": "body3",
              "type": "string",
              "value": "Hi {customer_name},\n\nI haven't heard back on the quote for {quote_amount}, so I'll assume the timing isn't right and close the file for now \u2014 no hard feelings at all.\n\nIf things change, just reply to this email and we'll pick it right back up.\n\nAll the best,\n{your_name}\n{business_name} \u00b7 {your_phone}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "f2a00003-0000-4000-8000-00000000000c",
      "name": "Read all quotes",
      "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": "f2a00004-0000-4000-8000-00000000000d",
      "name": "Pick the follow-up due today",
      "type": "n8n-nodes-base.code",
      "position": [
        540,
        320
      ],
      "parameters": {
        "jsCode": "// Decide which follow-up (if any) is due for each open quote.\n// Double idempotency guard: status must be 'open' AND the step's flag must be empty.\nconst cfg = $('\u2699\ufe0f Your settings').first().json;\nconst today = new Date();\ntoday.setHours(0, 0, 0, 0);\nconst due = [];\n\nfor (const item of $('Read all quotes').all()) {\n  const q = item.json;\n  if (String(q.status || '').trim().toLowerCase() !== 'open') continue;\n  if (!q.sent_date || !q.customer_email) continue;\n\n  const sent = new Date(q.sent_date);\n  if (isNaN(sent.getTime())) continue;\n  sent.setHours(0, 0, 0, 0);\n  const days = Math.floor((today - sent) / 86400000);\n\n  let step = null;\n  if (days >= cfg.day3Offset && !q.followup_3) step = 3;\n  else if (days >= cfg.day2Offset && !q.followup_2) step = 2;\n  else if (days >= cfg.day1Offset && !q.followup_1) step = 1;\n  if (!step) continue;\n\n  const fill = (t) => String(t)\n    .replaceAll('{customer_name}', q.customer_name || 'there')\n    .replaceAll('{quote_amount}', q.quote_amount || 'your project')\n    .replaceAll('{your_name}', cfg.yourName)\n    .replaceAll('{business_name}', cfg.businessName)\n    .replaceAll('{your_phone}', cfg.yourPhone);\n\n  due.push({\n    json: {\n      quote_id: q.quote_id,\n      customer_email: q.customer_email,\n      step,\n      subject: fill(cfg['subject' + step]),\n      body: fill(cfg['body' + step]),\n      flag_column: 'followup_' + step,\n    },\n  });\n}\n\nreturn due;"
      },
      "typeVersion": 2
    },
    {
      "id": "f2a00005-0000-4000-8000-00000000000e",
      "name": "One quote at a time",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        720,
        320
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "f2a00006-0000-4000-8000-00000000000f",
      "name": "Send the follow-up email",
      "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": "f2a00007-0000-4000-8000-000000000010",
      "name": "Stamp the sheet (sent)",
      "type": "n8n-nodes-base.googleSheets",
      "maxTries": 3,
      "position": [
        1260,
        336
      ],
      "parameters": {
        "columns": {
          "value": {
            "quote_id": "={{ $('One quote at a time').item.json.quote_id }}",
            "={{ $('One quote at a time').item.json.flag_column }}": "={{ $now.toFormat('yyyy-MM-dd') }}"
          },
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "quote_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": {
    "Read all quotes": {
      "main": [
        [
          {
            "node": "Pick the follow-up due today",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every day at 9am": {
      "main": [
        [
          {
            "node": "\u2699\ufe0f Your settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "One quote at a time": {
      "main": [
        [],
        [
          {
            "node": "Send the follow-up email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u2699\ufe0f Your settings": {
      "main": [
        [
          {
            "node": "Read all quotes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Stamp the sheet (sent)": {
      "main": [
        [
          {
            "node": "One quote at a time",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send the follow-up email": {
      "main": [
        [
          {
            "node": "Stamp the sheet (sent)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick the follow-up due today": {
      "main": [
        [
          {
            "node": "One quote at a time",
            "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 to read open quotes from Google Sheets, calculates how many days have passed since each quote was sent, and sends the appropriate 3-, 7-, or 14-day follow-up email via Gmail, then writes a sent date back to the sheet to prevent duplicates. Runs every day…

Source: https://n8n.io/workflows/16975/ — 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