Consumer goods and retail brands run on data, deadlines, and distributor relationships — but most of the operational glue is still duct tape: manual spreadsheet exports, copy-pasted emails to buyers, pricing checks done by eye.
This isn't an article for Shopify store owners. This is for CPG brand operators — the people managing sell-through across Walmart, Target, Amazon, and independent grocery chains, tracking promotional compliance, and coordinating product launches across 50+ SKUs.
Here are five n8n automations that cover the highest-friction points in retail brand operations.
Why self-hosted n8n is the right call for CPG brands
Before we dive in: brand pricing strategies, promotional contract terms, retailer sell-through data, and distributor agreements are commercially sensitive. Routing any of that through Zapier or Make.com means your data flows through a third-party cloud. Self-hosted n8n keeps everything inside your own infrastructure — no usage caps, no per-operation billing, no vendor exposure.
For enterprise retail brands under CCPA, GDPR, or operating under NDA with major retailers, that's not optional.
1. Retail Channel Sell-Through Aggregator (Daily Brand Sales Digest)
Your POS data comes in from 5 different channels in 5 different formats. This workflow collects it all every evening, computes brand-wide KPIs, and emails the summary to your brand manager.
What it does:
- Runs at 9 PM weekdays
- Reads sell-through rows from a Google Sheet (fed by your POS integrations)
- Aggregates: total units sold, revenue, AOV, top channel, top SKU
- Builds an HTML table and emails it to the brand team
{
"name": "CPG Daily Sales Digest",
"nodes": [
{
"id": "1",
"name": "9PM Weekday Cron",
"type": "n8n-nodes-base.scheduleTrigger",
"parameters": {
"rule": { "interval": [{ "field": "cronExpression", "expression": "0 21 * * 1-5" }] }
},
"position": [240, 300]
},
{
"id": "2",
"name": "Get Sales Data",
"type": "n8n-nodes-base.googleSheets",
"parameters": {
"operation": "readRows",
"documentId": "YOUR_SHEET_ID",
"sheetName": "daily_sales"
},
"position": [440, 300]
},
{
"id": "3",
"name": "Aggregate KPIs",
"type": "n8n-nodes-base.code",
"parameters": {
"jsCode": "const rows = $input.all();\nconst today = new Date().toISOString().slice(0,10);\nconst todayRows = rows.filter(r => r.json.date === today);\nconst totalUnits = todayRows.reduce((s,r) => s + Number(r.json.units||0), 0);\nconst totalRevenue = todayRows.reduce((s,r) => s + Number(r.json.revenue||0), 0);\nconst aov = totalUnits > 0 ? (totalRevenue / totalUnits).toFixed(2) : 0;\nconst byChannel = {};\ntodayRows.forEach(r => {\n const ch = r.json.channel;\n byChannel[ch] = byChannel[ch] || { units: 0, revenue: 0 };\n byChannel[ch].units += Number(r.json.units||0);\n byChannel[ch].revenue += Number(r.json.revenue||0);\n});\nconst topCh = Object.entries(byChannel).sort((a,b) => b[1].revenue - a[1].revenue)[0];\nconst bySku = {};\ntodayRows.forEach(r => {\n bySku[r.json.sku] = bySku[r.json.sku] || { name: r.json.product_name, units: 0 };\n bySku[r.json.sku].units += Number(r.json.units||0);\n});\nconst topSku = Object.entries(bySku).sort((a,b) => b[1].units - a[1].units)[0];\nreturn [{ json: { date: today, totalUnits, totalRevenue: totalRevenue.toFixed(2), aov, topChannel: topCh ? topCh[0] : 'N/A', topChannelRevenue: topCh ? topCh[1].revenue.toFixed(2) : 0, topSku: topSku ? topSku[1].name : 'N/A', topSkuUnits: topSku ? topSku[1].units : 0 } }];"
},
"position": [640, 300]
},
{
"id": "4",
"name": "Email Brand Manager",
"type": "n8n-nodes-base.gmail",
"parameters": {
"operation": "send",
"toList": "brand-manager@yourcompany.com",
"subject": "={{ 'Daily Brand Report — ' + $json.date + ' | $' + $json.totalRevenue }}",
"message": "={{ '<h2>Daily Brand Sales Report — ' + $json.date + '</h2><table border=1 cellpadding=6><tr><th>Metric</th><th>Value</th></tr><tr><td>Total Units</td><td>' + $json.totalUnits + '</td></tr><tr><td>Revenue</td><td>$' + $json.totalRevenue + '</td></tr><tr><td>Avg Revenue/Unit</td><td>$' + $json.aov + '</td></tr><tr><td>Top Channel</td><td>' + $json.topChannel + ' ($' + $json.topChannelRevenue + ')</td></tr><tr><td>Top SKU</td><td>' + $json.topSku + ' (' + $json.topSkuUnits + ' units)</td></tr></table>' }}",
"options": { "appendAttribution": false }
},
"position": [840, 300]
}
],
"connections": {
"9PM Weekday Cron": { "main": [[{ "node": "Get Sales Data", "type": "main", "index": 0 }]] },
"Get Sales Data": { "main": [[{ "node": "Aggregate KPIs", "type": "main", "index": 0 }]] },
"Aggregate KPIs": { "main": [[{ "node": "Email Brand Manager", "type": "main", "index": 0 }]] }
}
}
Setup: Create a Google Sheet with columns: date, channel, sku, product_name, units, revenue. Pull from your POS integrations (most have webhook or CSV export options).
2. Promotional Pricing Compliance Monitor
You ran a promo with a major retailer — agreed price $4.99. Three days in, they're still selling at $5.99. By the time your account manager notices, you've burned half the promotional window.
This workflow checks retailer listing prices daily against your promotional contract terms.
{
"name": "Promo Price Compliance Monitor",
"nodes": [
{
"id": "1",
"name": "Daily 8AM",
"type": "n8n-nodes-base.scheduleTrigger",
"parameters": {
"rule": { "interval": [{ "field": "cronExpression", "expression": "0 8 * * *" }] }
},
"position": [240, 300]
},
{
"id": "2",
"name": "Get Active Promos",
"type": "n8n-nodes-base.googleSheets",
"parameters": {
"operation": "readRows",
"documentId": "YOUR_SHEET_ID",
"sheetName": "active_promos"
},
"position": [440, 300]
},
{
"id": "3",
"name": "Filter Active Only",
"type": "n8n-nodes-base.code",
"parameters": {
"jsCode": "const today = new Date().toISOString().slice(0,10);\nreturn $input.all().filter(r => {\n const start = r.json.promo_start;\n const end = r.json.promo_end;\n return start <= today && today <= end && r.json.retailer_url;\n});"
},
"position": [640, 300]
},
{
"id": "4",
"name": "Check Live Price (HTTP)",
"type": "n8n-nodes-base.httpRequest",
"parameters": {
"method": "GET",
"url": "={{ 'https://your-price-scraper-api.com/extract?url=' + encodeURIComponent($json.retailer_url) }}",
"responseFormat": "json"
},
"position": [840, 300]
},
{
"id": "5",
"name": "Evaluate Compliance",
"type": "n8n-nodes-base.code",
"parameters": {
"jsCode": "const promo = $('Filter Active Only').item.json;\nconst livePrice = $input.first().json.price;\nconst contractPrice = Number(promo.promo_price);\nconst variance = livePrice - contractPrice;\nconst pct = ((variance / contractPrice) * 100).toFixed(1);\nconst compliant = Math.abs(variance) <= 0.02;\nreturn [{ json: { sku: promo.sku, product_name: promo.product_name, retailer: promo.retailer, contractPrice, livePrice, variance: variance.toFixed(2), pct, compliant, retailer_url: promo.retailer_url, account_manager_email: promo.account_manager_email } }];"
},
"position": [1040, 300]
},
{
"id": "6",
"name": "Violation?",
"type": "n8n-nodes-base.if",
"parameters": {
"conditions": {
"options": { "caseSensitive": false },
"conditions": [{ "leftValue": "={{ $json.compliant }}", "rightValue": false, "operator": { "type": "boolean", "operation": "equals" } }]
}
},
"position": [1240, 300]
},
{
"id": "7",
"name": "Alert Slack #trade-compliance",
"type": "n8n-nodes-base.slack",
"parameters": {
"operation": "post",
"channel": "#trade-compliance",
"text": "={{ '⚠️ PROMO VIOLATION: *' + $json.product_name + '* at ' + $json.retailer + ' — Contract: $' + $json.contractPrice + ', Live: $' + $json.livePrice + ' (' + ($json.variance > 0 ? '+' : '') + $json.pct + '%) ' + $json.retailer_url }}"
},
"position": [1440, 200]
},
{
"id": "8",
"name": "Email Account Manager",
"type": "n8n-nodes-base.gmail",
"parameters": {
"operation": "send",
"toList": "={{ $json.account_manager_email }}",
"subject": "={{ 'Promo Compliance Alert: ' + $json.product_name + ' at ' + $json.retailer }}",
"message": "={{ 'Hi — <strong>' + $json.product_name + '</strong> at ' + $json.retailer + ' is showing $' + $json.livePrice + ' vs contracted $' + $json.contractPrice + ' (' + $json.pct + '% variance). Please follow up. <a href=\"' + $json.retailer_url + '\">View listing</a>' }}",
"options": { "appendAttribution": false }
},
"position": [1440, 400]
}
],
"connections": {
"Daily 8AM": { "main": [[{ "node": "Get Active Promos", "type": "main", "index": 0 }]] },
"Get Active Promos": { "main": [[{ "node": "Filter Active Only", "type": "main", "index": 0 }]] },
"Filter Active Only": { "main": [[{ "node": "Check Live Price (HTTP)", "type": "main", "index": 0 }]] },
"Check Live Price (HTTP)": { "main": [[{ "node": "Evaluate Compliance", "type": "main", "index": 0 }]] },
"Evaluate Compliance": { "main": [[{ "node": "Violation?", "type": "main", "index": 0 }]] },
"Violation?": { "main": [[{ "node": "Alert Slack #trade-compliance", "type": "main", "index": 0 }], [{ "node": "Email Account Manager", "type": "main", "index": 0 }]] }
}
}
Setup: Sheet active_promos with columns: sku, product_name, retailer, retailer_url, promo_price, promo_start, promo_end, account_manager_email. Replace the HTTP request URL with a price-scraping API (e.g., Oxylabs, Bright Data, or a self-hosted Playwright scraper).
3. New Retailer Onboarding Automation
Getting a new retail partner over the line takes 8 manual emails: intro, portal credentials, brand assets, brand standards, pricing sheet, EDI setup, compliance forms, and a follow-up for whatever they didn't send. This workflow handles the entire sequence.
{
"name": "Retailer Onboarding Sequence",
"nodes": [
{
"id": "1",
"name": "New Retailer Webhook",
"type": "n8n-nodes-base.webhook",
"parameters": { "httpMethod": "POST", "path": "new-retailer-onboard" },
"position": [240, 300]
},
{
"id": "2",
"name": "Log to CRM Sheet",
"type": "n8n-nodes-base.googleSheets",
"parameters": {
"operation": "append",
"documentId": "YOUR_SHEET_ID",
"sheetName": "retailer_pipeline",
"columns": { "mappingMode": "autoMapInputData" }
},
"position": [440, 300]
},
{
"id": "3",
"name": "Welcome + Brand Kit Email",
"type": "n8n-nodes-base.gmail",
"parameters": {
"operation": "send",
"toList": "={{ $json.buyer_email }}",
"subject": "={{ 'Welcome to ' + $json.brand_name + ' — Your Onboarding Checklist' }}",
"message": "={{ '<h2>Welcome, ' + $json.buyer_name + '</h2><p>We are excited to partner with ' + $json.retailer_name + '. Here is everything you need to get started:</p><ol><li>Brand assets: <a href=\"' + $json.brand_kit_url + '\">Download brand kit</a></li><li>Pricing sheet: Attached separately</li><li>EDI setup: Reply to this email with your EDI provider details</li><li>Vendor compliance: <a href=\"' + $json.compliance_form_url + '\">Complete vendor form</a></li></ol><p>Your dedicated account manager is ' + $json.account_manager_name + ' (' + $json.account_manager_email + ').</p>' }}",
"options": { "appendAttribution": false }
},
"position": [640, 300]
},
{
"id": "4",
"name": "Wait 3 Days",
"type": "n8n-nodes-base.wait",
"parameters": { "resume": "timeInterval", "unit": "days", "amount": 3 },
"position": [840, 300]
},
{
"id": "5",
"name": "Follow-Up Email",
"type": "n8n-nodes-base.gmail",
"parameters": {
"operation": "send",
"toList": "={{ $json.buyer_email }}",
"subject": "={{ 'Quick check-in — ' + $json.retailer_name + ' onboarding' }}",
"message": "={{ '<p>Hi ' + $json.buyer_name + ' — just checking in on your onboarding checklist. Let us know if you have any questions on the brand assets, pricing, or EDI setup. Happy to jump on a quick call.</p>' }}",
"options": { "appendAttribution": false }
},
"position": [1040, 300]
},
{
"id": "6",
"name": "Notify Account Manager (Slack)",
"type": "n8n-nodes-base.slack",
"parameters": {
"operation": "post",
"channel": "#retail-sales",
"text": "={{ '🤝 New retailer onboarding started: *' + $json.retailer_name + '* (' + $json.buyer_name + ', ' + $json.buyer_email + '). AM: ' + $json.account_manager_name + '. Follow-up scheduled in 3 days.' }}"
},
"position": [1240, 300]
}
],
"connections": {
"New Retailer Webhook": { "main": [[{ "node": "Log to CRM Sheet", "type": "main", "index": 0 }]] },
"Log to CRM Sheet": { "main": [[{ "node": "Welcome + Brand Kit Email", "type": "main", "index": 0 }]] },
"Welcome + Brand Kit Email": { "main": [[{ "node": "Wait 3 Days", "type": "main", "index": 0 }]] },
"Wait 3 Days": { "main": [[{ "node": "Follow-Up Email", "type": "main", "index": 0 }]] },
"Follow-Up Email": { "main": [[{ "node": "Notify Account Manager (Slack)", "type": "main", "index": 0 }]] }
}
}
4. Product Launch & Channel Rollout Coordinator
When you launch a new SKU, distributors, retailers, and internal teams all need different information at different times. Doing it manually means some partners get notified late and the launch window shrinks.
{
"name": "Product Launch Coordinator",
"nodes": [
{
"id": "1",
"name": "Launch Webhook",
"type": "n8n-nodes-base.webhook",
"parameters": { "httpMethod": "POST", "path": "product-launch" },
"position": [240, 300]
},
{
"id": "2",
"name": "Build Launch Summary",
"type": "n8n-nodes-base.code",
"parameters": {
"jsCode": "const l = $input.first().json;\nconst launchDate = new Date(l.launch_date);\nconst daysUntil = Math.floor((launchDate - new Date()) / 86400000);\nreturn [{ json: { ...l, daysUntil, urgency: daysUntil <= 7 ? 'URGENT' : daysUntil <= 14 ? 'HEADS UP' : 'UPCOMING' } }];"
},
"position": [440, 300]
},
{
"id": "3",
"name": "Email Distributors",
"type": "n8n-nodes-base.gmail",
"parameters": {
"operation": "send",
"toList": "={{ $json.distributor_email }}",
"subject": "={{ '[' + $json.urgency + '] New SKU Launch: ' + $json.product_name + ' — ' + $json.launch_date }}",
"message": "={{ '<h2>' + $json.urgency + ': ' + $json.product_name + ' Launches ' + $json.launch_date + '</h2><p>SKU: ' + $json.sku + ' | UPC: ' + $json.upc + ' | MSRP: $' + $json.msrp + ' | Case pack: ' + $json.case_pack + '</p><p>Channels: ' + $json.channels + '</p><p>' + (l.distributor_notes || '') + '</p>' }}",
"options": { "appendAttribution": false }
},
"position": [640, 200]
},
{
"id": "4",
"name": "Slack #product-launch",
"type": "n8n-nodes-base.slack",
"parameters": {
"operation": "post",
"channel": "#product-launch",
"text": "={{ '🚀 *' + $json.product_name + '* (' + $json.sku + ') — ' + $json.urgency + ': Launching ' + $json.launch_date + ' (' + $json.daysUntil + ' days). MSRP: $' + $json.msrp + '. Channels: ' + $json.channels }}"
},
"position": [640, 400]
},
{
"id": "5",
"name": "Log to Launch Tracker",
"type": "n8n-nodes-base.googleSheets",
"parameters": {
"operation": "append",
"documentId": "YOUR_SHEET_ID",
"sheetName": "launch_tracker",
"columns": { "mappingMode": "autoMapInputData" }
},
"position": [840, 300]
}
],
"connections": {
"Launch Webhook": { "main": [[{ "node": "Build Launch Summary", "type": "main", "index": 0 }]] },
"Build Launch Summary": { "main": [[{ "node": "Email Distributors", "type": "main", "index": 0 }, { "node": "Slack #product-launch", "type": "main", "index": 0 }]] },
"Email Distributors": { "main": [[{ "node": "Log to Launch Tracker", "type": "main", "index": 0 }]] }
}
}
5. Weekly Retailer Performance & Sell-Through Digest
You have 20 retail partners. Every Friday your sales team manually pulls sell-through from each, builds a spreadsheet, and emails it to leadership. This workflow does it automatically — and flags underperformers.
{
"name": "Weekly Retailer Performance Digest",
"nodes": [
{
"id": "1",
"name": "Friday 4PM",
"type": "n8n-nodes-base.scheduleTrigger",
"parameters": {
"rule": { "interval": [{ "field": "cronExpression", "expression": "0 16 * * 5" }] }
},
"position": [240, 300]
},
{
"id": "2",
"name": "Get Sell-Through Data",
"type": "n8n-nodes-base.googleSheets",
"parameters": {
"operation": "readRows",
"documentId": "YOUR_SHEET_ID",
"sheetName": "weekly_sellthrough"
},
"position": [440, 300]
},
{
"id": "3",
"name": "Compute Retailer Rankings",
"type": "n8n-nodes-base.code",
"parameters": {
"jsCode": "const rows = $input.all();\nconst thisWeek = new Date();\nconst weekStr = thisWeek.toISOString().slice(0,10);\nconst byRetailer = {};\nrows.forEach(r => {\n const ret = r.json.retailer;\n byRetailer[ret] = byRetailer[ret] || { retailer: ret, units: 0, revenue: 0, units_prev: 0, revenue_prev: 0, sellthrough_pct: 0 };\n if (r.json.week === weekStr) {\n byRetailer[ret].units += Number(r.json.units||0);\n byRetailer[ret].revenue += Number(r.json.revenue||0);\n byRetailer[ret].stock_on_hand = Number(r.json.stock_on_hand||0);\n }\n const prevWeekStr = new Date(thisWeek - 7*86400000).toISOString().slice(0,10);\n if (r.json.week === prevWeekStr) {\n byRetailer[ret].units_prev += Number(r.json.units||0);\n }\n});\nconst retailers = Object.values(byRetailer).map(r => ({\n ...r,\n wow_pct: r.units_prev > 0 ? ((r.units - r.units_prev) / r.units_prev * 100).toFixed(1) : '—',\n sellthrough_pct: (r.stock_on_hand + r.units) > 0 ? ((r.units / (r.stock_on_hand + r.units)) * 100).toFixed(1) : '—',\n flag: r.units < r.units_prev * 0.7 ? '🔴 UNDERPERFORMING' : r.units > r.units_prev * 1.2 ? '🟢 OUTPERFORMING' : ''\n})).sort((a,b) => b.revenue - a.revenue);\nconst totalRevenue = retailers.reduce((s,r) => s + r.revenue, 0);\nconst totalUnits = retailers.reduce((s,r) => s + r.units, 0);\nreturn [{ json: { weekStr, retailers, totalRevenue: totalRevenue.toFixed(2), totalUnits, reportDate: new Date().toLocaleDateString() } }];"
},
"position": [640, 300]
},
{
"id": "4",
"name": "Build HTML Report",
"type": "n8n-nodes-base.code",
"parameters": {
"jsCode": "const d = $input.first().json;\nconst rows = d.retailers.map(r => `<tr><td>${r.retailer}</td><td>${r.units}</td><td>$${r.revenue.toFixed(2)}</td><td>${r.wow_pct}%</td><td>${r.sellthrough_pct}%</td><td>${r.flag}</td></tr>`).join('');\nconst html = `<h2>Weekly Retailer Performance — ${d.reportDate}</h2><p><strong>Total:</strong> ${d.totalUnits} units | $${d.totalRevenue}</p><table border=1 cellpadding=6><tr><th>Retailer</th><th>Units</th><th>Revenue</th><th>WoW%</th><th>Sell-Through%</th><th>Status</th></tr>${rows}</table><p><small>Auto-generated by n8n</small></p>`;\nreturn [{ json: { ...d, html } }];"
},
"position": [840, 300]
},
{
"id": "5",
"name": "Email Sales Leadership",
"type": "n8n-nodes-base.gmail",
"parameters": {
"operation": "send",
"toList": "sales-leadership@yourcompany.com",
"subject": "={{ 'Weekly Retailer Performance — ' + $json.reportDate + ' | $' + $json.totalRevenue }}",
"message": "={{ $json.html }}",
"options": { "appendAttribution": false }
},
"position": [1040, 300]
},
{
"id": "6",
"name": "Slack Underperformers Alert",
"type": "n8n-nodes-base.code",
"parameters": {
"jsCode": "const underperformers = $input.first().json.retailers.filter(r => r.flag.includes('UNDERPERFORMING'));\nif (underperformers.length === 0) return [];\nreturn [{ json: { alert: underperformers.map(r => r.retailer + ' (' + r.wow_pct + '% WoW)').join(', ') } }];"
},
"position": [1040, 450]
},
{
"id": "7",
"name": "Slack #retail-sales",
"type": "n8n-nodes-base.slack",
"parameters": {
"operation": "post",
"channel": "#retail-sales",
"text": "={{ '🔴 Underperforming retailers this week: ' + $json.alert + ' — review and escalate.' }}"
},
"position": [1240, 450]
}
],
"connections": {
"Friday 4PM": { "main": [[{ "node": "Get Sell-Through Data", "type": "main", "index": 0 }]] },
"Get Sell-Through Data": { "main": [[{ "node": "Compute Retailer Rankings", "type": "main", "index": 0 }]] },
"Compute Retailer Rankings": { "main": [[{ "node": "Build HTML Report", "type": "main", "index": 0 }], [{ "node": "Slack Underperformers Alert", "type": "main", "index": 0 }]] },
"Build HTML Report": { "main": [[{ "node": "Email Sales Leadership", "type": "main", "index": 0 }]] },
"Slack Underperformers Alert": { "main": [[{ "node": "Slack #retail-sales", "type": "main", "index": 0 }]] }
}
}
The ROI math
| Workflow | Manual time saved | Value/week (at $60/hr ops cost) |
|---|---|---|
| Daily Sales Digest | 1.5 hr/day, 5 days | $450 |
| Promo Price Monitor | 30 min/day check | $75 + violation recovery |
| Retailer Onboarding | 2 hr per new retailer | $120/onboarding |
| Launch Coordinator | 3 hr per launch | $180/launch |
| Weekly Digest | 3 hr/week | $180 |
Conservative total: $700–1,000/week in ops time freed up.
But the bigger value is in recoveries — catching a promo pricing violation on day 1 vs day 10 can mean the difference between a successful promotional period and a complete write-off.
n8n vs Zapier/Make for CPG brands
| Factor | n8n (self-hosted) | Zapier | Make.com |
|---|---|---|---|
| Pricing data routing | Stays inside your infra | Third-party cloud | Third-party cloud |
| Retailer NDA compliance | ✅ data sovereignty | ❌ vendor exposure | ❌ vendor exposure |
| CCPA/GDPR | On-prem, no vendor DPA needed | DPA required | DPA required |
| Operations cost at 50k ops/mo | ~$5 server | $299/mo | $99/mo |
| Custom business logic | JavaScript in Code nodes | Formatter nodes only | Formula nodes only |
What to build next
If you want pre-built versions of the Daily Sales Digest, Price Monitor, and Retailer Onboarding Sequence — ready to import into your n8n instance with documentation — they are available at stripeai.gumroad.com.
The Daily Report Generator ($29) and Price Monitor ($29) are the ones that map most directly to workflows 1 and 2 above. Both include the workflow JSON + a setup guide + a sample Google Sheet template.
Questions about adapting these for your retail stack? Drop a comment below.
Top comments (0)