RetailTech SaaS vendors — POS platforms, loyalty/retention engines, inventory management SaaS, e-commerce platform operators — run the data layer for modern commerce. When your internal automation routes through Zapier or Make, PCI cardholder data, CCPA-protected purchase histories, and GDPR-regulated EU shopper profiles pass through a third-party multi-tenant cloud you do not control.
n8n changes the equation. Self-host your automation layer inside your own VPC, and data never leaves your infrastructure boundary. Your PCI scope stays defined. Your CCPA "third party" list shrinks. Your EU sub-processor DPA is clean.
Five production-ready workflows below — each with full import-ready JSON.
1. New Retail Brand Client Onboarding Drip
Problem: New enterprise retailers need immediate ROI evidence to justify the seat count. A generic welcome email is not that. Tier-aware onboarding sends the right setup wizard, integration guide, and check-in call at exactly day 0, 3, and 7 — scoped to their store count and SKU volume.
Why n8n: The PII in your onboarding sequence (contact name, email, company size, revenue tier) is logged in every Zapier task execution — a CC7.2 SOC2 gap and a CCPA third-party disclosure you have to document.
{
"name": "RetailTech: New Client Onboarding Drip",
"nodes": [
{
"parameters": {
"documentId": "YOUR_SHEET_ID",
"sheetName": "New Clients",
"event": "rowAdded"
},
"type": "n8n-nodes-base.googleSheetsTrigger",
"name": "New Client Added",
"position": [0, 0]
},
{
"parameters": {
"jsCode": "const stores = $json.store_count || 0;\nconst seats = $json.seat_count || 0;\nconst gmv = $json.annual_gmv_usd || 0;\nconst tier = (() => {\n if (stores >= 500 || seats >= 1000 || gmv >= 50000000) return 'ENTERPRISE_CHAIN';\n if (stores >= 50 || seats >= 100 || gmv >= 5000000) return 'MID_SIZE_RETAILER';\n if (stores >= 5 || seats >= 10 || gmv >= 500000) return 'SMALL_BUSINESS';\n return 'STARTER';\n})();\nconst tierConfig = {\n ENTERPRISE_CHAIN: { sla: '2h', csm: true, dedicated_slack: true, integrations: 'POS+ERP+Loyalty+WMS' },\n MID_SIZE_RETAILER: { sla: '4h', csm: true, dedicated_slack: false, integrations: 'POS+Inventory' },\n SMALL_BUSINESS: { sla: 'next_business_day', csm: false, dedicated_slack: false, integrations: 'POS' },\n STARTER: { sla: 'email_only', csm: false, dedicated_slack: false, integrations: 'basic' }\n};\nreturn [{ json: { ...$json, tier, config: tierConfig[tier], onboard_ts: new Date().toISOString() } }];"
},
"type": "n8n-nodes-base.code",
"name": "Classify Tier",
"position": [200, 0]
},
{
"parameters": {
"fromEmail": "onboarding@yourplatform.com",
"toEmail": "={{ $json.contact_email }}",
"subject": "={{ $json.tier === 'ENTERPRISE_CHAIN' ? 'Your dedicated setup team is ready — ' : 'Get your first store live in 30 minutes — ' }}{{ $json.company_name }}",
"html": "={{ '<p>Hi ' + $json.contact_name + ',</p><p>Tier: ' + $json.tier + '. Integrations ready: ' + $json.config.integrations + '</p>' }}"
},
"type": "n8n-nodes-base.gmail",
"name": "Day 0 Email",
"position": [400, 0]
},
{
"parameters": { "amount": 3, "unit": "days" },
"type": "n8n-nodes-base.wait",
"name": "Wait 3 Days",
"position": [600, 0]
},
{
"parameters": {
"fromEmail": "onboarding@yourplatform.com",
"toEmail": "={{ $json.contact_email }}",
"subject": "Day 3 check-in — how is the POS integration going?",
"html": "={{ '<p>Hi ' + $json.contact_name + ',</p><p>Quick check-in: your ' + $json.config.integrations + ' integration should be live. Any blockers?</p>' }}"
},
"type": "n8n-nodes-base.gmail",
"name": "Day 3 Check-In",
"position": [800, 0]
},
{
"parameters": {
"authentication": "oAuth2",
"operation": "appendOrUpdate",
"documentId": "YOUR_AUDIT_SHEET_ID",
"sheetName": "OnboardingLog",
"columns": {
"mappingMode": "defineBelow",
"values": [
{ "column": "client_id", "fieldValue": "={{ $json.client_id }}" },
{ "column": "tier", "fieldValue": "={{ $json.tier }}" },
{ "column": "onboard_ts", "fieldValue": "={{ $json.onboard_ts }}" },
{ "column": "compliance_note", "fieldValue": "SOC2_CC7.1_AUDIT / CCPA_THIRD_PARTY_DPA_NOT_REQUIRED" }
]
}
},
"type": "n8n-nodes-base.googleSheets",
"name": "Audit Log",
"position": [1000, 0]
}
],
"connections": {
"New Client Added": { "main": [[{ "node": "Classify Tier", "type": "main", "index": 0 }]] },
"Classify Tier": { "main": [[{ "node": "Day 0 Email", "type": "main", "index": 0 }]] },
"Day 0 Email": { "main": [[{ "node": "Wait 3 Days", "type": "main", "index": 0 }]] },
"Wait 3 Days": { "main": [[{ "node": "Day 3 Check-In", "type": "main", "index": 0 }]] },
"Day 3 Check-In": { "main": [[{ "node": "Audit Log", "type": "main", "index": 0 }]] }
}
}
2. POS API & Inventory Health Monitor
Problem: When your POS integration goes down at 11 AM on a Saturday — peak retail hours — the first person who knows is the store manager, not your team. By then you have 30 angry support tickets. A 3-minute health monitor pages your on-call engineer before the first ticket.
Why n8n: Monitor polling runs continuously on a schedule. Zapier workflow execution is billed per task — 3-min polling = 20 runs/hour = 480 runs/day per endpoint. At 1,000 retail clients each with 3 POS endpoints, that is 1.44 million tasks per day. n8n runs for $60/month on a VPS regardless of event volume.
{
"name": "RetailTech: POS & Inventory Health Monitor",
"nodes": [
{
"parameters": { "rule": { "interval": [{ "field": "minutes", "minutesInterval": 3 }] } },
"type": "n8n-nodes-base.scheduleTrigger",
"name": "Every 3 Minutes",
"position": [0, 0]
},
{
"parameters": {
"operation": "getAll",
"documentId": "YOUR_SHEET_ID",
"sheetName": "POSEndpoints",
"returnAll": true
},
"type": "n8n-nodes-base.googleSheets",
"name": "Get Endpoints",
"position": [200, 0]
},
{
"parameters": {
"url": "={{ $json.health_url }}",
"options": { "timeout": 8000 },
"continueOnFail": true
},
"type": "n8n-nodes-base.httpRequest",
"name": "Ping Endpoint",
"position": [400, 0]
},
{
"parameters": {
"jsCode": "const item = $json;\nconst prev = $getWorkflowStaticData('node');\nconst now = Date.now();\nconst lastAlert = prev[item.endpoint_id] || 0;\nconst suppress = (now - lastAlert) < 30 * 60 * 1000;\nconst responseTime = item.$response?.responseTime || 9999;\nconst lastUpdated = item.last_data_ts ? (now - new Date(item.last_data_ts).getTime()) / 60000 : 999;\nlet status;\nif (item.$error || responseTime > 8000) status = 'DOWN';\nelse if (lastUpdated > 15) status = 'STALE_DATA';\nelse if (responseTime > 3000) status = 'DEGRADED';\nelse status = 'OK';\nif (status !== 'OK' && !suppress) prev[item.endpoint_id] = now;\nreturn [{ json: { ...item, status, responseTime, suppress, alertable: status !== 'OK' && !suppress } }];"
},
"type": "n8n-nodes-base.code",
"name": "Classify Status",
"position": [600, 0]
},
{
"parameters": { "conditions": { "boolean": [{ "value1": "={{ $json.alertable }}", "value2": true }] } },
"type": "n8n-nodes-base.filter",
"name": "Filter Alertable",
"position": [800, 0]
},
{
"parameters": {
"authentication": "oAuth2",
"channel": "#platform-ops",
"text": "={{ '🔴 [' + $json.status + '] ' + $json.client_name + ' — ' + $json.endpoint_name + ' (' + $json.endpoint_id + ')\nResponse: ' + $json.responseTime + 'ms | Region: ' + $json.region + '\nAction: ' + (($json.status === 'DOWN') ? 'Check integration bridge + retry auth' : 'Check data pipeline sync') }}"
},
"type": "n8n-nodes-base.slack",
"name": "Alert Slack",
"position": [1000, -100]
},
{
"parameters": {
"operation": "insert",
"schema": "public",
"table": "pos_incidents",
"columns": "client_id, endpoint_id, status, response_time_ms, checked_at",
"additionalFields": { "conflictAction": "ignore" }
},
"type": "n8n-nodes-base.postgres",
"name": "Log Incident",
"position": [1000, 100]
}
],
"connections": {
"Every 3 Minutes": { "main": [[{ "node": "Get Endpoints", "type": "main", "index": 0 }]] },
"Get Endpoints": { "main": [[{ "node": "Ping Endpoint", "type": "main", "index": 0 }]] },
"Ping Endpoint": { "main": [[{ "node": "Classify Status", "type": "main", "index": 0 }]] },
"Classify Status": { "main": [[{ "node": "Filter Alertable", "type": "main", "index": 0 }]] },
"Filter Alertable": { "main": [[{ "node": "Alert Slack", "type": "main", "index": 0 }, { "node": "Log Incident", "type": "main", "index": 0 }]] }
}
}
3. PCI-DSS & Retail Compliance Deadline Tracker
Problem: RetailTech SaaS vendors carry their own PCI-DSS SAQ/QSA obligation, CCPA data inventory and DSR deadlines, GDPR Art.30 records-of-processing review, and a growing list of US state privacy laws (Iowa CPA, Texas TDPSA, Colorado CPA, Virginia VCDPA). Miss a deadline and you face a QSA finding, a $2,500–$7,500/violation CCPA penalty, or a GDPR Art.83 fine.
Why n8n: Your compliance deadlines are internal operational data. Routing them through Zapier adds a third-party to your data chain that must appear on your Art.30 records. n8n keeps the loop inside your boundary.
{
"name": "RetailTech: PCI & Compliance Deadline Tracker",
"nodes": [
{
"parameters": { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 8 * * 1-5" }] } },
"type": "n8n-nodes-base.scheduleTrigger",
"name": "Weekdays 8 AM",
"position": [0, 0]
},
{
"parameters": {
"operation": "getAll",
"documentId": "YOUR_SHEET_ID",
"sheetName": "ComplianceDeadlines",
"returnAll": true
},
"type": "n8n-nodes-base.googleSheets",
"name": "Get Deadlines",
"position": [200, 0]
},
{
"parameters": {
"jsCode": "const today = new Date();\nconst deadline = new Date($json.deadline_date);\nconst daysLeft = Math.ceil((deadline - today) / 86400000);\nconst actionMap = {\n PCI_DSS_SAQ_ANNUAL: 'Complete SAQ-D self-assessment + submit to acquiring bank',\n PCI_DSS_QSA_ANNUAL: 'Schedule QSA audit + prepare ROC evidence package',\n PCI_DSS_ASV_SCAN: 'Run ASV scan via approved vendor + remediate failures',\n CCPA_DATA_INVENTORY: 'Complete annual data inventory — all categories of PI collected, sources, purposes',\n CCPA_DSR_30DAY: 'Respond to pending data subject requests within 30-day CCPA window',\n GDPR_DPA_REVIEW: 'Review data processing agreements with all EU data processors',\n GDPR_ART_30: 'Update records of processing activities — all vendors + data flows',\n SOC2_TYPE2_AUDIT: 'Begin evidence collection for SOC2 Type II audit window',\n IOWA_CPA_DSR: 'Iowa Consumer Privacy Act: respond to pending DSRs within 90 days',\n TEXAS_TDPSA_DPIA: 'Texas TDPSA: complete data protection impact assessment for high-risk processing',\n COLORADO_CPA_OPT_OUT: 'Colorado CPA: confirm opt-out signals honored in ad targeting integrations',\n FTC_SAFEGUARDS: 'FTC Safeguards Rule: annual security program review for financial data elements'\n};\nconst tier = daysLeft < 0 ? 'OVERDUE' : daysLeft <= 3 ? 'CRITICAL' : daysLeft <= 7 ? 'URGENT' : daysLeft <= 14 ? 'WARNING' : 'NOTICE';\nreturn [{ json: { ...$json, daysLeft, tier, action: actionMap[$json.compliance_type] || 'Review compliance requirement' } }];"
},
"type": "n8n-nodes-base.code",
"name": "Classify Deadline",
"position": [400, 0]
},
{
"parameters": {
"conditions": { "string": [{ "value1": "={{ $json.tier }}", "operation": "notEqual", "value2": "NOTICE" }] }
},
"type": "n8n-nodes-base.filter",
"name": "Filter Actionable",
"position": [600, 0]
},
{
"parameters": {
"dataPropertyName": "tier",
"rules": {
"rules": [
{ "value": "OVERDUE", "output": 0 },
{ "value": "CRITICAL", "output": 1 },
{ "value": "URGENT", "output": 2 },
{ "value": "WARNING", "output": 3 }
]
}
},
"type": "n8n-nodes-base.switch",
"name": "Route by Tier",
"position": [800, 0]
},
{
"parameters": {
"authentication": "oAuth2",
"channel": "#compliance-ops",
"text": "={{ '⚠️ [' + $json.tier + '] ' + $json.compliance_type + '\n' + ($json.daysLeft < 0 ? 'OVERDUE by ' + Math.abs($json.daysLeft) + ' days' : $json.daysLeft + ' days remaining') + '\nOwner: ' + $json.owner_email + '\nAction: ' + $json.action }}"
},
"type": "n8n-nodes-base.slack",
"name": "Slack #compliance-ops",
"position": [1000, 0]
},
{
"parameters": {
"fromEmail": "compliance@yourplatform.com",
"toEmail": "={{ $json.owner_email }}",
"subject": "={{ '[ACTION REQUIRED] ' + $json.compliance_type + ' — ' + ($json.daysLeft < 0 ? 'OVERDUE' : $json.daysLeft + ' days left') }}",
"text": "={{ $json.action + '\n\nDeadline: ' + $json.deadline_date + '\nTier: ' + $json.tier }}"
},
"type": "n8n-nodes-base.gmail",
"name": "Email Owner",
"position": [1000, 200]
}
],
"connections": {
"Weekdays 8 AM": { "main": [[{ "node": "Get Deadlines", "type": "main", "index": 0 }]] },
"Get Deadlines": { "main": [[{ "node": "Classify Deadline", "type": "main", "index": 0 }]] },
"Classify Deadline": { "main": [[{ "node": "Filter Actionable", "type": "main", "index": 0 }]] },
"Filter Actionable": { "main": [[{ "node": "Route by Tier", "type": "main", "index": 0 }]] },
"Route by Tier": {
"main": [
[{ "node": "Slack #compliance-ops", "type": "main", "index": 0 }, { "node": "Email Owner", "type": "main", "index": 0 }],
[{ "node": "Slack #compliance-ops", "type": "main", "index": 0 }, { "node": "Email Owner", "type": "main", "index": 0 }],
[{ "node": "Slack #compliance-ops", "type": "main", "index": 0 }],
[{ "node": "Slack #compliance-ops", "type": "main", "index": 0 }]
]
}
}
}
4. Customer GMV Churn & Account Health Alert
Problem: A retail brand goes quiet — fewer POS transactions, fewer inventory syncs, no logins for two weeks. That is a churn signal. Catching it on day 3 is a save. Catching it on day 21 is a post-mortem.
Why n8n: Customer transaction volumes and usage patterns are the most commercially sensitive data your platform holds. Routing them through Zapier each day means they appear in a third-party activity log tied to your Zapier account — a CCPA third-party disclosure and a SOC2 confidentiality gap.
{
"name": "RetailTech: Customer Health Alert",
"nodes": [
{
"parameters": { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 * * *" }] } },
"type": "n8n-nodes-base.scheduleTrigger",
"name": "Daily 9 AM",
"position": [0, 0]
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT a.client_id, a.company_name, a.csm_email, m.gmv_7d, m.gmv_7d_prev, m.api_calls_7d, m.api_calls_7d_prev, m.last_login_ts, m.active_store_count, m.active_store_count_prev FROM accounts a JOIN client_metrics m ON a.client_id = m.client_id WHERE a.status = 'active'"
},
"type": "n8n-nodes-base.postgres",
"name": "Query Metrics",
"position": [200, 0]
},
{
"parameters": {
"jsCode": "const now = new Date();\nconst lastLogin = $json.last_login_ts ? new Date($json.last_login_ts) : null;\nconst daysSinceLogin = lastLogin ? Math.floor((now - lastLogin) / 86400000) : 999;\nconst gmvDrop = $json.gmv_7d_prev > 0 ? (($json.gmv_7d_prev - $json.gmv_7d) / $json.gmv_7d_prev) * 100 : 0;\nconst apiDrop = $json.api_calls_7d_prev > 0 ? (($json.api_calls_7d_prev - $json.api_calls_7d) / $json.api_calls_7d_prev) * 100 : 0;\nconst storeDrop = $json.active_store_count_prev > 0 ? (($json.active_store_count_prev - $json.active_store_count) / $json.active_store_count_prev) * 100 : 0;\nconst isRed = gmvDrop >= 50 || apiDrop >= 40 || daysSinceLogin >= 21 || storeDrop >= 30;\nconst isAmber = !isRed && (gmvDrop >= 20 || apiDrop >= 20 || daysSinceLogin >= 14 || storeDrop >= 15);\nconst tier = isRed ? 'RED' : isAmber ? 'AMBER' : 'GREEN';\nreturn [{ json: { ...$json, tier, gmvDrop: gmvDrop.toFixed(1), apiDrop: apiDrop.toFixed(1), storeDrop: storeDrop.toFixed(1), daysSinceLogin } }];"
},
"type": "n8n-nodes-base.code",
"name": "Score Health",
"position": [400, 0]
},
{
"parameters": {
"conditions": { "string": [{ "value1": "={{ $json.tier }}", "operation": "notEqual", "value2": "GREEN" }] }
},
"type": "n8n-nodes-base.filter",
"name": "Filter At-Risk",
"position": [600, 0]
},
{
"parameters": {
"authentication": "oAuth2",
"channel": "={{ $json.tier === 'RED' ? '#cs-churn-risk' : '#cs-watch-list' }}",
"text": "={{ ($json.tier === 'RED' ? '🔴 CHURN RISK' : '🟡 WATCH') + ': ' + $json.company_name + '\nGMV drop: ' + $json.gmvDrop + '% | API drop: ' + $json.apiDrop + '% | Last login: ' + $json.daysSinceLogin + 'd ago | Stores offline: ' + $json.storeDrop + '%\nCSM: ' + $json.csm_email }}"
},
"type": "n8n-nodes-base.slack",
"name": "Slack Alert",
"position": [800, -100]
},
{
"parameters": {
"conditions": { "string": [{ "value1": "={{ $json.tier }}", "value2": "RED" }] }
},
"type": "n8n-nodes-base.filter",
"name": "RED Only",
"position": [800, 100]
},
{
"parameters": {
"fromEmail": "success@yourplatform.com",
"toEmail": "={{ $json.csm_email }}",
"subject": "={{ '[CHURN RISK] ' + $json.company_name + ' — GMV down ' + $json.gmvDrop + '%' }}",
"text": "={{ 'Please reach out today. GMV -' + $json.gmvDrop + '% WoW, API calls -' + $json.apiDrop + '%, last login ' + $json.daysSinceLogin + ' days ago.' }}"
},
"type": "n8n-nodes-base.gmail",
"name": "Email CSM",
"position": [1000, 100]
}
],
"connections": {
"Daily 9 AM": { "main": [[{ "node": "Query Metrics", "type": "main", "index": 0 }]] },
"Query Metrics": { "main": [[{ "node": "Score Health", "type": "main", "index": 0 }]] },
"Score Health": { "main": [[{ "node": "Filter At-Risk", "type": "main", "index": 0 }]] },
"Filter At-Risk": { "main": [[{ "node": "Slack Alert", "type": "main", "index": 0 }, { "node": "RED Only", "type": "main", "index": 0 }]] },
"RED Only": { "main": [[{ "node": "Email CSM", "type": "main", "index": 0 }]] }
}
}
5. Weekly RetailTech Platform KPI Dashboard
Problem: Your VP Sales, VP Customer Success, and CEO need Monday morning numbers: GMV processed, active stores, POS uptime, churn risk accounts, trial-to-paid conversion. Building this manually every Monday is 2 hours of copy-paste from dashboards that should talk to each other.
Why n8n: Platform KPIs aggregate revenue figures, churn rates, and customer counts — commercially sensitive data. Self-host the aggregation; send the result.
{
"name": "RetailTech: Weekly KPI Dashboard",
"nodes": [
{
"parameters": { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 8 * * 1" }] } },
"type": "n8n-nodes-base.scheduleTrigger",
"name": "Monday 8 AM",
"position": [0, 0]
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT SUM(gmv_7d) as total_gmv_7d, SUM(gmv_7d_prev) as total_gmv_7d_prev, COUNT(*) FILTER (WHERE status='active') as active_clients, COUNT(*) FILTER (WHERE status='trial') as trial_clients, COUNT(*) FILTER (WHERE status='churned' AND churned_at > NOW()-INTERVAL '7 days') as churned_7d, SUM(active_store_count) as total_stores FROM accounts a JOIN client_metrics m ON a.client_id = m.client_id"
},
"type": "n8n-nodes-base.postgres",
"name": "Platform Metrics",
"position": [200, -100]
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT COUNT(*) FILTER (WHERE tier='RED') as churn_risk_red, COUNT(*) FILTER (WHERE tier='AMBER') as churn_risk_amber, AVG(CASE WHEN status='active' THEN 1.0 ELSE 0 END)*100 as pos_uptime_pct FROM client_metrics m JOIN accounts a ON m.client_id=a.client_id"
},
"type": "n8n-nodes-base.postgres",
"name": "Account Health",
"position": [200, 100]
},
{
"parameters": { "mode": "combine", "combinationMode": "multiplex" },
"type": "n8n-nodes-base.merge",
"name": "Merge",
"position": [400, 0]
},
{
"parameters": {
"jsCode": "const prev = $getWorkflowStaticData('global');\nconst d = $json;\nconst gmvWoW = d.total_gmv_7d_prev > 0 ? (((d.total_gmv_7d - d.total_gmv_7d_prev) / d.total_gmv_7d_prev) * 100).toFixed(1) : 'N/A';\nprev.last_gmv = d.total_gmv_7d;\nprev.last_active = d.active_clients;\nconst trialConversion = d.trial_clients > 0 ? ((d.active_clients / (d.active_clients + d.trial_clients)) * 100).toFixed(1) : 'N/A';\nconst gmvColor = parseFloat(gmvWoW) >= 5 ? '#27ae60' : parseFloat(gmvWoW) >= 0 ? '#f39c12' : '#e74c3c';\nconst html = [\n '<h2>RetailTech Weekly KPIs — ' + new Date().toISOString().slice(0,10) + '</h2>',\n '<table border=\"1\" cellpadding=\"8\" style=\"border-collapse:collapse;font-family:Arial\">',\n '<tr><th>Metric</th><th>Value</th><th>WoW</th></tr>',\n '<tr><td>Total GMV (7d)</td><td>$' + Number(d.total_gmv_7d).toLocaleString() + '</td><td style=\"color:' + gmvColor + '\">' + gmvWoW + '%</td></tr>',\n '<tr><td>Active Clients</td><td>' + d.active_clients + '</td><td>—</td></tr>',\n '<tr><td>Total Stores Live</td><td>' + d.total_stores + '</td><td>—</td></tr>',\n '<tr><td>Churn Risk (RED)</td><td style=\"color:#e74c3c\">' + d.churn_risk_red + '</td><td>—</td></tr>',\n '<tr><td>Churn Risk (AMBER)</td><td style=\"color:#f39c12\">' + d.churn_risk_amber + '</td><td>—</td></tr>',\n '<tr><td>Trial→Paid Rate</td><td>' + trialConversion + '%</td><td>—</td></tr>',\n '<tr><td>Churned (7d)</td><td>' + d.churned_7d + '</td><td>—</td></tr>',\n '</table>'\n].join('');\nreturn [{ json: { ...d, html, gmvWoW, trialConversion } }];"
},
"type": "n8n-nodes-base.code",
"name": "Build KPI HTML",
"position": [600, 0]
},
{
"parameters": {
"fromEmail": "kpis@yourplatform.com",
"toEmail": "ceo@yourplatform.com",
"ccEmail": "vp-cs@yourplatform.com, vp-sales@yourplatform.com, coo@yourplatform.com",
"subject": "RetailTech Weekly KPIs — {{ $json.gmvWoW }}% GMV WoW",
"html": "={{ $json.html }}"
},
"type": "n8n-nodes-base.gmail",
"name": "Email KPI Report",
"position": [800, -100]
},
{
"parameters": {
"authentication": "oAuth2",
"channel": "#exec-kpis",
"text": "={{ '📊 Weekly: GMV $' + Number($json.total_gmv_7d).toLocaleString() + ' (' + $json.gmvWoW + '% WoW) | ' + $json.active_clients + ' clients | ' + $json.total_stores + ' stores | RED: ' + $json.churn_risk_red }}"
},
"type": "n8n-nodes-base.slack",
"name": "Slack One-Liner",
"position": [800, 100]
}
],
"connections": {
"Monday 8 AM": { "main": [[{ "node": "Platform Metrics", "type": "main", "index": 0 }, { "node": "Account Health", "type": "main", "index": 0 }]] },
"Platform Metrics": { "main": [[{ "node": "Merge", "type": "main", "index": 0 }]] },
"Account Health": { "main": [[{ "node": "Merge", "type": "main", "index": 1 }]] },
"Merge": { "main": [[{ "node": "Build KPI HTML", "type": "main", "index": 0 }]] },
"Build KPI HTML": { "main": [[{ "node": "Email KPI Report", "type": "main", "index": 0 }, { "node": "Slack One-Liner", "type": "main", "index": 0 }]] }
}
}
Why RetailTech SaaS Vendors Choose n8n Over Zapier
| Requirement | Zapier | n8n (self-hosted) |
|---|---|---|
| PCI-DSS CHD isolation | Multi-tenant task log = scope expansion | Air-gapped VPC — no PCI scope bleed |
| CCPA third-party disclosure | Zapier = "third party" in your data map | No external disclosure |
| GDPR Art.28 sub-processor DPA | Must include Zapier in DPA chain | Off the sub-processor list |
| SOC2 CC7.2 log retention | 30-day task log max | Permanent audit trail in your DB |
| State privacy (Iowa/TX/CO/VA) | DSR request data flows through Zapier | Internal loop only |
| 100M GMV events/mo cost | ~$50,000+/mo | ~$500/mo VPS |
| Air-gapped POS terminal | Not possible | Raspberry Pi edge node |
Ready to Deploy?
All five workflows are available as import-ready JSON templates at stripeai.gumroad.com — individual templates from $12, complete bundle $97.
The n8n self-hosting docs are at docs.n8n.io. A $6/month VPS handles the full stack.
What RetailTech automation challenge are you solving? Drop it in the comments.
Top comments (0)