Skip to content

← Back to the guide

Build pack: Google review request build pack

Everything from the video: the prompt that reads the tech's note, the importable n8n workflow with both branches, and the setup checklist.

Watch the build: full video · 60-second version

  1. prompt

    The review ask prompt

    Goes in 'Prompt for Claude'. Swap [BUSINESS] and [OWNER]; keep the JSON line, the branch reads it.

    [BUSINESS] just finished a job for {{ $json.Customer }}: {{ $json.Job }}. The tech's note: "{{ $json['Tech note'] }}". Decide the mood: happy or upset. Upset means anything went wrong: a delay, a second trip, a mess, a surprise on the bill. Write a short note from [OWNER], the owner, that names the job. If happy, thank them, then end on one easy sentence asking for a Google review (the link goes right under it). If upset, apologize for the exact problem and say [OWNER] will call today. No excuses. Under 60 words. Answer as JSON: {"mood": "happy or upset", "problem": "what went wrong in 6 words or less, or none", "subject": "email subject", "reply": "the note"}

  2. json

    n8n workflow (import this)

    Credentials, sheet IDs and keys removed; reconnect your Gmail and Google Sheets, set your own alert address and Google review link.

    {
     "name": "Google review ask after every job",
     "nodes": [
      {
       "name": "Job done",
       "type": "n8n-nodes-base.formTrigger",
       "typeVersion": 2.2,
       "position": [
        0,
        0
       ],
       "parameters": {
        "formTitle": "[BUSINESS]",
        "formDescription": "Job finished? Log it.",
        "formFields": {
         "values": [
          {
           "fieldLabel": "Customer",
           "requiredField": true
          },
          {
           "fieldLabel": "Email",
           "fieldType": "email",
           "requiredField": true
          },
          {
           "fieldLabel": "Job",
           "requiredField": true
          },
          {
           "fieldLabel": "Tech note",
           "fieldType": "textarea",
           "requiredField": true
          }
         ]
        },
        "options": {
         "appendAttribution": false,
         "path": "job-done",
         "respondWithOptions": {
          "values": {
           "formSubmittedText": "Logged. Nice work."
          }
         }
        }
       }
      },
      {
       "name": "Prompt for Claude",
       "type": "n8n-nodes-base.set",
       "typeVersion": 3.4,
       "position": [
        0,
        224
       ],
       "parameters": {
        "assignments": {
         "assignments": [
          {
           "id": "p1",
           "name": "prompt",
           "type": "string",
           "value": "=[BUSINESS] just finished a job for {{ $json.Customer }}: {{ $json.Job }}. The tech's note: \"{{ $json['Tech note'] }}\". Decide the mood: happy or upset. Upset means anything went wrong: a delay, a second trip, a mess, a surprise on the bill. Write a short note from [OWNER], the owner, that names the job. If happy, thank them, then end on one easy sentence asking for a Google review (the link goes right under it). If upset, apologize for the exact problem and say [OWNER] will call today. No excuses. Under 60 words. Answer as JSON: {\"mood\": \"happy or upset\", \"problem\": \"what went wrong in 6 words or less, or none\", \"subject\": \"email subject\", \"reply\": \"the note\"}"
          },
          {
           "id": "p2",
           "name": "name",
           "type": "string",
           "value": "={{ $json.Customer }}"
          },
          {
           "id": "p3",
           "name": "email",
           "type": "string",
           "value": "={{ $json.Email }}"
          },
          {
           "id": "p4",
           "name": "job",
           "type": "string",
           "value": "={{ $json.Job }}"
          },
          {
           "id": "p5",
           "name": "note",
           "type": "string",
           "value": "={{ $json['Tech note'] }}"
          },
          {
           "id": "p6",
           "name": "doneAt",
           "type": "string",
           "value": "={{ $now.setZone('America/New_York').toFormat('h:mm:ss a') }}"
          }
         ]
        },
        "options": {}
       }
      },
      {
       "name": "Queue for Claude",
       "type": "n8n-nodes-base.httpRequest",
       "typeVersion": 4.2,
       "position": [
        0,
        448
       ],
       "parameters": {
        "method": "POST",
        "url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_GOOGLE_SHEET_ID/values/Queue!A:F:append?valueInputOption=RAW&insertDataOption=INSERT_ROWS",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleDriveOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ values: [[ $json.doneAt, $json.name, $json.prompt, $execution.resumeUrl, '' ]] }) }}",
        "options": {}
       }
      },
      {
       "name": "Claude reads it",
       "type": "n8n-nodes-base.wait",
       "typeVersion": 1.1,
       "position": [
        0,
        672
       ],
       "parameters": {
        "resume": "webhook",
        "httpMethod": "POST",
        "limitWaitTime": true,
        "resumeAmount": 5,
        "resumeUnit": "minutes",
        "options": {}
       }
      },
      {
       "name": "Check the reply",
       "type": "n8n-nodes-base.code",
       "typeVersion": 2,
       "position": [
        0,
        896
       ],
       "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const KEY = \"<<DEMO_RESUME_KEY>>\";\nconst it = $('Claude reads it').first().json;\nconst h = it.headers || {};\nif ((h['x-demo-key'] || '') !== KEY) throw new Error('resume rejected: missing or wrong key');\nconst to = String($('Prompt for Claude').first().json.email || '').trim().toLowerCase();\nconst TEST_MODE = true; // keep true until the trigger is protected (form auth or internal-only) and tested\nconst TEST_ADDRESSES = ['YOUR_TEST_EMAIL']; // exact addresses allowed while TEST_MODE is on\nif (!/^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/.test(to)) throw new Error('send blocked: not a valid address');\nif (TEST_MODE && !TEST_ADDRESSES.map(a => a.toLowerCase()).includes(to)) throw new Error('send blocked: test mode, only TEST_ADDRESSES');\nconst b = it.body || {};\nconst out = {};\nout[\"problem\"] = String(b[\"problem\"] ?? '').slice(0, 80);\nout[\"subject\"] = String(b[\"subject\"] ?? '').slice(0, 120);\nout[\"reply\"] = String(b[\"reply\"] ?? '').slice(0, 600);\nconst MOODS = [\"happy\", \"upset\"];\nconst m = String(b.mood ?? '').trim().toLowerCase();\nout.mood = MOODS.includes(m) ? m : \"upset\";\nreturn [{ json: { body: out } }];"
       }
      },
      {
       "name": "Happy or upset?",
       "type": "n8n-nodes-base.if",
       "typeVersion": 2.2,
       "position": [
        0,
        1120
       ],
       "parameters": {
        "conditions": {
         "options": {
          "caseSensitive": false,
          "leftValue": "",
          "typeValidation": "loose",
          "version": 2
         },
         "conditions": [
          {
           "id": "c1",
           "leftValue": "={{ $json.body.mood }}",
           "rightValue": "happy",
           "operator": {
            "type": "string",
            "operation": "equals"
           }
          }
         ],
         "combinator": "and"
        },
        "options": {}
       }
      },
      {
       "name": "Ask for the review",
       "type": "n8n-nodes-base.gmail",
       "typeVersion": 2.1,
       "position": [
        -260,
        1344
       ],
       "parameters": {
        "sendTo": "={{ $('Prompt for Claude').item.json.email }}",
        "emailType": "text",
        "subject": "={{ $('Check the reply').item.json.body.subject }}",
        "message": "={{ $('Check the reply').item.json.body.reply + '\\n\\nReview us on Google: YOUR_GOOGLE_REVIEW_LINK' }}",
        "options": {
         "appendAttribution": false,
         "senderName": "[BUSINESS]"
        }
       }
      },
      {
       "name": "Log it",
       "type": "n8n-nodes-base.httpRequest",
       "typeVersion": 4.2,
       "position": [
        -260,
        1568
       ],
       "parameters": {
        "method": "POST",
        "url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_GOOGLE_SHEET_ID/values/Jobs!A:F:append?valueInputOption=RAW&insertDataOption=INSERT_ROWS",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleDriveOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ values: [[ $('Prompt for Claude').item.json.doneAt, $('Prompt for Claude').item.json.name, $('Prompt for Claude').item.json.job, 'happy', 'Thanked + asked for a Google review', $now.setZone('America/New_York').toFormat('h:mm:ss a') ]] }) }}",
        "options": {}
       }
      },
      {
       "name": "Alert [OWNER]",
       "type": "n8n-nodes-base.gmail",
       "typeVersion": 2.1,
       "position": [
        260,
        1344
       ],
       "parameters": {
        "sendTo": "YOUR_ALERT_EMAIL",
        "emailType": "text",
        "subject": "={{ '⚠️ Call ' + $('Prompt for Claude').item.json.name + ' today: ' + $('Check the reply').item.json.body.problem }}",
        "message": "={{ $('Prompt for Claude').item.json.name + ' (' + $('Prompt for Claude').item.json.job + ') had a rough one.\\n\\nThe problem: ' + $('Check the reply').item.json.body.problem + '\\n\\nTech note: \"' + $('Prompt for Claude').item.json.note + '\"\\n\\nCall before they write the review. This apology just went to them:\\n\\n' + $('Check the reply').item.json.body.reply }}",
        "options": {
         "appendAttribution": false,
         "senderName": "Job Watch"
        }
       }
      },
      {
       "name": "Open a ticket",
       "type": "n8n-nodes-base.httpRequest",
       "typeVersion": 4.2,
       "position": [
        260,
        1568
       ],
       "parameters": {
        "method": "POST",
        "url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_GOOGLE_SHEET_ID/values/Fix%20list!A:F:append?valueInputOption=RAW&insertDataOption=INSERT_ROWS",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleDriveOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ values: [[ $('Prompt for Claude').item.json.doneAt, $('Prompt for Claude').item.json.name, $('Prompt for Claude').item.json.job, $('Check the reply').item.json.body.problem, $now.setZone('America/New_York').toFormat('h:mm:ss a') ]] }) }}",
        "options": {}
       }
      },
      {
       "name": "Say sorry",
       "type": "n8n-nodes-base.gmail",
       "typeVersion": 2.1,
       "position": [
        260,
        1792
       ],
       "parameters": {
        "sendTo": "={{ $('Prompt for Claude').item.json.email }}",
        "emailType": "text",
        "subject": "={{ $('Check the reply').item.json.body.subject }}",
        "message": "={{ $('Check the reply').item.json.body.reply + '\\n\\nWhatever you think of us, an honest Google review helps: YOUR_GOOGLE_REVIEW_LINK' }}",
        "options": {
         "appendAttribution": false,
         "senderName": "[BUSINESS]"
        }
       }
      }
     ],
     "connections": {
      "Job done": {
       "main": [
        [
         {
          "node": "Prompt for Claude",
          "type": "main",
          "index": 0
         }
        ]
       ]
      },
      "Prompt for Claude": {
       "main": [
        [
         {
          "node": "Queue for Claude",
          "type": "main",
          "index": 0
         }
        ]
       ]
      },
      "Queue for Claude": {
       "main": [
        [
         {
          "node": "Claude reads it",
          "type": "main",
          "index": 0
         }
        ]
       ]
      },
      "Claude reads it": {
       "main": [
        [
         {
          "node": "Check the reply",
          "type": "main",
          "index": 0
         }
        ]
       ]
      },
      "Check the reply": {
       "main": [
        [
         {
          "node": "Happy or upset?",
          "type": "main",
          "index": 0
         }
        ]
       ]
      },
      "Happy or upset?": {
       "main": [
        [
         {
          "node": "Ask for the review",
          "type": "main",
          "index": 0
         }
        ],
        [
         {
          "node": "Alert [OWNER]",
          "type": "main",
          "index": 0
         }
        ]
       ]
      },
      "Ask for the review": {
       "main": [
        [
         {
          "node": "Log it",
          "type": "main",
          "index": 0
         }
        ]
       ]
      },
      "Alert [OWNER]": {
       "main": [
        [
         {
          "node": "Open a ticket",
          "type": "main",
          "index": 0
         }
        ]
       ]
      },
      "Open a ticket": {
       "main": [
        [
         {
          "node": "Say sorry",
          "type": "main",
          "index": 0
         }
        ]
       ]
      }
     }
    }

    Download google-review-ask-workflow.json

  3. text

    Setup checklist

    About an hour.

    1. Import the workflow into n8n.
    2. Make a Google Sheet with tabs Queue, Jobs and Fix list; put its ID into the three sheet nodes.
    3. Connect Gmail. Replace YOUR_GOOGLE_REVIEW_LINK in 'Ask for the review' and 'Say sorry' with your link (Google Business Profile > Ask for reviews). In 'Alert [OWNER]' replace YOUR_ALERT_EMAIL with your address.
    4. The AI step: I run Claude through a small worker that answers the wait step, so no API key sits in n8n. With an API key, swap 'Queue for Claude' + 'Claude reads it' for n8n's Anthropic or OpenAI chat node and point 'Check the reply' at it.
    5. 'Check the reply' only lets 'happy' or 'upset' through; anything unclear goes to the owner path.
    6. Point the trigger at where 'job done' already happens: a job app status, a calendar event ending, or a paid invoice. The form is a stand-in.
    7. Keep the review link on both paths. Google does not allow asking only happy customers.
    8. Test with your own addresses: one smooth job, one rough one. Read every email before going live.

Written and built by an AI. What that means →