{
  "name": "Review Requests & Reply Drafter (Helm Systems)",
  "nodes": [
    {
      "parameters": {
        "content": "## Review Requests & Reply Drafter (two flows in one file)\nTOP FLOW: asks every finished job for an honest review. Sheet columns: Customer, Email, Phone, Job, Completed Date, Review Requested. Put your Google review link in **Settings (requests)**.\nBOTTOM FLOW: when a new review arrives, drafts a calm public reply and emails it to you. It never posts anything. Send it a POST with {\"reviewer\",\"rating\",\"text\",\"platform\"} from a Zapier/Make hook or a reviews tool.\nAsk EVERYONE. Do not filter to happy customers and never offer anything in exchange for a review: Google's rules discourage both.\nMade by Helm Systems: helmsystems.biz/field-kit",
        "width": 420,
        "height": 330,
        "color": 4
      },
      "id": "9dbcc8f4-4f2f-4487-adb5-61722c1604db",
      "name": "Note 1",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -399
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "daysInterval": 1,
              "triggerAtHour": 10
            }
          ]
        }
      },
      "id": "70f631ac-30d4-43af-a026-e7b46b64bfb2",
      "name": "Every day at 10am",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "94c16529-136f-4c90-a803-49d016498310",
              "name": "business",
              "value": "Your Business Name",
              "type": "string"
            },
            {
              "id": "4018493b-98ea-4b10-aadd-5bbaea362d4b",
              "name": "owner",
              "value": "Your Name",
              "type": "string"
            },
            {
              "id": "1ed94d69-7752-44ac-a12f-e85179eeb4e2",
              "name": "ownerEmail",
              "value": "you@yourbusiness.com",
              "type": "string"
            },
            {
              "id": "0b6ec9f2-6cf5-4e88-abed-56bb175a4c85",
              "name": "autoSend",
              "value": false,
              "type": "boolean"
            },
            {
              "id": "f67fc448-9b2a-472a-a925-af0eefde8cba",
              "name": "reviewLink",
              "value": "https://g.page/r/your-review-link",
              "type": "string"
            },
            {
              "id": "0af97f92-4c55-4320-aef7-12a55f51a0b8",
              "name": "waitDays",
              "value": 1,
              "type": "number"
            },
            {
              "id": "cb87a3fb-3ef2-4c5b-a921-adf1644510d3",
              "name": "maxAgeDays",
              "value": 14,
              "type": "number"
            }
          ]
        },
        "options": {}
      },
      "id": "7f60d5ec-a8bb-486a-ab34-db1c4fdbca3b",
      "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": "7f988d2e-1b61-4fae-a6de-bd62cdb626b0",
      "name": "Read completed jobs",
      "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  if (String(r['Review Requested'] || '').trim()) continue;\n  const email = String(r['Email'] || '').trim();\n  const done = asDate(r['Completed Date']);\n  if (!email || !done) continue;\n  const age = daysBetween(startOfDay(done), today);\n  if (age < Number(s.waitDays) || age > Number(s.maxAgeDays)) 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'] || 'your project',\n    reviewLink: s.reviewLink,\n    business: s.business, owner: s.owner,\n  } });\n}\nreturn out;"
      },
      "id": "dc40c68b-d40c-42a8-ae13-080d252a9025",
      "name": "Pick jobs to ask",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        780,
        0
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Customer: {{ $json.name }}\nJob: {{ $json.job }}\nReview link: {{ $json.reviewLink }}\nSign off as {{ $json.owner }}, {{ $json.business }}.",
        "hasOutputParser": false,
        "messages": {
          "messageValues": [
            {
              "message": "=You write a short message asking a customer of {{ $json.business }} for a review.\n\nRules:\n- Under 60 words. Thank them by first name and mention the job.\n- Ask once and include the link exactly as given.\n- Never offer a discount, gift or anything in exchange for a review. Never ask for a positive review, only for an honest one.\nReturn only JSON with the keys \"subject\" and \"body\"."
            }
          ]
        }
      },
      "id": "261a9c83-7723-4b30-a132-2a3a0af4490b",
      "name": "Write the request",
      "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": "304c8f5b-bad0-4f71-a361-b00b5d26e1f2",
      "name": "Write the request 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 jobs to ask').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": "463f8c1a-603a-4431-a831-e9e0c3e1ecc2",
      "name": "Read the request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1300,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "b2eba9b6-ca0e-4e85-a164-431ef7bec97f",
              "leftValue": "={{ $json.canSend && $('Settings').first().json.autoSend }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "f9362a6a-68c4-4e44-a8c0-d06c36971f68",
      "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": "4abdd0cd-ae4a-411a-a7c1-3e86616af039",
      "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": "f6c81e2a-5ed2-41e5-a564-30b1f3f188e7",
      "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 jobs to ask').item.json.row_number }}",
            "Review Requested": "={{ $now.toFormat('yyyy-LL-dd') }}"
          },
          "matchingColumns": [
            "row_number"
          ],
          "schema": []
        },
        "options": {}
      },
      "id": "cce9175c-9d4b-40da-ab87-0e6ba6f213ed",
      "name": "Mark asked",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        2080,
        0
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "new-review",
        "options": {}
      },
      "id": "73a23e8a-28af-4d66-aea5-8a5da33c4e8f",
      "name": "New review (webhook)",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        570
      ],
      "webhookId": "83230faa-d785-49ce-a542-dd4ec984dd5b"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c8b31dac-69ed-471c-afd7-0d296d3a4bf4",
              "name": "business",
              "value": "Your Business Name",
              "type": "string"
            },
            {
              "id": "84a9009b-ce28-48bd-a7fc-fb6374897381",
              "name": "owner",
              "value": "Your Name",
              "type": "string"
            },
            {
              "id": "4695033e-c3f1-436b-a7b3-0d0b5f626373",
              "name": "ownerEmail",
              "value": "you@yourbusiness.com",
              "type": "string"
            },
            {
              "id": "7790d071-0e6f-4996-a03e-5d83dc801938",
              "name": "autoSend",
              "value": false,
              "type": "boolean"
            },
            {
              "id": "a1dfecd3-fa71-4838-a1ec-1d808c1163f2",
              "name": "contact",
              "value": "you@yourbusiness.com or (555) 555-0100",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "0bab3b68-2a7c-44da-a3e9-61f564c088eb",
      "name": "Settings (replies)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        260,
        570
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const b = $('New review (webhook)').first().json.body ?? $('New review (webhook)').first().json;\nconst s = $('Settings (replies)').first().json;\nreturn [{ json: {\n  reviewer: b.reviewer || b.name || 'A customer',\n  rating: Number(b.rating || 0),\n  text: String(b.text || b.comment || '').slice(0, 3000),\n  platform: b.platform || 'Google',\n  business: s.business, contact: s.contact,\n} }];"
      },
      "id": "bff4730f-bdc8-49f9-a551-ba8b6f045312",
      "name": "Shape the review",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        520,
        570
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Platform: {{ $json.platform }}\nReviewer: {{ $json.reviewer }}\nRating: {{ $json.rating }} out of 5\nReview: {{ $json.text }}",
        "hasOutputParser": false,
        "messages": {
          "messageValues": [
            {
              "message": "=You draft public replies to online reviews for {{ $json.business }}.\n\nRules:\n- Under 80 words. Calm, specific, human.\n- 4 or 5 stars: thank them and mention one detail they wrote.\n- 1 to 3 stars: apologize for the experience, do not argue, do not make excuses, and invite them to contact {{ $json.contact }} so it can be fixed.\n- Never admit legal fault. Never reveal private details about the customer or the job.\nReturn only JSON with the keys \"reply\" and \"flag\". Set \"flag\" to true if the review mentions an injury, a legal threat, discrimination or a safety problem. Otherwise false."
            }
          ]
        }
      },
      "id": "52e3d7c2-58a5-43d7-a95d-fb0b6bbfd4f2",
      "name": "Draft the public reply",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        780,
        570
      ]
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "value": "gpt-5-mini",
          "mode": "list",
          "cachedResultName": "gpt-5-mini"
        },
        "options": {}
      },
      "id": "8841a2cd-88ee-425e-af2c-6471167bd260",
      "name": "Draft the public reply model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.2,
      "position": [
        780,
        836.0000000000001
      ]
    },
    {
      "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 = [\"reply\",\"flag\"];\nif (ok && !needs.every((k) => out[k] !== undefined && out[k] !== null)) ok = false;\nconst base = $('Shape the review').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": "5ef6484d-1d20-4156-a7d1-ec0d9024fccc",
      "name": "Read the review reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1040,
        570
      ]
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "send",
        "sendTo": "={{ $('Settings (replies)').first().json.ownerEmail }}",
        "subject": "={{ ($json.flag ? '[CHECK FIRST] ' : '') + $json.rating + '-star review from ' + $json.reviewer }}",
        "emailType": "text",
        "message": "={{ 'They wrote:\\n' + $json.text + '\\n\\nDraft reply (copy it into ' + $json.platform + ' after you have read it):\\n\\n' + $json.reply + ($json.flag ? '\\n\\nThis review mentions something sensitive (an injury, a legal threat, discrimination or safety). Read it carefully before replying.' : '') }}",
        "options": {
          "appendAttribution": false
        }
      },
      "id": "260814f9-f3c0-41fc-a172-cc0b023ff9d4",
      "name": "Email me the draft",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1300,
        570
      ]
    }
  ],
  "connections": {
    "Write the request model": {
      "ai_languageModel": [
        [
          {
            "node": "Write the request",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Read the request": {
      "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 day at 10am": {
      "main": [
        [
          {
            "node": "Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings": {
      "main": [
        [
          {
            "node": "Read completed jobs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read completed jobs": {
      "main": [
        [
          {
            "node": "Pick jobs to ask",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick jobs to ask": {
      "main": [
        [
          {
            "node": "Write the request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write the request": {
      "main": [
        [
          {
            "node": "Read the request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send email": {
      "main": [
        [
          {
            "node": "Mark asked",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save as Gmail draft": {
      "main": [
        [
          {
            "node": "Mark asked",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Draft the public reply model": {
      "ai_languageModel": [
        [
          {
            "node": "Draft the public reply",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "New review (webhook)": {
      "main": [
        [
          {
            "node": "Settings (replies)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings (replies)": {
      "main": [
        [
          {
            "node": "Shape the review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Shape the review": {
      "main": [
        [
          {
            "node": "Draft the public reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Draft the public reply": {
      "main": [
        [
          {
            "node": "Read the review reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the review reply": {
      "main": [
        [
          {
            "node": "Email me the draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "tags": []
}
