{
  "name": "Draft AI replies to Google reviews with Gmail, OpenAI and Google Sheets",
  "nodes": [
    {
      "id": "f1a0s001-0000-4000-8000-000000000001",
      "name": "Sticky Note \u2014 read me first",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -800,
        -40
      ],
      "parameters": {
        "width": 700,
        "height": 760,
        "content": "## Draft AI replies to Google reviews \u2014 you stay in control\n\n### How it works\nGoogle emails you every time your business gets a new review. This workflow watches your Gmail for those alerts, has AI write a reply draft that follows Google's reply-moderation rules (specific, no links, no promo language), then emails the draft to you. You read it, tweak it if you like, and paste it on your Google reviews page. **Nothing is ever posted automatically** \u2014 the draft lands in your inbox, and a ledger row lands in Google Sheets so you always know what was handled.\n\n### Setup\n1. Connect **Gmail** and **Google Sheets** (both work with n8n's one-click Google sign-in).\n2. Add your **OpenAI API key** to the model node.\n3. Create a Google Sheet with headers: `processed_at, gmail_message_id, rating, reviewer, review_excerpt, reply_draft, status` and select it in both Sheets nodes.\n4. Fill in the **\u2699\ufe0f Your settings** node: business name, your email, tone, contact channel.\n5. Activate. Send yourself a test by re-forwarding an old review alert.\n\n### Customization tips\nAdjust tone and sign-off in \u2699\ufe0f Your settings. Swap OpenAI for Gemini or Claude by replacing the model node."
      },
      "typeVersion": 1
    },
    {
      "id": "f1a0s002-0000-4000-8000-000000000002",
      "name": "Sticky Note \u2014 section 1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -60,
        -40
      ],
      "parameters": {
        "color": 7,
        "width": 1000,
        "height": 760,
        "content": "## 1. Catch the alert\nGmail trigger fires on Google's \"new review\" notification emails. The Sheets lookup + IF guard make re-runs safe: a review that is already in the ledger is never processed twice (no duplicate drafts in your inbox)."
      },
      "typeVersion": 1
    },
    {
      "id": "f1a0s003-0000-4000-8000-000000000003",
      "name": "Sticky Note \u2014 section 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        980,
        -40
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 760,
        "content": "## 2. Think\nOne AI call extracts the rating, reviewer name and review text from the alert email, then drafts a reply that follows Google's moderation rules: references a concrete detail, no links, no promotions, apologetic-but-not-liable for 1\u20132\u2605. Uses your business name, tone and contact channel from \u2699\ufe0f Your settings."
      },
      "typeVersion": 1
    },
    {
      "id": "f1a0s004-0000-4000-8000-000000000004",
      "name": "Sticky Note \u2014 section 3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1500,
        -40
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 760,
        "content": "## 3. Deliver + log\nThe draft is emailed to you with a one-click link to your Google reviews page, and the ledger row is appended to Google Sheets \u2014 so you can see every review, every draft, and what you did with it."
      },
      "typeVersion": 1
    },
    {
      "id": "f1a00001-0000-4000-8000-00000000000a",
      "name": "Watch Gmail for new review alerts",
      "type": "n8n-nodes-base.gmailTrigger",
      "position": [
        0,
        320
      ],
      "parameters": {
        "simple": false,
        "filters": {
          "q": "from:user@example.com subject:(review)"
        },
        "options": {}
      },
      "typeVersion": 1.2
    },
    {
      "id": "f1a00002-0000-4000-8000-00000000000b",
      "name": "\u2699\ufe0f Your settings",
      "type": "n8n-nodes-base.set",
      "position": [
        200,
        320
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "businessName",
              "type": "string",
              "value": "YOUR BUSINESS NAME"
            },
            {
              "id": "a2",
              "name": "ownerEmail",
              "type": "string",
              "value": "you@example.com"
            },
            {
              "id": "a3",
              "name": "brandTone",
              "type": "string",
              "value": "warm, professional, first-person plural (\"we\")"
            },
            {
              "id": "a4",
              "name": "contactChannel",
              "type": "string",
              "value": "call us at (555) 000-0000 or email hello@example.com"
            },
            {
              "id": "a5",
              "name": "signOff",
              "type": "string",
              "value": "\u2014 The YOUR BUSINESS NAME team"
            },
            {
              "id": "a6",
              "name": "replyPageUrl",
              "type": "string",
              "value": "https://business.google.com/reviews"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "f1a00003-0000-4000-8000-00000000000c",
      "name": "Check the ledger (already handled?)",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        400,
        320
      ],
      "parameters": {
        "options": {},
        "filtersUI": {
          "values": [
            {
              "lookupValue": "={{ $('Watch Gmail for new review alerts').item.json.id }}",
              "lookupColumn": "gmail_message_id"
            }
          ]
        },
        "operation": "lookup",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "typeVersion": 4.5,
      "alwaysOutputData": true
    },
    {
      "id": "f1a00004-0000-4000-8000-00000000000d",
      "name": "Only continue if it is new",
      "type": "n8n-nodes-base.if",
      "position": [
        600,
        320
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "c1",
              "operator": {
                "type": "string",
                "operation": "empty",
                "singleValue": true
              },
              "leftValue": "={{ $json.gmail_message_id || '' }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "f1a00005-0000-4000-8000-00000000000e",
      "name": "Already handled \u2014 stop",
      "type": "n8n-nodes-base.noOp",
      "position": [
        800,
        500
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "f1a00006-0000-4000-8000-00000000000f",
      "name": "Draft the reply (AI)",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "maxTries": 3,
      "position": [
        1100,
        304
      ],
      "parameters": {
        "text": "=You are drafting a public reply to a Google review on behalf of {{ $('\u2699\ufe0f Your settings').item.json.businessName }}.\n\nBelow is the full text of a Google Business Profile \"new review\" notification email. First extract: the star rating (1-5), the reviewer's first name, and the review text. Then write reply_draft following ALL rules.\n\nRules for the reply:\n- Tone: {{ $('\u2699\ufe0f Your settings').item.json.brandTone }}. Maximum 110 words.\n- Address the reviewer by first name only (privacy: never use a full name).\n- Reference one concrete detail from their review. Never write a reply that could fit any review \u2014 generic AI boilerplate gets rejected by Google's reply moderation.\n- Never include: links, promotions, discounts, upsells, keyword stuffing, or requests to change the rating (these also get rejected).\n- 4-5 stars: thank them warmly and specifically.\n- 3 stars: thank them, acknowledge the specific gap they mention, state one concrete improvement.\n- 1-2 stars: apologize sincerely without admitting legal fault, address their specific issue, invite them to continue the conversation offline: {{ $('\u2699\ufe0f Your settings').item.json.contactChannel }}.\n- End with: {{ $('\u2699\ufe0f Your settings').item.json.signOff }}\n\nNOTIFICATION EMAIL:\n{{ $('Watch Gmail for new review alerts').item.json.text || $('Watch Gmail for new review alerts').item.json.snippet }}",
        "promptType": "define",
        "hasOutputParser": true
      },
      "retryOnFail": true,
      "typeVersion": 1.5,
      "waitBetweenTries": 5000
    },
    {
      "id": "f1a00007-0000-4000-8000-000000000010",
      "name": "OpenAI model (bring your own key)",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        1080,
        560
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini"
        },
        "options": {
          "temperature": 0.4
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "f1a00008-0000-4000-8000-000000000011",
      "name": "Force clean JSON output",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        1280,
        560
      ],
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"rating\": { \"type\": \"number\", \"description\": \"Star rating 1-5\" },\n    \"reviewer_first_name\": { \"type\": \"string\" },\n    \"review_excerpt\": { \"type\": \"string\", \"description\": \"The review text, max 200 chars\" },\n    \"reply_draft\": { \"type\": \"string\" }\n  },\n  \"required\": [\"rating\", \"reviewer_first_name\", \"review_excerpt\", \"reply_draft\"]\n}"
      },
      "typeVersion": 1.2
    },
    {
      "id": "f1a00009-0000-4000-8000-000000000012",
      "name": "Email the draft to the owner",
      "type": "n8n-nodes-base.gmail",
      "maxTries": 3,
      "position": [
        1560,
        304
      ],
      "parameters": {
        "sendTo": "={{ $('\u2699\ufe0f Your settings').item.json.ownerEmail }}",
        "message": "=<div style=\"font-family:Arial,sans-serif;max-width:560px\">\n<p><b>{{ $json.output.reviewer_first_name }}</b> left a <b>{{ $json.output.rating }}\u2605</b> review:</p>\n<blockquote style=\"border-left:3px solid #f5a24b;margin:0;padding:8px 14px;color:#555\">{{ $json.output.review_excerpt }}</blockquote>\n<p><b>Suggested reply</b> (copy, tweak if you like, then paste it on Google):</p>\n<div style=\"background:#f7f3ec;border-radius:8px;padding:14px 16px;white-space:pre-wrap\">{{ $json.output.reply_draft }}</div>\n<p style=\"margin-top:18px\"><a href=\"{{ $('\u2699\ufe0f Your settings').item.json.replyPageUrl }}\" style=\"background:#1a73e8;color:#fff;padding:10px 18px;border-radius:6px;text-decoration:none\">Open my Google reviews page \u2192</a></p>\n<p style=\"color:#888;font-size:12px\">Drafted automatically by your n8n workflow. Nothing is ever posted without you.</p>\n</div>",
        "options": {},
        "subject": "=\u270d\ufe0f Reply draft: {{ $json.output.rating }}\u2605 review from {{ $json.output.reviewer_first_name }} \u2014 {{ $('\u2699\ufe0f Your settings').item.json.businessName }}"
      },
      "retryOnFail": true,
      "typeVersion": 2.1,
      "waitBetweenTries": 5000
    },
    {
      "id": "f1a00010-0000-4000-8000-000000000013",
      "name": "Log to the reply ledger",
      "type": "n8n-nodes-base.googleSheets",
      "maxTries": 3,
      "position": [
        1760,
        304
      ],
      "parameters": {
        "columns": {
          "value": {
            "rating": "={{ $('Draft the reply (AI)').item.json.output.rating }}",
            "status": "draft-sent-to-owner",
            "reviewer": "={{ $('Draft the reply (AI)').item.json.output.reviewer_first_name }}",
            "reply_draft": "={{ $('Draft the reply (AI)').item.json.output.reply_draft }}",
            "processed_at": "={{ $now.toISO() }}",
            "review_excerpt": "={{ $('Draft the reply (AI)').item.json.output.review_excerpt }}",
            "gmail_message_id": "={{ $('Watch Gmail for new review alerts').item.json.id }}"
          },
          "mappingMode": "defineBelow"
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.5,
      "waitBetweenTries": 5000
    }
  ],
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Draft the reply (AI)": {
      "main": [
        [
          {
            "node": "Email the draft to the owner",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u2699\ufe0f Your settings": {
      "main": [
        [
          {
            "node": "Check the ledger (already handled?)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Force clean JSON output": {
      "ai_outputParser": [
        [
          {
            "node": "Draft the reply (AI)",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Only continue if it is new": {
      "main": [
        [
          {
            "node": "Draft the reply (AI)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Already handled \u2014 stop",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email the draft to the owner": {
      "main": [
        [
          {
            "node": "Log to the reply ledger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI model (bring your own key)": {
      "ai_languageModel": [
        [
          {
            "node": "Draft the reply (AI)",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Watch Gmail for new review alerts": {
      "main": [
        [
          {
            "node": "\u2699\ufe0f Your settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check the ledger (already handled?)": {
      "main": [
        [
          {
            "node": "Only continue if it is new",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}