DEV Community

Alex Kane
Alex Kane

Posted on

n8n for RetailTech SaaS: 5 Automations That Scale Retail Ops and Keep Customer Data Compliant (Free Workflow JSON)

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 }]] }
  }
}
Enter fullscreen mode Exit fullscreen mode

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 }]] }
  }
}
Enter fullscreen mode Exit fullscreen mode

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 }]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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 }]] }
  }
}
Enter fullscreen mode Exit fullscreen mode

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 }]] }
  }
}
Enter fullscreen mode Exit fullscreen mode

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)