{
  "name": "Estimate Follow-Up Engine (Helm Systems)",
  "nodes": [
    {
      "parameters": {
        "content": "## Estimate Follow-Up Engine\n1. Make a Google Sheet with these columns: Date Sent, Customer, Email, Job, Amount, Status, Follow-ups Sent, Last Follow-up. Set Status to Open, Won or Lost.\n2. Pick that sheet in **Read estimates** and **Mark followed up**.\n3. Fill in **Settings**: how many days to wait and how many follow-ups to send.\n4. Connect Gmail and the AI model credentials.\n5. It runs every weekday at 9am (check the timezone in n8n settings). With autoSend off you get Gmail drafts to review and send.\nMade by Helm Systems: helmsystems.biz/field-kit",
        "width": 420,
        "height": 290,
        "color": 4
      },
      "id": "f85e8b9c-7bf6-4afd-a7d2-dec5eed0180f",
      "name": "Note 1",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -323
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "weeksInterval": 1,
              "triggerAtDay": [
                1,
                2,
                3,
                4,
                5
              ],
              "triggerAtHour": 9
            }
          ]
        }
      },
      "id": "54dc867d-11fe-4174-af60-0b9ba44e434f",
      "name": "Every weekday at 9am",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "1ade0790-856f-4760-aee7-785d9ebf5291",
              "name": "business",
              "value": "Your Business Name",
              "type": "string"
            },
            {
              "id": "4104490d-1b92-4c1f-a1d8-9aa61e2d2aac",
              "name": "owner",
              "value": "Your Name",
              "type": "string"
            },
            {
              "id": "835b8628-22a7-427d-a690-99cf58138406",
              "name": "ownerEmail",
              "value": "you@yourbusiness.com",
              "type": "string"
            },
            {
              "id": "853dd0f7-cb60-45af-abd5-cb5462a5f0cc",
              "name": "autoSend",
              "value": false,
              "type": "boolean"
            },
            {
              "id": "50677281-aa70-46bb-a1e5-f2db991518ad",
              "name": "waitDays",
              "value": 3,
              "type": "number"
            },
            {
              "id": "dd182225-828d-4325-ad7d-b153926353ce",
              "name": "maxFollowUps",
              "value": 3,
              "type": "number"
            }
          ]
        },
        "options": {}
      },
      "id": "2155f599-d398-4f9c-abb1-0f3975423528",
      "name": "Settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        260,
        0
      ]
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "options": {}
      },
      "id": "9b04741d-4f45-451f-a4f3-756bb807574b",
      "name": "Read estimates",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        520,
        0
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const asDate = (v) => { if (!v) return null; const d = new Date(v); return isNaN(d) ? null : d; };\nconst startOfDay = (d) => { const x = new Date(d); x.setHours(0, 0, 0, 0); return x; };\nconst today = startOfDay(new Date());\nconst daysBetween = (a, b) => Math.round((b - a) / 86400000);\nconst s = $('Settings').first().json;\nconst out = [];\nfor (const item of $input.all()) {\n  const r = item.json;\n  const status = String(r['Status'] || '').trim().toLowerCase();\n  if (status && status !== 'open') continue;\n  const email = String(r['Email'] || '').trim();\n  const sent = asDate(r['Date Sent']);\n  if (!email || !sent) continue;\n  const done = Number(r['Follow-ups Sent'] || 0);\n  if (done >= Number(s.maxFollowUps)) continue;\n  const last = asDate(r['Last Follow-up']) || sent;\n  if (daysBetween(startOfDay(last), today) < Number(s.waitDays)) continue;\n  out.push({ json: {\n    row_number: r.row_number,\n    name: String(r['Customer'] || '').split(' ')[0] || 'there',\n    email,\n    job: r['Job'] || 'the job',\n    amount: r['Amount'] || '',\n    days: daysBetween(startOfDay(sent), today),\n    stage: done + 1,\n    business: s.business, owner: s.owner,\n  } });\n}\nreturn out;"
      },
      "id": "9b806301-93a4-40df-a963-59b33ffaea3b",
      "name": "Pick estimates to follow up",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        780,
        0
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Customer: {{ $json.name }}\nJob: {{ $json.job }}\nEstimate amount: {{ $json.amount }}\nSent {{ $json.days }} days ago.\nSign off as {{ $json.owner }}, {{ $json.business }}.",
        "hasOutputParser": false,
        "messages": {
          "messageValues": [
            {
              "message": "=You write short follow-up emails for {{ $json.business }} about an estimate the customer has not answered.\n\nRules:\n- Under 100 words. Sound like one person writing to another.\n- Never pressure. Never write \"just checking in\" or \"touching base\". Never invent a discount or a deadline.\n- This is follow-up {{ $json.stage }} of 3.\n  Follow-up 1: friendly, make sure they received it, offer to answer questions.\n  Follow-up 2: offer to adjust the scope or talk through anything unclear.\n  Follow-up 3: a polite last note saying you will close the estimate out unless they would like to go ahead, no hard feelings.\nReturn only JSON with the keys \"subject\" and \"body\"."
            }
          ]
        }
      },
      "id": "0857e30d-096e-428d-ac11-be97481a8830",
      "name": "Write the follow-up",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        1040,
        0
      ]
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "value": "gpt-5-mini",
          "mode": "list",
          "cachedResultName": "gpt-5-mini"
        },
        "options": {}
      },
      "id": "9630b91b-a86b-426e-a773-bced97a6565f",
      "name": "Write the follow-up model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.2,
      "position": [
        1040,
        266
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const raw = String($json.text ?? '').trim().replace(/^```(?:json)?/i, '').replace(/```$/, '').trim();\nlet out = {};\nlet ok = true;\ntry { out = JSON.parse(raw); } catch (e) { ok = false; }\nconst needs = [\"subject\",\"body\"];\nif (ok && !needs.every((k) => out[k] !== undefined && out[k] !== null)) ok = false;\nconst base = $('Pick estimates to follow up').item.json;\n\nif (!ok) {\n  out = { ...out, subject: out.subject || 'Check this one: the AI reply could not be read', body: out.body || ('Original AI output:\\n' + raw) };\n}\nreturn { json: { ...base, ...out, canSend: ok } };"
      },
      "id": "6ba1952b-bea8-4212-a227-05f40ab1c9e6",
      "name": "Read the reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1300,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "5456d783-11ed-43f3-a6d7-5b108f374150",
              "leftValue": "={{ $json.canSend && $('Settings').first().json.autoSend }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "39d34b1f-db30-4a74-a42e-a4c9184318df",
      "name": "Auto-send on?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1560,
        0
      ]
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "send",
        "sendTo": "={{ $json.email }}",
        "subject": "={{ $json.subject }}",
        "emailType": "text",
        "message": "={{ $json.body }}",
        "options": {
          "appendAttribution": false
        }
      },
      "id": "f850227b-f6a7-4942-aa67-089dbad1a3e8",
      "name": "Send email",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1820,
        -85.5
      ]
    },
    {
      "parameters": {
        "resource": "draft",
        "operation": "create",
        "subject": "={{ $json.subject }}",
        "emailType": "text",
        "message": "={{ $json.body }}",
        "options": {
          "sendTo": "={{ $json.email }}"
        }
      },
      "id": "6076d620-62b7-497d-ad7d-740534f5ccef",
      "name": "Save as Gmail draft",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1820,
        85.5
      ]
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "row_number": "={{ $('Pick estimates to follow up').item.json.row_number }}",
            "Follow-ups Sent": "={{ $('Pick estimates to follow up').item.json.stage }}",
            "Last Follow-up": "={{ $now.toFormat('yyyy-LL-dd') }}"
          },
          "matchingColumns": [
            "row_number"
          ],
          "schema": []
        },
        "options": {}
      },
      "id": "9eb23f76-07ac-4f9c-aeb3-fa675bcff355",
      "name": "Mark followed up",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        2080,
        0
      ]
    }
  ],
  "connections": {
    "Write the follow-up model": {
      "ai_languageModel": [
        [
          {
            "node": "Write the follow-up",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Read the reply": {
      "main": [
        [
          {
            "node": "Auto-send on?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Auto-send on?": {
      "main": [
        [
          {
            "node": "Send email",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Save as Gmail draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every weekday at 9am": {
      "main": [
        [
          {
            "node": "Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings": {
      "main": [
        [
          {
            "node": "Read estimates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read estimates": {
      "main": [
        [
          {
            "node": "Pick estimates to follow up",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick estimates to follow up": {
      "main": [
        [
          {
            "node": "Write the follow-up",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write the follow-up": {
      "main": [
        [
          {
            "node": "Read the reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send email": {
      "main": [
        [
          {
            "node": "Mark followed up",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save as Gmail draft": {
      "main": [
        [
          {
            "node": "Mark followed up",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "tags": []
}
