AutomationFlowsE-commerce › Automate Shopify Inventory Reordering with Predictive Analytics and Google…

Automate Shopify Inventory Reordering with Predictive Analytics and Google…

Original n8n title: Automate Shopify Inventory Reordering with Predictive Analytics and Google Sheets

ByȚugui Dragoș @tuguidragos on n8n.io

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 based on sales velocity, and automate supplier communication. The workflow…

Cron / scheduled trigger★★★★★ complexity63 nodesShopifyGoogle SheetsGmailHTTP RequestSlack
E-commerce Trigger: Cron / scheduled Nodes: 63 Complexity: ★★★★★ Added:
Automate Shopify Inventory Reordering with Predictive Analytics and Google… — n8n workflow card showing Shopify, Google Sheets, Gmail integration

This workflow corresponds to n8n.io template #11799 — we link there as the canonical source.

This workflow follows the Gmail → Google Sheets 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 →

Download .json
{
  "id": "Fgyk456vwePSFyoP",
  "name": "Automate Shopify Inventory Reordering with Predictive Analytics",
  "tags": [
    {
      "id": "BozSnIOP0arZwOpu",
      "name": "tuguidragos.com",
      "createdAt": "2025-12-14T19:49:53.386Z",
      "updatedAt": "2025-12-14T19:49:53.386Z"
    }
  ],
  "nodes": [
    {
      "id": "5c80dabe-2c5c-4494-83ff-5582c12b5b7b",
      "name": "Hourly Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -3056,
        464
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours"
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "4ff8cf32-8507-4c02-96a1-31c7d12d5a81",
      "name": "Workflow Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        -2880,
        464
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "id-1",
              "name": "shopifyStoreUrl",
              "type": "string",
              "value": "https://your-store.myshopify.com"
            },
            {
              "id": "id-2",
              "name": "reorderPointMultiplier",
              "type": "number",
              "value": 1.5
            },
            {
              "id": "id-3",
              "name": "safetyStockDays",
              "type": "number",
              "value": 7
            },
            {
              "id": "id-4",
              "name": "budgetLimit",
              "type": "number",
              "value": 50000
            },
            {
              "id": "id-5",
              "name": "largeOrderThreshold",
              "type": "number",
              "value": 10000
            },
            {
              "id": "id-6",
              "name": "slowMoverThresholdDays",
              "type": "number",
              "value": 90
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "d43787b7-fa03-498b-a18e-c00efd42e2d0",
      "name": "Get Inventory Levels",
      "type": "n8n-nodes-base.shopify",
      "position": [
        -2352,
        -32
      ],
      "parameters": {
        "resource": "inventoryLevel",
        "authentication": "accessToken"
      },
      "typeVersion": 1
    },
    {
      "id": "aee4a064-6758-4170-84b2-bc26245a3e3a",
      "name": "Get Product Details",
      "type": "n8n-nodes-base.shopify",
      "position": [
        -2352,
        176
      ],
      "parameters": {
        "resource": "product",
        "operation": "getAll",
        "returnAll": true,
        "additionalFields": {}
      },
      "typeVersion": 1
    },
    {
      "id": "4a9b52a8-ab2f-4bd9-ae09-f928f86155d7",
      "name": "Get Last 30 Days Orders",
      "type": "n8n-nodes-base.shopify",
      "position": [
        -2352,
        384
      ],
      "parameters": {
        "options": {
          "createdAtMin": "={{ $now.minus({ days: 30 }).toISO() }}"
        },
        "operation": "getAll",
        "returnAll": true
      },
      "typeVersion": 1
    },
    {
      "id": "29c8e322-a08f-4cee-a282-2b82eddff268",
      "name": "Update Inventory Fields",
      "type": "n8n-nodes-base.shopify",
      "position": [
        -912,
        880
      ],
      "parameters": {
        "resource": "inventoryLevel"
      },
      "typeVersion": 1
    },
    {
      "id": "1ff716f3-684d-4522-9ca8-21fa184b5475",
      "name": "Read Inventory Master",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -2352,
        752
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Inventory Master"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.INVENTORY_MASTER_SHEET_ID || \"<__PLACEHOLDER_VALUE__Inventory Master Google Sheet ID__>\" }}"
        },
        "authentication": "serviceAccount"
      },
      "typeVersion": 4.7
    },
    {
      "id": "597cef4d-5a47-4af3-941e-41cae31fe4dd",
      "name": "Read Suppliers",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -2352,
        944
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Suppliers"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.SUPPLIERS_SHEET_ID || \"<__PLACEHOLDER_VALUE__Suppliers Google Sheet ID__>\" }}"
        },
        "authentication": "serviceAccount"
      },
      "typeVersion": 4.7
    },
    {
      "id": "520ad36f-aae0-41d8-8a40-8f3084799311",
      "name": "Read Purchase Order Log",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -2352,
        1136
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "PO Log"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.PO_LOG_SHEET_ID || \"<__PLACEHOLDER_VALUE__Purchase Order Log Google Sheet ID__>\" }}"
        },
        "authentication": "serviceAccount"
      },
      "typeVersion": 4.7
    },
    {
      "id": "e70cf1ca-d6dc-4a16-a409-8c5a7e306ded",
      "name": "Merge All Data Sources",
      "type": "n8n-nodes-base.merge",
      "position": [
        -1824,
        416
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition",
        "numberInputs": 6
      },
      "typeVersion": 3.2
    },
    {
      "id": "4dfebbf6-8cc0-42e4-89b8-b79828f01347",
      "name": "Calculate Sales Velocity",
      "type": "n8n-nodes-base.code",
      "position": [
        -1456,
        480
      ],
      "parameters": {
        "jsCode": "// Calculate Sales Velocity for each SKU\n// Input: Merged data from inventory, products, orders, sheets\n// Output: Items with velocity_7d, velocity_30d, avg_daily_sales\n\nconst items = $input.all();\n\n// Extract order data and inventory data\nconst orders = [];\nconst inventoryMap = new Map();\nconst productMap = new Map();\nconst inventoryMasterMap = new Map();\nconst suppliersMap = new Map();\n\n// Parse input items to separate orders, inventory, and products\nfor (const item of items) {\n  const data = item.json;\n  \n  // Check if this is order data (from Get Last 30 Days Orders)\n  if (data.line_items && Array.isArray(data.line_items)) {\n    orders.push(data);\n  }\n  \n  // Check if this is inventory level data (from Get Inventory Levels)\n  if (data.inventory_item_id && data.location_id) {\n    const inventoryItemId = data.inventory_item_id;\n    inventoryMap.set(inventoryItemId, data);\n  }\n  \n  // Check if this is product data (from Get Product Details)\n  if (data.variants && Array.isArray(data.variants)) {\n    for (const variant of data.variants) {\n      if (variant.sku) {\n        productMap.set(variant.sku, {\n          ...variant,\n          product_title: data.title,\n          product_id: data.id,\n          product_type: data.product_type,\n          vendor: data.vendor\n        });\n        // Also map by inventory_item_id for cross-referencing\n        if (variant.inventory_item_id) {\n          productMap.set(variant.inventory_item_id, {\n            ...variant,\n            product_title: data.title,\n            product_id: data.id,\n            product_type: data.product_type,\n            vendor: data.vendor\n          });\n        }\n      }\n    }\n  }\n  \n  // Check if this is Inventory Master sheet data\n  if (data.SKU && (data.reorder_point || data.safety_stock)) {\n    inventoryMasterMap.set(data.SKU, data);\n  }\n  \n  // Check if this is Suppliers sheet data\n  if (data.supplier_id || data.supplier_name) {\n    const supplierId = data.supplier_id || data.id;\n    if (supplierId) {\n      suppliersMap.set(supplierId, data);\n    }\n  }\n}\n\n// Calculate sales by SKU\nconst salesBySku = new Map();\nconst now = new Date();\nconst sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);\nconst thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);\n\nfor (const order of orders) {\n  const orderDate = new Date(order.created_at);\n  \n  for (const lineItem of order.line_items) {\n    const sku = lineItem.sku;\n    if (!sku) continue;\n    \n    if (!salesBySku.has(sku)) {\n      salesBySku.set(sku, {\n        total_30d: 0,\n        total_7d: 0,\n        total_all: 0,\n        last_sale_date: null\n      });\n    }\n    \n    const sales = salesBySku.get(sku);\n    const quantity = lineItem.quantity || 0;\n    \n    sales.total_all += quantity;\n    \n    if (orderDate >= thirtyDaysAgo) {\n      sales.total_30d += quantity;\n    }\n    \n    if (orderDate >= sevenDaysAgo) {\n      sales.total_7d += quantity;\n    }\n    \n    // Track last sale date\n    if (!sales.last_sale_date || orderDate > new Date(sales.last_sale_date)) {\n      sales.last_sale_date = orderDate.toISOString();\n    }\n  }\n}\n\n// Build output with velocity calculations\nconst outputItems = [];\n\n// Get all unique SKUs from all sources\nconst allSkus = new Set([\n  ...productMap.keys(),\n  ...salesBySku.keys(),\n  ...inventoryMasterMap.keys()\n]);\n\nfor (const sku of allSkus) {\n  // Skip if this is an inventory_item_id (numeric) and we have the actual SKU\n  if (typeof sku === 'number' || /^\\d+$/.test(sku)) {\n    continue;\n  }\n  \n  const product = productMap.get(sku) || {};\n  const sales = salesBySku.get(sku) || { total_7d: 0, total_30d: 0, total_all: 0, last_sale_date: null };\n  const inventoryMaster = inventoryMasterMap.get(sku) || {};\n  \n  // Get inventory level data by matching inventory_item_id\n  const inventoryItemId = product.inventory_item_id;\n  const inventory = inventoryItemId ? inventoryMap.get(inventoryItemId) : {};\n  \n  // Calculate velocities\n  const velocity_7d = sales.total_7d / 7;\n  const velocity_30d = sales.total_30d / 30;\n  const avg_daily_sales = velocity_30d; // Use 30-day as primary average\n  \n  // Get supplier data if available\n  const supplierId = inventoryMaster.supplier_id || product.vendor;\n  const supplier = supplierId ? suppliersMap.get(supplierId) : {};\n  \n  outputItems.push({\n    json: {\n      sku: sku,\n      inventory_item_id: inventoryItemId,\n      location_id: inventory.location_id,\n      available_quantity: inventory.available,\n      current_stock: inventory.available || 0,\n      product_title: product.product_title,\n      product_id: product.product_id,\n      product_type: product.product_type,\n      vendor: product.vendor,\n      variant_id: product.id,\n      price: product.price,\n      sales_7d: sales.total_7d,\n      sales_30d: sales.total_30d,\n      velocity_7d: Math.round(velocity_7d * 100) / 100,\n      velocity_30d: Math.round(velocity_30d * 100) / 100,\n      avg_daily_sales: Math.round(avg_daily_sales * 100) / 100,\n      last_sale_date: sales.last_sale_date,\n      // Include inventory master data\n      safety_stock: inventoryMaster.safety_stock || 0,\n      max_stock: inventoryMaster.max_stock,\n      supplier_id: inventoryMaster.supplier_id || supplierId,\n      // Include supplier data\n      supplier_name: supplier.supplier_name || supplier.name,\n      supplier_email: supplier.supplier_email || supplier.email,\n      lead_time_days: supplier.lead_time_days || 7,\n      moq: supplier.moq || supplier.minimum_order_quantity || 1,\n      unit_cost: supplier.unit_cost || product.price || 0,\n      calculated_at: now.toISOString()\n    }\n  });\n}\n\nreturn outputItems;"
      },
      "typeVersion": 2
    },
    {
      "id": "7bae862d-8108-43dd-ac01-a029762e47b2",
      "name": "Calculate Dynamic Reorder Point",
      "type": "n8n-nodes-base.code",
      "position": [
        -1120,
        480
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Calculate Dynamic Reorder Point\n// Formula: reorder_point = avg_daily_sales * lead_time + safety_stock\n\nconst item = $input.item.json;\n\n// Get avg_daily_sales from previous Calculate Sales Velocity node\nconst avg_daily_sales = item.avg_daily_sales || 0;\n\n// Get lead_time from supplier data (merged from Read Suppliers node)\nconst lead_time = item.lead_time_days || item.supplier_lead_time || 7; // default 7 days\n\n// Get safety_stock from workflow config\nconst safetyStockDays = $('Workflow Configuration').first().json.safetyStockDays || 7;\nconst safety_stock = avg_daily_sales * safetyStockDays;\n\n// Calculate reorder point\nconst reorder_point = Math.ceil(avg_daily_sales * lead_time + safety_stock);\n\n// Get current stock level\nconst current_stock = item.current_stock || item.available_quantity || item.inventory_quantity || 0;\n\n// Determine if reorder is needed\nconst needs_reorder = current_stock <= reorder_point;\n\n// Return enriched item with reorder calculations\nreturn {\n  ...item,\n  reorder_point: reorder_point,\n  current_stock: current_stock,\n  needs_reorder: needs_reorder,\n  avg_daily_sales: avg_daily_sales,\n  lead_time_days: lead_time,\n  safety_stock: safety_stock,\n  days_until_stockout: avg_daily_sales > 0 ? Math.floor(current_stock / avg_daily_sales) : 999,\n  recommended_order_qty: needs_reorder ? Math.ceil(reorder_point - current_stock) : 0\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "39399f85-c850-4bc3-bd83-d4c8549ac673",
      "name": "Check Reorder Point Reached",
      "type": "n8n-nodes-base.if",
      "position": [
        -1104,
        288
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "id-1",
              "operator": {
                "type": "number",
                "operation": "lte"
              },
              "leftValue": "={{ $json.current_stock }}",
              "rightValue": "={{ $json.reorder_point }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "446bc9c0-8cc4-4bf8-9d50-f8c9d4049704",
      "name": "Check High Stockout Risk",
      "type": "n8n-nodes-base.if",
      "position": [
        -496,
        128
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "id-1",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $json.stockout_risk }}",
              "rightValue": "0.7"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "a4bbb6f1-c633-4569-9d6a-37d375e27906",
      "name": "Check Warehouse Redistribution Possible",
      "type": "n8n-nodes-base.if",
      "position": [
        0,
        112
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "id-1",
              "operator": {
                "type": "string",
                "operation": "exists"
              },
              "leftValue": "={{ $json.warehouse_overstock }}"
            },
            {
              "id": "id-2",
              "operator": {
                "type": "string",
                "operation": "exists"
              },
              "leftValue": "={{ $json.warehouse_understock }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "41973a11-986d-425f-a9f6-6c22a402119f",
      "name": "Check Supplier Availability",
      "type": "n8n-nodes-base.if",
      "position": [
        0,
        496
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "id-1",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.supplier_available }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "084287bf-6a54-425c-9c3f-7e12140613c8",
      "name": "Check Business Day",
      "type": "n8n-nodes-base.if",
      "position": [
        576,
        480
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "id-1",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $now.weekday >= 1 && $now.weekday <= 5 }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "cf4d4a4c-fe43-41ff-8532-a598daaf90ed",
      "name": "Check MOQ Met",
      "type": "n8n-nodes-base.if",
      "position": [
        576,
        672
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "id-1",
              "operator": {
                "type": "number",
                "operation": "gte"
              },
              "leftValue": "={{ $json.order_quantity }}",
              "rightValue": "={{ $json.moq }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "3cca4ca1-382f-437a-b684-8cc6c233817d",
      "name": "Check Promotional Period",
      "type": "n8n-nodes-base.if",
      "position": [
        832,
        656
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "id-1",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.is_promotional_period }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "d479aaa4-30c4-4f11-8bef-b1fa4d4180c4",
      "name": "Check Budget Limit",
      "type": "n8n-nodes-base.if",
      "position": [
        576,
        864
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "id-1",
              "operator": {
                "type": "number",
                "operation": "lt"
              },
              "leftValue": "={{ $json.total_po_value }}",
              "rightValue": "={{ $('Workflow Configuration').first().json.budgetLimit }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "33782cfd-eb06-487e-b8a9-30eb9ac0e1aa",
      "name": "Check Large Order Approval Needed",
      "type": "n8n-nodes-base.if",
      "position": [
        832,
        848
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "id-1",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.total_po_value > $('Workflow Configuration').first().json.largeOrderThreshold }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "92b2309b-3af2-4515-ae50-f416e4a083fa",
      "name": "Multi-Warehouse Distribution Logic",
      "type": "n8n-nodes-base.code",
      "position": [
        192,
        96
      ],
      "parameters": {
        "jsCode": "// Multi-Warehouse Distribution Logic\n// Analyzes inventory across warehouses to identify transfer opportunities\n\nconst items = $input.all();\nconst transferRecommendations = [];\n\n// Group inventory by SKU and warehouse\nconst inventoryBySku = {};\n\nfor (const item of items) {\n  const sku = item.json.sku;\n  const warehouse = item.json.warehouse || 'main';\n  const currentStock = item.json.current_stock || 0;\n  const reorderPoint = item.json.reorder_point || 0;\n  const maxStock = item.json.max_stock || reorderPoint * 3;\n  \n  if (!inventoryBySku[sku]) {\n    inventoryBySku[sku] = {};\n  }\n  \n  inventoryBySku[sku][warehouse] = {\n    current_stock: currentStock,\n    reorder_point: reorderPoint,\n    max_stock: maxStock,\n    overstock: Math.max(0, currentStock - maxStock),\n    understock: Math.max(0, reorderPoint - currentStock)\n  };\n}\n\n// Analyze each SKU for transfer opportunities\nfor (const sku in inventoryBySku) {\n  const warehouses = inventoryBySku[sku];\n  const warehouseNames = Object.keys(warehouses);\n  \n  // Compare each pair of warehouses\n  for (let i = 0; i < warehouseNames.length; i++) {\n    for (let j = 0; j < warehouseNames.length; j++) {\n      if (i === j) continue;\n      \n      const fromWarehouse = warehouseNames[i];\n      const toWarehouse = warehouseNames[j];\n      const from = warehouses[fromWarehouse];\n      const to = warehouses[toWarehouse];\n      \n      // Check if warehouse A has overstock and warehouse B has understock\n      if (from.overstock > 0 && to.understock > 0) {\n        // Calculate optimal transfer quantity\n        // Transfer the minimum of: overstock amount, understock need, or safe transfer amount\n        const safeTransferAmount = Math.floor(from.current_stock - from.reorder_point);\n        const transferQuantity = Math.min(\n          from.overstock,\n          to.understock,\n          Math.max(0, safeTransferAmount)\n        );\n        \n        if (transferQuantity > 0) {\n          transferRecommendations.push({\n            json: {\n              sku: sku,\n              from_warehouse: fromWarehouse,\n              to_warehouse: toWarehouse,\n              transfer_quantity: transferQuantity,\n              from_current_stock: from.current_stock,\n              from_after_transfer: from.current_stock - transferQuantity,\n              to_current_stock: to.current_stock,\n              to_after_transfer: to.current_stock + transferQuantity,\n              priority: to.understock / to.reorder_point, // Higher priority for more critical understocks\n              estimated_cost_savings: transferQuantity * 10, // Placeholder for cost calculation\n              recommendation_date: new Date().toISOString()\n            }\n          });\n        }\n      }\n    }\n  }\n}\n\n// Sort by priority (highest first)\ntransferRecommendations.sort((a, b) => b.json.priority - a.json.priority);\n\nreturn transferRecommendations.length > 0 ? transferRecommendations : [{ json: { message: 'No transfer recommendations at this time' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "10a4acc0-f331-47ef-959a-03772f3b23d7",
      "name": "Structure PO Line Items",
      "type": "n8n-nodes-base.set",
      "position": [
        -544,
        1536
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "id-1",
              "name": "po_line_item_id",
              "type": "string",
              "value": "={{ $json.sku }}_{{ $now.toMillis() }}"
            },
            {
              "id": "id-2",
              "name": "supplier_id",
              "type": "string",
              "value": "={{ $json.supplier_id }}"
            },
            {
              "id": "id-3",
              "name": "sku",
              "type": "string",
              "value": "={{ $json.sku }}"
            },
            {
              "id": "id-4",
              "name": "quantity",
              "type": "number",
              "value": "={{ $json.order_quantity }}"
            },
            {
              "id": "id-5",
              "name": "unit_price",
              "type": "number",
              "value": "={{ $json.unit_price }}"
            },
            {
              "id": "id-6",
              "name": "total_price",
              "type": "number",
              "value": "={{ $json.order_quantity * $json.unit_price }}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "745f377c-6d25-4fb3-af5e-82a60aa68e2e",
      "name": "Structure Inventory Updates",
      "type": "n8n-nodes-base.set",
      "position": [
        -1104,
        880
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "id-1",
              "name": "inventory_item_id",
              "type": "string",
              "value": "={{ $json.inventory_item_id }}"
            },
            {
              "id": "id-2",
              "name": "location_id",
              "type": "string",
              "value": "={{ $json.location_id }}"
            },
            {
              "id": "id-3",
              "name": "available",
              "type": "number",
              "value": "={{ $json.reorder_point }}"
            },
            {
              "id": "id-4",
              "name": "updated_at",
              "type": "string",
              "value": "={{ $now.toISO() }}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "ee4cb73c-0934-46b3-b449-94224da55324",
      "name": "Structure Analytics Data",
      "type": "n8n-nodes-base.set",
      "position": [
        -544,
        688
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "id-1",
              "name": "inventory_turnover",
              "type": "number",
              "value": "={{ $json.velocity_30d / $json.current_stock }}"
            },
            {
              "id": "id-2",
              "name": "carrying_cost",
              "type": "number",
              "value": "={{ $json.current_stock * $json.unit_price * 0.25 }}"
            },
            {
              "id": "id-3",
              "name": "stockout_saves",
              "type": "number",
              "value": "={{ $json.prevented_stockouts * $json.avg_order_value }}"
            },
            {
              "id": "id-4",
              "name": "timestamp",
              "type": "string",
              "value": "={{ $now.toISO() }}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "fa5926c9-26e5-4832-8782-07a4aa06e492",
      "name": "Structure Transfer Recommendations",
      "type": "n8n-nodes-base.set",
      "position": [
        416,
        96
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "id-1",
              "name": "transfer_id",
              "type": "string",
              "value": "={{ $json.sku }}_transfer_{{ $now.toMillis() }}"
            },
            {
              "id": "id-2",
              "name": "from_warehouse",
              "type": "string",
              "value": "={{ $json.from_warehouse }}"
            },
            {
              "id": "id-3",
              "name": "to_warehouse",
              "type": "string",
              "value": "={{ $json.to_warehouse }}"
            },
            {
              "id": "id-4",
              "name": "sku",
              "type": "string",
              "value": "={{ $json.sku }}"
            },
            {
              "id": "id-5",
              "name": "transfer_quantity",
              "type": "number",
              "value": "={{ $json.transfer_quantity }}"
            },
            {
              "id": "id-6",
              "name": "created_at",
              "type": "string",
              "value": "={{ $now.toISO() }}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "36367dbd-e441-4fd2-bc82-3265a17c6db5",
      "name": "Structure Slow-Mover Data",
      "type": "n8n-nodes-base.set",
      "position": [
        -1344,
        1232
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "id-1",
              "name": "sku",
              "type": "string",
              "value": "={{ $json.sku }}"
            },
            {
              "id": "id-2",
              "name": "days_no_sales",
              "type": "number",
              "value": "={{ $json.days_no_sales }}"
            },
            {
              "id": "id-3",
              "name": "current_stock",
              "type": "number",
              "value": "={{ $json.current_stock }}"
            },
            {
              "id": "id-4",
              "name": "suggested_discount",
              "type": "string",
              "value": "={{ $json.suggested_discount }}"
            },
            {
              "id": "id-5",
              "name": "suggested_bundle",
              "type": "string",
              "value": "={{ $json.suggested_bundle }}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "2168e9de-1d54-416b-83cd-d24bf23ea109",
      "name": "Prepare PO Email Context",
      "type": "n8n-nodes-base.code",
      "position": [
        -368,
        1536
      ],
      "parameters": {
        "jsCode": "// Prepare PO Email Context\n// Group line items by supplier, calculate totals, format supplier details\n\nconst items = $input.all();\nconst posBySupplier = {};\n\n// Group items by supplier\nfor (const item of items) {\n  const data = item.json;\n  const supplierId = data.supplier_id || data.supplierId;\n  const supplierName = data.supplier_name || data.supplierName || 'Unknown Supplier';\n  const supplierEmail = data.supplier_email || data.supplierEmail || '';\n  const supplierApiEndpoint = data.supplier_api_endpoint || data.supplierApiEndpoint || '';\n  \n  if (!posBySupplier[supplierId]) {\n    posBySupplier[supplierId] = {\n      supplier_id: supplierId,\n      supplier_name: supplierName,\n      supplier_email: supplierEmail,\n      supplier_api_endpoint: supplierApiEndpoint,\n      line_items: [],\n      total_amount: 0\n    };\n  }\n  \n  // Add line item\n  const lineItem = {\n    sku: data.sku || data.product_sku,\n    product_name: data.product_name || data.productName,\n    quantity: data.order_quantity || data.orderQuantity || 0,\n    unit_price: data.unit_cost || data.unitCost || 0,\n    line_total: (data.order_quantity || 0) * (data.unit_cost || 0)\n  };\n  \n  posBySupplier[supplierId].line_items.push(lineItem);\n  posBySupplier[supplierId].total_amount += lineItem.line_total;\n}\n\n// Convert to array and generate PO numbers\nconst result = [];\nlet poCounter = 1;\nconst now = new Date();\nconst dateStr = now.toISOString().split('T')[0].replace(/-/g, '');\n\nfor (const supplierId in posBySupplier) {\n  const po = posBySupplier[supplierId];\n  po.po_number = `PO-${dateStr}-${String(poCounter).padStart(4, '0')}`;\n  po.po_date = now.toISOString().split('T')[0]; // Add PO date in YYYY-MM-DD format\n  po.total_amount = Math.round(po.total_amount * 100) / 100; // Round to 2 decimals\n  \n  // Format line items as HTML table rows\n  const lineItemsHtml = po.line_items.map(item => {\n    return `<tr>\n      <td>${item.sku}</td>\n      <td>${item.product_name}</td>\n      <td>${item.quantity}</td>\n      <td>$${item.unit_price.toFixed(2)}</td>\n      <td>$${item.line_total.toFixed(2)}</td>\n    </tr>`;\n  }).join('\\n');\n  \n  po.line_items = lineItemsHtml;\n  \n  // Add default delivery instructions\n  po.delivery_instructions = 'Please deliver to our main warehouse during business hours (9 AM - 5 PM, Monday-Friday). Contact receiving department at least 24 hours before delivery.';\n  \n  result.push({ json: po });\n  poCounter++;\n}\n\nreturn result;"
      },
      "typeVersion": 2
    },
    {
      "id": "c761423d-acc4-4b07-a392-0b9c76c51da7",
      "name": "Send PO Email",
      "type": "n8n-nodes-base.gmail",
      "position": [
        144,
        1232
      ],
      "parameters": {
        "sendTo": "={{ $json.supplier_email || \"<__PLACEHOLDER_VALUE__Supplier email address__>\" }}",
        "message": "=<h2>Purchase Order</h2>\n<p><strong>Supplier:</strong> {{ $json.supplier_name }}</p>\n<p><strong>PO Number:</strong> {{ $json.po_number }}</p>\n<p><strong>Date:</strong> {{ $json.po_date }}</p>\n\n<h3>Line Items</h3>\n<table border=\"1\" cellpadding=\"8\" cellspacing=\"0\" style=\"border-collapse: collapse; width: 100%;\">\n  <thead>\n    <tr style=\"background-color: #f2f2f2;\">\n      <th>SKU</th>\n      <th>Product Name</th>\n      <th>Quantity</th>\n      <th>Unit Price</th>\n      <th>Total</th>\n    </tr>\n  </thead>\n  <tbody>\n    {{ $json.line_items }}\n  </tbody>\n</table>\n\n<p><strong>Total Amount:</strong> ${{ $json.total_amount.toFixed(2) }}</p>\n\n<h3>Delivery Instructions</h3>\n<p>{{ $json.delivery_instructions }}</p>\n\n<p>Please confirm receipt of this purchase order.</p>\n<p>Thank you for your business.</p>",
        "options": {},
        "subject": "=Purchase Order - {{ $json.po_number }}",
        "authentication": "serviceAccount"
      },
      "typeVersion": 2.1
    },
    {
      "id": "c31bd278-c720-4c06-b0f5-0cba015d1782",
      "name": "Send PO to Supplier API",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        144,
        1648
      ],
      "parameters": {
        "url": "={{ $json.supplier_api_endpoint || \"<__PLACEHOLDER_VALUE__Supplier API endpoint__>\" }}",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "sendBody": true,
        "sendHeaders": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "po_number",
              "value": "={{ $json.po_number }}"
            },
            {
              "name": "line_items",
              "value": "={{ $json.line_items }}"
            },
            {
              "name": "total_amount",
              "value": "={{ $json.total_amount }}"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "={{ $env.SUPPLIER_API_TOKEN || \"<__PLACEHOLDER_VALUE__Supplier API token__>\" }}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "f506f27e-5e39-45d8-af1e-6087fe441c00",
      "name": "Wait for PO Confirmation",
      "type": "n8n-nodes-base.wait",
      "position": [
        608,
        1232
      ],
      "parameters": {
        "unit": "hours",
        "amount": 1
      },
      "typeVersion": 1.1
    },
    {
      "id": "6ba4434b-77a1-4ff4-97d4-e4a1246cf926",
      "name": "Update Purchase Order Log",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        816,
        1232
      ],
      "parameters": {
        "columns": {
          "value": {
            "status": "sent",
            "po_number": "={{ $json.po_number }}",
            "created_at": "={{ $json.created_at || $now.toISO() }}",
            "supplier_id": "={{ $json.supplier_id }}",
            "confirmed_at": "={{ $json.confirmed_at || \"\" }}",
            "total_amount": "={{ $json.total_amount }}"
          },
          "schema": [
            {
              "id": "po_number",
              "required": false,
              "displayName": "po_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "supplier_id",
              "required": false,
              "displayName": "supplier_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "total_amount",
              "required": false,
              "displayName": "total_amount",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "created_at",
              "required": false,
              "displayName": "created_at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "confirmed_at",
              "required": false,
              "displayName": "confirmed_at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "po_number"
          ]
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "PO Log"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.PO_LOG_SHEET_ID || \"<__PLACEHOLDER_VALUE__Purchase Order Log Google Sheet ID__>\" }}"
        },
        "authentication": "serviceAccount"
      },
      "typeVersion": 4.7
    },
    {
      "id": "86c29c8b-d48b-4f3c-bcd4-e117071d940d",
      "name": "Alert Critical Stock Risk",
      "type": "n8n-nodes-base.slack",
      "position": [
        -688,
        -80
      ],
      "parameters": {
        "text": "=\ud83d\udea8 CRITICAL STOCK RISK: SKU {{ $json.sku }} has high stockout risk ({{ $json.stockout_risk }}). Current stock: {{ $json.current_stock }}, Reorder point: {{ $json.reorder_point }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.SLACK_CRITICAL_CHANNEL || \"<__PLACEHOLDER_VALUE__Slack channel ID for critical alerts__>\" }}"
        },
        "otherOptions": {}
      },
      "typeVersion": 2.3
    },
    {
      "id": "e3eec370-7613-4b01-bb10-8bc74c0744bb",
      "name": "Alert PO Sent",
      "type": "n8n-nodes-base.slack",
      "position": [
        144,
        1456
      ],
      "parameters": {
        "text": "=\u2705 Purchase Order Sent: PO #={{ $json.po_number }} to {{ $json.supplier_name }} for $={{ $json.total_amount }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.SLACK_PO_CHANNEL || \"<__PLACEHOLDER_VALUE__Slack channel ID for PO notifications__>\" }}"
        },
        "otherOptions": {}
      },
      "typeVersion": 2.3
    },
    {
      "id": "50187ac0-c76c-4f5e-80a9-d1b33d17e84f",
      "name": "Alert Slow-Mover Suggestions",
      "type": "n8n-nodes-base.slack",
      "position": [
        -1120,
        1232
      ],
      "parameters": {
        "text": "=\ud83d\udce6 Slow-Mover Alert: SKU {{ $json.sku }} has not sold in {{ $json.days_no_sales }} days. Current stock: {{ $json.current_stock }}. Suggested action: {{ $json.suggested_discount }}% discount or bundle with {{ $json.suggested_bundle }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.SLACK_SLOWMOVER_CHANNEL || \"<__PLACEHOLDER_VALUE__Slack channel ID for slow-mover alerts__>\" }}"
        },
        "otherOptions": {}
      },
      "typeVersion": 2.3
    },
    {
      "id": "08fdf235-0d62-46bc-be46-c70b96a4e3d8",
      "name": "Send Daily Summary",
      "type": "n8n-nodes-base.slack",
      "position": [
        1280,
        1232
      ],
      "parameters": {
        "text": "=\ud83d\udcca Daily Inventory Summary: Total POs sent: {{ $json.total_pos }}, Total value: ${{ $json.total_value }}, Critical alerts: {{ $json.critical_alerts }}, Slow-movers identified: {{ $json.slow_movers_count }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.SLACK_SUMMARY_CHANNEL || \"<__PLACEHOLDER_VALUE__Slack channel ID for daily summary__>\" }}"
        },
        "otherOptions": {}
      },
      "typeVersion": 2.3
    },
    {
      "id": "b6ea39ac-7159-4d73-a4a3-c3c1a3fcfc87",
      "name": "Sync to Accounting System",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -272,
        688
      ],
      "parameters": {
        "url": "={{ $env.ACCOUNTING_API_ENDPOINT || \"<__PLACEHOLDER_VALUE__Accounting API endpoint URL__>\" }}",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "sendBody": true,
        "authentication": "genericCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "total_inventory_value",
              "value": "={{ $json.total_inventory_value }}"
            },
            {
              "name": "carrying_costs",
              "value": "={{ $json.carrying_costs }}"
            },
            {
              "name": "turnover_rate",
              "value": "={{ $json.turnover_rate }}"
            },
            {
              "name": "timestamp",
              "value": "={{ $json.timestamp }}"
            }
          ]
        },
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.3
    },
    {
      "id": "c4525794-44be-4fd1-826a-c5980dca1531",
      "name": "Write Dashboard Metrics",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -272,
        528
      ],
      "parameters": {
        "columns": {
          "value": {
            "timestamp": "={{ $json.timestamp }}",
            "carrying_cost": "={{ $json.carrying_cost }}",
            "stockout_saves": "={{ $json.stockout_saves }}",
            "inventory_turnover": "={{ $json.inventory_turnover }}",
            "total_inventory_value": "={{ $json.total_inventory_value }}"
          },
          "schema": [
            {
              "id": "timestamp",
              "required": false,
              "displayName": "timestamp",
              "defaultMatch": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "inventory_turnover",
              "required": false,
              "displayName": "inventory_turnover",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "carrying_cost",
              "required": false,
              "displayName": "carrying_cost",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "stockout_saves",
              "required": false,
              "displayName": "stockout_saves",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "total_inventory_value",
              "required": false,
              "displayName": "total_inventory_value",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "timestamp"
          ]
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Metrics"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.DASHBOARD_METRICS_SHEET_ID || \"<__PLACEHOLDER_VALUE__Dashboard Metrics Google Sheet ID__>\" }}"
        },
        "authentication": "serviceAccount"
      },
      "typeVersion": 4.7
    },
    {
      "id": "e8d45593-db32-4f38-88a7-5b90090bcf3b",
      "name": "Write Scenario Planning",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -272,
        848
      ],
      "parameters": {
        "columns": {
          "value": {
            "sku": "={{ $json.sku }}",
            "lead_time": "={{ $json.lead_time }}",
            "forecast_30d": "={{ $json.forecast_30d }}",
            "recommended_stock_promo": "={{ $json.recommended_stock_promo }}",
            "recommended_stock_normal": "={{ $json.recommended_stock_normal }}"
          },
          "schema": [
            {
              "id": "sku",
              "required": false,
              "displayName": "sku",
              "defaultMatch": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "recommended_stock_promo",
              "required": false,
              "displayName": "recommended_stock_promo",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "recommended_stock_normal",
              "required": false,
              "displayName": "recommended_stock_normal",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "forecast_30d",
              "required": false,
              "displayName": "forecast_30d",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "lead_time",
              "required": false,
              "displayName": "lead_time",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "sku"
          ]
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Scenarios"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.SCENARIO_PLANNING_SHEET_ID || \"<__PLACEHOLDER_VALUE__Scenario Planning Google Sheet ID__>\" }}"
        },
        "authentication": "serviceAccount"
      },
      "typeVersion": 4.7
    },
    {
      "id": "840774b2-f2ee-4022-840c-b60fcfea51a8",
      "name": "Detect Slow-Movers",
      "type": "n8n-nodes-base.code",
      "position": [
        -1568,
        1232
      ],
      "parameters": {
        "jsCode": "// Detect slow-moving inventory items and generate actionable recommendations\n\nconst items = $input.all();\nconst slowMovers = [];\n\n// Configuration\nconst SLOW_MOVER_THRESHOLD = 2; // velocity below this is considered slow\nconst today = new Date();\n\nfor (const item of items) {\n  const velocity = item.json.velocity_30d || 0;\n  const lastSaleDate = item.json.last_sale_date ? new Date(item.json.last_sale_date) : null;\n  \n  // Check if item is a slow mover\n  if (velocity < SLOW_MOVER_THRESHOLD) {\n    // Calculate days since last sale\n    let daysSinceLastSale = 0;\n    if (lastSaleDate) {\n      const diffTime = Math.abs(today - lastSaleDate);\n      daysSinceLastSale = Math.ceil(diffTime / (1000 * 60 * 60 * 24));\n    } else {\n      daysSinceLastSale = 90; // Default if no sale date available\n    }\n    \n    // Calculate suggested discount based on days without sales\n    let suggestedDiscount = 10; // Base discount\n    if (daysSinceLastSale > 60) {\n      suggestedDiscount = 30;\n    } else if (daysSinceLastSale > 30) {\n      suggestedDiscount = 20;\n    } else if (daysSinceLastSale > 14) {\n      suggestedDiscount = 15;\n    }\n    \n    // Suggest bundle partners based on category\n    const category = item.json.category || item.json.product_type || 'General';\n    const sku = item.json.sku || item.json.product_id;\n    \n    // Find potential bundle partners from same category with better velocity\n    const bundlePartners = items\n      .filter(i => {\n        const iCategory = i.json.category || i.json.product_type || 'General';\n        const iVelocity = i.json.velocity_30d || 0;\n        const iSku = i.json.sku || i.json.product_id;\n        return iCategory === category && iVelocity > velocity && iSku !== sku;\n      })\n      .slice(0, 3)\n      .map(i => i.json.sku || i.json.product_id || i.json.title)\n      .join(', ');\n    \n    slowMovers.push({\n      json: {\n        ...item.json,\n        days_no_sales: daysSinceLastSale,\n        suggested_discount: suggestedDiscount,\n        suggested_bundle: bundlePartners || 'No suitable bundle partners found',\n        slow_mover_flag: true,\n        recommendation_date: today.toISOString()\n      }\n    });\n  }\n}\n\n// Return slow movers or empty array if none found\nreturn slowMovers.length > 0 ? slowMovers : [{ json: { message: 'No slow-moving items detected' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "919a409f-0c67-40d4-8883-8d6099d52406",
      "name": "Optimize Profit Priority",
      "type": "n8n-nodes-base.code",
      "position": [
        1328,
        832
      ],
      "parameters": {
        "jsCode": "// Optimize Profit Priority\n// Calculate profit score and prioritize SKUs based on profitability\n\nconst items = $input.all();\nconst optimizedItems = [];\n\nfor (const item of items) {\n  const data = item.json;\n  \n  // Extract relevant metrics with defaults - using correct field names from previous nodes\n  const unitPrice = data.unit_price || data.unit_cost || 0;\n  const unitCost = data.unit_cost || data.supplier_unit_cost || unitPrice * 0.6; // Estimate if not available\n  const margin = unitPrice > 0 ? (unitPrice - unitCost) / unitPrice : 0;\n  \n  // Use velocity_30d from Calculate Sales Velocity node\n  const velocity = data.velocity_30d || data.avg_daily_sales || 0;\n  \n  // Use carrying_cost if available, otherwise calculate as percentage of unit cost\n  const carryingCost = data.carrying_cost || (data.current_stock * unitCost * 0.25) || 0.01;\n  \n  // Use stockout_risk from Calculate Stockout Risk node\n  const stockoutRisk = data.stockout_risk || 0.01;\n  \n  // Calculate profit score: (margin * velocity) / (carrying_cost + stockout_risk)\n  // Higher margin and velocity increase score, higher costs and risk decrease it\n  const profitScore = velocity > 0 ? (margin * velocity) / (carryingCost + stockoutRisk) : 0;\n  \n  optimizedItems.push({\n    ...data,\n    profit_score: Math.round(profitScore * 100) / 100,\n    margin: Math.round(margin * 10000) / 100, // Convert to percentage with 2 decimals\n    velocity: velocity,\n    carrying_cost: Math.round(carryingCost * 100) / 100,\n    stockout_risk: Math.round(stockoutRisk * 100) / 100\n  });\n}\n\n// Sort by profit_score descending (highest profit priority first)\noptimizedItems.sort((a, b) => b.profit_score - a.profit_score);\n\n// Add priority rank\nconst rankedItems = optimizedItems.map((item, index) => ({\n  json: {\n    ...item,\n    priority_rank: index + 1,\n    priority_tier: index < optimizedItems.length * 0.2 ? 'High' : \n                   index < optimizedItems.length * 0.5 ? 'Medium' : 'Low'\n  }\n}));\n\nreturn rankedItems;"
      },
      "typeVersion": 2
    },
    {
      "id": "13c5f844-a067-4106-af5f-4000d3c91524",
      "name": "Calculate Stockout Risk",
      "type": "n8n-nodes-base.code",
      "position": [
        -688,
        128
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Calculate Stockout Risk\n// Assess risk level based on days until stockout vs lead time\n\nconst item = $input.item.json;\n\n// Get days until stockout and lead time\nconst daysUntilStockout = item.days_until_stockout || 999;\nconst leadTime = item.lead_time_days || 7;\n\n// Calculate stockout risk score (0-1 scale)\nlet stockoutRisk = 0;\n\nif (daysUntilStockout <= leadTime) {\n  // Critical: Will stockout before next order arrives\n  stockoutRisk = 0.9;\n} else if (daysUntilStockout <= leadTime * 1.5) {\n  // High: Very close to lead time threshold\n  stockoutRisk = 0.7;\n} else if (daysUntilStockout <= leadTime * 2) {\n  // Medium: Some buffer but still concerning\n  stockoutRisk = 0.5;\n} else if (daysUntilStockout <= leadTime * 3) {\n  // Low-Medium: Reasonable buffer\n  stockoutRisk = 0.3;\n} else {\n  // Low: Plenty of stock\n  stockoutRisk = 0.1;\n}\n\n// Return item with stockout risk score\nreturn {\n  ...item,\n  stockout_risk: stockoutRisk\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "8650d25c-8ff9-44b7-84bd-527b517cb277",
      "name": "Enrich with Supplier Data",
      "type": "n8n-nodes-base.code",
      "position": [
        240,
        480
      ],
      "parameters": {
        "jsCode": "// Enrich with Supplier Data\n// Match items with supplier information and add supplier fields\n\nconst items = $input.all();\nconst suppliers = $('Read Suppliers').all();\nconst supplierMap = new Map();\n\n// Build supplier lookup map with multiple ID field variations\nfor (const supplier of suppliers) {\n  const data = supplier.json;\n  const supplierId = data.supplier_id || data.id || data.supplierId || data.ID;\n  \n  if (supplierId) {\n    supplierMap.set(String(supplierId).trim(), data);\n  }\n}\n\n// Enrich each item with supplier data\nconst enrichedItems = items.map(item => {\n  const itemData = item.json;\n  \n  // Try multiple supplier ID field variations\n  const supplierId = String(\n    itemData.supplier_id || \n    itemData.preferred_supplier_id || \n    itemData.supplierId || \n    itemData.preferredSupplierId ||\n    ''\n  ).trim();\n  \n  // Get supplier data from map\n  const supplier = supplierMap.get(supplierId) || {};\n  \n  return {\n    json: {\n      ...itemData,\n      supplier_id: supplierId || itemData.supplier_id,\n      supplier_name: supplier.supplier_name || supplier.name || supplier.supplierName || 'Unknown Supplier',\n      supplier_email: supplier.supplier_email || supplier.email || supplier.supplierEmail || '',\n      supplier_api_endpoint: supplier.api_endpoint || supplier.apiEndpoint || supplier.supplier_api_endpoint || '',\n      supplier_available: supplier.available !== false && supplier.is_active !== false,\n      lead_time_days: supplier.lead_time_days || supplier.leadTimeDays || supplier.lead_time || 7,\n      moq: supplier.moq || supplier.minimum_order_quantity || supplier.minimumOrderQuantity || supplier.MOQ || 1,\n      unit_cost: supplier.unit_cost || supplier.unitCost || supplier.cost || itemData.unit_price || itemData.unitPrice || 0\n    }\n  };\n});\n\nreturn enrichedItems;"
      },
      "typeVersion": 2
    },
    {
      "id": "e8b15fd5-f61f-4009-84e9-ac3802d76449",
      "name": "Calculate Order Quantity and Value",
      "type": "n8n-nodes-base.code",
      "position": [
        1136,
        832
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Calculate Order Quantity and Value\n// Ensures MOQ is met and calculates total PO value\n\nconst item = $input.item.json;\n\n// Get recommended order quantity\nconst orderQuantity = item.recommended_order_qty || 0;\n\n// Get unit cost from supplier data\nconst unitCost = item.unit_cost || item.unit_price || 0;\n\n// Get minimum order quantity\nconst moq = item.moq || 1;\n\n// Adjust quantity to meet MOQ\nconst adjustedQuantity = Math.max(orderQuantity, moq);\n\n// Calculate total PO value\nconst totalPoValue = adjustedQuantity * unitCost;\n\n// Get current timestamp\nconst now = new Date();\n\n// Return enriched item with order calculations\nreturn {\n  ...item,\n  order_quantity: adjustedQuantity,\n  unit_price: unitCost,\n  total_po_value: Math.round(totalPoValue * 100) / 100,\n  po_date: now.toISOString(),\n  created_at: now.toISOString(),\n  status: 'pending'\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "56a1c2a6-2273-4462-af07-e39f56cb7bac",
      "name": "Aggregate Daily Summary",
      "type": "n8n-nodes-base.code",
      "position": [
        1024,
        1232
      ],
      "parameters": {
        "jsCode": "// Aggregate Daily Summary with safe error handling\n\nlet totalPos = 0;\nlet totalValue = 0;\nlet criticalAlerts = 0;\nlet slowMovers = 0;\n\n// Safely get PO items\ntry {\n  const poItems = $('Update Purchase Order Log').all();\n  for (const po of poItems) {\n    totalPos++;\n    totalValue += po.json.total_amount || 0;\n  }\n} catch (error) {\n  console.log('Update Purchase Order Log node not executed or has no data');\n}\n\n// Safely get critical alerts count\ntry {\n  const criticalAlertItems = $('Alert Critical Stock Risk').all();\n  criticalAlerts = criticalAlertItems.length;\n} catch (error) {\n  console.log('Alert Critical Stock Risk node not executed or has no data');\n}\n\n// Safely get slow movers count\ntry {\n  const slowMoverItems = $('Alert Slow-Mover Suggestions').all();\n  slowMovers = slowMoverItems.length;\n} catch (error) {\n  console.log('Alert Slow-Mover Suggestions node not executed or has no data');\n}\n\nreturn [{\n  json: {\n    total_pos: totalPos,\n    total_value: Math.round(totalValue * 100) / 100,\n    critical_alerts: criticalAlerts,\n    slow_movers_count: slowMovers,\n    summary_date: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "e7643129-c7de-49aa-b2d1-81f0c7b121f5",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3840,
        -224
      ],
      "parameters": {
        "width": 1200,
        "height": 528,
        "content": "## How it works\nThis workflow automates inventory monitoring, predictive reordering, and supplier communication using Shopify + Google Sheets.\n\nIt runs hourly to pull live inventory levels, product details, and recent order history from Shopify. In parallel, it reads your Inventory Master, Supplier list, and Purchase Order Log from Google Sheets, then merges all sources into a single SKU-level dataset.\n\nFor each SKU, it calculates sales velocity (7/30-day) and derives a dynamic reorder point based on demand, lead time, and safety stock settings. If current stock drops below the reorder point, it evaluates stockout risk and sends a Slack alert for critical items. Where possible, it checks multi-warehouse redistribution logic to prevent stockouts before ordering.\n\nWhen replenishment is needed, it enriches each SKU with supplier data, verifies availability, calculates optimal orde
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

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 based on sales velocity, and automate supplier communication. The workflow…

Source: https://n8n.io/workflows/11799/ — original creator credit. Request a take-down →

More E-commerce workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

E-commerce

E-commerce store owners and sales managers who want AI-powered insights from their Shopify data without manually crunching numbers every week.

Shopify, HTTP Request, Slack +2
E-commerce

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,

Shopify, Google Sheets, Chain Llm +4
E-commerce

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

HTTP Request, Google Sheets, Gmail
E-commerce

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

Stop And Error, Slack, Gmail +1
E-commerce

This n8n automation identifies Magento 2 orders that have been stuck in the same status (like "processing") for the past 7 weekdays (excluding weekends), compiles them into a clean Google Sheet report

HTTP Request, Google Sheets, Gmail