{
  "name": "Five Star Review Engine",
  "nodes": [
    {
      "parameters": {
        "content": "## Five star review engine\n\nHappy customers get your Google review link. Unhappy ones reach you privately first.\n\n**Setup**\n1. Have your CRM or job software call the **Job Complete** URL when a job is done or an invoice is paid (fields: job_id, name, phone).\n2. Connect Twilio, Gmail and Google Sheets. Tab **Review Requests**: job_id, phone, name, status, rating, asked_at, reminded.\n3. Put your Google review link (Business Profile > Ask for reviews) and a private feedback form link in **Route Rating**.\n4. Point your Twilio number's messaging webhook at the **Rating Reply** URL.",
        "height": 400,
        "width": 440
      },
      "id": "1555bfc7-6716-4769-aa16-ef740c006fc3",
      "name": "Note 0",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -460,
        -240
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "job-complete",
        "options": {}
      },
      "id": "8401c060-a7c2-4b09-b026-af2f608b266f",
      "name": "Job Complete",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        0
      ],
      "webhookId": "3182cfa2-6895-4ca4-b411-7cf1095951ff"
    },
    {
      "parameters": {
        "jsCode": "// Normalise an Australian or international number to E.164 (+61...). Change DEFAULT_CC for your country.\nconst DEFAULT_CC = '61';\nfunction e164(raw) {\n  let d = String(raw || '').replace(/[^\\d+]/g, '');\n  if (d.startsWith('+')) return d;\n  if (d.startsWith('00')) return '+' + d.slice(2);\n  if (d.startsWith('0')) return '+' + DEFAULT_CC + d.slice(1);\n  return d ? '+' + d : '';\n}\nconst b = $json.body || $json;\nreturn [{ json: { job_id: String(b.job_id || b.id), name: String(b.name || '').split(' ')[0], phone: e164(b.phone) } }];",
        "mode": "runOnceForEachItem"
      },
      "id": "32012b7d-63e1-44ef-a1b8-bf88dad4fbab",
      "name": "Prepare Job",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Review Requests"
        },
        "filtersUI": {
          "values": [
            {
              "lookupColumn": "job_id",
              "lookupValue": "={{ $json.job_id }}"
            }
          ]
        },
        "options": {}
      },
      "id": "1bdcfaf8-171b-4a48-b581-aa122b342901",
      "name": "Already Asked?",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        440,
        0
      ],
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "b1e1cedc-d69a-4645-b5ec-2ab05643eed2",
              "leftValue": "={{ $json.job_id }}",
              "operator": {
                "type": "string",
                "operation": "empty",
                "singleValue": true
              },
              "rightValue": ""
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "f2132102-9228-4e0d-8c6f-8a2be94ff6eb",
      "name": "First Time?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        660,
        0
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Review Requests"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "job_id": "={{ $('Prepare Job').item.json.job_id }}",
            "phone": "={{ $('Prepare Job').item.json.phone }}",
            "name": "={{ $('Prepare Job').item.json.name }}",
            "status": "waiting",
            "asked_at": "",
            "reminded": "no"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "job_id",
              "displayName": "job_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "phone",
              "displayName": "phone",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "name",
              "displayName": "name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "displayName": "status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "asked_at",
              "displayName": "asked_at",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "reminded",
              "displayName": "reminded",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {}
      },
      "id": "f80e5cd9-f91e-4c5b-9ab2-f0c1d5d082d7",
      "name": "Log Request",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        880,
        -20
      ]
    },
    {
      "parameters": {
        "amount": 2,
        "unit": "hours"
      },
      "id": "d7082234-b94d-4a2b-87b3-e61d8c7aba82",
      "name": "Wait 2 Hours",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1100,
        -20
      ]
    },
    {
      "parameters": {
        "from": "YOUR_TWILIO_NUMBER",
        "to": "={{ $('Prepare Job').item.json.phone }}",
        "message": "=Hi {{ $('Prepare Job').item.json.name || 'there' }}, thanks for choosing YOUR BUSINESS NAME today. Quick one: how did we do, 1 to 5? Just reply with a number. Reply STOP to opt out.",
        "options": {}
      },
      "id": "2cf57e7a-cc63-4ab6-bd7c-086237a8d341",
      "name": "Ask for Rating",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [
        1320,
        -20
      ]
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Review Requests"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "job_id": "={{ $('Prepare Job').item.json.job_id }}",
            "status": "asked",
            "asked_at": "={{ $now.toISO() }}"
          },
          "matchingColumns": [
            "job_id"
          ],
          "schema": [
            {
              "id": "job_id",
              "displayName": "job_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "displayName": "status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "asked_at",
              "displayName": "asked_at",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {}
      },
      "id": "5af01339-748d-4fac-a1b3-a20dda681f20",
      "name": "Mark Asked",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1540,
        -20
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "review-reply",
        "options": {},
        "responseMode": "responseNode"
      },
      "id": "5f83d410-8da5-4616-9b91-ae129a3787d8",
      "name": "Rating Reply",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        400
      ],
      "webhookId": "6d73ee58-c007-406e-a900-cdc4612e9778"
    },
    {
      "parameters": {
        "respondWith": "text",
        "responseBody": "<Response></Response>",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/xml"
              }
            ]
          }
        }
      },
      "id": "bf1153b1-807b-4601-9bec-5e34929769e7",
      "name": "Empty Reply",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        220,
        300
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Review Requests"
        },
        "filtersUI": {
          "values": [
            {
              "lookupColumn": "phone",
              "lookupValue": "={{ $json.body.From }}"
            }
          ]
        },
        "options": {}
      },
      "id": "9e5c1d20-99a8-472e-9581-002c0a0dcc45",
      "name": "Find Request",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        220,
        480
      ],
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "const REVIEW_LINK = 'YOUR_GOOGLE_REVIEW_LINK';     // g.page/r/.../review\nconst FEEDBACK_LINK = 'YOUR_PRIVATE_FEEDBACK_FORM';\nconst body = $('Rating Reply').first().json.body;\nconst open = $input.all().map(i => i.json).filter(r => r.status === 'asked').pop();\nif (!open) return [];                               // not a rating reply we're waiting on\nconst m = String(body.Body).match(/[1-5]/);\nif (!m) return [];\nconst rating = Number(m[0]);\nreturn [{ json: { ...open, rating, happy: rating >= 4, link: rating >= 4 ? REVIEW_LINK : FEEDBACK_LINK } }];"
      },
      "id": "7b77d95c-333f-4cb7-82c0-90c8c90ca12f",
      "name": "Route Rating",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        480
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "f8a8c120-bd57-4d19-8711-a73eb60c9add",
              "leftValue": "={{ $json.happy }}",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "rightValue": ""
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "b1e54ea2-e137-4851-8081-f90afc4cb0d4",
      "name": "Happy?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        660,
        480
      ]
    },
    {
      "parameters": {
        "from": "YOUR_TWILIO_NUMBER",
        "to": "={{ $json.phone }}",
        "message": "=Thank you, that means a lot! Would you mind sharing that on Google? It takes 30 seconds: {{ $json.link }}",
        "options": {}
      },
      "id": "6a6558b5-a7ae-49c0-b1d3-149f7290a631",
      "name": "Send Review Link",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [
        880,
        400
      ]
    },
    {
      "parameters": {
        "from": "YOUR_TWILIO_NUMBER",
        "to": "={{ $json.phone }}",
        "message": "=Sorry we missed the mark. We'd really like to make it right. Could you tell us what happened? {{ $json.link }}",
        "options": {}
      },
      "id": "06312388-764a-46f8-babc-84274935a358",
      "name": "Send Feedback Form",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [
        880,
        580
      ]
    },
    {
      "parameters": {
        "sendTo": "YOUR_EMAIL",
        "subject": "=Unhappy customer: {{ $('Route Rating').item.json.name }} rated {{ $('Route Rating').item.json.rating }}/5",
        "emailType": "text",
        "message": "=Job {{ $('Route Rating').item.json.job_id }}, phone {{ $('Route Rating').item.json.phone }}. They've been sent the private feedback form. Worth a call today.",
        "options": {
          "appendAttribution": false
        }
      },
      "id": "7a3b33e8-ef5c-4afd-99b7-2f7bc7d91730",
      "name": "Alert Owner",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1100,
        580
      ]
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Review Requests"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "job_id": "={{ $('Route Rating').item.json.job_id }}",
            "rating": "={{ $('Route Rating').item.json.rating }}",
            "status": "={{ $('Route Rating').item.json.happy ? 'review link sent' : 'feedback requested' }}"
          },
          "matchingColumns": [
            "job_id"
          ],
          "schema": [
            {
              "id": "job_id",
              "displayName": "job_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "rating",
              "displayName": "rating",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "displayName": "status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {}
      },
      "id": "d8fce747-b97f-4e89-af81-93ed6d2583f8",
      "name": "Save Rating",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1320,
        480
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 10 * * *"
            }
          ]
        }
      },
      "id": "022153d0-335b-4458-ba4b-30a93b4302f8",
      "name": "Every Morning",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        820
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Review Requests"
        },
        "options": {}
      },
      "id": "2327b7c4-dbbc-42cc-8775-2a6bfc5c49b5",
      "name": "Get Requests",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        220,
        820
      ]
    },
    {
      "parameters": {
        "jsCode": "// Asked 3+ days ago, no reply, never reminded. Never ask twice for the same job.\nconst cutoff = Date.now() - 3 * 864e5;\nreturn $input.all().filter(i => i.json.status === 'asked' && i.json.reminded !== 'yes' && Date.parse(i.json.asked_at) < cutoff);"
      },
      "id": "32a07558-03e8-4a91-a90d-6917d974a04b",
      "name": "Needs Reminder",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        820
      ]
    },
    {
      "parameters": {
        "from": "YOUR_TWILIO_NUMBER",
        "to": "={{ $json.phone }}",
        "message": "=Hi {{ $json.name || 'there' }}, just a quick reminder: how did we go on your recent job, 1 to 5? Reply with a number. Thanks! Reply STOP to opt out.",
        "options": {}
      },
      "id": "e139c359-807e-40eb-8d84-33edc51503c6",
      "name": "Send Reminder",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [
        660,
        820
      ]
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Review Requests"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "job_id": "={{ $('Needs Reminder').item.json.job_id }}",
            "reminded": "yes"
          },
          "matchingColumns": [
            "job_id"
          ],
          "schema": [
            {
              "id": "job_id",
              "displayName": "job_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "reminded",
              "displayName": "reminded",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {}
      },
      "id": "ccb28c9b-246e-4e45-961b-63ef99081830",
      "name": "Mark Reminded",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        880,
        820
      ]
    }
  ],
  "connections": {
    "Job Complete": {
      "main": [
        [
          {
            "node": "Prepare Job",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Job": {
      "main": [
        [
          {
            "node": "Already Asked?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Already Asked?": {
      "main": [
        [
          {
            "node": "First Time?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "First Time?": {
      "main": [
        [
          {
            "node": "Log Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Request": {
      "main": [
        [
          {
            "node": "Wait 2 Hours",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 2 Hours": {
      "main": [
        [
          {
            "node": "Ask for Rating",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ask for Rating": {
      "main": [
        [
          {
            "node": "Mark Asked",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rating Reply": {
      "main": [
        [
          {
            "node": "Empty Reply",
            "type": "main",
            "index": 0
          },
          {
            "node": "Find Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Request": {
      "main": [
        [
          {
            "node": "Route Rating",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route Rating": {
      "main": [
        [
          {
            "node": "Happy?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Happy?": {
      "main": [
        [
          {
            "node": "Send Review Link",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Feedback Form",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Review Link": {
      "main": [
        [
          {
            "node": "Save Rating",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Feedback Form": {
      "main": [
        [
          {
            "node": "Alert Owner",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Alert Owner": {
      "main": [
        [
          {
            "node": "Save Rating",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every Morning": {
      "main": [
        [
          {
            "node": "Get Requests",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Requests": {
      "main": [
        [
          {
            "node": "Needs Reminder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Needs Reminder": {
      "main": [
        [
          {
            "node": "Send Reminder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Reminder": {
      "main": [
        [
          {
            "node": "Mark Reminded",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false
}