{
  "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
          }
        ]
      ]
    }
  }
}