This workflow corresponds to n8n.io template #14440 — 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 →
{
"id": "V65pgMismecPnp55",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Automated Vendor Follow-up & PO Tracker",
"tags": [],
"nodes": [
{
"id": "a3553b21-5120-430e-b93e-54a53a49c7c2",
"name": "Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-544,
-32
],
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 9
}
]
}
},
"typeVersion": 1
},
{
"id": "0f9a7f55-d2f1-4b15-85ad-127abe34c0f3",
"name": "Read PO",
"type": "n8n-nodes-base.googleSheets",
"position": [
-304,
-112
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1-5B5R9TrFjr8qO0S2dOoloJcGTVWWLxqb1sTFHMmIS4",
"cachedResultName": "Purchase Order log Book"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4
},
{
"id": "b08b50af-e1a4-4f6e-bb95-1c8e3e195697",
"name": "Filter + Normalize",
"type": "n8n-nodes-base.code",
"position": [
-80,
-112
],
"parameters": {
"jsCode": "const now = DateTime.now();\nconst sevenDaysAgo = now.minus({ days: 7 });\n\nfunction parseDate(dateStr) {\n if (!dateStr) return null;\n let dt = DateTime.fromISO(dateStr);\n if (!dt.isValid) dt = DateTime.fromFormat(dateStr, 'yyyy-MM-dd');\n if (!dt.isValid) dt = DateTime.fromFormat(dateStr, 'dd/MM/yyyy');\n if (!dt.isValid) dt = DateTime.fromFormat(dateStr, 'M/d/yyyy');\n return dt.isValid ? dt : null;\n}\n\nreturn items\n .map(item => {\n const delivery = parseDate(item.json['Delivery Date']);\n const follow = parseDate(item.json['Last Follow-up Date']);\n return {\n json: {\n ...item.json,\n deliveryDateParsed: delivery,\n followUpParsed: follow\n }\n };\n })\n .filter(item => {\n const d = item.json.deliveryDateParsed;\n const f = item.json.followUpParsed;\n const status = item.json['Delivery Status'];\n if (!d) return false;\n const isDue = d <= now;\n const notComplete = status !== 'Complete';\n const followMissing = !f;\n const followOld = f && f < sevenDaysAgo;\n return isDue && notComplete && (followMissing || followOld);\n });"
},
"typeVersion": 2
},
{
"id": "45123449-2ed4-4bc5-a7cc-f1e2eb35d552",
"name": "Read Vendors",
"type": "n8n-nodes-base.googleSheets",
"position": [
-304,
96
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1mM9qa1DJVLK4olhtJodt3F8PlENewVRIbqx2p_fRg6I",
"cachedResultName": "Vendor Base"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4
},
{
"id": "f1b20f6b-ee5b-4905-a3d9-74c6da01ea8b",
"name": "Group by Vendor",
"type": "n8n-nodes-base.code",
"position": [
416,
-112
],
"parameters": {
"jsCode": "// 1. Check if there is even data coming in\nif (items.length === 0) return [];\n\nconst map = {};\n\nfor (const item of items) {\n const data = item.json;\n \n // 2. Flexible property lookup (handles \"supplierEmail\", \"Supplier Email\", \"Email\", etc.)\n const email = data.supplierEmail || data['Supplier Email'] || data['Email'];\n const vendorId = data['Vendor ID'] || data.vendorId;\n const vendorName = data.supplierName || data['Supplier Name'] || data['Vendor Name'] || 'Vendor';\n\n // 3. Skip if we don't have an email or a Vendor ID\n if (!email || !vendorId) continue;\n\n if (!map[vendorId]) {\n map[vendorId] = {\n name: vendorName,\n email: email,\n pos: []\n };\n }\n\n map[vendorId].pos.push(data);\n}\n\n// 4. Transform the map into n8n items\nconst result = Object.values(map).map(v => {\n const poLines = v.pos\n .map(p => {\n const poNum = p['PO Number'] || p.poNumber;\n const dueDate = p['Delivery Date'] || p.deliveryDate;\n return `\u2022 PO ${poNum} | Due: ${dueDate}`;\n })\n .join('\\n');\n\n return {\n json: {\n email: v.email,\n poNumbers: v.pos.map(p => p['PO Number'] || p.poNumber),\n subject: `Follow-up: ${v.pos.length} overdue PO(s)`,\n body: `Dear ${v.name},\\n\\nThe following POs are overdue:\\n\\n${poLines}\\n\\nPlease share an update.\\n\\nThanks.`\n }\n };\n});\n\n// 5. If result is still empty, throw a descriptive error or return a warning\nif (result.length === 0) {\n throw new Error(\"No matches found. Check if 'Vendor ID' and 'Supplier Email' exist in the input data.\");\n}\n\nreturn result;"
},
"typeVersion": 2
},
{
"id": "b66e3ba5-ff49-4f15-8c59-dda7c85c0a0f",
"name": "Send Email",
"type": "n8n-nodes-base.gmail",
"position": [
640,
-112
],
"parameters": {
"sendTo": "={{ $json.email }}",
"message": "={{ $json.body }}",
"options": {},
"subject": "={{ $json.subject }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2
},
{
"id": "f09bb165-7559-4d7b-b204-40cedf9fbff9",
"name": "Merge",
"type": "n8n-nodes-base.merge",
"position": [
176,
-32
],
"parameters": {
"mode": "combine",
"options": {},
"fieldsToMatchString": "['Vendor ID']"
},
"typeVersion": 3
},
{
"id": "2cf5e7e2-7ae5-48b5-a0e2-f25e393d1578",
"name": "Update PO Sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [
416,
80
],
"parameters": {
"columns": {
"value": {
"Vendor ID": "={{ $json['Vendor ID'] }}",
"Last Follow-up Date": "={{ $today.format('yyyy-MM-dd') }}"
},
"schema": [
{
"id": "PO Number",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "PO Number",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Vendor ID",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Vendor ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Delivery Date",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Delivery Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Delivery Status",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Delivery Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Last Follow-up Date",
"type": "string",
"display": true,
"required": false,
"displayName": "Last Follow-up Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "row_number",
"type": "number",
"display": true,
"removed": true,
"readOnly": true,
"required": false,
"displayName": "row_number",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"Vendor ID"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "update",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1-5B5R9TrFjr8qO0S2dOoloJcGTVWWLxqb1sTFHMmIS4/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1-5B5R9TrFjr8qO0S2dOoloJcGTVWWLxqb1sTFHMmIS4",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1-5B5R9TrFjr8qO0S2dOoloJcGTVWWLxqb1sTFHMmIS4/edit?usp=drivesdk",
"cachedResultName": "Purchase Order log Book"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4
},
{
"id": "f54200cf-3ccf-486c-aee0-907667cdb3d6",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1072,
-304
],
"parameters": {
"width": 400,
"height": 688,
"content": "How it works\nChasing vendors manually is a time-sink. This workflow automates the \"nagging\" so you don't have to. Every morning at 9:00 AM, the system scans your Purchase Order Log to find orders that are past their delivery date and not yet marked as \"Complete.\"\n\nTo keep your vendors happy, it includes an \"anti-spam\" filter: it only sends a follow-up if the Last Follow-up Date is empty or more than 7 days old. Before sending, it merges your PO data with your Vendor Base to get the right email addresses. Finally, it groups all overdue POs for a single vendor into one consolidated email and updates your spreadsheet with today\u2019s date so you don't double-email them tomorrow.\n\nSetup\nGoogle Sheets: Connect your Google account. In the Read PO and Read Vendors nodes, select your specific spreadsheets. Ensure your column headers (like Vendor ID, Delivery Date, and Supplier Email) match the names used in the Code nodes.\n\nGmail: Authenticate your Gmail account in the Send Email node.\n\nDate Formats: The workflow uses Luxon to parse dates. If your spreadsheet uses a non-standard date format, you may need to adjust the parseDate function in the Filter + Normalize node.\n\nTesting: Run the workflow manually once to ensure the \"Update PO Sheet\" node correctly writes back to your spreadsheet."
},
"typeVersion": 1
},
{
"id": "108c5f02-19ae-485d-a2eb-27cda499d26e",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-384,
-288
],
"parameters": {
"color": 7,
"width": 208,
"height": 576,
"content": "1. Data Retrieval\nThis section triggers the daily run and pulls the raw data from both your Purchase Order logs and your Vendor contact database."
},
"typeVersion": 1
},
{
"id": "284549f3-4694-4bd8-a1cc-76b13a11b1f2",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-96,
-256
],
"parameters": {
"color": 7,
"width": 384,
"height": 544,
"content": "2. Validation & Merging\nHere, the workflow filters out completed orders and checks the \"7-day rule.\" It then joins the PO data with vendor emails using the Vendor ID."
},
"typeVersion": 1
},
{
"id": "42307d7e-d7bd-4f0c-a609-94605900ac00",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
368,
-288
],
"parameters": {
"color": 7,
"width": 448,
"height": 528,
"content": "3. Communication & Tracking\nThis final stage batches multiple POs into a single professional email and \"checks the box\" in your spreadsheet by logging the follow-up date."
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "a7ca01c5-de7f-4d7e-b225-c59c3170d344",
"connections": {
"Merge": {
"main": [
[
{
"node": "Group by Vendor",
"type": "main",
"index": 0
},
{
"node": "Update PO Sheet",
"type": "main",
"index": 0
}
]
]
},
"Read PO": {
"main": [
[
{
"node": "Filter + Normalize",
"type": "main",
"index": 0
}
]
]
},
"Trigger": {
"main": [
[
{
"node": "Read PO",
"type": "main",
"index": 0
},
{
"node": "Read Vendors",
"type": "main",
"index": 0
}
]
]
},
"Read Vendors": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 1
}
]
]
},
"Group by Vendor": {
"main": [
[
{
"node": "Send Email",
"type": "main",
"index": 0
}
]
]
},
"Filter + Normalize": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
gmailOAuth2googleSheetsOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Chasing vendors for overdue Purchase Orders (POs) is a manual, repetitive task that eats up hours of procurement time. This workflow automates that entire process—intelligently.
Source: https://n8n.io/workflows/14440/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This n8n workflow template, "Email Outreach Automation," is designed to help you set up an automated email outreach system using tools you might already be familiar with: Google Sheets and Google Docs
How it works:
Auto Follow-up Email (AI). Uses gmail, openAi, googleSheets. Scheduled trigger; 7 nodes.
This workflow automates the entire process of managing event participants, from registration and payment to sending reminders and follow-up communications. It's designed for event organizers who want
How it works time trigger using the cron format, every weekday at 5pm gets CentralStationCRM people updates of today checks for tag "Outreach" if true, sends message on gmail (predefine in node) waits