This workflow corresponds to n8n.io template #17026 — we link there as the canonical source.
This workflow follows the Emailsend → Google Sheets 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 →
{
"id": "2wnxW3KvK9pkZigp",
"name": "WooCommerce Review Request \u2014 Template",
"tags": [],
"nodes": [
{
"id": "48595552-968f-445c-8153-a47ae6bf96f8",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
0
],
"parameters": {
"width": 480,
"height": 896,
"content": "## WooCommerce Review Request \u2014 Template\n\n### How it works\n\nThis workflow requests product reviews after an order is completed, accepting either WooCommerce webhooks or an external order payload. It normalizes the order, enriches product data from the WooCommerce API, stores a review form record in Google Sheets, optionally shortens the form link and delays delivery, then sends the customer an SMTP email. Separate webhooks serve the tokenized review form and process submitted reviews back into WooCommerce while marking the form as completed.\n\n### Setup steps\n\n- Configure the WooCommerce order webhook URL and any external order webhook source that should trigger review requests.\n- Set WooCommerce API credentials and site URL used for fetching products and posting reviews.\n- Connect Google Sheets credentials and point the workflow to the sheet used to store review form tokens, order data, and completion status.\n- Configure SMTP credentials and sender details for the review request email.\n- If URL shortening is enabled, add a Dub.co API token and domain settings in the configuration node.\n- Review the main configuration node for form URLs, delay settings, sheet column names, email copy, and store-specific defaults.\n\n### Customization\n\nAdjust the configuration node to change timing, email wording, form branding, URL-shortening behavior, and the mapping of incoming order fields or Google Sheets columns."
},
"typeVersion": 1
},
{
"id": "94bf2553-ce0e-4830-930b-3739af46e104",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
560,
0
],
"parameters": {
"color": 7,
"width": 496,
"height": 464,
"content": "## WooCommerce order intake\n\nReceives WooCommerce order-completed webhooks, immediately acknowledges the request, and normalizes the WooCommerce payload for downstream processing."
},
"typeVersion": 1
},
{
"id": "8b60138b-9295-4b17-8af0-67245a7bb497",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
560,
512
],
"parameters": {
"color": 7,
"width": 496,
"height": 464,
"content": "## External order intake\n\nReceives non-WooCommerce order payloads, sends an immediate 200 response, and converts the external body into the shared order format."
},
"typeVersion": 1
},
{
"id": "2de02c84-c42e-43b5-9029-26ee0d92c3dc",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
1168,
256
],
"parameters": {
"color": 7,
"height": 368,
"content": "## Apply workflow config\n\nCentralizes workflow settings and combines the normalized order stream before product-level processing begins."
},
"typeVersion": 1
},
{
"id": "bda016a4-9e58-48c4-a087-632603fea4f8",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
1456,
336
],
"parameters": {
"color": 7,
"width": 800,
"height": 304,
"content": "## Enrich order products\n\nSplits the order into product items, retrieves product details through the WooCommerce API, and aggregates the enriched product data back into the order context."
},
"typeVersion": 1
},
{
"id": "b68c4eae-c7d7-4d6a-89b1-b63a4b64ff82",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
2368,
304
],
"parameters": {
"color": 7,
"width": 480,
"height": 320,
"content": "## Save form record\n\nCreates the review form tracking row in Google Sheets and restores the working order data after the sheet write operation."
},
"typeVersion": 1
},
{
"id": "c1f8e9c3-ae2a-4082-8784-8dfe9e4df23b",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
2960,
176
],
"parameters": {
"color": 7,
"width": 800,
"height": 624,
"content": "## Choose review URL\n\nBranches based on whether URL shortening is enabled, either creating a Dub.co short link and extracting it or preserving the direct review form URL."
},
"typeVersion": 1
},
{
"id": "e52bcc79-baba-44ce-8868-33103b2bf7d9",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
3856,
288
],
"parameters": {
"color": 7,
"height": 336,
"content": "## Merge URL result\n\nRejoins the shortened and direct URL paths into one stream for the remaining email delivery steps."
},
"typeVersion": 1
},
{
"id": "c53e3121-c9fc-4c0d-9b30-481745a053ba",
"name": "Sticky Note8",
"type": "n8n-nodes-base.stickyNote",
"position": [
4160,
176
],
"parameters": {
"color": 7,
"width": 800,
"height": 464,
"content": "## Handle send delay\n\nChecks whether sending should be delayed, waits when configured, and merges the delayed and immediate paths before rendering the email."
},
"typeVersion": 1
},
{
"id": "5b0b07d0-5759-4bf6-8490-feaf78516509",
"name": "Sticky Note9",
"type": "n8n-nodes-base.stickyNote",
"position": [
5056,
336
],
"parameters": {
"color": 7,
"width": 496,
"height": 304,
"content": "## Send review email\n\nRenders the customer-facing email HTML and sends the review request via SMTP."
},
"typeVersion": 1
},
{
"id": "c1d22552-1801-4dbe-8d4b-dc3c85beb1fe",
"name": "Sticky Note10",
"type": "n8n-nodes-base.stickyNote",
"position": [
560,
1024
],
"parameters": {
"color": 7,
"width": 800,
"height": 304,
"content": "## Load review form\n\nHandles incoming review form page requests, looks up the corresponding Google Sheets row, and validates the form token."
},
"typeVersion": 1
},
{
"id": "daa38dd3-7513-4f29-896c-f06414d32ed1",
"name": "Sticky Note11",
"type": "n8n-nodes-base.stickyNote",
"position": [
1456,
896
],
"parameters": {
"color": 7,
"width": 800,
"height": 576,
"content": "## Render form response\n\nBranches on token validity and returns either the review form HTML or an invalid-link error page."
},
"typeVersion": 1
},
{
"id": "84075046-950f-4288-a972-e1049a8e8d77",
"name": "Sticky Note12",
"type": "n8n-nodes-base.stickyNote",
"position": [
560,
1536
],
"parameters": {
"color": 7,
"width": 800,
"height": 304,
"content": "## Receive review submission\n\nReceives submitted review form data, retrieves the stored form row, and parses and validates the submitted reviews."
},
"typeVersion": 1
},
{
"id": "aa635d4e-758c-4c55-b94c-a61493216169",
"name": "Sticky Note13",
"type": "n8n-nodes-base.stickyNote",
"position": [
1456,
1504
],
"parameters": {
"color": 7,
"width": 496,
"height": 640,
"content": "## Validate submission path\n\nRoutes valid submissions toward review posting and immediately returns an error response for invalid or rejected submissions."
},
"typeVersion": 1
},
{
"id": "19506d4e-d46c-41bf-83da-d7e66f5a241d",
"name": "Sticky Note14",
"type": "n8n-nodes-base.stickyNote",
"position": [
2064,
1504
],
"parameters": {
"color": 7,
"width": 784,
"height": 304,
"content": "## Publish submitted reviews\n\nPosts the parsed product reviews to WooCommerce, updates the form tracking row as completed, and returns the thank-you response."
},
"typeVersion": 1
},
{
"id": "a56bc1c5-6350-423c-9cfb-37ec4b3dbd78",
"name": "WooCommerce Order Complete",
"type": "n8n-nodes-base.webhook",
"position": [
608,
240
],
"parameters": {
"path": "wc-order-complete",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "7a15dd8a-83b2-486d-adc2-90584eb03bb9",
"name": "When External Order Received",
"type": "n8n-nodes-base.webhook",
"position": [
608,
736
],
"parameters": {
"path": "review-request",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "0b7ebca9-b818-415e-8f2c-751579cad8eb",
"name": "Acknowledge WooCommerce Order",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
912,
160
],
"parameters": {
"options": {},
"respondWith": "text",
"responseBody": "OK"
},
"typeVersion": 1
},
{
"id": "dae0b80a-f8c2-470c-833a-06914a2c1c06",
"name": "Acknowledge External Order",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
912,
816
],
"parameters": {
"options": {},
"respondWith": "text",
"responseBody": "OK"
},
"typeVersion": 1
},
{
"id": "eb356280-b05f-40a4-acc8-aac07305af3f",
"name": "Normalize WooCommerce Order",
"type": "n8n-nodes-base.code",
"position": [
912,
304
],
"parameters": {
"jsCode": "// Normalize WooCommerce order.completed webhook payload\nconst order = $json.body || $json;\nconst firstName = order.billing?.first_name || '';\nconst lastName = order.billing?.last_name || '';\nreturn {\n orderId: String(order.id || order.number || ''),\n customerName: [firstName, lastName].filter(Boolean).join(' ') || 'Customer',\n customerEmail: order.billing?.email || '',\n customerPhone: order.billing?.phone || '',\n products: (order.line_items || [])\n .map(item => ({ id: item.product_id, name: item.name || '', sku: item.sku || '' }))\n .filter(p => p.id)\n};"
},
"typeVersion": 2
},
{
"id": "5fad2521-bbc8-44e8-a49e-1d2c379d9210",
"name": "Format External Order Data",
"type": "n8n-nodes-base.code",
"position": [
912,
672
],
"parameters": {
"jsCode": "// Normalize external POST body\nconst b = $json.body || $json;\nreturn {\n orderId: String(b.orderId || b.order_id || ''),\n customerName: b.customerName || b.customer_name || 'Customer',\n customerEmail: b.customerEmail || b.email || '',\n customerPhone: b.customerPhone || b.phone || '',\n products: (b.products || [])\n .map(p => ({ id: Number(p.id || p.product_id), name: p.name || '', sku: p.sku || '' }))\n .filter(p => p.id)\n};"
},
"typeVersion": 2
},
{
"id": "d4644674-e176-4f21-a929-ef80250b5246",
"name": "Configure Parameters",
"type": "n8n-nodes-base.code",
"position": [
1216,
464
],
"parameters": {
"jsCode": "// ============================================================\n// \u2699\ufe0f CONFIGURATION \u2014 edit all values marked with YOUR_\n// ============================================================\nconst CONFIG = {\n // WooCommerce site URL (no trailing slash)\n siteUrl: 'https://YOUR_SITE_URL',\n\n // Shop name shown in emails and form\n shopName: 'YOUR_SHOP_NAME',\n\n // Logo image URL for email header (leave '' to hide)\n logoUrl: 'YOUR_LOGO_URL',\n\n // n8n public URL (where this n8n instance is hosted)\n n8nUrl: 'https://YOUR_N8N_URL',\n\n // Webhook paths \u2014 must match the webhook nodes below\n formWebhookPath: 'review-form',\n submitWebhookPath: 'submit-review',\n\n // Days until review link expires\n expiryDays: 30,\n\n // Set true to delay the email (configure Wait node)\n enableDelay: false,\n\n // Set true to shorten URLs via Dub.co\n enableUrlShortening: false,\n\n // Dub.co API key (only used if enableUrlShortening: true)\n dubApiKey: 'YOUR_DUB_API_KEY'\n};\n\nconst order = $json;\nconst formId = crypto.randomUUID();\nconst expiresAt = new Date(Date.now() + CONFIG.expiryDays * 86400000).toISOString();\nconst formUrl = `${CONFIG.n8nUrl}/webhook/${CONFIG.formWebhookPath}?fid=${formId}`;\n\nreturn { ...order, config: CONFIG, formId, formUrl, expiresAt, createdAt: new Date().toISOString() };"
},
"typeVersion": 2
},
{
"id": "9c1a57c2-5f87-4cb8-a78b-8e53859402c3",
"name": "Split Order by Product",
"type": "n8n-nodes-base.code",
"position": [
1504,
464
],
"parameters": {
"jsCode": "// Emit one item per product so HTTP Request loops\nconst data = $json;\nreturn (data.products || []).map(p => ({ ...data, _product: p }));"
},
"typeVersion": 2
},
{
"id": "b0cfe6bb-af19-40ca-863e-6f38ed9354e2",
"name": "Fetch WooCommerce Product",
"type": "n8n-nodes-base.httpRequest",
"position": [
1808,
464
],
"parameters": {
"url": "={{ $json.config.siteUrl }}/wp-json/wc/v3/products/{{ $json._product.id }}",
"options": {
"response": {
"response": {
"neverError": true
}
}
},
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth"
},
"typeVersion": 4
},
{
"id": "e808f69b-67d2-4316-a5c8-1b466d04f693",
"name": "Aggregate Product Details",
"type": "n8n-nodes-base.code",
"position": [
2112,
464
],
"parameters": {
"jsCode": "// Merge WC API product details back with order data\nconst items = $input.all();\nconst first = items[0].json;\nconst baseKey = ['orderId','customerName','customerEmail','customerPhone','config','formId','formUrl','expiresAt','createdAt'];\nconst base = {};\nbaseKey.forEach(k => { base[k] = first[k]; });\n\nconst products = items.map(item => {\n const wc = item.json; // WC API product response\n const src = item.json._product; // original product stub\n return {\n id: wc.id || src.id,\n name: wc.name || src.name || '',\n sku: wc.sku || src.sku || '',\n image: (wc.images && wc.images[0]) ? wc.images[0].src : '',\n permalink: wc.permalink || ''\n };\n}).filter(p => p.id);\n\nreturn { ...base, products };"
},
"typeVersion": 2
},
{
"id": "9ea56b1c-0ca8-4e81-9c04-d54245c2cbac",
"name": "Append Form Data to Sheets",
"type": "n8n-nodes-base.googleSheets",
"position": [
2416,
464
],
"parameters": {
"columns": {
"value": {
"formId": "={{ $json.formId }}",
"status": "pending",
"formUrl": "={{ $json.formUrl }}",
"orderId": "={{ $json.orderId }}",
"products": "={{ JSON.stringify($json.products) }}",
"createdAt": "={{ $json.createdAt }}",
"expiresAt": "={{ $json.expiresAt }}",
"customerName": "={{ $json.customerName }}",
"customerEmail": "={{ $json.customerEmail }}"
},
"mappingMode": "defineBelow"
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "ReviewForms"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "YOUR_SHEET_ID"
}
},
"typeVersion": 4
},
{
"id": "bf749722-6230-4b94-97d2-8bfbbbfa3033",
"name": "Retrieve Saved Data",
"type": "n8n-nodes-base.code",
"position": [
2704,
464
],
"parameters": {
"jsCode": "return $('Aggregate Product Details').first().json;"
},
"typeVersion": 2
},
{
"id": "e8efa76f-7a8b-4920-bbb6-9e9f4f114dc1",
"name": "Check URL Shortening Active",
"type": "n8n-nodes-base.if",
"position": [
3008,
464
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "bbb6c792-1ab5-43e6-a78f-d6d7c3f42418",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.config.enableUrlShortening }}",
"rightValue": true
}
]
}
},
"typeVersion": 2
},
{
"id": "d6953775-e77a-4be1-9a83-7e0e73eb7561",
"name": "Shorten URL with Dub.co",
"type": "n8n-nodes-base.httpRequest",
"position": [
3312,
304
],
"parameters": {
"url": "https://api.dub.co/links",
"method": "POST",
"options": {},
"jsonBody": "{\n \"url\": \"{{ $json.formUrl }}\"\n}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $json.config.dubApiKey }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4
},
{
"id": "073f7d76-1e91-49e7-9f18-4717c092282f",
"name": "Assign Shortened URL",
"type": "n8n-nodes-base.code",
"position": [
3616,
304
],
"parameters": {
"jsCode": "const base = $('Retrieve Saved Data').first().json;\nreturn { ...base, reviewLink: $json.shortLink || $json.url || base.formUrl };"
},
"typeVersion": 2
},
{
"id": "8aaf00e1-0217-46a6-9207-2d596c460c1f",
"name": "Assign Direct URL",
"type": "n8n-nodes-base.code",
"position": [
3312,
624
],
"parameters": {
"jsCode": "return { ...$json, reviewLink: $json.formUrl };"
},
"typeVersion": 2
},
{
"id": "27c34c9f-215d-4e6c-8cd1-2d1010586c22",
"name": "Combine URL Data",
"type": "n8n-nodes-base.merge",
"position": [
3904,
464
],
"parameters": {},
"typeVersion": 3
},
{
"id": "bd0c48f5-cbb2-43b6-977b-4c9625828682",
"name": "Check for Delay Activation",
"type": "n8n-nodes-base.if",
"position": [
4208,
464
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "bba217a7-8765-4d2b-b142-b14978233166",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.config.enableDelay }}",
"rightValue": true
}
]
}
},
"typeVersion": 2
},
{
"id": "d92091c0-a92b-43b1-b987-0449d2feddcc",
"name": "Wait 3 Days for Review",
"type": "n8n-nodes-base.wait",
"position": [
4512,
304
],
"parameters": {
"unit": "days",
"amount": 3
},
"typeVersion": 1
},
{
"id": "ea193bcd-76f2-4dff-b46d-fb12a873e699",
"name": "Aggregate Post-Delay Data",
"type": "n8n-nodes-base.merge",
"position": [
4816,
464
],
"parameters": {},
"typeVersion": 3
},
{
"id": "5bb334c3-28dd-482a-90e1-e43b898fc4dc",
"name": "Build Email HTML Content",
"type": "n8n-nodes-base.code",
"position": [
5104,
464
],
"parameters": {
"jsCode": "\nconst d = $json;\nconst cfg = d.config;\n\nconst productsHtml = (d.products || []).map(p => {\n const img = p.image\n ? `<img src=\"${p.image}\" alt=\"${p.name.replace(/\"/g,'')}\" width=\"52\" height=\"52\" style=\"display:block;object-fit:cover;border:1px solid #eee;\">`\n : `<div style=\"width:52px;height:52px;background:#f0f0f0;border:1px solid #eee;\"></div>`;\n return `\n <tr>\n <td style=\"padding:14px 0;border-top:1px solid #f0f0f0;\">\n <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n <tr>\n <td width=\"68\" valign=\"middle\">${img}</td>\n <td valign=\"middle\" style=\"font-size:14px;color:#111;font-family:Arial,sans-serif;\">${p.name}</td>\n </tr>\n </table>\n </td>\n </tr>`;\n}).join('');\n\nconst logoBlock = cfg.logoUrl\n ? `<img src=\"${cfg.logoUrl}\" alt=\"${cfg.shopName}\" style=\"max-height:46px;display:block;\">`\n : `<span style=\"font-size:18px;font-weight:bold;color:#111;font-family:Arial,sans-serif;\">${cfg.shopName}</span>`;\n\nconst html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n <title>How did we do?</title>\n</head>\n<body style=\"margin:0;padding:0;background:#f5f5f5;font-family:Georgia,serif;\">\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" bgcolor=\"#f5f5f5\">\n<tr><td align=\"center\" style=\"padding:40px 16px;\">\n <table width=\"560\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" bgcolor=\"#ffffff\" style=\"max-width:560px;\">\n <tr>\n <td align=\"center\" style=\"padding:36px 40px 28px;border-bottom:1px solid #ebebeb;\">\n ${logoBlock}\n </td>\n </tr>\n <tr>\n <td style=\"padding:36px 40px 16px;\">\n <p style=\"margin:0 0 10px;font-size:11px;color:#aaa;text-transform:uppercase;letter-spacing:1.5px;font-family:Arial,sans-serif;\">Order #${d.orderId}</p>\n <h1 style=\"margin:0 0 14px;font-size:24px;font-weight:normal;color:#111;line-height:1.3;\">Hi ${d.customerName},<br>how did we do?</h1>\n <p style=\"margin:0;font-size:15px;color:#666;line-height:1.7;font-family:Arial,sans-serif;\">Your review helps other shoppers choose with confidence \u2014 and takes less than a minute.</p>\n </td>\n </tr>\n <tr>\n <td style=\"padding:8px 40px 28px;\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n ${productsHtml}\n </table>\n </td>\n </tr>\n <tr>\n <td align=\"center\" style=\"padding:8px 40px 48px;\">\n <a href=\"${d.reviewLink}\" target=\"_blank\"\n style=\"display:inline-block;background:#111;color:#fff;text-decoration:none;padding:16px 52px;font-size:13px;letter-spacing:1.5px;text-transform:uppercase;font-family:Arial,sans-serif;\">\n Leave a Review\n </a>\n <p style=\"margin:16px 0 0;font-size:12px;color:#bbb;font-family:Arial,sans-serif;\">Link valid for ${cfg.expiryDays} days</p>\n </td>\n </tr>\n <tr>\n <td bgcolor=\"#f9f9f9\" style=\"padding:20px 40px;border-top:1px solid #ebebeb;\">\n <p style=\"margin:0;font-size:11px;color:#bbb;text-align:center;line-height:1.7;font-family:Arial,sans-serif;\">\n ${cfg.shopName} · <a href=\"${cfg.siteUrl}\" style=\"color:#bbb;text-decoration:none;\">${cfg.siteUrl.replace('https://','')}</a><br>\n If you did not place this order, please disregard this email.\n </p>\n </td>\n </tr>\n </table>\n</td></tr>\n</table>\n</body>\n</html>`;\n\nreturn {\n html,\n subject: `${d.customerName}, how did we do? \ud83d\udecd`,\n toEmail: d.customerEmail,\n toName: d.customerName,\n fromEmail: `noreply@${cfg.siteUrl.replace(/https?:\\/\\//,'').split('/')[0]}`,\n fromName: cfg.shopName\n};\n"
},
"typeVersion": 2
},
{
"id": "516d3f9d-4aa6-4601-9e0a-e1629aa73f99",
"name": "Send Review Email",
"type": "n8n-nodes-base.emailSend",
"position": [
5408,
464
],
"parameters": {
"html": "={{ $json.html }}",
"options": {},
"subject": "={{ $json.subject }}",
"toEmail": "={{ $json.toEmail }}",
"fromEmail": "={{ $json.fromEmail }}",
"emailFormat": "html"
},
"typeVersion": 2
},
{
"id": "ec768900-7d22-4887-9f90-6af1a29a7e2d",
"name": "Serve Review Form to User",
"type": "n8n-nodes-base.webhook",
"position": [
608,
1152
],
"parameters": {
"path": "review-form",
"options": {},
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "448c9eef-d594-48fd-82df-8d56fd423603",
"name": "Read Form Data for Serving",
"type": "n8n-nodes-base.googleSheets",
"position": [
912,
1152
],
"parameters": {
"options": {},
"filtersUI": {
"values": [
{
"lookupValue": "={{ $json.query.fid }}",
"lookupColumn": "formId"
}
]
},
"sheetName": {
"__rl": true,
"mode": "name",
"value": "ReviewForms"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "YOUR_SHEET_ID"
}
},
"typeVersion": 4
},
{
"id": "cee9ebec-6f5c-4f43-9bac-061799725cd0",
"name": "Check Form Token Validity",
"type": "n8n-nodes-base.code",
"position": [
1216,
1152
],
"parameters": {
"jsCode": "const row = $json;\nif (!row.formId) return [{ valid: false, reason: 'not_found' }];\nif (row.status === 'completed') return [{ valid: false, reason: 'already_used' }];\nif (new Date(row.expiresAt) < new Date()) return [{ valid: false, reason: 'expired' }];\nreturn [{ ...row, products: JSON.parse(row.products || '[]'), valid: true }];"
},
"typeVersion": 2
},
{
"id": "de28179a-97fd-44d4-8ed5-2157e692f881",
"name": "Validate Form Token Status",
"type": "n8n-nodes-base.if",
"position": [
1504,
1152
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "93ff0962-544f-4d59-8cb2-3dda0ac525bf",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.valid }}",
"rightValue": true
}
]
}
},
"typeVersion": 2
},
{
"id": "524b708e-a88b-4152-b6a7-32de5b3ef76b",
"name": "Generate Review Form HTML",
"type": "n8n-nodes-base.code",
"position": [
1808,
1024
],
"parameters": {
"jsCode": "\nconst d = $json;\n\nconst productCards = (d.products || []).map((p, i) => {\n const img = p.image\n ? `<img src=\"${p.image}\" alt=\"${p.name.replace(/\"/g,'')}\" onerror=\"this.style.display='none'\">`\n : '';\n return `\n <div class=\"card\">\n <div class=\"product-row\">\n ${img}\n <div class=\"product-name\">${p.name}</div>\n </div>\n <input type=\"hidden\" name=\"w_${i}\" value=\"${p.id}\">\n <input type=\"hidden\" name=\"p_${i}\" value=\"${p.name.replace(/\"/g,'"')}\">\n <span class=\"label\">Rating *</span>\n <div class=\"stars\" id=\"stars_${i}\">\n ${[5,4,3,2,1].map(v => `\n <span class=\"star\" data-val=\"${v}\" data-idx=\"${i}\">\u2605</span>`).join('')}\n </div>\n <input type=\"hidden\" name=\"r_${i}\" id=\"r_${i}\" required>\n <span class=\"label\" style=\"margin-top:14px;\">Review <span style=\"color:#bbb;\">(optional)</span></span>\n <textarea name=\"t_${i}\" placeholder=\"What did you like or dislike?\"></textarea>\n </div>`;\n}).join('');\n\nconst html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n <title>Leave a Review</title>\n <style>\n *, *::before, *::after { box-sizing: border-box; margin:0; padding:0; }\n body { background:#f5f5f5; font-family:Arial,sans-serif; color:#111; min-height:100vh; }\n .wrap { max-width:580px; margin:0 auto; padding:40px 16px 80px; }\n .logo-row { text-align:center; padding-bottom:24px; }\n .logo-row img { max-height:44px; }\n h1 { font-size:22px; font-weight:normal; text-align:center; margin-bottom:6px; }\n .sub { font-size:14px; color:#888; text-align:center; margin-bottom:28px; }\n .card { background:#fff; border:1px solid #e4e4e4; padding:24px; margin-bottom:14px; }\n .product-row { display:flex; align-items:center; gap:14px; margin-bottom:18px; }\n .product-row img { width:52px; height:52px; object-fit:cover; border:1px solid #eee; flex-shrink:0; }\n .product-name { font-size:14px; color:#111; }\n .label { display:block; font-size:11px; color:#888; text-transform:uppercase; letter-spacing:.8px; margin-bottom:8px; }\n .stars { display:flex; gap:8px; margin-bottom:4px; }\n .star { font-size:30px; color:#ddd; cursor:pointer; user-select:none; transition:color .1s; }\n .star.active { color:#111; }\n textarea { width:100%; border:1px solid #ddd; padding:10px 12px; font-size:14px;\n resize:vertical; min-height:76px; font-family:Arial,sans-serif; margin-top:4px; }\n textarea:focus { outline:none; border-color:#111; }\n .err { font-size:12px; color:#c00; margin-top:8px; display:none; }\n .btn { display:block; width:100%; background:#111; color:#fff; border:none; padding:16px;\n font-size:13px; letter-spacing:1.5px; text-transform:uppercase; cursor:pointer; margin-top:22px; }\n .btn:hover { background:#333; }\n .btn:disabled { opacity:.5; cursor:default; }\n .success { display:none; text-align:center; padding:80px 20px; }\n .success h2 { font-size:20px; font-weight:normal; margin-bottom:10px; }\n .success p { font-size:14px; color:#888; }\n </style>\n</head>\n<body>\n<div class=\"wrap\">\n <div class=\"logo-row\">\n <img src=\"\" id=\"logo-img\" onerror=\"this.style.display='none'\" alt=\"\">\n </div>\n <h1>Hi ${d.customerName}, how did we do?</h1>\n <p class=\"sub\">Your honest review helps other shoppers</p>\n\n <form id=\"rf\">\n <input type=\"hidden\" name=\"formId\" value=\"${d.formId}\">\n <input type=\"hidden\" name=\"customerName\" value=\"${d.customerName.replace(/\"/g,'"')}\">\n <input type=\"hidden\" name=\"customerEmail\" value=\"${d.customerEmail}\">\n ${productCards}\n <p class=\"err\" id=\"err-msg\">Please rate all products before submitting.</p>\n <button type=\"submit\" class=\"btn\" id=\"submit-btn\">Submit Reviews</button>\n </form>\n\n <div class=\"success\" id=\"success-box\">\n <h2>Thank you! \ud83c\udf89</h2>\n <p>Your review has been submitted and will be published shortly.</p>\n </div>\n</div>\n<script>\n // Star rating interaction\n document.querySelectorAll('.stars').forEach(group => {\n const idx = group.id.replace('stars_','');\n group.querySelectorAll('.star').forEach(star => {\n star.addEventListener('click', () => {\n const val = star.dataset.val;\n document.getElementById('r_' + idx).value = val;\n group.querySelectorAll('.star').forEach(s =>\n s.classList.toggle('active', Number(s.dataset.val) <= Number(val))\n );\n });\n });\n });\n\n document.getElementById('rf').addEventListener('submit', async e => {\n e.preventDefault();\n const form = e.target;\n const data = Object.fromEntries(new FormData(form));\n const count = ${d.products.length};\n for (let i = 0; i < count; i++) {\n if (!data['r_' + i]) {\n document.getElementById('err-msg').style.display = 'block';\n return;\n }\n }\n document.getElementById('err-msg').style.display = 'none';\n const btn = document.getElementById('submit-btn');\n btn.disabled = true; btn.textContent = 'Submitting\u2026';\n try {\n const res = await fetch(window.location.origin + '/webhook/submit-review', {\n method: 'POST',\n headers: {'Content-Type':'application/json'},\n body: JSON.stringify(data)\n });\n if (res.ok) {\n form.style.display = 'none';\n document.getElementById('success-box').style.display = 'block';\n } else { throw new Error('server'); }\n } catch {\n btn.disabled = false; btn.textContent = 'Submit Reviews';\n alert('Something went wrong. Please try again.');\n }\n });\n</script>\n</body>\n</html>`;\n\nreturn { html };\n"
},
"typeVersion": 2
},
{
"id": "cd377f30-5972-4d28-b5f7-79fc5fedeb2d",
"name": "Return Form HTML to Browser",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
2112,
1024
],
"parameters": {
"options": {},
"respondWith": "html"
},
"typeVersion": 1
},
{
"id": "e9c8100a-dc16-4f27-ba0f-10b9a313d867",
"name": "Generate Error Response",
"type": "n8n-nodes-base.code",
"position": [
1808,
1296
],
"parameters": {
"jsCode": "\nconst reason = $json.reason || 'invalid';\nconst title = reason === 'already_used' ? 'Already Submitted' : 'Link Expired';\nconst msg = reason === 'already_used'\n ? 'You have already submitted your review. Thank you!'\n : 'This review link has expired or is no longer valid.';\nconst html = `<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>${title}</title>\n<style>body{font-family:Arial,sans-serif;background:#f5f5f5;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;}\n.box{background:#fff;border:1px solid #e4e4e4;padding:60px 40px;text-align:center;max-width:420px;}\nh2{font-weight:normal;font-size:20px;margin-bottom:10px;color:#111;}\np{color:#888;font-size:14px;line-height:1.6;}</style></head>\n<body><div class=\"box\"><h2>${title}</h2><p>${msg}</p></div></body></html>`;\nreturn { html };"
},
"typeVersion": 2
},
{
"id": "7a5dacb9-be58-4161-984d-d9bd504b2ca1",
"name": "Return Invalid Link Error",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
2112,
1296
],
"parameters": {
"options": {},
"respondWith": "html"
},
"typeVersion": 1
},
{
"id": "5a5a3c2c-0ac7-43eb-8d40-80f3ff1ed4b8",
"name": "Accept Review Submission",
"type": "n8n-nodes-base.webhook",
"position": [
608,
1664
],
"parameters": {
"path": "submit-review",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "4fe09ea9-a0ab-40e3-b80e-ef2843202c6d",
"name": "Retrieve Submission Data",
"type": "n8n-nodes-base.googleSheets",
"position": [
912,
1664
],
"parameters": {
"options": {},
"filtersUI": {
"values": [
{
"lookupValue": "={{ $json.body.formId }}",
"lookupColumn": "formId"
}
]
},
"sheetName": {
"__rl": true,
"mode": "name",
"value": "ReviewForms"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "YOUR_SHEET_ID"
}
},
"typeVersion": 4
},
{
"id": "3055dc6b-feb9-4e22-b625-ff5a1fddc18b",
"name": "Process Submitted Reviews",
"type": "n8n-nodes-base.code",
"position": [
1216,
1664
],
"parameters": {
"jsCode": "const row = $json;\nconst body = $('Accept Review Submission').first().json.body || {};\n\nif (!row.formId) return [{ valid: false, reason: 'not_found' }];\nif (row.status === 'completed') return [{ valid: false, reason: 'already_used' }];\nif (new Date(row.expiresAt) < new Date()) return [{ valid: false, reason: 'expired' }];\n\nconst siteUrl = body.siteUrl || 'https://YOUR_SITE_URL'; // fallback\n\nconst reviews = [];\nfor (let i = 0; body['w_' + i] !== undefined; i++) {\n const wpId = parseInt(body['w_' + i]);\n const rating = parseInt(body['r_' + i]) || 5;\n const text = (body['t_' + i] || '').trim();\n if (!wpId) break;\n reviews.push({\n wpId,\n productName: body['p_' + i] || '',\n rating,\n review: text || `Rating: ${rating} out of 5`\n });\n}\n\nreturn [{\n valid: true,\n formId: row.formId,\n customerName: body.customerName || row.customerName || 'Customer',\n customerEmail: body.customerEmail || row.customerEmail || '',\n siteUrl,\n reviews\n}];"
},
"typeVersion": 2
},
{
"id": "7f5acc07-8259-417b-9b1c-0e95121f85d5",
"name": "Validate Review Submission",
"type": "n8n-nodes-base.if",
"position": [
1504,
1808
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "a3b50321-edab-4bbf-b664-4e40d0f2276e",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.valid }}",
"rightValue": true
}
]
}
},
"typeVersion": 2
},
{
"id": "dde4bdb5-ff38-49b0-b39e-ab515536bb5b",
"name": "Split Review Entries",
"type": "n8n-nodes-base.code",
"position": [
1808,
1664
],
"parameters": {
"jsCode": "const d = $json; return (d.reviews || []).map(r => ({ ...d, ...r }));"
},
"typeVersion": 2
},
{
"id": "3ae8f2b5-03cb-4e21-a00b-bd6f7b4e9e31",
"name": "Upload Review to WooCommerce",
"type": "n8n-nodes-base.httpRequest",
"position": [
2112,
1632
],
"parameters": {
"url": "={{ $json.siteUrl }}/wp-json/wc/v3/products/reviews",
"method": "POST",
"options": {
"response": {
"response": {
"neverError": true
}
}
},
"jsonBody": "{\n \"product_id\": {{ $json.wpId }},\n \"review\": {{ JSON.stringify($json.review) }},\n \"reviewer\": {{ JSON.stringify($json.customerName) }},\n \"reviewer_email\": \"{{ $json.customerEmail }}\",\n \"rating\": {{ $json.rating }},\n \"status\": \"hold\"\n}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth"
},
"typeVersion": 4
},
{
"id": "82980e75-de37-47f4-a1f3-e5a8e75c208d",
"name": "Update Review Completion Status",
"type": "n8n-nodes-base.googleSheets",
"position": [
2416,
1632
],
"parameters": {
"columns": {
"value": {
"formId": "={{ $('Process Submitted Reviews').first().json.formId }}",
"status": "completed"
},
"mappingMode": "defineBelow"
},
"options": {},
"operation": "update",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "ReviewForms"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "YOUR_SHEET_ID"
}
},
"typeVersion": 4
},
{
"id": "14d3dfe7-1bec-4967-9bb3-1b6724db40b3",
"name": "Return Thank You Message",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
2704,
1632
],
"parameters": {
"options": {},
"respondWith": "json",
"responseBody": "{\"success\": true, \"message\": \"Thank you! Your review has been submitted.\"}"
},
"typeVersion": 1
},
{
"id": "8446964e-7bcf-4ccd-937c-8e92e9551ff5",
"name": "Return Submission Error Response",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
1808,
1984
],
"parameters": {
"options": {
"responseCode": 400
},
"respondWith": "json",
"responseBody": "={\"success\": false, \"reason\": \"{{ $json.reason || \\\"invalid\\\" }}\"}"
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
},
"versionId": "8c96aca6-6845-4c34-814e-5f51d4cdd443",
"nodeGroups": [],
"connections": {
"Combine URL Data": {
"main": [
[
{
"node": "Check for Delay Activation",
"type": "main",
"index": 0
}
]
]
},
"Assign Direct URL": {
"main": [
[
{
"node": "Combine URL Data",
"type": "main",
"index": 1
}
]
]
},
"Retrieve Saved Data": {
"main": [
[
{
"node": "Check URL Shortening Active",
"type": "main",
"index": 0
}
]
]
},
"Assign Shortened URL": {
"main": [
[
{
"node": "Combine URL Data",
"type": "main",
"index": 0
}
]
]
},
"Configure Parameters": {
"main": [
[
{
"node": "Split Order by Product",
"type": "main",
"index": 0
}
]
]
},
"Split Review Entries": {
"main": [
[
{
"node": "Upload Review to WooCommerce",
"type": "main",
"index": 0
}
]
]
},
"Split Order by Product": {
"main": [
[
{
"node": "Fetch WooCommerce Product",
"type": "main",
"index": 0
}
]
]
},
"Wait 3 Days for Review": {
"main": [
[
{
"node": "Aggregate Post-Delay Data",
"type": "main",
"index": 0
}
]
]
},
"Generate Error Response": {
"main": [
[
{
"node": "Return Invalid Link Error",
"type": "main",
"index": 0
}
]
]
},
"Shorten URL with Dub.co": {
"main": [
[
{
"node": "Assign Shortened URL",
"type": "main",
"index": 0
}
]
]
},
"Accept Review Submission": {
"main": [
[
{
"node": "Retrieve Submission Data",
"type": "main",
"index": 0
}
]
]
},
"Build Email HTML Content": {
"main": [
[
{
"node": "Send Review Email",
"type": "main",
"index": 0
}
]
]
},
"Retrieve Submission Data": {
"main": [
[
{
"node": "Process Submitted Reviews",
"type": "main",
"index": 0
}
]
]
},
"Aggregate Post-Delay Data": {
"main": [
[
{
"node": "Build Email HTML Content",
"type": "main",
"index": 0
}
]
]
},
"Aggregate Product Details": {
"main": [
[
{
"node": "Append Form Data to Sheets",
"type": "main",
"index": 0
}
]
]
},
"Check Form Token Validity": {
"main": [
[
{
"node": "Validate Form Token Status",
"type": "main",
"index": 0
}
]
]
},
"Fetch WooCommerce Product": {
"main": [
[
{
"node": "Aggregate Product Details",
"type": "main",
"index": 0
}
]
]
},
"Generate Review Form HTML": {
"main": [
[
{
"node": "Return Form HTML to Browser",
"type": "main",
"index": 0
}
]
]
},
"Process Submitted Reviews": {
"main": [
[
{
"node": "Validate Review Submission",
"type": "main",
"index": 0
}
]
]
},
"Serve Review Form to User": {
"main": [
[
{
"node": "Read Form Data for Serving",
"type": "main",
"index": 0
}
]
]
},
"Append Form Data to Sheets": {
"main": [
[
{
"node": "Retrieve Saved Data",
"type": "main",
"index": 0
}
]
]
},
"Check for Delay Activation": {
"main": [
[
{
"node": "Wait 3 Days for Review",
"type": "main",
"index": 0
}
],
[
{
"node": "Aggregate Post-Delay Data",
"type": "main",
"index": 1
}
]
]
},
"Format External Order Data": {
"main": [
[
{
"node": "Configure Parameters",
"type": "main",
"index": 0
}
]
]
},
"Read Form Data for Serving": {
"main": [
[
{
"node": "Check Form Token Validity",
"type": "main",
"index": 0
}
]
]
},
"Validate Form Token Status": {
"main": [
[
{
"node": "Generate Review Form HTML",
"type": "main",
"index": 0
}
],
[
{
"node": "Generate Error Response",
"type": "main",
"index": 0
}
]
]
},
"Validate Review Submission": {
"main": [
[
{
"node": "Split Review Entries",
"type": "main",
"index": 0
}
],
[
{
"node": "Return Submission Error Response",
"type": "main",
"index": 0
}
]
]
},
"WooCommerce Order Complete": {
"main": [
[
{
"node": "Acknowledge WooCommerce Order",
"type": "main",
"index": 0
},
{
"node": "Normalize WooCommerce Order",
"type": "main",
"index": 0
}
]
]
},
"Check URL Shortening Active": {
"main": [
[
{
"node": "Shorten URL with Dub.co",
"type": "main",
"index": 0
}
],
[
{
"node": "Assign Direct URL",
"type": "main",
"index": 0
}
]
]
},
"Normalize WooCommerce Order": {
"main": [
[
{
"node": "Configure Parameters",
"type": "main",
"index": 0
}
]
]
},
"Upload Review to WooCommerce": {
"main": [
[
{
"node": "Update Review Completion Status",
"type": "main",
"index": 0
}
]
]
},
"When External Order Received": {
"main": [
[
{
"node": "Acknowledge External Order",
"type": "main",
"index": 0
},
{
"node": "Format External Order Data",
"type": "main",
"index": 0
}
]
]
},
"Update Review Completion Status": {
"main": [
[
{
"node": "Return Thank You Message",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow sends post-purchase review request emails for WooCommerce orders, stores expiring form links in Google Sheets, optionally shortens links with Dub.co and delays delivery, serves a hosted rating form via n8n webhooks, and submits collected reviews back to…
Source: https://n8n.io/workflows/17026/ — 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.
Convalidaciones Académicas - Estructura Base. Uses googleSheets, emailSend, googleDrive, httpRequest. Webhook trigger; 35 nodes.
Are you tired of manually entering open house visitor information into your CRM? Losing hot leads because you didn't follow up fast enough? This powerful n8n workflow automatically syncs every SignSna
FlowV4. Uses googleSheets, emailSend, googleDrive, httpRequest. Webhook trigger; 31 nodes.
This is a production-ready, end-to-end workflow that automatically compares hotel prices across multiple booking platforms and delivers beautiful email reports to users. Unlike basic building blocks,
Messenger Responder (FB + IG). Uses httpRequest, googleSheets, emailSend. Webhook trigger; 15 nodes.