{
  "name": "Inventory Reorder & Vendor PO Automation",
  "nodes": [
    {
      "id": "schedule_inventory_check",
      "name": "Daily Inventory Check",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [0, 300],
      "parameters": {
        "rule": {
          "interval": [
            { "field": "cronExpression", "expression": "0 7 * * *" }
          ]
        }
      }
    },
    {
      "id": "http_stock_levels",
      "name": "Get Current Stock Levels",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [220, 300],
      "onError": "continueRegularOutput",
      "parameters": {
        "method": "GET",
        "url": "https://REPLACE_WITH_YOUR_WAREHOUSE_API/inventory",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      }
    },
    {
      "id": "sheets_sales_velocity",
      "name": "Get Sales Velocity",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [440, 300],
      "onError": "continueRegularOutput",
      "parameters": {
        "resource": "sheet",
        "operation": "read",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "REPLACE_WITH_YOUR_SHEET_ID",
          "cachedResultName": "Sales Velocity Tracker"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "30-Day Velocity",
          "cachedResultName": "30-Day Velocity"
        },
        "range": "A:D",
        "options": {}
      }
    },
    {
      "id": "code_reorder_points",
      "name": "Calculate Reorder Points",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [660, 300],
      "parameters": {
        "language": "javaScript",
        "mode": "runOnceForAllItems",
        "jsCode": "const LEAD_TIME_DAYS = 7;\nconst SAFETY_STOCK_DAYS = 3;\nconst stock = $('Get Current Stock Levels').all().map(i => i.json);\nconst velocity = $('Get Sales Velocity').all().map(i => i.json);\nconst velocityBySku = Object.fromEntries(velocity.map(v => [v.sku, Number(v.units_per_day ?? 0)]));\nconst out = [];\nfor (const s of stock) {\n  const dailyVelocity = velocityBySku[s.sku] ?? 0;\n  const reorderPoint = dailyVelocity * (LEAD_TIME_DAYS + SAFETY_STOCK_DAYS);\n  const onHand = Number(s.quantity ?? 0);\n  const needsReorder = dailyVelocity > 0 && onHand <= reorderPoint;\n  const targetStock = dailyVelocity * (LEAD_TIME_DAYS + SAFETY_STOCK_DAYS) * 2;\n  const recommendedQty = needsReorder ? Math.ceil(targetStock - onHand) : 0;\n  out.push({\n    json: {\n      sku: s.sku,\n      vendor: s.vendor,\n      unitCost: Number(s.unit_cost ?? 0),\n      onHand,\n      dailyVelocity,\n      reorderPoint: Math.round(reorderPoint),\n      needsReorder,\n      recommendedQty,\n    },\n  });\n}\nreturn out;"
      }
    },
    {
      "id": "if_any_reorder",
      "name": "Any SKUs Below Reorder Point?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [880, 300],
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 },
          "combinator": "and",
          "conditions": [
            {
              "id": "reorder-check-1",
              "leftValue": "={{ $json.needsReorder }}",
              "rightValue": "",
              "operator": { "type": "boolean", "operation": "true", "singleValue": true }
            }
          ]
        }
      }
    },
    {
      "id": "noop_no_action",
      "name": "No Action Needed",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [1100, 460],
      "parameters": {}
    },
    {
      "id": "code_group_by_vendor",
      "name": "Group SKUs by Vendor",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1100, 200],
      "parameters": {
        "language": "javaScript",
        "mode": "runOnceForAllItems",
        "jsCode": "const lines = $input.all().map(i => i.json);\nconst byVendor = {};\nfor (const l of lines) {\n  byVendor[l.vendor] ??= { vendor: l.vendor, lines: [], total: 0 };\n  byVendor[l.vendor].lines.push({ sku: l.sku, qty: l.recommendedQty, unitCost: l.unitCost });\n  byVendor[l.vendor].total += l.recommendedQty * l.unitCost;\n}\nreturn Object.values(byVendor).map(v => ({ json: v }));"
      }
    },
    {
      "id": "split_loop_vendor",
      "name": "Loop Per Vendor",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [1320, 200],
      "parameters": {
        "batchSize": 1,
        "options": {}
      }
    },
    {
      "id": "slack_all_sent",
      "name": "Notify Ops — All POs Sent",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.4,
      "position": [1540, 460],
      "onError": "continueRegularOutput",
      "parameters": {
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": { "__rl": true, "mode": "name", "value": "#inventory-ops", "cachedResultName": "inventory-ops" },
        "text": "={{ \":package: Reorder run complete — all vendor POs sent for \" + $today.toISODate() + \".\" }}"
      }
    },
    {
      "id": "openai_draft_po",
      "name": "Draft PO Email Copy",
      "type": "n8n-nodes-base.openAi",
      "typeVersion": 1.1,
      "position": [1540, 120],
      "onError": "continueRegularOutput",
      "parameters": {
        "resource": "chat",
        "operation": "complete",
        "model": "gpt-4o-mini",
        "prompt": {
          "messages": [
            {
              "role": "system",
              "content": "You write short, professional purchase-order emails for a supply-chain ops team. Given a vendor name and a list of SKUs with quantities and unit costs, write a concise PO email body (plain text, no markdown, under 150 words) requesting the listed quantities, referencing the total order value, and asking for a confirmed ship date."
            },
            {
              "role": "user",
              "content": "={{ \"Vendor: \" + $json.vendor + \"\\nLines: \" + JSON.stringify($json.lines) + \"\\nTotal: $\" + $json.total.toFixed(2) }}"
            }
          ]
        },
        "maxTokens": 300,
        "options": { "temperature": 0.3 }
      }
    },
    {
      "id": "code_po_total",
      "name": "Calculate PO Total",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1760, 120],
      "parameters": {
        "language": "javaScript",
        "mode": "runOnceForAllItems",
        "jsCode": "const out = [];\nfor (const item of $input.all()) {\n  const vendorData = $('Loop Per Vendor').first().json;\n  const draft = item.json?.message?.content ?? item.json?.text ?? item.json?.choices?.[0]?.message?.content ?? '';\n  out.push({\n    json: {\n      ...vendorData,\n      poEmailBody: draft,\n    },\n  });\n}\nreturn out;"
      }
    },
    {
      "id": "if_approval_threshold",
      "name": "PO Total Above Approval Threshold?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [1980, 120],
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 },
          "combinator": "and",
          "conditions": [
            {
              "id": "approval-check-1",
              "leftValue": "={{ $json.total }}",
              "rightValue": 2000,
              "operator": { "type": "number", "operation": "gt" }
            }
          ]
        }
      }
    },
    {
      "id": "slack_request_approval",
      "name": "Request Manager Approval",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.4,
      "position": [2200, 40],
      "onError": "continueRegularOutput",
      "parameters": {
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": { "__rl": true, "mode": "name", "value": "#inventory-ops", "cachedResultName": "inventory-ops" },
        "text": "={{ \":large_orange_diamond: Large PO queued — \" + $json.vendor + \", $\" + $json.total.toFixed(2) + \". Sending automatically; flagging for visibility since it's above the $2,000 threshold.\" }}"
      }
    },
    {
      "id": "gmail_send_po",
      "name": "Send PO to Vendor",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [2420, 120],
      "onError": "continueRegularOutput",
      "parameters": {
        "resource": "message",
        "operation": "send",
        "sendTo": "={{ $json.vendor }}@REPLACE_WITH_VENDOR_DOMAIN.com",
        "subject": "={{ \"Purchase Order — \" + $today.toISODate() }}",
        "emailType": "text",
        "message": "={{ $json.poEmailBody }}",
        "options": {}
      }
    },
    {
      "id": "sheets_log_po",
      "name": "Log PO in Tracker",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [2640, 120],
      "onError": "continueRegularOutput",
      "parameters": {
        "resource": "sheet",
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "REPLACE_WITH_YOUR_SHEET_ID",
          "cachedResultName": "PO Tracker"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Purchase Orders",
          "cachedResultName": "Purchase Orders"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "date": "={{ $today.toISODate() }}",
            "vendor": "={{ $json.vendor }}",
            "total": "={{ $json.total }}",
            "status": "sent"
          }
        },
        "options": {}
      }
    }
  ],
  "connections": {
    "Daily Inventory Check": {
      "main": [[{ "node": "Get Current Stock Levels", "type": "main", "index": 0 }]]
    },
    "Get Current Stock Levels": {
      "main": [[{ "node": "Get Sales Velocity", "type": "main", "index": 0 }]]
    },
    "Get Sales Velocity": {
      "main": [[{ "node": "Calculate Reorder Points", "type": "main", "index": 0 }]]
    },
    "Calculate Reorder Points": {
      "main": [[{ "node": "Any SKUs Below Reorder Point?", "type": "main", "index": 0 }]]
    },
    "Any SKUs Below Reorder Point?": {
      "main": [
        [{ "node": "Group SKUs by Vendor", "type": "main", "index": 0 }],
        [{ "node": "No Action Needed", "type": "main", "index": 0 }]
      ]
    },
    "Group SKUs by Vendor": {
      "main": [[{ "node": "Loop Per Vendor", "type": "main", "index": 0 }]]
    },
    "Loop Per Vendor": {
      "main": [
        [{ "node": "Notify Ops — All POs Sent", "type": "main", "index": 0 }],
        [{ "node": "Draft PO Email Copy", "type": "main", "index": 0 }]
      ]
    },
    "Draft PO Email Copy": {
      "main": [[{ "node": "Calculate PO Total", "type": "main", "index": 0 }]]
    },
    "Calculate PO Total": {
      "main": [[{ "node": "PO Total Above Approval Threshold?", "type": "main", "index": 0 }]]
    },
    "PO Total Above Approval Threshold?": {
      "main": [
        [{ "node": "Request Manager Approval", "type": "main", "index": 0 }],
        [{ "node": "Send PO to Vendor", "type": "main", "index": 0 }]
      ]
    },
    "Request Manager Approval": {
      "main": [[{ "node": "Send PO to Vendor", "type": "main", "index": 0 }]]
    },
    "Send PO to Vendor": {
      "main": [[{ "node": "Log PO in Tracker", "type": "main", "index": 0 }]]
    },
    "Log PO in Tracker": {
      "main": [[{ "node": "Loop Per Vendor", "type": "main", "index": 0 }]]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
