From d6462d672174bc50ae8a9de90202a72887a9bfee Mon Sep 17 00:00:00 2001 From: esaci Date: Fri, 14 Nov 2025 14:05:16 +0100 Subject: [PATCH] Updated Folder --- .../README.md | 74 + .../workflow.json | 882 +++++ n8n-workflows | 1 - .../README.md | 85 + .../workflow.json | 1352 +++++++ .../README.md | 74 + .../workflow.json | 677 ++++ .../README.md | 63 + .../workflow.json | 477 +++ .../README.md | 71 + .../workflow.json | 644 ++++ .../README.md | 54 + .../workflow.json | 754 ++++ .../README.md | 58 + .../workflow.json | 336 ++ .../README.md | 71 + .../workflow.json | 589 +++ .../README.md | 90 + .../workflow.json | 835 ++++ .../README.md | 70 + .../workflow.json | 743 ++++ .../README.md | 64 + .../workflow.json | 3428 +++++++++++++++++ .../README.md | 68 + .../workflow.json | 568 +++ .../README.md | 62 + .../workflow.json | 1514 ++++++++ .../README.md | 55 + .../workflow.json | 440 +++ .../README.md | 62 + .../workflow.json | 373 ++ .../README.md | 70 + .../workflow.json | 734 ++++ .../README.md | 61 + .../workflow.json | 293 ++ .../README.md | 70 + .../workflow.json | 1217 ++++++ .../README.md | 73 + .../workflow.json | 664 ++++ .../README.md | 66 + .../workflow.json | 437 +++ .../README.md | 66 + .../workflow.json | 429 +++ .../README.md | 103 + .../workflow.json | 576 +++ .../README.md | 59 + .../workflow.json | 351 ++ .../README.md | 69 + .../workflow.json | 1595 ++++++++ .../README.md | 71 + .../workflow.json | 268 ++ .../README.md | 69 + .../workflow.json | 561 +++ 53 files changed, 22535 insertions(+), 1 deletion(-) create mode 100644 Inventory ABC & Pareto Analysis with Google Sheets for Supply Chain Optimization/README.md create mode 100644 Inventory ABC & Pareto Analysis with Google Sheets for Supply Chain Optimization/workflow.json delete mode 160000 n8n-workflows create mode 100644 ♻️ AI Multi-Stop Planner for Circular Logistics with GPT-4o & Open Route API/README.md create mode 100644 ♻️ AI Multi-Stop Planner for Circular Logistics with GPT-4o & Open Route API/workflow.json create mode 100644 ✈️ CO2 Emissions of Business Travels with Carbon Interface API and GPT-4o/README.md create mode 100644 ✈️ CO2 Emissions of Business Travels with Carbon Interface API and GPT-4o/workflow.json create mode 100644 ✍️ AI Agent to Create Linkedin Posts for Blog Promotion with GPT-4o/README.md create mode 100644 ✍️ AI Agent to Create Linkedin Posts for Blog Promotion with GPT-4o/workflow.json create mode 100644 ✍️ Blog Image SEO & Size Auditor with Ghost and Google Sheets/README.md create mode 100644 ✍️ Blog Image SEO & Size Auditor with Ghost and Google Sheets/workflow.json create mode 100644 🉑 Generate Anki Flash Cards for Language Learning with Google Translate and GPT/README.md create mode 100644 🉑 Generate Anki Flash Cards for Language Learning with Google Translate and GPT/workflow.json create mode 100644 🌲 AI Agent for Sustainability Report Audit with Gmail and GPT-40/README.md create mode 100644 🌲 AI Agent for Sustainability Report Audit with Gmail and GPT-40/workflow.json create mode 100644 🌳 EU Green Legislation Tracker with GPT-4o, Google Sheets and Tasks/README.md create mode 100644 🌳 EU Green Legislation Tracker with GPT-4o, Google Sheets and Tasks/workflow.json create mode 100644 🍅 Custom Pomodoro Tracker using Telegram and Google Sheet/README.md create mode 100644 🍅 Custom Pomodoro Tracker using Telegram and Google Sheet/workflow.json create mode 100644 🏤 Scrapping of European Union Events with Google Sheets/README.md create mode 100644 🏤 Scrapping of European Union Events with Google Sheets/workflow.json create mode 100644 💰 Automate Currency Rates Update in Invoices with Google Sheet, ExchangeRate API/README.md create mode 100644 💰 Automate Currency Rates Update in Invoices with Google Sheet, ExchangeRate API/workflow.json create mode 100644 📈 Hourly Monitoring of Crypto Rates with Alpha Vantage API and Google Sheets/README.md create mode 100644 📈 Hourly Monitoring of Crypto Rates with Alpha Vantage API and Google Sheets/workflow.json create mode 100644 📦 Electronic Data Interchange (EDI) Message Parsing with Gmail and Google Sheet/README.md create mode 100644 📦 Electronic Data Interchange (EDI) Message Parsing with Gmail and Google Sheet/workflow.json create mode 100644 📰 AI-Powered EU News Digest by Topic with RSS, GPT & Gmail/README.md create mode 100644 📰 AI-Powered EU News Digest by Topic with RSS, GPT & Gmail/workflow.json create mode 100644 📸 Automate Photo Background Removal with Photoroom API and Google Drive/README.md create mode 100644 📸 Automate Photo Background Removal with Photoroom API and Google Drive/workflow.json create mode 100644 🗞️ AI-Powered Sustainability Newsletter for Marketing with Gmail, GPT-4o/README.md create mode 100644 🗞️ AI-Powered Sustainability Newsletter for Marketing with Gmail, GPT-4o/workflow.json create mode 100644 🗼 AI Powered Supply Chain Control Tower with BigQuery and GPT-4o/README.md create mode 100644 🗼 AI Powered Supply Chain Control Tower with BigQuery and GPT-4o/workflow.json create mode 100644 🚚 Automate Delivery Confirmation with Telegram Bot, Google Drive and Gmail/README.md create mode 100644 🚚 Automate Delivery Confirmation with Telegram Bot, Google Drive and Gmail/workflow.json create mode 100644 🚚 CO2 Emissions of Freight Shipments with Carbon Interface API and GPT-4o/README.md create mode 100644 🚚 CO2 Emissions of Freight Shipments with Carbon Interface API and GPT-4o/workflow.json create mode 100644 🚚 Estimate Driving Time and Distance for Logistics with Open Route API/README.md create mode 100644 🚚 Estimate Driving Time and Distance for Logistics with Open Route API/workflow.json create mode 100644 🚛🗺️ Geocoding for Logistics with Open Route API and Google Sheets/README.md create mode 100644 🚛🗺️ Geocoding for Logistics with Open Route API and Google Sheets/workflow.json create mode 100644 🚦 AI Agent for Debugging Workflow Executions with HTTP, Webhook/README.md create mode 100644 🚦 AI Agent for Debugging Workflow Executions with HTTP, Webhook/workflow.json create mode 100644 🛻 AI Agent for Logistics Order Processing with GPT-4o, Gmail and Google Sheet/README.md create mode 100644 🛻 AI Agent for Logistics Order Processing with GPT-4o, Gmail and Google Sheet/workflow.json create mode 100644 🤖🚚 AI agent for Transportation Orders Management with GPT-4o and Open Route API/README.md create mode 100644 🤖🚚 AI agent for Transportation Orders Management with GPT-4o and Open Route API/workflow.json create mode 100644 🧑‍🎓 AI Powered Language Teacher with Telegram, Google Sheet and GPT-4o/README.md create mode 100644 🧑‍🎓 AI Powered Language Teacher with Telegram, Google Sheet and GPT-4o/workflow.json create mode 100644 🧑‍🦯Improve your Website Accessibility with GPT-4o and Google Sheet/README.md create mode 100644 🧑‍🦯Improve your Website Accessibility with GPT-4o and Google Sheet/workflow.json diff --git a/Inventory ABC & Pareto Analysis with Google Sheets for Supply Chain Optimization/README.md b/Inventory ABC & Pareto Analysis with Google Sheets for Supply Chain Optimization/README.md new file mode 100644 index 0000000..18f9dde --- /dev/null +++ b/Inventory ABC & Pareto Analysis with Google Sheets for Supply Chain Optimization/README.md @@ -0,0 +1,74 @@ +***Tags**: Supply Chain, Inventory Management, ABC Analysis, Pareto Principle, Demand Variability, Automation, Google Sheets* + +### Context + +Hi! I’m Samir — a Supply Chain Engineer and Data Scientist based in Paris, and founder of [LogiGreen Consulting](https://logi-green.com). + +I help companies **optimise inventory and logistics operations** by combining **data analytics** and **workflow automation**. + +This workflow is part of our **inventory optimisation toolkit**, allowing businesses to perform **ABC classification and Pareto analysis** directly from their transactional sales data. + +> Automate inventory segmentation with n8n! + +📬 For business inquiries, feel free to connect with me on [LinkedIn](https://www.linkedin.com/in/samir-saci) + +### Who is this template for? + +This workflow is designed for **supply chain analysts**, **demand planners**, or **inventory managers** who want to: + +- Identify their top-performing items (Pareto 80/20 principle) +- Classify products into **ABC categories** based on sales contribution +- Evaluate demand variability (XYZ classification support) + +Imagine you have a **Google Sheet** where daily sales transactions are stored: + +![Example Sheet](https://www.samirsaci.com/content/images/size/w1000/2025/09/image.png) + +The workflow aggregates sales by item, calculates cumulative contribution, and assigns **A, B, or C** classes. It also computes **mean, standard deviation, and coefficient of variation (CV)** to highlight demand volatility. + +![Workflow](https://www.samirsaci.com/content/images/2025/09/image-1.png) + +### How does it work? + +This workflow automates the process of **ABC & Pareto analysis from raw sales data**: + +- 📊 Google Sheets input provides daily transactional sales +- 🧮 Aggregation & code nodes compute sales, turnover, and cumulative shares +- 🧠 ABC class mapping assigns items into A/B/C buckets +- 📈 Demand variability metrics (XYZ) are calculated +- 📑 Results are appended into dedicated Google Sheets tabs for reporting + +[![Tutorial](https://www.samirsaci.com/content/images/2025/09/temp.png)](https://www.youtube.com/watch?v=YbAA-cq9X_E) + +🎥 [Watch My Tutorial](https://www.youtube.com/watch?v=YbAA-cq9X_E) + +### Steps: + +1. 📝 Load daily sales records from Google Sheets +2. 🔎 Filter out items with zero sales +3. 📊 Aggregate sales by **store**, **item**, and **day** +4. 📈 Perform Pareto analysis to calculate cumulative turnover share +5. 🧮 Compute demand variability (mean, stdev, CV) +6. 🧠 Assign ABC classes based on cumulative share thresholds +7. 📥 Append results into **ABC XYZ** and **Pareto** output sheets + +![workflow](https://www.samirsaci.com/content/images/size/w1000/2025/09/image-2.png) + +### What do I need to get started? + +You’ll need: + +- A **Google Sheet** with sales transactions (date, item, quantity, turnover) that is available here: [Test Sheet](https://docs.google.com/spreadsheets/d/1nsS_pj3o8yMEIQ59QBU80sx20HtkkTgCKJB32sOge1o/edit?usp=sharing) +- A **Google Sheets account** connected in n8n +- Basic knowledge of inventory analysis (ABC/XYZ) + +### Next Steps + +🗒️ Use the sticky notes in the n8n canvas to: + +- Add your Google Sheets credentials +- Replace the Sheet ID with your own sales dataset +- Run the workflow and check the output tabs: **ABC XYZ**, **Pareto**, and **Store Sales** + +*This template was built using n8n v1.107.3* +*Submitted: September 15, 2025* diff --git a/Inventory ABC & Pareto Analysis with Google Sheets for Supply Chain Optimization/workflow.json b/Inventory ABC & Pareto Analysis with Google Sheets for Supply Chain Optimization/workflow.json new file mode 100644 index 0000000..da66108 --- /dev/null +++ b/Inventory ABC & Pareto Analysis with Google Sheets for Supply Chain Optimization/workflow.json @@ -0,0 +1,882 @@ +{ + "meta": { + "instanceId": "", + "templateCredsSetupCompleted": true + }, + "nodes": [ + { + "id": "dd09796e-b4c4-43df-8401-bba895d330b3", + "name": "When clicking ‘Execute workflow’", + "type": "n8n-nodes-base.manualTrigger", + "position": [ + -1056, + -400 + ], + "parameters": {}, + "typeVersion": 1 + }, + { + "id": "5f502423-b87c-4322-aac6-10d07e685087", + "name": "Get row(s) in sheet", + "type": "n8n-nodes-base.googleSheets", + "position": [ + -832, + -400 + ], + "parameters": { + "options": {}, + "sheetName": { + "__rl": true, + "mode": "list", + "value": "gid=0", + "cachedResultUrl": "", + "cachedResultName": "Input Data" + }, + "documentId": { + "__rl": true, + "mode": "list", + "value": "", + "cachedResultUrl": "", + "cachedResultName": "Data Analytics" + } + }, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "", + "name": "" + } + }, + "notesInFlow": true, + "typeVersion": 4.7 + }, + { + "id": "15e8b11a-83d9-41cb-84e4-dbfe0ddf970f", + "name": "Filter Out Zero Sales", + "type": "n8n-nodes-base.filter", + "position": [ + -608, + -400 + ], + "parameters": { + "options": {}, + "conditions": { + "options": { + "version": 2, + "leftValue": "", + "caseSensitive": true, + "typeValidation": "strict" + }, + "combinator": "and", + "conditions": [ + { + "id": "39ce1523-ed7e-404a-82db-099200bcb82c", + "operator": { + "type": "number", + "operation": "notEquals" + }, + "leftValue": "={{ $json.QTY }}", + "rightValue": "={{ 0 }}" + } + ] + } + }, + "typeVersion": 2.2 + }, + { + "id": "2cd9b9a0-dc4f-4d99-b4c4-43f6d165cad8", + "name": "Transpose", + "type": "n8n-nodes-base.code", + "position": [ + -160, + -592 + ], + "parameters": { + "jsCode": "const input = $input.all().map(i => i.json);\n\n// Collect store IDs\nconst stores = input.map(row => row.STORE);\n// Collect items (from keys)\nconst items = Object.keys(input[0]).filter(k => k !== \"STORE\");\n\n// Transpose\nconst result = items.map(ITEM => {\n let row = { ITEM };\n for (const store of stores) {\n const storeRow = input.find(r => r.STORE === store);\n row[store] = storeRow[ITEM];\n }\n return { json: row };\n});\n\nreturn result;\n" + }, + "typeVersion": 2 + }, + { + "id": "7bedeb08-f47e-4809-9936-7cdc8a9236d1", + "name": "Daily Sales per Store", + "type": "n8n-nodes-base.code", + "position": [ + -384, + -304 + ], + "parameters": { + "jsCode": "const agg = {};\nfor (const {json} of items) {\n const k = `${json.STORE}|${json.DAY}`;\n if (!agg[k]) agg[k] = { STORE: json.STORE, DAY: json.DAY, QTY: 0, TO: 0 };\n agg[k].QTY += Number(json.QTY||0);\n agg[k].TO += Number(json.TO||0);\n}\nreturn Object.values(agg).map(r => ({ json: r }));" + }, + "typeVersion": 2 + }, + { + "id": "b0134581-ef21-4485-bd75-8a5d7f865ad4", + "name": "If", + "type": "n8n-nodes-base.if", + "position": [ + -160, + -304 + ], + "parameters": { + "options": {}, + "conditions": { + "options": { + "version": 2, + "leftValue": "", + "caseSensitive": true, + "typeValidation": "strict" + }, + "combinator": "and", + "conditions": [ + { + "id": "59bb6e5b-0765-4812-a842-19a3bc7a0357", + "operator": { + "name": "filter.operator.equals", + "type": "string", + "operation": "equals" + }, + "leftValue": "={{ $json.STORE }}", + "rightValue": "STORE-1" + } + ] + } + }, + "typeVersion": 2.2 + }, + { + "id": "95a2efc7-9c7b-4263-aab4-c00bbe902714", + "name": "Sales Store 1", + "type": "n8n-nodes-base.googleSheets", + "position": [ + 64, + -400 + ], + "parameters": { + "columns": { + "value": { + "TO": "={{ $json.TO }}", + "DAY": "={{ $json.DAY }}", + "QTY": "={{ $json.QTY }}" + }, + "schema": [ + { + "id": "DAY", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "DAY", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "QTY", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "QTY", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "TO", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "TO", + "defaultMatch": false, + "canBeUsedToMatch": true + } + ], + "mappingMode": "defineBelow", + "matchingColumns": [], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {}, + "operation": "append", + "sheetName": { + "__rl": true, + "mode": "list", + "value": 421687347, + "cachedResultUrl": "", + "cachedResultName": "Daily Sales Store 1" + }, + "documentId": { + "__rl": true, + "mode": "list", + "value": "", + "cachedResultUrl": "", + "cachedResultName": "Data Analytics" + } + }, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "", + "name": "" + } + }, + "notesInFlow": true, + "typeVersion": 4.7 + }, + { + "id": "fdffd98a-36ac-425b-861c-b3f738828fe2", + "name": "Sales Store 2", + "type": "n8n-nodes-base.googleSheets", + "position": [ + 64, + -208 + ], + "parameters": { + "columns": { + "value": { + "TO": "={{ $json.TO }}", + "DAY": "={{ $json.DAY }}", + "QTY": "={{ $json.QTY }}" + }, + "schema": [ + { + "id": "DAY", + "type": "string", + "display": true, + "required": false, + "displayName": "DAY", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "QTY", + "type": "string", + "display": true, + "required": false, + "displayName": "QTY", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "TO", + "type": "string", + "display": true, + "required": false, + "displayName": "TO", + "defaultMatch": false, + "canBeUsedToMatch": true + } + ], + "mappingMode": "defineBelow", + "matchingColumns": [], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {}, + "operation": "append", + "sheetName": { + "__rl": true, + "mode": "list", + "value": 749936247, + "cachedResultUrl": "", + "cachedResultName": "Daily Sales Store 2" + }, + "documentId": { + "__rl": true, + "mode": "list", + "value": "", + "cachedResultUrl": "", + "cachedResultName": "Data Analytics" + } + }, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "", + "name": "" + } + }, + "notesInFlow": true, + "typeVersion": 4.7 + }, + { + "id": "8a3afe9e-d873-4a02-9fb1-45fc9c380601", + "name": "Pareto Analysis", + "type": "n8n-nodes-base.code", + "position": [ + -160, + -784 + ], + "parameters": { + "jsCode": "// Normalize + sort by turnover (TO) desc\nconst rows = items\n .map(i => ({\n ...i.json,\n TO: Number(i.json.TO || 0), // turnover = QTY x Unit Price\n QTY: Number(i.json.QTY || 0),\n }))\n .sort((a, b) => b.TO - a.TO);\n\nconst n = rows.length;\nconst totalTO = rows.reduce((s, r) => s + r.TO, 0) || 1;\n\nlet cumTO = 0;\nrows.forEach((r, idx) => {\n cumTO += r.TO;\n r.cum_turnover = cumTO; // absolute cumulative TO\n r.cum_share = +(cumTO / totalTO).toFixed(6); // 0..1\n r.sku_rank = idx + 1;\n r.cum_skus = +((idx + 1) / n).toFixed(6); // 0..1\n r.cum_skus_pct = +(r.cum_skus * 100).toFixed(2); // 0..100 for charts\n});\n\n// Return table\nreturn rows.map(r => ({ json: r }));\n\n" + }, + "typeVersion": 2 + }, + { + "id": "e8e1409d-b571-43b8-ae9e-bb8da228cefd", + "name": "TO, QTY GroupBy ITEM", + "type": "n8n-nodes-base.code", + "position": [ + -384, + -784 + ], + "parameters": { + "jsCode": "const agg = {};\nfor (const {json} of items) {\n const ITEM = String(json.ITEM);\n const TO = Number(json.TO);\n const QTY = Number(json.QTY);\n if (!agg[ITEM]) agg[ITEM] = { ITEM, TO: 0, QTY: 0 };\n agg[ITEM].TO += TO;\n agg[ITEM].QTY += QTY;\n}\nconst rows = Object.values(agg).sort((a,b)=> b.TO - a.TO);\nreturn rows.map(r => ({ json: r }));" + }, + "typeVersion": 2 + }, + { + "id": "9e02d5cb-701d-4a40-b2bf-c2fae43af8ef", + "name": "TO GroupBy (STORE, ITEM)", + "type": "n8n-nodes-base.code", + "position": [ + -384, + -592 + ], + "parameters": { + "jsCode": "// Build dictionaries of unique stores & skus\nconst stores = new Set();\nconst skus = new Set();\n// Append dictionnaries with values\nfor (const {json} of items) { stores.add(json.STORE); skus.add(json.ITEM); }\n// List of Unique SKU\nconst skuList = Array.from(skus).sort();\n\n// Accumulate sums\nconst mat = {}; // store_id -> sku -> sum\nfor (const {json} of items) {\n const s = json.STORE;\n const k = json.ITEM;\n const v = Number(json.TO) || 0;\n if (!mat[s]) mat[s] = {};\n mat[s][k] = (mat[s][k] || 0) + v;\n}\n\n// Output wide rows with 0 fill\nconst rows = [];\nfor (const s of Array.from(stores).sort()) {\n const row = { STORE: s };\n for (const k of skuList) row[k] = mat[s][k] || 0;\n rows.push(row);\n}\nreturn rows.map(r => ({ json: r }));\n" + }, + "typeVersion": 2 + }, + { + "id": "efe6f052-ff5d-4755-ae07-4a7bc480ddbb", + "name": "Multi-Store Sales", + "type": "n8n-nodes-base.googleSheets", + "position": [ + 64, + -592 + ], + "parameters": { + "columns": { + "value": { + "TO": "={{ $json.TO }}", + "DAY": "={{ $json.DAY }}", + "QTY": "={{ $json.QTY }}" + }, + "schema": [ + { + "id": "DAY", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "DAY", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "QTY", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "QTY", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "TO", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "TO", + "defaultMatch": false, + "canBeUsedToMatch": true + } + ], + "mappingMode": "defineBelow", + "matchingColumns": [], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {}, + "operation": "append", + "sheetName": { + "__rl": true, + "mode": "list", + "value": 421687347, + "cachedResultUrl": "", + "cachedResultName": "Daily Sales Store 1" + }, + "documentId": { + "__rl": true, + "mode": "list", + "value": "", + "cachedResultUrl": "", + "cachedResultName": "Data Analytics" + } + }, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "", + "name": "" + } + }, + "notesInFlow": true, + "typeVersion": 4.7 + }, + { + "id": "5bd88834-9c20-4d9e-bd31-146dc31e2c18", + "name": "Demand Variability x Sales %", + "type": "n8n-nodes-base.code", + "position": [ + -384, + -16 + ], + "parameters": { + "jsCode": "// INPUT items: rows with fields ITEM, DAY, QTY (others optional)\n\nfunction mean(a){ return a.reduce((s,x)=>s + x, 0) / (a.length || 1); }\nfunction stdev_samp(a){\n if (a.length <= 1) return 0;\n const m = mean(a);\n const v = a.reduce((s,x)=> s + (x - m) ** 2, 0) / (a.length - 1);\n return Math.sqrt(v);\n}\n\n// 1) build daily series per ITEM (sum across stores)\nconst series = {}; // ITEM -> { day -> qty_sum }\nlet totalQty = 0;\n\nfor (const { json } of items) {\n const item = String(json.ITEM);\n const day = String(json.DAY);\n const qty = Number(json.QTY || 0);\n\n if (!series[item]) series[item] = {};\n series[item][day] = (series[item][day] || 0) + qty;\n totalQty += qty;\n}\n\n// 2) compute metrics per ITEM\nconst out = [];\nfor (const [item, dayMap] of Object.entries(series)) {\n const daily = Object.values(dayMap); // array of daily qty\n const qty_total = daily.reduce((s,x)=>s+x, 0);\n const m = mean(daily);\n const sd = stdev_samp(daily);\n const cv = m ? sd / m : null;\n const share_qty_pct = totalQty ? (qty_total / totalQty) * 100 : 0;\n\n out.push({\n ITEM: item,\n qty_total,\n share_qty_pct: Number(share_qty_pct.toFixed(2)),\n mean_qty: Number(m.toFixed(3)),\n std_qty: Number(sd.toFixed(3)),\n cv_qty: cv == null ? null : Number(cv.toFixed(3)),\n });\n}\n\n// Optional: sort by share desc\nout.sort((a,b) => b.share_qty_pct - a.share_qty_pct);\n\n// Return as n8n items\nreturn out.map(r => ({ json: r }));\n" + }, + "typeVersion": 2 + }, + { + "id": "805e6a80-bb24-446e-ab9c-a0e0c68f555a", + "name": "ABC XYZ Analysis", + "type": "n8n-nodes-base.googleSheets", + "position": [ + 64, + -16 + ], + "parameters": { + "columns": { + "value": { + "ABC": "={{ $json.ABC }}", + "ITEM": "={{ $json.ITEM }}", + "cv_qty": "={{ $json.cv_qty }}", + "std_qty": "={{ $json.std_qty }}", + "mean_qty": "={{ $json.mean_qty }}", + "cum_share": "={{ $json.cum_share }}", + "qty_total": "={{ $json.qty_total }}", + "share_qty_pct": "={{ $json.share_qty_pct }}" + }, + "schema": [ + { + "id": "ITEM", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "ITEM", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "qty_total", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "qty_total", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "share_qty_pct", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "share_qty_pct", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "cv_qty", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "cv_qty", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "ABC", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "ABC", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "mean_qty", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "mean_qty", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "std_qty", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "std_qty", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "cum_share", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "cum_share", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "limit_cov", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "limit_cov", + "defaultMatch": false, + "canBeUsedToMatch": true + } + ], + "mappingMode": "defineBelow", + "matchingColumns": [], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": { + "useAppend": false + }, + "operation": "append", + "sheetName": { + "__rl": true, + "mode": "list", + "value": 1281688243, + "cachedResultUrl": "", + "cachedResultName": "ABC XYZ" + }, + "documentId": { + "__rl": true, + "mode": "list", + "value": "", + "cachedResultUrl": "", + "cachedResultName": "Data Analytics" + } + }, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "", + "name": "" + } + }, + "notesInFlow": true, + "typeVersion": 4.7 + }, + { + "id": "fb1b23cb-cad9-4a75-bb41-5d87209b51c5", + "name": "Update Pareto Sheet", + "type": "n8n-nodes-base.googleSheets", + "position": [ + 64, + -784 + ], + "parameters": { + "columns": { + "value": { + "TO": "={{ $json.TO }}", + "QTY": "={{ $json.QTY }}", + "ITEM": "={{ $json.ITEM }}", + "cum_skus": "={{ $json.cum_skus }}", + "sku_rank": "={{ $json.sku_rank }}", + "cum_share": "={{ $json.cum_share }}", + "cum_skus_pct": "={{ $json.cum_skus_pct }}", + "cum_turnover": "={{ $json.cum_turnover }}" + }, + "schema": [ + { + "id": "ITEM", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "ITEM", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "TO", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "TO", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "QTY", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "QTY", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "cum_turnover", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "cum_turnover", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "cum_share", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "cum_share", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "sku_rank", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "sku_rank", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "cum_skus", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "cum_skus", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "cum_skus_pct", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "cum_skus_pct", + "defaultMatch": false, + "canBeUsedToMatch": true + } + ], + "mappingMode": "defineBelow", + "matchingColumns": [], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {}, + "operation": "append", + "sheetName": { + "__rl": true, + "mode": "list", + "value": 1760689869, + "cachedResultUrl": "", + "cachedResultName": "Pareto" + }, + "documentId": { + "__rl": true, + "mode": "list", + "value": "", + "cachedResultUrl": "", + "cachedResultName": "Data Analytics" + } + }, + "credentials": { + "googleSheetsOAuth2Api": { + "id": "", + "name": "" + } + }, + "notesInFlow": true, + "typeVersion": 4.7 + }, + { + "id": "82773b29-a1dc-445c-a8ea-d9c393d36eb4", + "name": "ABC Class Mapping", + "type": "n8n-nodes-base.code", + "position": [ + -160, + -16 + ], + "parameters": { + "jsCode": "// 1) Extract and sort by sales contribution\nlet rows = items.map(i => i.json);\nrows.sort((a, b) => b.share_qty_pct - a.share_qty_pct);\n\n// 2) Compute cumulative contribution\nlet cum = 0;\nfor (let r of rows) {\n cum += r.share_qty_pct;\n\n // 3) Assign class based on cumulative %\n if (cum <= 5) {\n r.ABC = 'A'; // top 5%\n } else if (cum <= 20) {\n r.ABC = 'B'; // next 15%\n } else {\n r.ABC = 'C'; // rest\n }\n\n r.cum_share = Number(cum.toFixed(2));\n}\n\n// 4) Return enriched rows\nreturn rows.map(r => ({ json: r }));\n" + }, + "typeVersion": 2 + }, + { + "id": "21867671-78aa-4580-b51f-53c34f6a1dfa", + "name": "Sticky Note2", + "type": "n8n-nodes-base.stickyNote", + "position": [ + -1166, + -784 + ], + "parameters": { + "color": 7, + "width": 540, + "height": 544, + "content": "### Trigger the workflow\nThis starts by collecting the transactional sales from the spreadsheet `Input Data` from the Google Sheet.\n\n#### How to setup?\n\n- **Load records in the Google Sheet Node**:\n 1. Add your Google Sheet API credentials to access the Google Sheet file\n 2. Select the file using the list, an URL or an ID\n 3. Select the sheet `Input Data`\nination**, **distance**, **duration**, **n_steps**\n [Learn more about the Google Sheet Node](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.googlesheets)\n" + }, + "typeVersion": 1 + } + ], + "pinData": {}, + "connections": { + "If": { + "main": [ + [ + { + "node": "Sales Store 1", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Sales Store 2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Transpose": { + "main": [ + [ + { + "node": "Multi-Store Sales", + "type": "main", + "index": 0 + } + ] + ] + }, + "Pareto Analysis": { + "main": [ + [ + { + "node": "Update Pareto Sheet", + "type": "main", + "index": 0 + } + ] + ] + }, + "ABC Class Mapping": { + "main": [ + [ + { + "node": "ABC XYZ Analysis", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get row(s) in sheet": { + "main": [ + [ + { + "node": "Filter Out Zero Sales", + "type": "main", + "index": 0 + } + ] + ] + }, + "TO, QTY GroupBy ITEM": { + "main": [ + [ + { + "node": "Pareto Analysis", + "type": "main", + "index": 0 + } + ] + ] + }, + "Daily Sales per Store": { + "main": [ + [ + { + "node": "If", + "type": "main", + "index": 0 + } + ] + ] + }, + "Filter Out Zero Sales": { + "main": [ + [ + { + "node": "Demand Variability x Sales %", + "type": "main", + "index": 0 + }, + { + "node": "TO, QTY GroupBy ITEM", + "type": "main", + "index": 0 + }, + { + "node": "TO GroupBy (STORE, ITEM)", + "type": "main", + "index": 0 + }, + { + "node": "Daily Sales per Store", + "type": "main", + "index": 0 + } + ] + ] + }, + "TO GroupBy (STORE, ITEM)": { + "main": [ + [ + { + "node": "Transpose", + "type": "main", + "index": 0 + } + ] + ] + }, + "Demand Variability x Sales %": { + "main": [ + [ + { + "node": "ABC Class Mapping", + "type": "main", + "index": 0 + } + ] + ] + }, + "When clicking ‘Execute workflow’": { + "main": [ + [ + { + "node": "Get row(s) in sheet", + "type": "main", + "index": 0 + } + ] + ] + } + } +} \ No newline at end of file diff --git a/n8n-workflows b/n8n-workflows deleted file mode 160000 index 929ff7c..0000000 --- a/n8n-workflows +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 929ff7c736fef102a13d7af7b589e2f8cc794354 diff --git a/♻️ AI Multi-Stop Planner for Circular Logistics with GPT-4o & Open Route API/README.md b/♻️ AI Multi-Stop Planner for Circular Logistics with GPT-4o & Open Route API/README.md new file mode 100644 index 0000000..528fa92 --- /dev/null +++ b/♻️ AI Multi-Stop Planner for Circular Logistics with GPT-4o & Open Route API/README.md @@ -0,0 +1,85 @@ +***Tags**: AI Agent, Supply Chain, Logistics, Circular Economy, Route Planning, Transportation, GPS API* + +### Context + +Hi! I’m Samir — a Supply Chain Engineer and Data Scientist based in Paris, and founder of [LogiGreen Consulting +](https: //logi-green.com). + +I help logistics teams reduce operational workload and errors by combining **AI automation**, **route optimisation APIs**, and **workflow automation**. + +This workflow is part of a **circular economy project**, where stores return **reusable packaging** (bins, crates, containers) to a central warehouse. + +> Let's create circular economies with AI-powered automation using n8n! + +📬 For business inquiries, you can find me on [LinkedIn +](https: //www.linkedin.com/in/samir-saci) + +### Who is this template for? + +This workflow is designed for logistics teams participating in **circular economy loops**. + +Let us imagine your transportation company receives this **pickup request**: + +![Example of Results +](https: //www.samirsaci.com/content/images/2025/06/image-15.png) + +The two **AI Agent nodes** connected to **Openroute Service API** will process the information and reply with the **detailed route plan**. + +![Workflow +](https: //www.samirsaci.com/content/images/2025/06/image-16.png) + +The results include **driving time** and the **optimal sequence** of stops generated by the multi-stop optimization endpoint of the API. + +### How does it work? + +This workflow automates the end-to-end processing of **multi-stop pickup requests for reusable packaging**: + +- 📨 Gmail Trigger listens for collection request emails +- 🧠 AI Agent parses the email into structured data (store ID, address, date) +- 📍 Each stop is geocoded into GPS coordinates +- 🗺️ OpenRouteService **optimizes the stop sequence** using truck-specific routing +- 📄 A second AI Agent formats a **confirmation email in HTML** with the ordered pickup plan +- 📧 The reply is sent back with all details including duration and route + +### Steps: + +1. 💌 Trigger on a new Gmail message +2. 🧠 Extract data using AI Agent (e.g., stores, addresses, times) +3. 📑 Store raw and processed data in Google Sheets +4. 📍 Enrich with GPS coordinates +5. 🚚 Optimize route using OpenRouteService (truck profile) +6. 📄 Format the confirmation using an AI Agent +7. 📬 Send reply to requester with route and timing + +![workflow +](https: //www.samirsaci.com/content/images/size/w1000/2025/06/image-14.png) + +### What do I need to get started? + +You’ll need: + +- A **Gmail account** to receive collection requests +- A **Google Sheet** to store and review data +- A free [OpenRouteService API key +](https: //openrouteservice.org/dev/#/signup) +- Access to OpenAI for using AI Agent nodes +- Sample pickup request emails to test + +### Next Steps + +🗒️ Use the sticky notes inside the n8n canvas to: + +- Plug in your Gmail and OpenRouteService credentials +- Try with a sample store collection email +- Validate the confirmation format and route accuracy + +### 📺 Learn More with n8n Tutorials + +Get hands-on experience with automation through my comprehensive **n8n tutorial playlist**, featuring 18+ complete, step-by-step videos and ready-to-use templates. + +[![n8n Playlist + ](https: //www.samirsaci.com/content/images/size/w1600/2025/11/image-9.png)](https://bit.ly/playlist-n8n-supplyscience) + + +*This template was built using n8n v1.93.0* +*Submitted: June 7, 2025* \ No newline at end of file diff --git a/♻️ AI Multi-Stop Planner for Circular Logistics with GPT-4o & Open Route API/workflow.json b/♻️ AI Multi-Stop Planner for Circular Logistics with GPT-4o & Open Route API/workflow.json new file mode 100644 index 0000000..4de4b28 --- /dev/null +++ b/♻️ AI Multi-Stop Planner for Circular Logistics with GPT-4o & Open Route API/workflow.json @@ -0,0 +1,1352 @@ +{ + "meta": { + "instanceId": "", + "templateCredsSetupCompleted": true + }, + "nodes": [ + { + "id": "9f8389be-ac2e-4a1d-bfe3-18a7d9900b36", + "name": "Gmail Trigger", + "type": "n8n-nodes-base.gmailTrigger", + "position": [ + 20, + 460 + ], + "parameters": { + "simple": false, + "filters": {}, + "options": { + "downloadAttachments": false + }, + "pollTimes": { + "item": [ + { + "mode": "everyMinute" + } + ] + } + }, + "notesInFlow": true, + "typeVersion": 1.2 + }, + { + "id": "5eb9eadc-3836-4cc9-9130-62bc655b9eb0", + "name": "Structured Output Parser", + "type": "@n8n/n8n-nodes-langchain.outputParserStructured", + "position": [ + 520, + 640 + ], + "parameters": { + "jsonSchemaExample": "[\n {\n \"collection_request_id\": \"04-06-4EDW-75\",\n \"store_id\":\"FRX-001\",\n \"address\": \"103 Rue du Faubourg Saint-Antoine, 75011 Paris\",\n \"collection_date\": \"June 7th, 2025\",\n \"collection_time\": \"10:00\"\n}\n]\n\n\n" + }, + "typeVersion": 1.2 + }, + { + "id": "1a98f859-b7ba-4778-a05b-7bd1c6da29fe", + "name": "AI Agent Parser", + "type": "@n8n/n8n-nodes-langchain.agent", + "position": [ + 300, + 460 + ], + "parameters": { + "text": "=Here is an email from a customer requesting a shipment:\n\nSubject: {{ $json.subject }}\nBody: {{ $json.text }}\n\nPlease extract all the structured logistics data based on the format provided.", + "options": { + "systemMessage": "=You are a logistics assistant working for a transportation company.\n\nYour job is to extract **structured data** from collection request emails sent by customers (typically retail chains like Franprix). These emails include a list of pickup addresses, a collection start time, store identifiers, and a unique Collection Request ID.\n\nYou must return an **array of JSON objects**, where each object represents one pickup location.\n\nEach object must contain:\n\n{\n \"collection_request_id\": \"\",\n \"store_id\": \"\",\n \"address\": \"\",\n \"collection_date\": \"\",\n \"collection_time\":\"\"\n}\n\n### Instructions:\n- Extract the value of `Collection Request ID` from the email and apply it to each object.\n- For each pickup location:\n - Extract the **store_id** (e.g., FRX-001)\n - Extract the full **address**\n - Use the same **collection_time_start** for all records, as provided in the email\n- If any field is missing, set its value to `null`.\n- Do not infer missing information.\n- Keep the original formatting of the time and date. Do not transform it.\n- Do not add extra fields. The output must be a **JSON array**.\n\nThis output will be processed by another system. Follow the format exactly.\n" + }, + "promptType": "define", + "hasOutputParser": true + }, + "typeVersion": 1.9 + }, + { + "id": "170e9ffb-23a6-42ce-bca9-ae31d18124f0", + "name": "Record Email Content", + "type": "n8n-nodes-base.googleSheets", + "position": [ + 1360, + 460 + ], + "parameters": { + "columns": { + "value": { + "address": "={{ $json.address }}", + "store_id": "={{ $json.store_id }}", + "location_id": "={{ $json.store_id }}_{{ $json.collection_request_id }}", + "collection_date": "={{ $json.collection_date }}", + "collection_time": "={{ $json.collection_time }}", + "collection_request_id": "={{ $json.collection_request_id }}" + }, + "schema": [ + { + "id": "collection_request_id", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "collection_request_id", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "store_id", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "store_id", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "address", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "address", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "location_id", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "location_id", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "longitude", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "longitude", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "latitude", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "latitude", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "gps_coordinates", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "gps_coordinates", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "collection_date", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "collection_date", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "collection_time", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "collection_time", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "sequence", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "sequence", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "total_duration", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "total_duration", + "defaultMatch": false, + "canBeUsedToMatch": true + } + ], + "mappingMode": "defineBelow", + "matchingColumns": [ + "shipment_number" + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {}, + "operation": "append", + "sheetName": { + "__rl": true, + "mode": "list", + "value": 1094697629, + "cachedResultUrl": "", + "cachedResultName": "Multi-Stop" + }, + "documentId": { + "__rl": true, + "mode": "list", + "value": "", + "cachedResultUrl": "", + "cachedResultName": "Transportation Orders" + } + }, + "notesInFlow": true, + "typeVersion": 4.6 + }, + { + "id": "4c3c818c-9931-4eed-b145-39190e405b50", + "name": "5 sec", + "type": "n8n-nodes-base.wait", + "position": [ + 2140, + 460 + ], + "webhookId": "8a602c69-d7ab-486c-990a-8d31b5e858f3", + "parameters": {}, + "typeVersion": 1.1 + }, + { + "id": "f567d1dd-432b-4e8e-9dec-5f27799ff61f", + "name": "Query Open Route API Pickup", + "type": "n8n-nodes-base.httpRequest", + "position": [ + 1560, + 460 + ], + "parameters": { + "url": "https://api.openrouteservice.org/geocode/search", + "options": {}, + "sendQuery": true, + "sendHeaders": true, + "authentication": "genericCredentialType", + "genericAuthType": "httpQueryAuth", + "queryParameters": { + "parameters": [ + { + "name": "text", + "value": "={{ $json.address }}" + }, + { + "name": "boundary.country", + "value": "=FR" + }, + { + "name": "sources", + "value": "openstreetmap" + }, + { + "name": "size", + "value": "1" + } + ] + }, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "name": "Accept", + "value": "application/json, application/geo+json, application/gpx+xml, img/png; charset=utf-8" + } + ] + } + }, + "credentials": { + "httpQueryAuth": { + "id": "CsPXdSa5p7fPrxpp", + "name": "Query Auth Open Route API" + }, + "httpBearerAuth": { + "id": "RwHLRH6TDUJaGZNF", + "name": "Bearer YOUR_TOKEN_HERE account" + }, + "httpHeaderAuth": { + "id": "uoa2pJ22gtZmYRDs", + "name": "Header Auth account" + } + }, + "notesInFlow": true, + "typeVersion": 4.2 + }, + { + "id": "a88ca302-325e-4309-b427-a9eb807ab0e9", + "name": "Save Pickup GPS", + "type": "n8n-nodes-base.googleSheets", + "position": [ + 1940, + 460 + ], + "parameters": { + "columns": { + "value": { + "latitude": "={{ $json.latitude }}", + "sequence": "=", + "longitude": "={{ $json.longitude }}", + "location_id": "={{ $('Record Email Content').item.json.location_id }}", + "gps_coordinates": "={{ $json.longitude }}, {{ $json.latitude }}" + }, + "schema": [ + { + "id": "collection_request_id", + "type": "string", + "display": true, + "removed": true, + "required": false, + "displayName": "collection_request_id", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "store_id", + "type": "string", + "display": true, + "removed": true, + "required": false, + "displayName": "store_id", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "address", + "type": "string", + "display": true, + "removed": true, + "required": false, + "displayName": "address", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "location_id", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "location_id", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "longitude", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "longitude", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "latitude", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "latitude", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "gps_coordinates", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "gps_coordinates", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "collection_date", + "type": "string", + "display": true, + "removed": true, + "required": false, + "displayName": "collection_date", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "collection_time", + "type": "string", + "display": true, + "removed": true, + "required": false, + "displayName": "collection_time", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "sequence", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "sequence", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "total_duration", + "type": "string", + "display": true, + "removed": false, + "required": false, + "displayName": "total_duration", + "defaultMatch": false, + "canBeUsedToMatch": true + }, + { + "id": "row_number", + "type": "string", + "display": true, + "removed": true, + "readOnly": true, + "required": false, + "displayName": "row_number", + "defaultMatch": false, + "canBeUsedToMatch": true + } + ], + "mappingMode": "defineBelow", + "matchingColumns": [ + "location_id" + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {}, + "operation": "update", + "sheetName": { + "__rl": true, + "mode": "list", + "value": 1094697629, + "cachedResultUrl": "", + "cachedResultName": "Multi-Stop" + }, + "documentId": { + "__rl": true, + "mode": "list", + "value": "17wbroY0w6xmw8AT-p2c_n1WbHzmfesz6d1DJbp39fn8", + "cachedResultUrl": "", + "cachedResultName": "Transportation Orders" + } + }, + "notesInFlow": true, + "typeVersion": 4.6 + }, + { + "id": "e07773a7-dd80-411e-b056-f6eb473d5dcb", + "name": "GPS Pickup", + "type": "n8n-nodes-base.set", + "position": [ + 1740, + 460 + ], + "parameters": { + "options": {}, + "assignments": { + "assignments": [ + { + "id": "e4c92077-f294-4e49-8276-20d1441c0c2c", + "name": "longitude", + "type": "string", + "value": "={{ $json.features[0].geometry.coordinates[0] }}" + }, + { + "id": "899e5304-e7a1-4ce1-bf9a-f403d74934fb", + "name": "latitude", + "type": "string", + "value": "={{ $json.features[0].geometry.coordinates[1] }}" + }, + { + "id": "5b35642b-dda4-42b8-b7bb-1275616bfb59", + "name": "borough", + "type": "string", + "value": "={{ $json.features[0].properties.borough }}" + }, + { + "id": "aa74bd9a-f543-48e1-a69b-0e6472bda053", + "name": "neighbourhood", + "type": "string", + "value": "={{ $json.features[0].properties.neighbourhood }}" + }, + { + "id": "7f303dc8-4e28-4d46-8e3e-9db00dbcac3a", + "name": "localadmin", + "type": "string", + "value": "={{ $json.features[0].properties.localadmin }}" + } + ] + } + }, + "notesInFlow": true, + "typeVersion": 3.4 + }, + { + "id": "121aa413-54e5-470a-be3b-ef243f3e9445", + "name": "Collect Coordinates", + "type": "n8n-nodes-base.googleSheets", + "position": [ + 840, + 700 + ], + "parameters": { + "options": {}, + "filtersUI": { + "values": [ + { + "lookupValue": "=04-06-4EDW-75", + "lookupColumn": "collection_request_id" + } + ] + }, + "sheetName": { + "__rl": true, + "mode": "list", + "value": 1094697629, + "cachedResultUrl": "", + "cachedResultName": "Multi-Stop" + }, + "documentId": { + "__rl": true, + "mode": "list", + "value": "17wbroY0w6xmw8AT-p2c_n1WbHzmfesz6d1DJbp39fn8", + "cachedResultUrl": "", + "cachedResultName": "Transportation Orders" + } + }, + "notesInFlow": true, + "typeVersion": 4.6 + }, + { + "id": "eca19e02-596a-47af-a448-b18ba2aa3af7", + "name": "Request Open Route API", + "type": "n8n-nodes-base.httpRequest", + "position": [ + 1360, + 700 + ], + "parameters": { + "url": "https://api.openrouteservice.org/optimization", + "body": "={{ JSON.stringify({\n jobs: $json.jobs,\n vehicles: $json.vehicles\n}) }}\n\n", + "method": "POST", + "options": { + "response": { + "response": {} + } + }, + "sendBody": true, + "contentType": "raw", + "sendHeaders": true, + "authentication": "genericCredentialType", + "rawContentType": "application/json", + "genericAuthType": "httpHeaderAuth", + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "name": "Accept", + "value": "application/json, application/geo+json, application/gpx+xml, img/png; charset=utf-8" + } + ] + } + }, + "credentials": { + "httpHeaderAuth": { + "id": "uoa2pJ22gtZmYRDs", + "name": "Header Auth Open Route API" + } + }, + "notesInFlow": true, + "typeVersion": 4.2 + }, + { + "id": "6f0027ec-68f9-495f-8958-14960dde3ccc", + "name": "Collect Shipment Information", + "type": "n8n-nodes-base.googleSheets", + "position": [ + 840, + 1520 + ], + "parameters": { + "options": {}, + "filtersUI": { + "values": [ + { + "lookupValue": "=04-06-4EDW-75", + "lookupColumn": "collection_request_id" + } + ] + }, + "sheetName": { + "__rl": true, + "mode": "list", + "value": 1094697629, + "cachedResultUrl": "", + "cachedResultName": "Multi-Stop" + }, + "documentId": { + "__rl": true, + "mode": "list", + "value": "17wbroY0w6xmw8AT-p2c_n1WbHzmfesz6d1DJbp39fn8", + "cachedResultUrl": "", + "cachedResultName": "Transportation Orders" + } + }, + "notesInFlow": true, + "typeVersion": 4.6 + }, + { + "id": "743b5224-10c1-4629-85e5-f32a57b85926", + "name": "OpenAI Chat Model", + "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", + "position": [ + 1140, + 1680 + ], + "parameters": { + "model": { + "__rl": true, + "mode": "list", + "value": "gpt-4o-mini" + }, + "options": {} + }, + "typeVersion": 1.2 + }, + { + "id": "93ed811e-041f-4f1b-92eb-b519c5b758f8", + "name": "AI Agent Reply", + "type": "@n8n/n8n-nodes-langchain.agent", + "position": [ + 1200, + 1520 + ], + "parameters": { + "text": "={{ $json.formatted_reply }}\n", + "options": { + "systemMessage": "=You are a logistics assistant working for a transportation company.\n\nYour job is to send a clear, professional, and customer-friendly confirmation email summarizing a **multi-stop pickup schedule**.\n\nUse the preformatted HTML content provided via `{{ $json.formatted_reply }}` to structure the message.\n\n✅ Do:\n- Keep a formal and polite tone.\n- Include the pickup request ID, scheduled date & time, and the ordered list of pickup stops.\n- Confirm the estimated total driving duration.\n- Use the content **as-is**. Do not modify or rephrase it.\n\n❌ Do not:\n- Add or invent data.\n- Change the structure or wording of the HTML provided.\n\n⚠️ Format the reply strictly in **HTML** using tags like `

`, `