{
  "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
          }
        ]
      ]
    }
  }
}