This workflow corresponds to n8n.io template #15972 — we link there as the canonical source.
This workflow follows the Error Trigger → Gmail 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": "4ZezIa3qn68GayGG",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Shopify Low Stock & Predictive Reorder Alerts",
"tags": [],
"nodes": [
{
"id": "ebb9d9be-f25f-4cba-8a52-92281b42ce97",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
11968,
4416
],
"parameters": {
"width": 480,
"height": 896,
"content": "## Shopify Low Stock & Predictive Reorder Alerts\n\n### How it works\n\nThis workflow runs on a daily schedule to monitor Shopify stock levels and recent order activity. It reads prior alert logs from Google Sheets, fetches recent orders and current inventory from Shopify, then calculates sales velocity and estimated depletion timing. When reorder or low-stock conditions are detected, it logs the alert, posts to Slack, and sends an HTML email; a separate error path posts workflow failures to Slack.\n\n### Setup steps\n\n- Configure the Daily Schedule Trigger with the desired monitoring frequency.\n- Add Shopify credentials and confirm the recent orders and inventory nodes target the correct store, products, and inventory locations.\n- Connect Google Sheets credentials and set the sheet URL/ranges used for reading previous logs and writing new alerts.\n- Configure the Set Inventory Config node with daysToCalculateVelocity, recipientMail, googleSheetUrl, and slackEscalationChannel.\n- Connect Slack credentials for both stock alerts and error alerts, and verify the target channels exist.\n- Connect Gmail credentials and confirm the recipient, sender, and email formatting are appropriate for stock alert notifications.\n\n### Customization\n\nAdjust the velocity window, low-stock thresholds, reorder rules, Slack channels, email recipients, and the HTML template to match operational needs."
},
"typeVersion": 1
},
{
"id": "bd08c3a8-5f2f-46cc-934e-d7f751359681",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
12528,
4544
],
"parameters": {
"color": 7,
"width": 720,
"height": 304,
"content": "## Schedule and configuration\n\nStarts the daily inventory check, sets workflow-wide configuration values, and reads previous Google Sheets logs used to avoid duplicate or repeated alerts."
},
"typeVersion": 1
},
{
"id": "be3048df-998e-484b-acdb-40971ec49033",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
13312,
4512
],
"parameters": {
"color": 7,
"width": 416,
"height": 320,
"content": "## Retrieve Shopify data\n\nFetches recent Shopify order activity and current inventory levels, providing the raw sales and stock data needed for forecasting."
},
"typeVersion": 1
},
{
"id": "3fe84fb5-fd93-4f67-adc6-c7dfae1aeae2",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
13776,
4416
],
"parameters": {
"color": 7,
"width": 240,
"height": 416,
"content": "## Calculate reorder forecast\n\nUses custom code to calculate sales velocity, projected depletion, and low-stock or reorder alert conditions from the Shopify data."
},
"typeVersion": 1
},
{
"id": "9bab24ee-fd3f-440d-ae12-30db2a134e60",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
14112,
4512
],
"parameters": {
"color": 7,
"width": 576,
"height": 496,
"content": "## Log and notify alerts\n\nRecords generated alerts in Google Sheets, posts the stock alert to Slack, builds an HTML email, and sends the email notification to the configured recipient."
},
"typeVersion": 1
},
{
"id": "1aa63120-3907-4800-9ae5-938eae55eb1a",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
12528,
5312
],
"parameters": {
"color": 7,
"width": 448,
"height": 320,
"content": "## Workflow error alerts\n\nA separate lower canvas cluster that catches global workflow errors and sends an error notification to Slack."
},
"typeVersion": 1
},
{
"id": "0e95dde7-7114-4cab-bd5e-4b7e48780b14",
"name": "When Daily at 7am",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
12576,
4672
],
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"triggerAtHour": 7,
"triggerAtMinute": 0
}
]
}
},
"typeVersion": 1.3
},
{
"id": "f8b6b7f8-84f5-4389-97de-a2d10a453ca4",
"name": "Set Inventory Settings",
"type": "n8n-nodes-base.set",
"position": [
12800,
4672
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "d1",
"name": "daysToCalculateVelocity",
"type": "number",
"value": 7
},
{
"id": "d3",
"name": "recipientMail",
"type": "string",
"value": "PASTE_RECIPIENT_MAIL_HERE"
},
{
"id": "d4",
"name": "googleSheetUrl",
"type": "string",
"value": "PASTE_GOOGLE_SHEET_URL_HERE"
},
{
"id": "d5",
"name": "slackEscalationChannel",
"type": "string",
"value": "PASTE_SLACK_CHANNEL_ID_HERE"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "60b17335-68ae-40d0-a48f-e4565e3e369f",
"name": "Fetch Shopify Orders",
"type": "n8n-nodes-base.shopify",
"position": [
13360,
4672
],
"parameters": {
"options": {
"createdAtMin": "={{ $now.minus({ days: $('Set Inventory Settings').item.json.daysToCalculateVelocity }).toISO() }}"
},
"operation": "getAll",
"returnAll": true,
"authentication": "oAuth2"
},
"credentials": {
"shopifyOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "b9baded1-30df-429d-8dc7-c380f7bfd9d2",
"name": "Fetch Shopify Inventory",
"type": "n8n-nodes-base.shopify",
"position": [
13584,
4672
],
"parameters": {
"resource": "product",
"operation": "getAll",
"returnAll": true,
"authentication": "oAuth2",
"additionalFields": {
"status": "active"
}
},
"credentials": {
"shopifyOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1,
"alwaysOutputData": true
},
{
"id": "a3d97281-207d-4a6a-a103-b24e94cf4cd6",
"name": "Read Inventory Logs from Sheets",
"type": "n8n-nodes-base.googleSheets",
"onError": "continueRegularOutput",
"position": [
13104,
4672
],
"parameters": {
"options": {},
"returnAll": true,
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "url",
"value": "={{ $('Set Inventory Settings').item.json.googleSheetUrl }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7,
"alwaysOutputData": true
},
{
"id": "e3e9f530-2e87-4cd2-9069-a30a34edafed",
"name": "Calculate Inventory Velocity",
"type": "n8n-nodes-base.code",
"position": [
13824,
4672
],
"parameters": {
"jsCode": "const config = $('Set Inventory Settings').first().json;\n\nconst orders = $('Fetch Shopify Orders')\n .all()\n .map(i => i.json);\n\nconst products = $('Fetch Shopify Inventory')\n .all()\n .map(i => i.json);\n\nconst sheetLogs = $('Read Inventory Logs from Sheets')\n .all()\n .map(i => i.json);\n\nconst velocityDays = config.daysToCalculateVelocity || 7;\nconst now = new Date();\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Category Thresholds\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst THRESHOLDS = {\n critical: 2,\n warning: 5,\n caution: 7,\n monitor: 14\n};\n\nfunction getCategory(daysLeft) {\n if (daysLeft <= THRESHOLDS.critical) return 'critical';\n if (daysLeft <= THRESHOLDS.warning) return 'warning';\n if (daysLeft <= THRESHOLDS.caution) return 'caution';\n if (daysLeft <= THRESHOLDS.monitor) return 'monitor';\n return 'healthy';\n}\n\nconst CATEGORY_EMOJI = {\n critical: '\ud83d\udd34',\n warning: '\ud83d\udfe1',\n caution: '\ud83d\udd35',\n monitor: '\ud83d\udfe3'\n};\n\nconst CATEGORY_ORDER = {\n critical: 0,\n warning: 1,\n caution: 2,\n monitor: 3\n};\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// 1. Count Sold Quantity Per Variant\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst salesCount = {};\n\norders.forEach(order => {\n if (order.cancelled_at) return;\n if (!['paid', 'partially_paid'].includes(order.financial_status)) return;\n if (!order.line_items) return;\n\n order.line_items.forEach(item => {\n const variantId = String(item.variant_id);\n salesCount[variantId] = (salesCount[variantId] || 0) + item.quantity;\n });\n});\n\nconst orderedVariantIds = new Set(Object.keys(salesCount));\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// 2. Deduplicate Products\n// Shopify returnAll can return duplicate pages\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst seenProductIds = new Set();\nconst uniqueProducts = products.filter(product => {\n if (seenProductIds.has(product.id)) return false;\n seenProductIds.add(product.id);\n return true;\n});\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// 3. Process Inventory & Velocity\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst lowStockItems = [];\n\nuniqueProducts.forEach(product => {\n\n if (!product.variants) return;\n if (product.status !== 'active') return;\n\n product.variants.forEach(variant => {\n\n if (!variant.inventory_management) return;\n\n const variantId = String(variant.id);\n\n // Zero-velocity variants: flag if stock is critically low regardless of sales\n const hasRecentSales = orderedVariantIds.has(variantId);\n\n const title = `${product.title}${variant.title !== 'Default Title' ? ' - ' + variant.title : ''}`.trim();\n\n const stock = variant.inventory_quantity ?? 0;\n\n // Show variant ID suffix if no SKU set\n const sku = variant.sku && variant.sku.trim() !== ''\n ? variant.sku\n : `VAR-${variantId.slice(-6)}`;\n\n const soldInWindow = salesCount[variantId] || 0;\n const dailyVelocity = soldInWindow / velocityDays;\n\n // For zero-velocity items: only flag if stock is below minimumStockFloor\n const minimumStockFloor = 5;\n if (!hasRecentSales) {\n if (stock > minimumStockFloor) return;\n }\n\n const daysLeft = dailyVelocity > 0\n ? Math.max(0, stock / dailyVelocity)\n : 999;\n\n const category = getCategory(daysLeft);\n\n if (category === 'healthy') return;\n\n // Skip if already alerted today (UTC)\n const todayUTC = now.toISOString().split('T')[0];\n\n const alreadyAlertedToday = sheetLogs.some(row => {\n const rowDateUTC = String(row.runTimestamp || '').split('T')[0];\n if (rowDateUTC !== todayUTC) return false;\n const variantIdList = String(row.variantIds || '')\n .split(',')\n .map(id => id.trim());\n return variantIdList.includes(variantId);\n });\n\n if (alreadyAlertedToday) return;\n\n lowStockItems.push({\n variantId,\n sku,\n title,\n stock,\n dailyVelocity: parseFloat(dailyVelocity.toFixed(2)),\n daysLeft: parseFloat(daysLeft.toFixed(1)),\n category\n });\n });\n});\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Stop Workflow If Nothing Found\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nif (lowStockItems.length === 0) return [];\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// 4. Sort By Urgency\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlowStockItems.sort((a, b) => {\n const categoryCompare =\n CATEGORY_ORDER[a.category] - CATEGORY_ORDER[b.category];\n if (categoryCompare !== 0) return categoryCompare;\n return a.daysLeft - b.daysLeft;\n});\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// 5. Build Timestamp\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst pad = n => String(n).padStart(2, '0');\nconst runTimestamp = `${now.getUTCFullYear()}-${pad(now.getUTCMonth() + 1)}-${pad(now.getUTCDate())}`;\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// 6. Alert Counts\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst itemCount = lowStockItems.length;\nconst criticalCount = lowStockItems.filter(i => i.category === 'critical').length;\nconst warningCount = lowStockItems.filter(i => i.category === 'warning').length;\nconst cautionCount = lowStockItems.filter(i => i.category === 'caution').length;\nconst monitorCount = lowStockItems.filter(i => i.category === 'monitor').length;\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// 7. Generate Slack Message\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst slackList = lowStockItems.map(item => {\n const emoji = CATEGORY_EMOJI[item.category];\n const depletionText = item.daysLeft === 0\n ? 'is already out of stock.'\n : `may go out of stock in ${item.daysLeft} days.`;\n return `${emoji} *${item.title}* (SKU: ${item.sku}) ${depletionText}`;\n}).join('\\n\\n');\n\nconst slackSummary = [\n `\ud83d\udce6 *Inventory Alert \u2014 ${itemCount} product${itemCount > 1 ? 's' : ''} need attention*`,\n `\ud83d\udd34 Critical: ${criticalCount} \ud83d\udfe1 Warning: ${warningCount} \ud83d\udd35 Caution: ${cautionCount} \ud83d\udfe3 Monitor: ${monitorCount}`,\n ``,\n `Based on the last ${velocityDays} days of sales:`,\n ``,\n slackList,\n ``,\n `\ud83d\udd50 _Predicted at ${runTimestamp} \u00b7 Inventory Monitor_`\n].join('\\n');\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// 8. Generate Email Subject\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst emailSubject = criticalCount > 0\n ? `\ud83d\udd34 CRITICAL: ${criticalCount} item${criticalCount > 1 ? 's' : ''} may stock out soon`\n : `\u26a0\ufe0f Inventory Alert: ${itemCount} item${itemCount > 1 ? 's' : ''} need attention`;\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// 9. Return Final Output\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nreturn [{\n json: {\n runTimestamp,\n variantIds: lowStockItems.map(i => i.variantId).join(', '),\n totalVariantsAlerted: itemCount,\n criticalCount,\n warningCount,\n cautionCount,\n monitorCount,\n slackSummary,\n emailSubject,\n lowStockItems,\n velocityDays\n }\n}];"
},
"retryOnFail": false,
"typeVersion": 2,
"alwaysOutputData": false
},
{
"id": "613e0200-dbda-4203-914f-9c4441835a69",
"name": "Append Alert to Sheets",
"type": "n8n-nodes-base.googleSheets",
"position": [
14160,
4672
],
"parameters": {
"columns": {
"value": {
"variantIds": "={{ $json.variantIds }}",
"runTimestamp": "={{ $json.runTimestamp }}",
"totalVariantsAlerted": "={{ $json.totalVariantsAlerted }}"
},
"schema": [
{
"id": "runTimestamp",
"type": "string",
"display": true,
"required": false,
"displayName": "runTimestamp",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "variantIds",
"type": "string",
"display": true,
"required": false,
"displayName": "variantIds",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "totalVariantsAlerted",
"type": "string",
"display": true,
"required": false,
"displayName": "totalVariantsAlerted",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "url",
"value": "={{ $('Set Inventory Settings').item.json.googleSheetUrl }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7,
"alwaysOutputData": true
},
{
"id": "4b410699-974f-4cd6-ad6c-03fa40bb51f1",
"name": "Post Inventory Alert to Slack",
"type": "n8n-nodes-base.slack",
"position": [
14544,
4672
],
"parameters": {
"text": "={{ $('Calculate Inventory Velocity').item.json.slackSummary }}",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Set Inventory Settings').item.json.slackEscalationChannel }}"
},
"otherOptions": {
"mrkdwn": true
}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 2
},
{
"id": "079b7afa-9a6a-40a5-8958-aab8e56f6776",
"name": "Build Email Content",
"type": "n8n-nodes-base.code",
"position": [
14336,
4848
],
"parameters": {
"jsCode": "const d = $('Calculate Inventory Velocity').first().json;\n\nconst itemRows = d.lowStockItems.map(item => {\n\n const depletionText = item.daysLeft === 0\n ? 'Already out of stock'\n : `May go out of stock in ${item.daysLeft} days`;\n\n const rowBg = {\n critical: '#fff5f5',\n warning: '#fffbeb',\n caution: '#eff6ff',\n monitor: '#faf5ff'\n };\n\n const textColor = {\n critical: '#dc2626',\n warning: '#d97706',\n caution: '#2563eb',\n monitor: '#7c3aed'\n };\n\n return `\n <tr style=\"background:${rowBg[item.category]};\">\n <td style=\"padding:14px 16px; border-bottom:1px solid #f0f0f0;\">\n <span style=\"color:#111827; font-size:14px; font-weight:600;\">${item.title}</span><br/>\n <span style=\"color:#6b7280; font-size:12px;\">SKU: ${item.sku}</span>\n </td>\n <td style=\"padding:14px 16px; border-bottom:1px solid #f0f0f0; color:#374151; font-size:14px; text-align:center;\">${item.stock}</td>\n <td style=\"padding:14px 16px; border-bottom:1px solid #f0f0f0; color:#374151; font-size:14px; text-align:center;\">${item.dailyVelocity}/day</td>\n <td style=\"padding:14px 16px; border-bottom:1px solid #f0f0f0; font-size:14px; font-weight:600; text-align:left; color:${textColor[item.category]};\">${depletionText}</td>\n </tr>`;\n}).join('');\n\nconst emailBody = `<!DOCTYPE html>\n<html lang=\"en\">\n<head><meta charset=\"UTF-8\"/><meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\"/></head>\n<body style=\"margin:0; padding:0; background:#f4f6f8; font-family:'Segoe UI',Arial,sans-serif;\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#f4f6f8; padding:32px 16px;\">\n <tr>\n <td align=\"center\">\n <table width=\"640\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#ffffff; border-radius:12px; overflow:hidden;\">\n <tr>\n <td style=\"background:#d97706; padding:28px 32px;\">\n <p style=\"margin:0; color:#fef3c7; font-size:12px; font-weight:600; text-transform:uppercase; letter-spacing:0.1em;\">Inventory Intelligence</p>\n <h1 style=\"margin:8px 0 0; color:#ffffff; font-size:22px; font-weight:700; line-height:1.3;\">\ud83d\udcc9 ${d.totalVariantsAlerted} Item${d.totalVariantsAlerted > 1 ? 's' : ''} Need Attention</h1>\n </td>\n </tr>\n <tr>\n <td style=\"padding:24px 32px 8px;\">\n <p style=\"margin:0; color:#374151; font-size:15px; line-height:1.7;\">\n Based on sales velocity over the last <strong>${d.velocityDays} days</strong>. Items are categorised as:\n <br/><br/>\n \ud83d\udd34 <strong>Critical</strong> \u2014 stockout in \u2264 2 days | \n \ud83d\udfe1 <strong>Warning</strong> \u2014 \u2264 5 days | \n \ud83d\udd35 <strong>Caution</strong> \u2014 \u2264 7 days | \n \ud83d\udfe3 <strong>Monitor</strong> \u2014 \u2264 14 days\n </p>\n </td>\n </tr>\n <tr>\n <td style=\"padding:16px 32px 24px;\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"border:1px solid #e5e7eb; border-radius:8px; overflow:hidden;\">\n <tr>\n <td style=\"padding:12px 16px; background:#f9fafb; border-bottom:1px solid #e5e7eb;\">\n <span style=\"color:#6b7280; font-size:12px; font-weight:600; text-transform:uppercase;\">Product</span>\n </td>\n <td style=\"padding:12px 16px; background:#f9fafb; border-bottom:1px solid #e5e7eb; text-align:center;\">\n <span style=\"color:#6b7280; font-size:12px; font-weight:600; text-transform:uppercase;\">Stock</span>\n </td>\n <td style=\"padding:12px 16px; background:#f9fafb; border-bottom:1px solid #e5e7eb; text-align:center;\">\n <span style=\"color:#6b7280; font-size:12px; font-weight:600; text-transform:uppercase;\">Burn Rate</span>\n </td>\n <td style=\"padding:12px 16px; background:#f9fafb; border-bottom:1px solid #e5e7eb; text-align:left;\">\n <span style=\"color:#6b7280; font-size:12px; font-weight:600; text-transform:uppercase;\">Status</span>\n</td>\n </tr>\n ${itemRows}\n </table>\n </td>\n </tr>\n <tr>\n <td style=\"background:#f9fafb; border-top:1px solid #e5e7eb; padding:16px 32px;\">\n <p style=\"margin:0; color:#9ca3af; font-size:12px; text-align:center;\">\n \ud83d\udd50 Monitored at <strong>${d.runTimestamp}</strong> \u00b7 Reorder Alert Monitor\n </p>\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n</body>\n</html>`;\n\nreturn [{\n json: {\n ...d,\n emailBody\n }\n}];"
},
"typeVersion": 2
},
{
"id": "1c3e7681-f5fe-45b8-86d9-f0396514a5fc",
"name": "Send Stock Alert Email",
"type": "n8n-nodes-base.gmail",
"position": [
14544,
4848
],
"parameters": {
"sendTo": "={{ $('Set Inventory Settings').item.json.recipientMail }}",
"message": "={{ $json.emailBody }}",
"options": {
"emailType": "html"
},
"subject": "={{ $json.emailSubject }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "5c8d2cac-9352-41f8-a7b2-0306d5c48b35",
"name": "On Global Error",
"type": "n8n-nodes-base.errorTrigger",
"position": [
12576,
5472
],
"parameters": {},
"typeVersion": 1
},
{
"id": "f891bcde-0132-4d14-89d1-353a2eb4fe67",
"name": "Post Error to Slack",
"type": "n8n-nodes-base.slack",
"position": [
12832,
5472
],
"parameters": {
"text": "={{ '\ud83d\udea8 *Workflow Error \u2014 Inventory Monitor*\\n\\n*Failed Node:* ' + $json.execution.error.node.name + '\\n*Error:* ' + $json.execution.error.message }}",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "id",
"value": ""
},
"otherOptions": {
"mrkdwn": true
}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 2
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "0b349aa8-534d-4786-8506-997b04e9c4c4",
"connections": {
"On Global Error": {
"main": [
[
{
"node": "Post Error to Slack",
"type": "main",
"index": 0
}
]
]
},
"When Daily at 7am": {
"main": [
[
{
"node": "Set Inventory Settings",
"type": "main",
"index": 0
}
]
]
},
"Build Email Content": {
"main": [
[
{
"node": "Send Stock Alert Email",
"type": "main",
"index": 0
}
]
]
},
"Fetch Shopify Orders": {
"main": [
[
{
"node": "Fetch Shopify Inventory",
"type": "main",
"index": 0
}
]
]
},
"Append Alert to Sheets": {
"main": [
[
{
"node": "Post Inventory Alert to Slack",
"type": "main",
"index": 0
},
{
"node": "Build Email Content",
"type": "main",
"index": 0
}
]
]
},
"Set Inventory Settings": {
"main": [
[
{
"node": "Read Inventory Logs from Sheets",
"type": "main",
"index": 0
}
]
]
},
"Fetch Shopify Inventory": {
"main": [
[
{
"node": "Calculate Inventory Velocity",
"type": "main",
"index": 0
}
]
]
},
"Calculate Inventory Velocity": {
"main": [
[
{
"node": "Append Alert to Sheets",
"type": "main",
"index": 0
}
]
]
},
"Read Inventory Logs from Sheets": {
"main": [
[
{
"node": "Fetch Shopify Orders",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
gmailOAuth2googleSheetsOAuth2ApishopifyOAuth2ApislackApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow runs daily to calculate Shopify product sales velocity, predict days until stockout, and alert on low inventory. It logs alerted variant IDs to Google Sheets to avoid duplicate notifications, then sends a Slack summary and a detailed HTML email via Gmail, with…
Source: https://n8n.io/workflows/15972/ — 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.
E-commerce store management made easy. The workflow pulls your daily Shopify orders, calculates essential metrics like revenue and fulfillment rates, and categorizes them by status (shipped, returned,
This workflow automates inventory management and predictive reordering for Shopify stores. It integrates Shopify, Google Sheets, and Slack to monitor inventory levels, calculate dynamic reorder points
E-commerce store owners and sales managers who want AI-powered insights from their Shopify data without manually crunching numbers every week.
Never miss a revenue-impacting failure. This n8n workflow monitors your Shopify store and triggers an alert if X minutes pass without a single new order. By automatically detecting unexpected drops in
A webhook or timer triggers the workflow to automatically fetch inventory data from multiple platforms. Stock levels are compared across stores to identify discrepancies, and any inconsistencies are u