This workflow corresponds to n8n.io template #16016 — 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 →
{
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Detect and report dead Shopify inventory via Gmail and Slack",
"tags": [],
"nodes": [
{
"id": "659fdcaf-54bd-4118-98c4-7536713d759e",
"name": "Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-736,
5424
],
"parameters": {
"color": 1,
"width": 496,
"height": 928,
"content": "## Shopify Dead Inventory Detector\n\n### Who is this for\nShopify store owners, inventory managers, and e-commerce agencies looking to optimize capital by identifying stagnant stock.\n\n### Requirements\n- Shopify account (API scopes for reading orders and products).\n- Gmail account connected via OAuth2.\n- Slack workspace for team alerts.\n\n### How it works\n1. The workflow triggers daily via the **Daily Inventory Check** schedule.\n2. It fetches recent orders and products using the **Fetch Shopify Orders** and **Fetch Shopify Products** nodes.\n3. The code node checks which products haven't moved within your set window.\n4. **Prepare CSV Report Data** formats it for CSV export.\n5. **Email Dead Inventory Report** sends the report via Gmail.\n6. **Send CSV Report to Slack** posts the file into a designated channel.\n\n### Setup steps\n- Set up Shopify credentials in the **Fetch Shopify Orders** and **Fetch Shopify Products** nodes.\n- In the **Set Detector Configuration** node, set your desired `daysWithoutSales`, `orderFetchDays`, `recipientMail`, and `slackEscalationChannel`.\n- Set up Gmail credentials in the **Email Dead Inventory Report** node.\n- Configure Slack credentials in the **Send CSV Report to Slack** and **Post Error to Slack** nodes.\n\n### Customization\nAdjust `daysWithoutSales` and `orderFetchDays` in the **Set Detector Configuration** node to adjust what counts as \"dead stock\" for your store. You can also modify the **Identify Dead Inventory** code for custom logic."
},
"typeVersion": 1
},
{
"id": "3bf4626c-e04d-4c27-ac44-3e873ff90ad2",
"name": "Trigger and Settings",
"type": "n8n-nodes-base.stickyNote",
"position": [
-208,
5424
],
"parameters": {
"color": 7,
"width": 992,
"height": 448,
"content": "## Trigger & Settings\n\nSets your detection window, pulls orders and products from Shopify, then hands everything to the detection logic."
},
"typeVersion": 1
},
{
"id": "f5516e9a-67a1-4d33-9ef3-c0fcfa69a5ed",
"name": "Dead Inventory Detection",
"type": "n8n-nodes-base.stickyNote",
"position": [
880,
5456
],
"parameters": {
"color": 7,
"width": 624,
"height": 336,
"content": "## Dead Inventory Detection\n\nProcesses fetched data to identify products meeting the dead inventory criteria."
},
"typeVersion": 1
},
{
"id": "47c904d8-55c6-401f-8286-dac394a0c8d8",
"name": "Report and Alerts",
"type": "n8n-nodes-base.stickyNote",
"position": [
1632,
5312
],
"parameters": {
"color": 7,
"width": 416,
"height": 528,
"content": "## Report Generation and Alerts\n\nCreates a CSV report from the identified dead inventory and sends alerts via email and Slack."
},
"typeVersion": 1
},
{
"id": "4d16bf98-1481-40e1-ba10-d7696f4cb5d5",
"name": "Error Handling",
"type": "n8n-nodes-base.stickyNote",
"position": [
-192,
6000
],
"parameters": {
"color": 7,
"width": 512,
"height": 320,
"content": "## Error Handling\n\nCatches and reports any global errors that occur during workflow execution to Slack."
},
"typeVersion": 1
},
{
"id": "6aa399b5-9499-4e64-8090-0a06757345b2",
"name": "Set Detector Configuration",
"type": "n8n-nodes-base.set",
"notes": "Start here. Set your detection window, recipient email, and Slack channel.",
"position": [
48,
5600
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "d1",
"name": "daysWithoutSales",
"type": "number",
"value": 60
},
{
"id": "45a2c7f2-7258-471e-b9b1-ea8d4355a6fb",
"name": "orderFetchDays",
"type": "number",
"value": 365
},
{
"id": "d3",
"name": "recipientMail",
"type": "string",
"value": "PASTE_RECIPIENT_MAIL_HERE"
},
{
"id": "d5",
"name": "slackEscalationChannel",
"type": "string",
"value": "PASTE_SLACK_CHANNEL_ID_HERE"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "86fe2291-903e-4087-b900-e02bdab52c09",
"name": "Fetch Shopify Orders",
"type": "n8n-nodes-base.shopify",
"position": [
336,
5536
],
"parameters": {
"options": {
"createdAtMin": "={{ $now.minus({ days: $('Set Detector Configuration').first().json.orderFetchDays }).toISO() }}",
"financialStatus": "paid"
},
"operation": "getAll",
"returnAll": true,
"authentication": "oAuth2"
},
"credentials": {
"shopifyOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "0cb405c4-77ca-45a7-bff2-f80ea7e77f6e",
"name": "Fetch Shopify Products",
"type": "n8n-nodes-base.shopify",
"position": [
336,
5696
],
"parameters": {
"resource": "product",
"operation": "getAll",
"returnAll": true,
"authentication": "oAuth2",
"additionalFields": {
"published_status": "published"
}
},
"credentials": {
"shopifyOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1,
"alwaysOutputData": true
},
{
"id": "f7921bed-a5d2-44d3-b333-292e0f1a5adb",
"name": "Identify Dead Inventory",
"type": "n8n-nodes-base.code",
"notes": "Adjust daysWithoutSales threshold inside Set Detector Configuration, not here.",
"position": [
928,
5600
],
"parameters": {
"jsCode": "const config = $('Set Detector Configuration').first().json;\nconst orders = $('Fetch Shopify Orders').all().map(i => i.json);\nconst products = $('Fetch Shopify Products').all().map(i => i.json);\n\nconst daysWithoutSales = config.daysWithoutSales || 60;\nconst now = new Date();\n\nconst cutoffDate = new Date(\n now.getTime() - daysWithoutSales * 24 * 60 * 60 * 1000\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. Build two maps from ALL orders:\n// - variantLastSaleDate: all-time last sale\n// - variantSoldInWindow: sold within lookback window\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 variantLastSaleDate = new Map();\nconst variantSoldInWindow = new Set();\n\norders.forEach(order => {\n\n // Skip cancelled orders\n if (order.cancelled_at) return;\n\n // Only count paid orders\n if (\n !['paid', 'partially_paid']\n .includes(order.financial_status)\n ) return;\n\n // Skip malformed orders\n if (!Array.isArray(order.line_items)) return;\n\n // Safely parse order date\n const parsedDate = new Date(order.created_at);\n\n const orderDate =\n order.created_at &&\n !isNaN(parsedDate.getTime())\n ? parsedDate\n : null;\n\n if (!orderDate) return;\n\n order.line_items.forEach(item => {\n\n // Skip invalid variants\n if (!item.variant_id) return;\n\n const variantId = String(item.variant_id);\n\n // Track latest sale date\n const existing = variantLastSaleDate.get(variantId);\n\n if (!existing || orderDate > existing) {\n variantLastSaleDate.set(variantId, orderDate);\n }\n\n // Track recent sales\n if (orderDate >= cutoffDate) {\n variantSoldInWindow.add(variantId);\n }\n\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// 2. Identify Dead Inventory\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 deadItems = [];\nconst seenProductIds = new Set();\n\nproducts.forEach(product => {\n\n // Prevent duplicate products\n if (seenProductIds.has(product.id)) return;\n\n seenProductIds.add(product.id);\n\n // Skip malformed products\n if (!Array.isArray(product.variants)) return;\n\n // Only active products\n if (product.status !== 'active') return;\n\n product.variants.forEach(variant => {\n\n const variantId = String(variant.id);\n const stock = variant.inventory_quantity ?? 0;\n\n // Skip zero or negative stock\n if (stock <= 0) return;\n\n // Skip recently sold variants\n if (variantSoldInWindow.has(variantId)) 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\n // Build display fields\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\n const title = `${product.title}${\n variant.title !== 'Default Title'\n ? ` - ${variant.title}`\n : ''\n }`.trim();\n\n const sku =\n variant.sku &&\n variant.sku.trim() !== ''\n ? variant.sku\n : `VAR-${variantId.slice(-6)}`;\n\n const price = Number(variant.price || 0);\n\n const totalValue = (\n stock * price\n ).toFixed(2);\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\n // Get real last sale date\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\n const lastSaleDateObj =\n variantLastSaleDate.get(variantId);\n\n const hasValidLastSale =\n lastSaleDateObj instanceof Date &&\n !isNaN(lastSaleDateObj.getTime());\n\n const daysSinceLastSale =\n hasValidLastSale\n ? Math.floor(\n (\n now.getTime() -\n lastSaleDateObj.getTime()\n ) / (1000 * 60 * 60 * 24)\n )\n : 9999;\n\n const lastPurchaseDate =\n hasValidLastSale\n ? lastSaleDateObj\n .toISOString()\n .split('T')[0]\n : 'Never Sold';\n\n deadItems.push({\n \"Product ID\": String(product.id),\n \"Variant ID\": variantId,\n \"Name\": title,\n \"SKU\": sku,\n \"Stock\": stock,\n \"Unit Price\": `$${price.toFixed(2)}`,\n \"Total Value\": `$${totalValue}`,\n \"Days Since Last Sale\":\n daysSinceLastSale === 9999\n ? 'Never Sold'\n : daysSinceLastSale,\n \"Last Purchase Date\":\n lastPurchaseDate\n });\n\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 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 (deadItems.length === 0) {\n\n return [{\n json: {\n status: 'clean',\n message: `No dead inventory found in the last ${daysWithoutSales} days.`,\n runDate: now.toISOString(),\n _meta: {\n itemCount: 0,\n totalValueTied: '0.00',\n runDate: now.toISOString(),\n daysWithoutSales\n }\n }\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// 3. Sort by highest capital tied up\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\ndeadItems.sort((a, b) =>\n parseFloat(\n b[\"Total Value\"].replace('$', '')\n ) -\n parseFloat(\n a[\"Total Value\"].replace('$', '')\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// 4. Summary metadata\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 totalValueTied = deadItems\n .reduce(\n (sum, item) =>\n sum +\n parseFloat(\n item[\"Total Value\"].replace('$', '')\n ),\n 0\n )\n .toFixed(2);\n\nconst pad = n =>\n String(n).padStart(2, '0');\n\nconst runDate = `${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// 5. Return results\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 deadItems.map((item, index) => ({\n json: {\n status: 'found',\n ...item,\n _meta:\n index === 0\n ? {\n itemCount: deadItems.length,\n totalValueTied,\n runDate,\n daysWithoutSales\n }\n : null\n }\n}));"
},
"retryOnFail": false,
"typeVersion": 2,
"alwaysOutputData": false
},
{
"id": "627008a5-e8de-43df-ba22-e5866063b4d0",
"name": "Email Dead Inventory Report",
"type": "n8n-nodes-base.gmail",
"position": [
1904,
5680
],
"parameters": {
"sendTo": "={{ $('Set Detector Configuration').first().json.recipientMail }}",
"message": "={{ '<html><body style=\"margin:0;padding:0;background:#f3f4f6;font-family:Segoe UI,Helvetica,Arial,sans-serif;\"><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#f3f4f6;padding:36px 16px;\"><tr><td align=\"center\"><table width=\"620\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#ffffff;border-radius:12px;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,0.08);\"><tr><td style=\"background:#111827;padding:32px 36px;\"><p style=\"margin:0 0 6px;color:#9ca3af;font-size:11px;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;\">Inventory Intelligence \u00b7 ' + $('Identify Dead Inventory').first().json._meta.runDate + '</p><h1 style=\"margin:0;color:#ffffff;font-size:22px;font-weight:700;line-height:1.3;\">\ud83d\udcc9 ' + $('Identify Dead Inventory').first().json._meta.itemCount + ' SKU' + ($('Identify Dead Inventory').first().json._meta.itemCount > 1 ? 's' : '') + \" Haven't Sold in \" + $('Identify Dead Inventory').first().json._meta.daysWithoutSales + ' Days</h1><p style=\"margin:10px 0 0;color:#d1d5db;font-size:14px;\">$' + $('Identify Dead Inventory').first().json._meta.totalValueTied + ' in capital is currently tied up in idle inventory.</p></td></tr><tr><td style=\"padding:24px 36px 8px;\"><div style=\"background:#fef3c7;border-left:4px solid #f59e0b;padding:16px;border-radius:6px;margin-bottom:12px;\"><p style=\"margin:0;font-size:14px;font-weight:600;color:#92400e;\">Recommended Actions</p><ul style=\"margin:8px 0 0;padding-left:20px;color:#78350f;font-size:13px;\"><li>Apply clearance discounts to move stock fast</li><li>Bundle with best-selling products</li><li>Activate targeted ad campaigns for stagnant SKUs</li><li>Archive items with no viable sales path</li></ul></div></td></tr><tr><td style=\"padding:8px 36px 28px;\"><p style=\"margin:0 0 10px;font-size:12px;font-weight:700;color:#111827;text-transform:uppercase;letter-spacing:0.06em;\">Report Summary</p><p style=\"margin:0;font-size:13px;color:#374151;\">\ud83d\udcce The full dead inventory breakdown is attached as a CSV file. It includes <strong>Product ID, Variant ID, Name, SKU, Stock, Unit Price, Total Value, Days Since Last Sale</strong>, and <strong>Last Purchase Date</strong> for each flagged item, sorted by capital tied up.</p></td></tr><tr><td style=\"background:#f9fafb;border-top:1px solid #e5e7eb;padding:14px 36px;\"><p style=\"margin:0;font-size:12px;color:#9ca3af;text-align:center;\">Dead Inventory Monitor \u00b7 Automated report for ' + $('Identify Dead Inventory').first().json._meta.runDate + '</p></td></tr></table></td></tr></table></body></html>' }}",
"options": {
"emailType": "HTML",
"attachmentsUi": {
"attachmentsBinary": [
{
"property": "data"
}
]
}
},
"subject": "={{ '\ud83d\udcc9 Dead Inventory Report \u2014 ' + $('Identify Dead Inventory').first().json._meta.itemCount + ' SKU' + ($('Identify Dead Inventory').first().json._meta.itemCount > 1 ? 's' : '') + ' Untouched for ' + $('Identify Dead Inventory').first().json._meta.daysWithoutSales + ' Days ($' + $('Identify Dead Inventory').first().json._meta.totalValueTied + ' at Risk)' }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "689976e5-d15f-4567-9244-90e8cf837f61",
"name": "On Workflow Error",
"type": "n8n-nodes-base.errorTrigger",
"position": [
-144,
6128
],
"parameters": {},
"typeVersion": 1
},
{
"id": "f3d4f45f-b868-478e-80ae-f7428649cbdc",
"name": "Post Error to Slack",
"type": "n8n-nodes-base.slack",
"position": [
112,
6128
],
"parameters": {
"text": "={{ '\ud83d\udea8 *Workflow Error \u2014 Dead Inventory Monitor*\\n\\n*Failed Node:* ' + $json.execution.error.node.name + '\\n*Error:* ' + $json.execution.error.message + '\\n\\n_Please check your n8n instance for details._' }}",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "id",
"value": "PASTE_SLACK_CHANNEL_ID_HERE"
},
"otherOptions": {
"mrkdwn": true
}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 2
},
{
"id": "b28c1361-0fa3-4bd6-bef8-b3c01cd7ee0d",
"name": "Create Dead Inventory CSV",
"type": "n8n-nodes-base.convertToFile",
"position": [
1680,
5600
],
"parameters": {
"options": {
"fileName": "={{ 'Dead_Inventory_Report_' + $('Identify Dead Inventory').first().json._meta.runDate + '.csv' }}"
}
},
"typeVersion": 1.1,
"alwaysOutputData": false
},
{
"id": "6e3648b3-024d-40c5-812b-687fbf7d1b96",
"name": "Send CSV Report to Slack",
"type": "n8n-nodes-base.slack",
"position": [
1904,
5472
],
"parameters": {
"options": {
"channelId": "={{ $('Set Detector Configuration').first().json.slackEscalationChannel }}",
"initialComment": "={{ '\ud83d\udcc9 *' + $('Identify Dead Inventory').first().json._meta.itemCount + ' SKU' + ($('Identify Dead Inventory').first().json._meta.itemCount > 1 ? 's' : '') + \" haven't sold in \" + $('Identify Dead Inventory').first().json._meta.daysWithoutSales + ' days.*\\n\\n\ud83d\udcb0 *$' + $('Identify Dead Inventory').first().json._meta.totalValueTied + '* in capital is currently tied up in idle inventory.\\n\\n*Recommended actions:*\\n\u2022 \ud83c\udff7\ufe0f Apply clearance discounts\\n\u2022 \ud83d\udce6 Bundle with high-velocity items\\n\u2022 \ud83d\udce3 Run targeted ad campaigns\\n\u2022 \ud83d\uddc2\ufe0f Archive products with no viable sales path\\n\\n\ud83d\udcce Full CSV report follows below.' }}"
},
"resource": "file"
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.4
},
{
"id": "554a83ab-1e89-4454-a838-466806a5183b",
"name": "Daily Inventory Check",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-160,
5600
],
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 7
}
]
}
},
"typeVersion": 1.3
},
{
"id": "f3f5d030-dade-4a0b-88bd-3c9c95f75de0",
"name": "Prepare CSV Report Data",
"type": "n8n-nodes-base.code",
"notes": "Strips _meta field before writing to CSV \u2014 do not remove this node.",
"position": [
1344,
5600
],
"parameters": {
"jsCode": "return $input.all().map(item => ({\n json: {\n \"Product ID\": item.json[\"Product ID\"],\n \"Variant ID\": item.json[\"Variant ID\"],\n \"Name\": item.json[\"Name\"],\n \"SKU\": item.json[\"SKU\"],\n \"Stock\": item.json[\"Stock\"],\n \"Unit Price\": item.json[\"Unit Price\"],\n \"Total Value\": item.json[\"Total Value\"],\n \"Days Since Last Sale\": item.json[\"Days Since Last Sale\"],\n \"Last Purchase Date\": item.json[\"Last Purchase Date\"]\n }\n}));"
},
"typeVersion": 2
},
{
"id": "8cde20b0-f3e2-49d6-a245-ad5b19571417",
"name": "If Dead Inventory Found",
"type": "n8n-nodes-base.if",
"position": [
1120,
5600
],
"parameters": {
"options": {
"ignoreCase": true
},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "ca6b7966-2808-4dcd-a384-58d656c3679e",
"operator": {
"type": "string",
"operation": "notEquals"
},
"leftValue": "={{ $json.status }}",
"rightValue": "clean"
}
]
}
},
"typeVersion": 2.3
},
{
"id": "add6a326-aedb-43c5-a49d-813432b30adb",
"name": "Merge Orders and Products",
"type": "n8n-nodes-base.merge",
"notes": "Waits for both Shopify fetches to complete before passing data to detection.",
"position": [
640,
5600
],
"parameters": {
"mode": "append",
"numberInputs": 2
},
"typeVersion": 3.2
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
},
"connections": {
"On Workflow Error": {
"main": [
[
{
"node": "Post Error to Slack",
"type": "main",
"index": 0
}
]
]
},
"Fetch Shopify Orders": {
"main": [
[
{
"node": "Merge Orders and Products",
"type": "main",
"index": 0
}
]
]
},
"Daily Inventory Check": {
"main": [
[
{
"node": "Set Detector Configuration",
"type": "main",
"index": 0
}
]
]
},
"Fetch Shopify Products": {
"main": [
[
{
"node": "Merge Orders and Products",
"type": "main",
"index": 1
}
]
]
},
"Identify Dead Inventory": {
"main": [
[
{
"node": "If Dead Inventory Found",
"type": "main",
"index": 0
}
]
]
},
"If Dead Inventory Found": {
"main": [
[
{
"node": "Prepare CSV Report Data",
"type": "main",
"index": 0
}
]
]
},
"Prepare CSV Report Data": {
"main": [
[
{
"node": "Create Dead Inventory CSV",
"type": "main",
"index": 0
}
]
]
},
"Create Dead Inventory CSV": {
"main": [
[
{
"node": "Email Dead Inventory Report",
"type": "main",
"index": 0
},
{
"node": "Send CSV Report to Slack",
"type": "main",
"index": 0
}
]
]
},
"Merge Orders and Products": {
"main": [
[
{
"node": "Identify Dead Inventory",
"type": "main",
"index": 0
}
]
]
},
"Set Detector Configuration": {
"main": [
[
{
"node": "Fetch Shopify Orders",
"type": "main",
"index": 0
},
{
"node": "Fetch Shopify Products",
"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.
gmailOAuth2shopifyOAuth2ApislackApi
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 detect dead Shopify inventory by comparing current product stock against recent paid order history, then generates a CSV report and sends it via Gmail and Slack, with global error notifications posted to Slack. Runs every day at 07:00 on a schedule…
Source: https://n8n.io/workflows/16016/ — 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.
Receive booking requests via webhook with automatic validation, duplicate detection, availability checking, confirmation emails, Google Calendar sync, and Slack notifications.
This workflow automatically detects bounced or invalid email addresses from your Gmail inbox and updates their status in Google Sheets. It fetches bounce notifications, extracts failed email addresses
This workflow automatically handles every resolved Jira bug by verifying the fix, notifying the customer, updating HubSpot, commenting on the Jira issue, alerting the team on Slack, and logging everyt
This workflow triggers when a new deal is created in HubSpot, sends a two-step follow-up sequence via Gmail, checks HubSpot for a recent email reply, then updates the deal stage, alerts a Slack channe
Automatically identify clients who haven’t been contacted in 14+ days and re-engage them with personalized Gmail follow-up emails, Google Sheets tracking, and Slack notifications for account managers.