This workflow follows the Error Trigger → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "review-reputation-autopilot",
"nodes": [
{
"id": "00000000-0000-0000-0000-000000000001",
"name": "\ud83d\udccb Documentation",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-900,
-500
],
"parameters": {
"width": 900,
"height": 620,
"color": 4,
"content": "# Review & Reputation Autopilot\n**Version:** 1.0.0 | **Author:** Otter Labs | **Trigger:** Webhook (job/order complete)\n\n## What This Workflow Does\n1. **Receives** a job/order completion event via webhook from your CRM or job management system\n2. **Waits** a configurable delay (default 24h) before reaching out\n3. **Sends** a review request via SMS (Twilio) or email (SendGrid) with a link to a micro-survey\n4. **Waits** for the customer to submit their star rating (1-5) via the micro-survey callback\n5. **Routes** based on rating:\n - **4-5 stars** \u2192 Sends Google/Yelp review links + Slack celebration notification\n - **1-3 stars** \u2192 Sends empathetic recovery message + Slack alert for save attempt\n\n## Required Credentials\n| Service | Type | Auth Detail |\n|---------|------|-------------|\n| Twilio | HTTP Basic Auth | Account SID (username) + Auth Token (password) |\n| SendGrid | HTTP Header Auth | name=Authorization, value=Bearer YOUR_API_KEY |\n| Slack | None (webhook URL) | Incoming Webhook URLs for review + save channels |\n\n## Micro-Survey Page\nThis workflow requires a hosted micro-survey page that:\n1. Displays \"How was your experience with {business}?\"\n2. Shows 5 clickable star buttons\n3. On click, POSTs `{ \"rating\": N }` to the `callback` URL from the query params\n4. Shows a thank-you confirmation\n\nThe callback URL is automatically generated per execution. Include a simple HTML page\nor use Typeform/Tally with a webhook.\n\n## Webhook Payload (Expected)\n```json\n{\n \"customerName\": \"John Smith\",\n \"customerPhone\": \"+15551234567\",\n \"customerEmail\": \"john@example.com\",\n \"jobId\": \"job_456\",\n \"jobType\": \"HVAC Repair\",\n \"technicianName\": \"Mike\"\n}\n```\nField names are flexible \u2014 see Normalize Data node for supported aliases.\n\n## Configuration\nSet all values in the **\u2699\ufe0f Config** node before activating.\nDo NOT hardcode values anywhere else."
}
},
{
"id": "00000000-0000-0000-0000-000000000002",
"name": "\ud83d\udd14 Phase 1: Trigger & Setup",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-240,
-400
],
"parameters": {
"width": 740,
"height": 180,
"color": 5,
"content": "## Phase 1 \u2014 Trigger & Setup\nWebhook receives the job completion event from your CRM (ServiceTitan, Jobber, Housecall Pro, or any system that can fire a POST). The Normalize node handles field name differences between CRMs so you don't have to rewrite the workflow per integration."
}
},
{
"id": "00000000-0000-0000-0000-000000000003",
"name": "\u23f0 Phase 2: Delay & Outreach",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
480,
-400
],
"parameters": {
"width": 980,
"height": 180,
"color": 6,
"content": "## Phase 2 \u2014 Delay & Outreach\nWaits the configured delay (default 24h \u2014 enough time for the customer to experience the result, not so long they forget). Then sends a review request via SMS or email depending on available contact info. The message includes a link to a micro-survey page with a unique callback URL so the workflow knows which execution to resume."
}
},
{
"id": "00000000-0000-0000-0000-000000000004",
"name": "\ud83d\udd00 Phase 3: Rating & Routing",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1680,
-800
],
"parameters": {
"width": 1300,
"height": 180,
"color": 7,
"content": "## Phase 3 \u2014 Rating Response & Routing\nWhen the customer submits their rating via the micro-survey, the workflow resumes. Ratings of 4-5 stars get redirected to Google/Yelp with a thank-you message \u2014 capitalizing on positive sentiment while it's fresh. Ratings of 1-3 stars trigger an empathetic recovery message and an immediate Slack alert so your team can attempt a save before the customer posts a negative public review."
}
},
{
"id": "00000000-0000-0000-0000-000000000005",
"name": "\ud83d\udd17 Job Complete",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
-240,
0
],
"parameters": {
"path": "job-complete",
"httpMethod": "POST",
"responseMode": "onReceived",
"options": {}
}
},
{
"id": "00000000-0000-0000-0000-000000000006",
"name": "\u2699\ufe0f Config",
"type": "n8n-nodes-base.set",
"typeVersion": 3.3,
"position": [
0,
0
],
"parameters": {
"mode": "manual",
"assignments": {
"assignments": [
{
"id": "cfg-01",
"name": "businessName",
"value": "YOUR_BUSINESS_NAME",
"type": "string"
},
{
"id": "cfg-02",
"name": "googleReviewUrl",
"value": "https://search.google.com/local/writereview?placeid=YOUR_GOOGLE_PLACE_ID",
"type": "string"
},
{
"id": "cfg-03",
"name": "yelpReviewUrl",
"value": "https://www.yelp.com/writeareview/biz/YOUR_YELP_BIZ_ID",
"type": "string"
},
{
"id": "cfg-04",
"name": "twilioAccountSid",
"value": "YOUR_TWILIO_ACCOUNT_SID",
"type": "string"
},
{
"id": "cfg-05",
"name": "twilioFromNumber",
"value": "+1XXXXXXXXXX",
"type": "string"
},
{
"id": "cfg-06",
"name": "sendgridFromEmail",
"value": "reviews@yourbusiness.com",
"type": "string"
},
{
"id": "cfg-07",
"name": "sendgridFromName",
"value": "YOUR_BUSINESS_NAME",
"type": "string"
},
{
"id": "cfg-08",
"name": "slackReviewChannelWebhook",
"value": "YOUR_SLACK_WEBHOOK_URL_FOR_REVIEWS",
"type": "string"
},
{
"id": "cfg-09",
"name": "slackSaveChannelWebhook",
"value": "YOUR_SLACK_WEBHOOK_URL_FOR_SAVES",
"type": "string"
},
{
"id": "cfg-10",
"name": "ratingThreshold",
"value": "4",
"type": "string"
},
{
"id": "cfg-11",
"name": "delayHours",
"value": "24",
"type": "string"
},
{
"id": "cfg-12",
"name": "n8nWebhookBaseUrl",
"value": "https://YOUR_N8N_INSTANCE.com",
"type": "string"
},
{
"id": "cfg-13",
"name": "microSurveyUrl",
"value": "https://YOUR_DOMAIN.com/rate",
"type": "string"
}
]
},
"options": {}
}
},
{
"id": "00000000-0000-0000-0000-000000000007",
"name": "\ud83d\udcdd Normalize Data",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
240,
0
],
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "// ============================================================\n// Normalize Job Completion Data\n// Supports common CRM field naming conventions so the workflow\n// works with ServiceTitan, Jobber, Housecall Pro, or any\n// system that can POST JSON.\n// ============================================================\n\nconst raw = $input.first().json;\nconst cfg = $('\u2699\ufe0f Config').first().json;\n\nconst normalized = {\n customerId: raw.customerId || raw.customer_id || raw.id || '',\n customerName: raw.customerName || raw.customer_name || raw.name ||\n `${raw.firstName || raw.first_name || ''} ${raw.lastName || raw.last_name || ''}`.trim() || 'Valued Customer',\n customerPhone: raw.customerPhone || raw.customer_phone || raw.phone || raw.mobile || '',\n customerEmail: raw.customerEmail || raw.customer_email || raw.email || '',\n jobId: raw.jobId || raw.job_id || raw.orderId || raw.order_id || raw.invoiceId || raw.invoice_id || '',\n jobType: raw.jobType || raw.job_type || raw.service || raw.serviceType || raw.service_type || '',\n technicianName: raw.technicianName || raw.technician_name || raw.assignee || raw.tech || '',\n completedAt: raw.completedAt || raw.completed_at || raw.completedDate || new Date().toISOString()\n};\n\nnormalized.hasPhone = !!normalized.customerPhone;\nnormalized.hasEmail = !!normalized.customerEmail;\nnormalized.channel = normalized.hasPhone ? 'sms' : 'email';\n\nif (!normalized.hasPhone && !normalized.hasEmail) {\n throw new Error(\n `No contact method for customer \"${normalized.customerName}\" (job ${normalized.jobId}). ` +\n `Webhook must include a phone number or email address.`\n );\n}\n\nconsole.log(`Job ${normalized.jobId} complete for ${normalized.customerName} \u2014 will contact via ${normalized.channel}`);\n\nreturn [{ json: normalized }];"
}
},
{
"id": "00000000-0000-0000-0000-000000000008",
"name": "\u23f3 Wait Before Sending",
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
480,
0
],
"parameters": {
"resume": "timeInterval",
"amount": "={{ parseInt($node['\u2699\ufe0f Config'].json.delayHours) || 24 }}",
"unit": "hours"
}
},
{
"id": "00000000-0000-0000-0000-000000000009",
"name": "\ud83d\udcdd Build Review Request",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
720,
0
],
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "// ============================================================\n// Build Review Request Messages (SMS + Email)\n// Constructs the micro-survey URL with a unique callback so\n// the workflow resumes when the customer submits their rating.\n// ============================================================\n\nconst customer = $('\ud83d\udcdd Normalize Data').first().json;\nconst cfg = $('\u2699\ufe0f Config').first().json;\n\n// Build the callback URL that the micro-survey page will POST to\nconst callbackUrl = `${cfg.n8nWebhookBaseUrl}/webhook-waiting/${$execution.id}`;\n\n// Build the survey URL the customer will click\nconst surveyUrl = `${cfg.microSurveyUrl}` +\n `?callback=${encodeURIComponent(callbackUrl)}` +\n `&business=${encodeURIComponent(cfg.businessName)}` +\n `&customer=${encodeURIComponent(customer.customerName)}`;\n\nconst firstName = customer.customerName.split(' ')[0];\n\n// --- SMS Body ---\nconst smsBody = `Hi ${firstName}! Thanks for choosing ${cfg.businessName}` +\n `${customer.jobType ? ` for your ${customer.jobType}` : ''}. ` +\n `We'd love your feedback \u2014 it only takes 10 seconds:\\n${surveyUrl}`;\n\n// --- Email ---\nconst emailSubject = `How was your experience with ${cfg.businessName}?`;\n\nconst emailHtml = [\n `<div style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 520px; margin: 0 auto; padding: 24px;\">`,\n `<p style=\"font-size: 16px; color: #1a1a1a;\">Hi ${firstName},</p>`,\n `<p style=\"font-size: 16px; color: #1a1a1a;\">Thank you for choosing <strong>${cfg.businessName}</strong>`,\n customer.jobType ? ` for your ${customer.jobType}` : '',\n `!</p>`,\n `<p style=\"font-size: 16px; color: #1a1a1a;\">We'd love to hear about your experience. It only takes 10 seconds:</p>`,\n `<p style=\"margin: 28px 0; text-align: center;\">`,\n `<a href=\"${surveyUrl}\" style=\"background: #2563eb; color: #ffffff; padding: 14px 32px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 16px;\">Rate Your Experience</a>`,\n `</p>`,\n `<p style=\"font-size: 14px; color: #6b7280;\">Your feedback helps us improve and helps other customers find us.</p>`,\n `<p style=\"font-size: 14px; color: #6b7280;\">Thanks,<br><strong>${cfg.businessName}</strong></p>`,\n `</div>`\n].join('\\n');\n\n// Pre-build SendGrid payload for the Email HTTP node\nconst sendgridPayload = {\n personalizations: [{ to: [{ email: customer.customerEmail }] }],\n from: { email: cfg.sendgridFromEmail, name: cfg.sendgridFromName || cfg.businessName },\n subject: emailSubject,\n content: [{ type: 'text/html', value: emailHtml }]\n};\n\nconsole.log(`Review request built for ${customer.customerName} \u2014 survey URL: ${surveyUrl}`);\n\nreturn [{\n json: {\n ...customer,\n surveyUrl,\n callbackUrl,\n smsBody,\n emailSubject,\n emailHtml,\n sendgridPayload,\n firstName\n }\n}];"
}
},
{
"id": "00000000-0000-0000-0000-000000000010",
"name": "\ud83d\udd00 Has Phone?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
960,
0
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "cond-has-phone",
"leftValue": "={{ $json.hasPhone }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true"
}
}
],
"combinator": "and"
},
"options": {}
}
},
{
"id": "00000000-0000-0000-0000-000000000011",
"name": "\ud83d\udcf1 SMS: Review Request",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1200,
-200
],
"parameters": {
"method": "POST",
"url": "=https://api.twilio.com/2010-04-01/Accounts/{{ $node['\u2699\ufe0f Config'].json.twilioAccountSid }}/Messages.json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendBody": true,
"contentType": "form-urlencoded",
"bodyParameters": {
"parameters": [
{
"name": "To",
"value": "={{ $json.customerPhone }}"
},
{
"name": "From",
"value": "={{ $node['\u2699\ufe0f Config'].json.twilioFromNumber }}"
},
{
"name": "Body",
"value": "={{ $json.smsBody }}"
}
]
},
"options": {
"response": {
"response": {
"responseFormat": "json",
"fullResponse": false
}
}
}
},
"credentials": {
"httpBasicAuth": {
"name": "<your credential>"
}
}
},
{
"id": "00000000-0000-0000-0000-000000000012",
"name": "\ud83d\udce7 Email: Review Request",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1200,
200
],
"parameters": {
"method": "POST",
"url": "https://api.sendgrid.com/v3/mail/send",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "json",
"jsonBody": "={{ JSON.stringify($json.sendgridPayload) }}",
"options": {
"response": {
"response": {
"responseFormat": "json",
"fullResponse": false
}
}
}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
}
},
{
"id": "00000000-0000-0000-0000-000000000013",
"name": "\u23f3 Wait for Rating",
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
1440,
0
],
"parameters": {
"resume": "webhook",
"options": {
"responseCode": 200
}
}
},
{
"id": "00000000-0000-0000-0000-000000000014",
"name": "\ud83d\udcdd Parse Rating",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1680,
0
],
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "// ============================================================\n// Parse Rating from Micro-Survey Callback\n// The Wait node outputs the webhook POST body from the\n// micro-survey page. We extract the rating and merge it\n// with the original customer data.\n// ============================================================\n\nconst ratingData = $input.first().json;\nconst customer = $('\ud83d\udcdd Normalize Data').first().json;\nconst cfg = $('\u2699\ufe0f Config').first().json;\n\n// Extract rating \u2014 support multiple field names\nconst rating = parseInt(\n ratingData.rating || ratingData.stars || ratingData.score || 0\n);\n\nconst comment = ratingData.comment || ratingData.feedback || ratingData.message || '';\n\nif (rating < 1 || rating > 5) {\n throw new Error(`Invalid rating received: ${rating}. Expected integer 1-5. Raw data: ${JSON.stringify(ratingData)}`);\n}\n\nconst threshold = parseInt(cfg.ratingThreshold) || 4;\nconst isPositive = rating >= threshold;\n\nconsole.log(`${customer.customerName} rated ${rating}/5 \u2014 ${isPositive ? 'POSITIVE \u2192 send review links' : 'NEGATIVE \u2192 save attempt'}`);\n\nreturn [{\n json: {\n rating,\n comment,\n isPositive,\n ...customer,\n ratedAt: new Date().toISOString()\n }\n}];"
}
},
{
"id": "00000000-0000-0000-0000-000000000015",
"name": "\u2b50 Rating \u2265 4?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1920,
0
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "cond-is-positive",
"leftValue": "={{ $json.isPositive }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true"
}
}
],
"combinator": "and"
},
"options": {}
}
},
{
"id": "00000000-0000-0000-0000-000000000016",
"name": "\ud83c\udf1f Build Redirect Message",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2160,
-400
],
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "// ============================================================\n// Build Positive Follow-Up\n// Thank the customer and send them to Google/Yelp to leave\n// a public review while their positive sentiment is fresh.\n// ============================================================\n\nconst data = $input.first().json;\nconst cfg = $('\u2699\ufe0f Config').first().json;\n\nconst firstName = data.customerName.split(' ')[0];\n\n// --- SMS ---\nconst smsBody = `Thank you for the ${data.rating}-star rating, ${firstName}! ` +\n `Would you mind sharing your experience? It really helps us:\\n\\n` +\n `Google: ${cfg.googleReviewUrl}\\n` +\n `Yelp: ${cfg.yelpReviewUrl}`;\n\n// --- Email ---\nconst emailSubject = `Thank you for your ${data.rating}-star rating!`;\nconst emailHtml = [\n `<div style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 520px; margin: 0 auto; padding: 24px;\">`,\n `<p style=\"font-size: 16px; color: #1a1a1a;\">Hi ${firstName},</p>`,\n `<p style=\"font-size: 16px; color: #1a1a1a;\">Thank you so much for the <strong>${data.rating}-star rating</strong>! We're thrilled you had a great experience.</p>`,\n `<p style=\"font-size: 16px; color: #1a1a1a;\">Would you mind taking 30 seconds to share on one of these platforms? It makes a huge difference for us:</p>`,\n `<p style=\"margin: 28px 0; text-align: center;\">`,\n `<a href=\"${cfg.googleReviewUrl}\" style=\"background: #4285f4; color: #ffffff; padding: 14px 24px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 14px; margin-right: 12px;\">Review on Google</a>`,\n `<a href=\"${cfg.yelpReviewUrl}\" style=\"background: #d32323; color: #ffffff; padding: 14px 24px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 14px;\">Review on Yelp</a>`,\n `</p>`,\n `<p style=\"font-size: 14px; color: #6b7280;\">Thanks again for choosing <strong>${cfg.businessName}</strong>!</p>`,\n `</div>`\n].join('\\n');\n\nconst sendgridPayload = {\n personalizations: [{ to: [{ email: data.customerEmail }] }],\n from: { email: cfg.sendgridFromEmail, name: cfg.sendgridFromName || cfg.businessName },\n subject: emailSubject,\n content: [{ type: 'text/html', value: emailHtml }]\n};\n\n// --- Slack ---\nconst stars = Array(data.rating).fill('\\u2b50').join('');\nconst slackMessage = `*${stars} ${data.rating}-Star Rating Received!*\\n\\n` +\n `*Customer:* ${data.customerName}\\n` +\n `*Job:* ${data.jobType || 'N/A'} (${data.jobId})\\n` +\n `*Rating:* ${stars}\\n` +\n (data.comment ? `*Comment:* ${data.comment}\\n` : '') +\n `\\n_Sent Google & Yelp review links._`;\n\nreturn [{\n json: {\n ...data,\n smsBody,\n emailSubject,\n emailHtml,\n sendgridPayload,\n slackMessage\n }\n}];"
}
},
{
"id": "00000000-0000-0000-0000-000000000017",
"name": "\ud83d\udd00 Redirect: Has Phone?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
2400,
-400
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "cond-redirect-phone",
"leftValue": "={{ $json.hasPhone }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true"
}
}
],
"combinator": "and"
},
"options": {}
}
},
{
"id": "00000000-0000-0000-0000-000000000018",
"name": "\ud83d\udcf1 SMS: Review Links",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2640,
-600
],
"parameters": {
"method": "POST",
"url": "=https://api.twilio.com/2010-04-01/Accounts/{{ $node['\u2699\ufe0f Config'].json.twilioAccountSid }}/Messages.json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendBody": true,
"contentType": "form-urlencoded",
"bodyParameters": {
"parameters": [
{
"name": "To",
"value": "={{ $json.customerPhone }}"
},
{
"name": "From",
"value": "={{ $node['\u2699\ufe0f Config'].json.twilioFromNumber }}"
},
{
"name": "Body",
"value": "={{ $json.smsBody }}"
}
]
},
"options": {
"response": {
"response": {
"responseFormat": "json",
"fullResponse": false
}
}
}
},
"credentials": {
"httpBasicAuth": {
"name": "<your credential>"
}
}
},
{
"id": "00000000-0000-0000-0000-000000000019",
"name": "\ud83d\udce7 Email: Review Links",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2640,
-200
],
"parameters": {
"method": "POST",
"url": "https://api.sendgrid.com/v3/mail/send",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "json",
"jsonBody": "={{ JSON.stringify($json.sendgridPayload) }}",
"options": {
"response": {
"response": {
"responseFormat": "json",
"fullResponse": false
}
}
}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
}
},
{
"id": "00000000-0000-0000-0000-000000000020",
"name": "\ud83d\udcac Slack: Great Review",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2880,
-400
],
"parameters": {
"method": "POST",
"url": "={{ $node['\u2699\ufe0f Config'].json.slackReviewChannelWebhook }}",
"sendBody": true,
"contentType": "json",
"jsonBody": "={{ JSON.stringify({ text: $json.slackMessage, unfurl_links: false }) }}",
"options": {}
}
},
{
"id": "00000000-0000-0000-0000-000000000021",
"name": "\ud83d\ude1f Build Recovery Message",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2160,
400
],
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "// ============================================================\n// Build Recovery Message for Low Ratings\n// Empathetic response that opens the door for resolution.\n// The goal: prevent a negative public review by showing\n// the customer you care and will make it right.\n// ============================================================\n\nconst data = $input.first().json;\nconst cfg = $('\u2699\ufe0f Config').first().json;\n\nconst firstName = data.customerName.split(' ')[0];\n\n// --- SMS ---\nconst smsBody = `Hi ${firstName}, thank you for your honest feedback. ` +\n `We're sorry your experience with ${cfg.businessName} wasn't what you expected. ` +\n `A member of our team will reach out to make things right.`;\n\n// --- Email ---\nconst emailSubject = `We want to make things right, ${firstName}`;\nconst emailHtml = [\n `<div style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 520px; margin: 0 auto; padding: 24px;\">`,\n `<p style=\"font-size: 16px; color: #1a1a1a;\">Hi ${firstName},</p>`,\n `<p style=\"font-size: 16px; color: #1a1a1a;\">Thank you for sharing your honest feedback about your experience with <strong>${cfg.businessName}</strong>.</p>`,\n `<p style=\"font-size: 16px; color: #1a1a1a;\">We're sorry we didn't meet your expectations`,\n data.jobType ? ` with your ${data.jobType}` : '',\n `. Your satisfaction matters to us, and a member of our team will be reaching out personally to make things right.</p>`,\n `<p style=\"font-size: 16px; color: #1a1a1a;\">If you'd like to reach us directly in the meantime, please reply to this email or call us.</p>`,\n `<p style=\"font-size: 14px; color: #6b7280;\">Thank you for giving us the chance to improve,<br><strong>${cfg.businessName}</strong></p>`,\n `</div>`\n].join('\\n');\n\nconst sendgridPayload = {\n personalizations: [{ to: [{ email: data.customerEmail }] }],\n from: { email: cfg.sendgridFromEmail, name: cfg.sendgridFromName || cfg.businessName },\n subject: emailSubject,\n content: [{ type: 'text/html', value: emailHtml }]\n};\n\n// --- Slack (urgent) ---\nconst stars = Array(data.rating).fill('\\u2b50').join('');\nconst slackMessage = `*\\ud83d\\udea8 Low Rating \u2014 Save Attempt Needed*\\n\\n` +\n `*Customer:* ${data.customerName}\\n` +\n `*Phone:* ${data.customerPhone || 'N/A'}\\n` +\n `*Email:* ${data.customerEmail || 'N/A'}\\n` +\n `*Job:* ${data.jobType || 'N/A'} (${data.jobId})\\n` +\n `*Rating:* ${stars} (${data.rating}/5)\\n` +\n (data.comment ? `*Comment:* \"${data.comment}\"\\n` : '') +\n `*Technician:* ${data.technicianName || 'N/A'}\\n` +\n `\\n_Recovery message sent. Follow up ASAP to prevent negative public review._`;\n\nreturn [{\n json: {\n ...data,\n smsBody,\n emailSubject,\n emailHtml,\n sendgridPayload,\n slackMessage\n }\n}];"
}
},
{
"id": "00000000-0000-0000-0000-000000000022",
"name": "\ud83d\udd00 Recovery: Has Phone?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
2400,
400
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "cond-recovery-phone",
"leftValue": "={{ $json.hasPhone }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true"
}
}
],
"combinator": "and"
},
"options": {}
}
},
{
"id": "00000000-0000-0000-0000-000000000023",
"name": "\ud83d\udcf1 SMS: Recovery",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2640,
200
],
"parameters": {
"method": "POST",
"url": "=https://api.twilio.com/2010-04-01/Accounts/{{ $node['\u2699\ufe0f Config'].json.twilioAccountSid }}/Messages.json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendBody": true,
"contentType": "form-urlencoded",
"bodyParameters": {
"parameters": [
{
"name": "To",
"value": "={{ $json.customerPhone }}"
},
{
"name": "From",
"value": "={{ $node['\u2699\ufe0f Config'].json.twilioFromNumber }}"
},
{
"name": "Body",
"value": "={{ $json.smsBody }}"
}
]
},
"options": {
"response": {
"response": {
"responseFormat": "json",
"fullResponse": false
}
}
}
},
"credentials": {
"httpBasicAuth": {
"name": "<your credential>"
}
}
},
{
"id": "00000000-0000-0000-0000-000000000024",
"name": "\ud83d\udce7 Email: Recovery",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2640,
600
],
"parameters": {
"method": "POST",
"url": "https://api.sendgrid.com/v3/mail/send",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "json",
"jsonBody": "={{ JSON.stringify($json.sendgridPayload) }}",
"options": {
"response": {
"response": {
"responseFormat": "json",
"fullResponse": false
}
}
}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
}
},
{
"id": "00000000-0000-0000-0000-000000000025",
"name": "\ud83d\udea8 Slack: Save Attempt",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2880,
400
],
"parameters": {
"method": "POST",
"url": "={{ $node['\u2699\ufe0f Config'].json.slackSaveChannelWebhook }}",
"sendBody": true,
"contentType": "json",
"jsonBody": "={{ JSON.stringify({ text: $json.slackMessage, unfurl_links: false }) }}",
"options": {}
}
},
{
"id": "00000000-0000-0000-0000-000000000026",
"name": "\ud83d\udea8 Error Trigger",
"type": "n8n-nodes-base.errorTrigger",
"typeVersion": 1,
"position": [
-240,
800
],
"parameters": {}
},
{
"id": "00000000-0000-0000-0000-000000000027",
"name": "\ud83d\udd34 Error Alert",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
0,
800
],
"parameters": {
"method": "POST",
"url": "={{ $node['\u2699\ufe0f Config'].json.slackSaveChannelWebhook }}",
"sendBody": true,
"contentType": "json",
"jsonBody": "={{ JSON.stringify({ text: `*\\ud83d\\udd34 Review Autopilot Failed*\\n\\n*Error:* ${$json.error?.message || 'Unknown error'}\\n*Node:* ${$json.execution?.lastNodeExecuted || 'Unknown'}\\n*Time:* ${new Date().toLocaleString()}\\n\\nCheck n8n for full execution details.`, unfurl_links: false }) }}",
"options": {}
}
}
],
"connections": {
"\ud83d\udd17 Job Complete": {
"main": [
[
{
"node": "\u2699\ufe0f Config",
"type": "main",
"index": 0
}
]
]
},
"\u2699\ufe0f Config": {
"main": [
[
{
"node": "\ud83d\udcdd Normalize Data",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udcdd Normalize Data": {
"main": [
[
{
"node": "\u23f3 Wait Before Sending",
"type": "main",
"index": 0
}
]
]
},
"\u23f3 Wait Before Sending": {
"main": [
[
{
"node": "\ud83d\udcdd Build Review Request",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udcdd Build Review Request": {
"main": [
[
{
"node": "\ud83d\udd00 Has Phone?",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udd00 Has Phone?": {
"main": [
[
{
"node": "\ud83d\udcf1 SMS: Review Request",
"type": "main",
"index": 0
}
],
[
{
"node": "\ud83d\udce7 Email: Review Request",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udcf1 SMS: Review Request": {
"main": [
[
{
"node": "\u23f3 Wait for Rating",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udce7 Email: Review Request": {
"main": [
[
{
"node": "\u23f3 Wait for Rating",
"type": "main",
"index": 0
}
]
]
},
"\u23f3 Wait for Rating": {
"main": [
[
{
"node": "\ud83d\udcdd Parse Rating",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udcdd Parse Rating": {
"main": [
[
{
"node": "\u2b50 Rating \u2265 4?",
"type": "main",
"index": 0
}
]
]
},
"\u2b50 Rating \u2265 4?": {
"main": [
[
{
"node": "\ud83c\udf1f Build Redirect Message",
"type": "main",
"index": 0
}
],
[
{
"node": "\ud83d\ude1f Build Recovery Message",
"type": "main",
"index": 0
}
]
]
},
"\ud83c\udf1f Build Redirect Message": {
"main": [
[
{
"node": "\ud83d\udd00 Redirect: Has Phone?",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udd00 Redirect: Has Phone?": {
"main": [
[
{
"node": "\ud83d\udcf1 SMS: Review Links",
"type": "main",
"index": 0
}
],
[
{
"node": "\ud83d\udce7 Email: Review Links",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udcf1 SMS: Review Links": {
"main": [
[
{
"node": "\ud83d\udcac Slack: Great Review",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udce7 Email: Review Links": {
"main": [
[
{
"node": "\ud83d\udcac Slack: Great Review",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\ude1f Build Recovery Message": {
"main": [
[
{
"node": "\ud83d\udd00 Recovery: Has Phone?",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udd00 Recovery: Has Phone?": {
"main": [
[
{
"node": "\ud83d\udcf1 SMS: Recovery",
"type": "main",
"index": 0
}
],
[
{
"node": "\ud83d\udce7 Email: Recovery",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udcf1 SMS: Recovery": {
"main": [
[
{
"node": "\ud83d\udea8 Slack: Save Attempt",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udce7 Email: Recovery": {
"main": [
[
{
"node": "\ud83d\udea8 Slack: Save Attempt",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udea8 Error Trigger": {
"main": [
[
{
"node": "\ud83d\udd34 Error Alert",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"saveManualExecutions": true,
"callerPolicy": "workflowsFromSameOwner",
"errorWorkflow": ""
},
"versionId": "1.0.0",
"meta": {
"templateCredsSetupCompleted": false
},
"staticData": null,
"tags": [
"review-management",
"reputation",
"twilio",
"sms",
"email",
"slack",
"sellable"
]
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
httpBasicAuthhttpHeaderAuth
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
review-reputation-autopilot. Uses httpRequest, errorTrigger. Webhook trigger; 27 nodes.
Source: https://github.com/jslizar/builder-lab/blob/dac901bc94368095e42e48185c9bb3f1c5c8e529/automations/n8n-workflows/review-reputation-autopilot.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
MLOps Pipeline EN-PT. Uses executeCommand, httpRequest, errorTrigger. Webhook trigger; 18 nodes.
Golden Sample: webhook → http → transform → respond (+error path). Uses httpRequest, errorTrigger, emailSend. Webhook trigger; 7 nodes.
This n8n template provides enterprise-level version control for your workflows using GitHub integration. Stop losing hours to broken workflows and manual exports – get proper commit history, visual di
This flow creates dummy files for every item added in your *Arrs (Radarr/Sonarr) with the tag .
This workflow receives webhook requests from a content calendar and uses the X API v2 to publish text posts, threads, image/video posts, and polls, as well as delete existing posts and run a credentia