{
  "name": "Ask for a Google review after a completed job and route unhappy customers privately",
  "tags": [],
  "nodes": [
    {
      "parameters": {
        "content": "## Ask for a Google review after a completed job and route unhappy customers privately\n\n### How it works\n\n1. A finished job is posted to the webhook, and the customer is emailed two buttons: happy, or not.\n2. A happy customer is redirected straight to the Google review form, with no landing page in between.\n3. An unhappy customer sees a short thank-you page instead, and the owner is told privately.\n4. Anything other than an explicit good counts as unhappy, so a mangled link can never push someone to the public page.\n5. Every request and every answer is written to a sheet, so response rates can be read off it.\n\n### Setup steps\n\n- Create a sheet named Reviews with these headers in row 1: job_id, requested_at, customer_name, email, job_summary, rating, responded_at.\n- Set BASE_URL, the company name and the sender in Build Feedback Request.\n- Replace the placeholder in Send To Google Reviews with your review link, found in your Google Business Profile under Ask for reviews.\n- Connect Google Sheets, Gmail and Telegram credentials, and select job_id under Column to Match On in Record The Rating.\n- Post a finished job to the webhook with job_id, customer_name, email and job_summary.\n\n### Customization\n\nSwap Telegram for Slack, Gmail or Microsoft Teams. Send the request by SMS instead of email with a Twilio node. Add a delay so the request lands a day after the visit rather than the same afternoon. This routes attention, it does not hide anything: every customer keeps the same right to leave a public review, and the business simply hears about a bad job first.",
        "width": 480,
        "height": 896
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -800,
        -64
      ],
      "id": "sticky-0",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "content": "## Ask after the job\n\nTakes a finished job and emails the customer two one-click buttons.",
        "width": 944,
        "height": 272,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -240,
        -48
      ],
      "id": "sticky-1",
      "name": "Sticky Note1"
    },
    {
      "parameters": {
        "content": "## Log the request\n\nWrites the request to the sheet so unanswered ones stay visible.",
        "width": 464,
        "height": 288,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        768,
        -64
      ],
      "id": "sticky-2",
      "name": "Sticky Note2"
    },
    {
      "parameters": {
        "content": "## Read the click\n\nReceives the click and decides whether the customer was happy.",
        "width": 672,
        "height": 272,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -240,
        512
      ],
      "id": "sticky-3",
      "name": "Sticky Note3"
    },
    {
      "parameters": {
        "content": "## Send happy customers on\n\nRedirects straight to the Google review form, with nothing in between.",
        "width": 240,
        "height": 320,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        512,
        384
      ],
      "id": "sticky-4",
      "name": "Sticky Note4"
    },
    {
      "parameters": {
        "content": "## Keep unhappy ones private\n\nShows a thank-you page and tells the owner before this becomes a public review.",
        "width": 448,
        "height": 288,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        512,
        768
      ],
      "id": "sticky-5",
      "name": "Sticky Note5"
    },
    {
      "parameters": {
        "content": "## Record the rating\n\nWrites the answer back against the original row.",
        "width": 448,
        "height": 272,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1072,
        512
      ],
      "id": "sticky-6",
      "name": "Sticky Note6"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "job-completed",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook-completed",
      "name": "When Job Completed",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        -192,
        64
      ],
      "webhookId": "b7c31e58-2a94-4d16-83f5-60ea1d92b447"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\"status\":\"received\"}",
        "options": {}
      },
      "id": "respond-completed",
      "name": "Confirm Receipt",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        16,
        64
      ]
    },
    {
      "parameters": {
        "jsCode": "// Builds the two one-click links and the message that carries them. Sent as HTML\n// on purpose: in plain text, mail clients swallow the line break after a long URL\n// and drag the next characters into the link, breaking the second option.\n\n// ---------- EDIT THIS BLOCK ----------\nconst BASE_URL = 'https://YOUR-N8N-HOST';   // no trailing slash\nconst COMPANY = 'Your Company';\nconst SENDER = 'Alex';\n// -------------------------------------\n\nconst esc = (value) => String(value ?? '')\n  .replace(/&/g, '&amp;')\n  .replace(/</g, '&lt;')\n  .replace(/>/g, '&gt;')\n  .replace(/\"/g, '&quot;');\n\nreturn $input.all().map((item) => {\n  const body = item.json.body || item.json || {};\n\n  const job_id = String(body.job_id || '').trim();\n  const customer_name = String(body.customer_name || body.name || '').trim();\n  const email = String(body.email || '').trim();\n  const job_summary = String(body.job_summary || body.job || 'the work').trim();\n\n  const link = (rating) =>\n    `${BASE_URL}/webhook/job-feedback` +\n    `?job_id=${encodeURIComponent(job_id)}` +\n    `&amp;name=${encodeURIComponent(customer_name)}` +\n    `&amp;rating=${rating}`;\n\n  const button = (rating, label, background) =>\n    `<p style=\"margin:0 0 12px\">` +\n    `<a href=\"${link(rating)}\" ` +\n    `style=\"display:inline-block;padding:12px 18px;background:${background};color:#ffffff;` +\n    `text-decoration:none;border-radius:4px;font-size:15px\">${label}</a></p>`;\n\n  const message =\n    `<div style=\"font-family:system-ui,Arial,sans-serif;font-size:15px;line-height:1.6;color:#14201f\">` +\n    `<p>Hi ${esc(customer_name) || 'there'},</p>` +\n    `<p>We have finished ${esc(job_summary)}. How did it go?</p>` +\n    button('good', 'Happy with the work', '#1f6b58') +\n    button('bad', 'Something was not right', '#8a3324') +\n    `<p>One tap is all it takes, and it genuinely helps a small business.</p>` +\n    `<p>${esc(SENDER)}<br>${esc(COMPANY)}</p>` +\n    `</div>`;\n\n  return {\n    json: {\n      job_id,\n      customer_name,\n      email,\n      job_summary,\n      requested_at: new Date().toISOString(),\n      rating: '',\n      responded_at: '',\n      subject: `How did we do, ${customer_name || 'neighbour'}?`,\n      message,\n    },\n  };\n});"
      },
      "id": "code-request",
      "name": "Build Feedback Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        288,
        64
      ]
    },
    {
      "parameters": {
        "sendTo": "={{ $json.email }}",
        "subject": "={{ $json.subject }}",
        "emailType": "html",
        "message": "={{ $json.message }}",
        "options": {
          "appendAttribution": false
        }
      },
      "id": "gmail-ask",
      "name": "Ask For Feedback",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        560,
        64
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Only the columns the sheet holds. The subject and body stay out of it.\nconst src = $('Build Feedback Request').item.json;\n\nreturn {\n  json: {\n    job_id: src.job_id,\n    requested_at: src.requested_at,\n    customer_name: src.customer_name,\n    email: src.email,\n    job_summary: src.job_summary,\n    rating: '',\n    responded_at: '',\n  },\n};"
      },
      "id": "code-log-request",
      "name": "Build Request Row",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        816,
        64
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "REPLACE_WITH_YOUR_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Reviews",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "id": "sheets-append",
      "name": "Log Feedback Request",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1088,
        64
      ]
    },
    {
      "parameters": {
        "httpMethod": "GET",
        "path": "job-feedback",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook-feedback",
      "name": "When Feedback Clicked",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        -192,
        624
      ],
      "webhookId": "d92f4a07-6c18-4b53-92da-1f7e35c8b061"
    },
    {
      "parameters": {
        "jsCode": "// Reads the click. Anything that is not an explicit \"good\" counts as unhappy,\n// so a malformed link can never send someone to the public review page.\nreturn $input.all().map((item) => {\n  const query = item.json.query || {};\n\n  const esc = (v) => String(v ?? '')\n    .replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;');\n\n  const job_id = String(query.job_id || '').trim();\n  const customer_name = String(query.name || '').trim();\n\n  return {\n    json: {\n      job_id,\n      customer_name,\n      rating: query.rating === 'good' ? 'good' : 'bad',\n      responded_at: new Date().toISOString(),\n      alert: `Unhappy customer\\n\\nJob: ${esc(job_id)}\\nCustomer: ${esc(customer_name) || 'name not given'}\\n\\nCall them before this turns into a public review.`,\n    },\n  };\n});"
      },
      "id": "code-result",
      "name": "Read The Feedback",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        16,
        624
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "is-happy",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.rating }}",
              "rightValue": "good"
            }
          ]
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "id": "if-happy",
      "name": "Happy Customer?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        288,
        624
      ]
    },
    {
      "parameters": {
        "respondWith": "redirect",
        "redirectURL": "REPLACE_WITH_YOUR_GOOGLE_REVIEW_LINK",
        "options": {}
      },
      "id": "respond-redirect",
      "name": "Send To Google Reviews",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        560,
        544
      ]
    },
    {
      "parameters": {
        "respondWith": "text",
        "responseBody": "<!doctype html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Thank you</title></head><body style=\"font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1.5rem;line-height:1.6\"><h1 style=\"font-size:1.5rem\">Thank you for telling us</h1><p>Sorry this one missed the mark. The owner has been notified and will be in touch shortly to put it right.</p></body></html>",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/html; charset=utf-8"
              }
            ]
          }
        }
      },
      "id": "respond-thanks",
      "name": "Show Thank You Page",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        560,
        896
      ]
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "sendMessage",
        "chatId": "REPLACE_WITH_YOUR_CHAT_ID",
        "text": "={{ $json.alert }}",
        "additionalFields": {
          "appendAttribution": false,
          "parse_mode": "HTML"
        }
      },
      "id": "telegram-owner",
      "name": "Alert Owner Privately",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        816,
        896
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Writes the outcome back against the original row.\nconst src = $('Read The Feedback').item.json;\n\nreturn {\n  json: {\n    job_id: src.job_id,\n    rating: src.rating,\n    responded_at: src.responded_at,\n  },\n};"
      },
      "id": "code-log-result",
      "name": "Build Result Row",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1120,
        624
      ]
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "REPLACE_WITH_YOUR_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Reviews",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [
            "job_id"
          ],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "id": "sheets-result",
      "name": "Record The Rating",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1376,
        624
      ]
    }
  ],
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "When Job Completed": {
      "main": [
        [
          {
            "node": "Confirm Receipt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Confirm Receipt": {
      "main": [
        [
          {
            "node": "Build Feedback Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Feedback Request": {
      "main": [
        [
          {
            "node": "Ask For Feedback",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ask For Feedback": {
      "main": [
        [
          {
            "node": "Build Request Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Request Row": {
      "main": [
        [
          {
            "node": "Log Feedback Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Feedback Clicked": {
      "main": [
        [
          {
            "node": "Read The Feedback",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read The Feedback": {
      "main": [
        [
          {
            "node": "Happy Customer?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Happy Customer?": {
      "main": [
        [
          {
            "node": "Send To Google Reviews",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Show Thank You Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send To Google Reviews": {
      "main": [
        [
          {
            "node": "Build Result Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Show Thank You Page": {
      "main": [
        [
          {
            "node": "Alert Owner Privately",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Alert Owner Privately": {
      "main": [
        [
          {
            "node": "Build Result Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Result Row": {
      "main": [
        [
          {
            "node": "Record The Rating",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
