AutomationFlowsData & Sheets › Capture Vendor Status Updates via Tokenized Email Links and Google Sheets

Capture Vendor Status Updates via Tokenized Email Links and Google Sheets

ByPatrick Graham @pgraham on n8n.io

This workflow sends scheduled vendor status ping emails with one-click buttons, then captures responses via an n8n webhook and logs the selected status and timestamp to Google Sheets using a one-time token per ping cycle. Runs every Monday at 8am on a schedule. Reads all vendors…

Cron / scheduled trigger★★★★☆ complexity20 nodesGoogle SheetsEmail Send
Data & Sheets Trigger: Cron / scheduled Nodes: 20 Complexity: ★★★★☆ Added:
Capture Vendor Status Updates via Tokenized Email Links and Google Sheets — n8n workflow card showing Google Sheets, Email Send integration

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

This workflow follows the Emailsend → 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
{
  "meta": {
    "description": "Stop manually chasing vendor status updates. This workflow sends scheduled ping emails to your vendors with four one-click status buttons (On Track, At Risk, Delayed, Needs PM Input). When a vendor clicks their button, the workflow validates their token and logs their response to a Google Sheet.\n\nWhat this workflow does:\n- Reads active vendors from a Google Sheet on a schedule\n- Generates a unique token per vendor per cycle\n- Sends a styled ping email with four status buttons\n- Captures vendor clicks via webhook and validates the token\n- Updates the vendor row in your Google Sheet with the response and timestamp\n- Shows the vendor a confirmation page\n\nThe workflow preserves clarity \u2014 if a vendor does not respond, their row stays blank. Nothing is assumed.\n\nRequirements:\n- Google Sheets (one sheet with a Vendors tab \u2014 see node notes for required columns)\n- Google Sheets OAuth2 credential in n8n\n- SMTP email credential (Gmail with App Password works)\n- n8n instance at a publicly accessible URL \u2014 vendors click links that point back to your n8n webhook\n- Set webhookBaseUrl in the Config node to your public n8n webhook base URL\n\nPrivacy note: Vendor data is stored in your Google Sheet and sent through your configured email provider. The workflow runs in your own n8n instance. Do not use this for confidential vendor or project information unless your organization permits this workflow design and your n8n, Google, and email credentials are configured appropriately.\n\nCommunity Edition includes the core one-click status capture pattern. The full Vendor Coordination Tracker adds comment capture, flagged-status PM alerts, weekly summary reporting, response threshold logic, setup guide, and sample Sheet templates.\n\nFull version at pmexecution.com",
    "templateCredsSetupCompleted": false
  },
  "name": "Automate Vendor Status Updates with Tokenized Email Links",
  "tags": [
    "project-management",
    "vendor-management",
    "google-sheets",
    "email",
    "webhook"
  ],
  "nodes": [
    {
      "id": "sticky-overview",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -560,
        60
      ],
      "parameters": {
        "width": 480,
        "height": 1020,
        "content": "## Automate Vendor Status Updates\n## with Tokenized Email Links\n\n### How it works\n\nThis workflow runs on a schedule to send status ping emails to your active vendors. Each email contains four one-click status buttons. When a vendor clicks a button, a webhook captures the response, validates the token, updates your Google Sheet, and shows the vendor a confirmation page.\n\n### Setup steps\n\n1. Open the **Config** node and set your n8n public webhook base URL and email address\n2. Add your Google Sheets OAuth2 credential and replace YOUR_GOOGLE_SHEET_ID in all Sheets nodes\n3. Add your SMTP credential and set your From Email in the Send Ping Email node\n4. Create your Google Sheet with the required columns (see Sheets node notes)\n5. Activate the workflow \u2014 both the schedule and the webhook become live\n6. Test with one vendor row before sending to real vendors\n\n### Required Google Sheet columns\n\n```\nvendor_id\nvendor_name\ncontact_name\ncontact_email\nproject_name\nscope\nstatus\nping_token\nping_sent_at\nresponse_status\nlast_response_at\n```\n\nColumn names are case-sensitive.\n\n### Community Edition\n\nThis version captures one-click status only. The full product at pmexecution.com adds a vendor comment form, PM alert emails for flagged statuses, weekly PM summary reports, and a complete setup guide."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-branch1",
      "name": "Branch 1 \u2014 Scheduled Ping",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        200,
        60
      ],
      "parameters": {
        "color": 7,
        "width": 1860,
        "height": 200,
        "content": "## Branch 1 \u2014 Scheduled Ping\n\nRuns every Monday at 8am. Reads active vendors from Google Sheets, generates a unique one-time token per vendor, writes the token to the sheet, and sends a ping email with four status buttons. Edit the **Config** node before activating."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-branch2",
      "name": "Branch 2 \u2014 Vendor Response Capture",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        200,
        600
      ],
      "parameters": {
        "color": 7,
        "width": 1400,
        "height": 180,
        "content": "## Branch 2 \u2014 Vendor Response Capture\n\nAlways-on webhook. Fires when a vendor clicks a status button in their ping email. Validates the one-time token against the sheet, updates the vendor row with status and timestamp, and serves a confirmation page. Invalid or expired tokens get an error page."
      },
      "typeVersion": 1
    },
    {
      "id": "schedule-trigger",
      "name": "Every Monday at 8am",
      "type": "n8n-nodes-base.scheduleTrigger",
      "notes": "Runs every Monday at 8am. Change the cron expression to adjust the schedule. Format: minute hour day month weekday. Example for daily at 9am: 0 9 * * *",
      "position": [
        240,
        420
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1"
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "config-node",
      "name": "Config",
      "type": "n8n-nodes-base.code",
      "notes": "START HERE. Replace webhookBaseUrl with your public n8n webhook URL and pmEmail with your email address. Save before activating.",
      "position": [
        460,
        420
      ],
      "parameters": {
        "jsCode": "// ============================================================\n// CONFIGURATION \u2014 edit these values before activating\n// ============================================================\n\n// Your n8n public webhook base URL \u2014 no trailing slash\n// Example: 'https://n8n.yourdomain.com/webhook'\nconst webhookBaseUrl = 'https://YOUR-N8N-DOMAIN.com/webhook';\n\n// Your email address \u2014 used as the From address on ping emails\nconst pmEmail = 'your@email.com';\n\n// Do not edit below this line\nreturn [{ json: { webhookBaseUrl, pmEmail } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "read-vendors",
      "name": "Read Vendors from Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "Reads all rows from the Vendors tab. Replace YOUR_GOOGLE_SHEET_ID with your Sheet ID from the URL. Required columns: vendor_id, vendor_name, contact_name, contact_email, project_name, scope, status, ping_token, ping_sent_at, response_status, last_response_at. Select your Google Sheets credential.",
      "position": [
        680,
        420
      ],
      "parameters": {
        "options": {
          "returnAllMatches": true
        },
        "operation": "getAll",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Vendors"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "filter-active",
      "name": "Filter Active Vendors Only",
      "type": "n8n-nodes-base.filter",
      "notes": "Only processes rows where status = Active. Set a vendor row to Inactive, On Hold, or Closed to stop pings.",
      "position": [
        900,
        420
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "active-filter",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.status }}",
              "rightValue": "Active"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "generate-tokens",
      "name": "Generate Tokens and Status Links",
      "type": "n8n-nodes-base.code",
      "notes": "Creates a unique 32-character token per vendor. Builds four status links with the token embedded. Enforces required vendor_id and contact_email fields. Reads webhook base URL from Config node.",
      "position": [
        1120,
        420
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nconst results = [];\n\nconst config = $('Config').first().json;\nconst base = config.webhookBaseUrl || 'https://YOUR-N8N-DOMAIN.com/webhook';\nconst statusUrl = `${base}/vendor-status`;\n\nfunction generateToken() {\n  const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';\n  let token = '';\n  for (let i = 0; i < 32; i++) {\n    token += chars[Math.floor(Math.random() * chars.length)];\n  }\n  return token;\n}\n\nfor (const item of items) {\n  const vendor = item.json;\n\n  if (!vendor.vendor_id) {\n    throw new Error(`Missing vendor_id for: ${vendor.vendor_name || 'unnamed row'}. vendor_id is required.`);\n  }\n  if (!vendor.contact_email) {\n    throw new Error(`Missing contact_email for vendor_id: ${vendor.vendor_id}. Add an email or set status to Inactive.`);\n  }\n\n  const token = generateToken();\n  const vendorId = encodeURIComponent(vendor.vendor_id);\n  const project = encodeURIComponent(vendor.project_name || 'Project');\n\n  const makeLink = (status) =>\n    `${statusUrl}?vendor_id=${vendorId}&status=${encodeURIComponent(status)}&token=${token}&project=${project}`;\n\n  results.push({\n    json: {\n      ...vendor,\n      ping_token: token,\n      ping_sent_at: new Date().toISOString(),\n      pm_email: config.pmEmail || vendor.pm_email,\n      link_on_track: makeLink('On Track'),\n      link_at_risk: makeLink('At Risk'),\n      link_delayed: makeLink('Delayed'),\n      link_needs_pm: makeLink('Needs PM Input')\n    }\n  });\n}\n\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "log-token",
      "name": "Write Token to Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "Writes the token and ping timestamp to the vendor row BEFORE sending the email. Clears prior response fields. If this write fails, the email will not send \u2014 preventing vendors from receiving links that can never validate.",
      "position": [
        1340,
        420
      ],
      "parameters": {
        "columns": {
          "value": {
            "vendor_id": "={{ $json.vendor_id }}",
            "ping_token": "={{ $json.ping_token }}",
            "ping_sent_at": "={{ $json.ping_sent_at }}",
            "response_status": "",
            "last_response_at": ""
          },
          "schema": [],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "vendor_id"
          ]
        },
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Vendors"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "preserve-payload",
      "name": "Preserve Vendor Payload",
      "type": "n8n-nodes-base.code",
      "notes": "Google Sheets update nodes only return the columns they wrote, dropping the status links and contact fields. This node restores the full generated payload so the email node has access to all fields.",
      "position": [
        1560,
        420
      ],
      "parameters": {
        "jsCode": "return $('Generate Tokens and Status Links').all();"
      },
      "typeVersion": 2
    },
    {
      "id": "send-ping-email",
      "name": "Send Ping Email to Vendor",
      "type": "n8n-nodes-base.emailSend",
      "notes": "Sends a styled HTML ping email with four status buttons. Select your Gmail SMTP credential. Update From Email to your sending address.",
      "position": [
        1780,
        420
      ],
      "parameters": {
        "html": "={{ \n(function() {\n  const item = $json;\n  function e(v) { return String(v||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }\n  return `<!DOCTYPE html><html><head><meta charset='UTF-8'><style>body{font-family:Arial,sans-serif;background:#f4f3ef;margin:0;padding:24px}.c{max-width:560px;margin:0 auto;background:#fff;border:1px solid #e0e0e0;border-radius:4px;overflow:hidden}.h{background:#1a1a1a;padding:20px 28px}.h h2{color:#fff;margin:0;font-size:17px;font-weight:600}.h p{color:rgba(255,255,255,.55);margin:4px 0 0;font-size:12px}.b{padding:24px 28px}.b p{color:#444;font-size:13px;line-height:1.7;margin:0 0 14px}.grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:20px 0}a.btn{display:block;padding:13px 10px;text-align:center;text-decoration:none;border-radius:3px;font-size:12px;font-weight:600;color:#fff}.f{padding:14px 28px;border-top:1px solid #e0e0e0}.f p{color:#777;font-size:10px;line-height:1.5;margin:0}.f a{color:#777}</style></head><body><div class='c'><div class='h'><h2>${e(item.project_name)}</h2><p>Status request \u2014 ${e(item.vendor_name)}</p></div><div class='b'><p>Hi ${e(item.contact_name||item.vendor_name)},</p><p>Quick status check-in for <strong>${e(item.scope)}</strong>.</p><div class='grid'><a href='${e(item.link_on_track)}' class='btn' style='background:#1D9E75'>On Track</a><a href='${e(item.link_at_risk)}' class='btn' style='background:#BA7517'>At Risk</a><a href='${e(item.link_delayed)}' class='btn' style='background:#C8392B'>Delayed</a><a href='${e(item.link_needs_pm)}' class='btn' style='background:#1a1a1a'>Needs PM Input</a></div></div><div class='f'><p>For coordination only. Not a formal schedule update or contractual notice.</p><p>Powered by <a href='https://pmexecution.com'>PM Execution Tools</a></p></div></div></body></html>`;\n})()\n}}",
        "options": {},
        "subject": "={{ $json.project_name + ' \u2014 Status Update Request: ' + $json.vendor_name }}",
        "toEmail": "={{ $json.contact_email }}",
        "emailType": "html",
        "fromEmail": "={{ $json.pm_email }}"
      },
      "typeVersion": 2.1
    },
    {
      "id": "vendor-webhook",
      "name": "When Vendor Clicks Status Link",
      "type": "n8n-nodes-base.webhook",
      "notes": "Always-on GET webhook. Path: vendor-status. Use the Production URL \u2014 not the Test URL.",
      "position": [
        240,
        800
      ],
      "parameters": {
        "path": "vendor-status",
        "options": {},
        "httpMethod": "GET",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "read-for-validation",
      "name": "Read Sheet to Validate Token",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "Reads the full Vendors tab to find the row matching the vendor_id from the URL and validate the token. Same Sheet ID and credential as Branch 1.",
      "position": [
        460,
        800
      ],
      "parameters": {
        "options": {
          "returnAllMatches": true
        },
        "operation": "getAll",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Vendors"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "validate-token",
      "name": "Validate Token and Status",
      "type": "n8n-nodes-base.code",
      "notes": "Matches vendor_id and token from URL params against the stored token. Whitelists the four valid status values. Returns valid: true/false with vendor details.",
      "position": [
        680,
        800
      ],
      "parameters": {
        "jsCode": "const params = $('When Vendor Clicks Status Link').first().json.query;\nconst vendors = $input.all();\n\nconst vendorId = decodeURIComponent(params.vendor_id || '');\nconst submittedToken = params.token || '';\nconst status = decodeURIComponent(params.status || '');\nconst project = decodeURIComponent(params.project || '');\n\nconst allowedStatuses = ['On Track', 'At Risk', 'Delayed', 'Needs PM Input'];\nif (!allowedStatuses.includes(status)) {\n  return [{ json: { valid: false, reason: 'Invalid status value', vendorId, status, project } }];\n}\n\nconst vendorRow = vendors.find(v => v.json.vendor_id === vendorId);\n\nif (!vendorRow) {\n  return [{ json: { valid: false, reason: 'Vendor not found', vendorId, status, project } }];\n}\n\nconst vendor = vendorRow.json;\nconst storedToken = vendor.ping_token || '';\n\nif (!storedToken || storedToken !== submittedToken) {\n  return [{ json: { valid: false, reason: 'Invalid or expired token', vendorId, status, project, vendor_name: vendor.vendor_name } }];\n}\n\nreturn [{\n  json: {\n    valid: true,\n    vendor_id: vendorId,\n    vendor_name: vendor.vendor_name,\n    project_name: vendor.project_name || project,\n    status,\n    responded_at: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "if-valid",
      "name": "If Token is Valid",
      "type": "n8n-nodes-base.if",
      "position": [
        900,
        800
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "valid-check",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.valid }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "update-status",
      "name": "Write Status to Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "Updates the vendor row with their response status and timestamp. Clears the token so it cannot be reused.",
      "position": [
        1120,
        720
      ],
      "parameters": {
        "columns": {
          "value": {
            "vendor_id": "={{ $json.vendor_id }}",
            "ping_token": "",
            "response_status": "={{ $json.status }}",
            "last_response_at": "={{ $json.responded_at }}"
          },
          "schema": [],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "vendor_id"
          ]
        },
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Vendors"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "build-confirmation",
      "name": "Build Confirmation Page",
      "type": "n8n-nodes-base.code",
      "notes": "Builds a clean confirmation page showing the vendor's status badge, project name, and vendor name. All values are HTML-escaped.",
      "position": [
        1340,
        720
      ],
      "parameters": {
        "jsCode": "const data = $('Validate Token and Status').first().json;\n\nfunction escapeHtml(value) {\n  return String(value || '')\n    .replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;')\n    .replace(/\"/g, '&quot;')\n    .replace(/'/g, '&#039;');\n}\n\nconst statusColors = {\n  'On Track': '#1D9E75',\n  'At Risk': '#BA7517',\n  'Delayed': '#C8392B',\n  'Needs PM Input': '#1a1a1a'\n};\nconst color = statusColors[data.status] || '#1a1a1a';\n\nconst html = `<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><style>*{box-sizing:border-box;margin:0;padding:0}body{font-family:Arial,sans-serif;background:#f4f3ef;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}.card{background:#fff;border:1px solid #e0e0e0;border-radius:4px;width:100%;max-width:420px;overflow:hidden}.header{background:#1a1a1a;padding:18px 24px}.header h1{color:#fff;font-size:16px;font-weight:600}.body{padding:20px 24px}.badge{display:inline-block;background:${color};color:#fff;padding:5px 12px;border-radius:3px;font-size:12px;font-weight:600;margin-bottom:14px}.detail{font-size:12px;color:#777;margin-bottom:5px}.footer{padding:12px 24px;border-top:1px solid #e0e0e0}.footer p{font-size:10px;color:#aaa}.footer a{color:#aaa}</style></head><body><div class=\"card\"><div class=\"header\"><h1>Status received. Thank you.</h1></div><div class=\"body\"><span class=\"badge\">${escapeHtml(data.status)}</span><p class=\"detail\">Project: ${escapeHtml(data.project_name)}</p><p class=\"detail\">Vendor: ${escapeHtml(data.vendor_name)}</p></div><div class=\"footer\"><p>Powered by <a href=\"https://pmexecution.com\">PM Execution Tools</a></p></div></div></body></html>`;\n\nreturn [{ json: { ...data, confirmationHtml: html } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "serve-confirmation",
      "name": "Show Confirmation Page to Vendor",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1560,
        720
      ],
      "parameters": {
        "options": {
          "responseCode": 200,
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/html; charset=utf-8"
              }
            ]
          }
        },
        "respondWith": "text",
        "responseBody": "={{ $json.confirmationHtml }}"
      },
      "typeVersion": 1
    },
    {
      "id": "build-error-page",
      "name": "Build Error Page",
      "type": "n8n-nodes-base.code",
      "position": [
        1120,
        920
      ],
      "parameters": {
        "jsCode": "const html = `<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><style>*{box-sizing:border-box;margin:0;padding:0}body{font-family:Arial,sans-serif;background:#f4f3ef;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}.card{background:#fff;border:1px solid #e0e0e0;border-radius:4px;width:100%;max-width:420px;overflow:hidden}.header{background:#1a1a1a;padding:18px 24px}.header h1{color:#fff;font-size:16px;font-weight:600}.body{padding:20px 24px}.body p{font-size:13px;color:#444;line-height:1.7;margin-bottom:10px}.footer{padding:12px 24px;border-top:1px solid #e0e0e0}.footer p{font-size:10px;color:#aaa}.footer a{color:#aaa}</style></head><body><div class=\"card\"><div class=\"header\"><h1>Link unavailable</h1></div><div class=\"body\"><p>This status link has expired or is no longer valid.</p><p>Please reply directly to the status request email or contact your project manager.</p></div><div class=\"footer\"><p>Powered by <a href=\"https://pmexecution.com\">PM Execution Tools</a></p></div></div></body></html>`;\nreturn [{ json: { errorHtml: html } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "serve-error",
      "name": "Show Error Page to Vendor",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1340,
        920
      ],
      "parameters": {
        "options": {
          "responseCode": 400,
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/html; charset=utf-8"
              }
            ]
          }
        },
        "respondWith": "text",
        "responseBody": "={{ $json.errorHtml }}"
      },
      "typeVersion": 1
    }
  ],
  "settings": {
    "executionOrder": "v1",
    "saveManualExecutions": true
  },
  "versionId": "community-1.0",
  "staticData": null,
  "connections": {
    "Config": {
      "main": [
        [
          {
            "node": "Read Vendors from Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Error Page": {
      "main": [
        [
          {
            "node": "Show Error Page to Vendor",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Token is Valid": {
      "main": [
        [
          {
            "node": "Write Status to Sheet",
            "type": "main",
            "index": 0
          },
          {
            "node": "Build Confirmation Page",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Error Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every Monday at 8am": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write Token to Sheet": {
      "main": [
        [
          {
            "node": "Preserve Vendor Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Confirmation Page": {
      "main": [
        [
          {
            "node": "Show Confirmation Page to Vendor",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Preserve Vendor Payload": {
      "main": [
        [
          {
            "node": "Send Ping Email to Vendor",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Vendors from Sheet": {
      "main": [
        [
          {
            "node": "Filter Active Vendors Only",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Token and Status": {
      "main": [
        [
          {
            "node": "If Token is Valid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Active Vendors Only": {
      "main": [
        [
          {
            "node": "Generate Tokens and Status Links",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Sheet to Validate Token": {
      "main": [
        [
          {
            "node": "Validate Token and Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Vendor Clicks Status Link": {
      "main": [
        [
          {
            "node": "Read Sheet to Validate Token",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Tokens and Status Links": {
      "main": [
        [
          {
            "node": "Write Token to Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

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

About this workflow

This workflow sends scheduled vendor status ping emails with one-click buttons, then captures responses via an n8n webhook and logs the selected status and timestamp to Google Sheets using a one-time token per ping cycle. Runs every Monday at 8am on a schedule. Reads all vendors…

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

More Data & Sheets workflows → · Browse all categories →

Related workflows

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

Data & Sheets

Security teams, DevOps engineers, vulnerability analysts, and automation builders who want to eliminate repetitive Nessus scan parsing, AI-based risk triage, and manual reporting. Designed for orgs fo

Email Send, HTTP Request, Google Sheets +1
Data & Sheets

This workflow fully automates the reconciliation process between your Local Database transactions and Payment Gateway transactions. It compares both data sources, identifies mismatches, categorizes di

Google Sheets, Email Send
Data & Sheets

This n8n workflow automatically finds apartments for rent in Germany, filters them by your city, rent budget, and number of rooms, and applies to them via email. Each application includes: A personali

HTTP Request, Google Drive, Email Send +1
Data & Sheets

👤 Who it’s for Blue Team leads, CISOs, and SOC managers who want automated visibility into threat metrics, endpoint alerts, and response actions — without needing a full SIEM or BI platform.

HTTP Request, Email Send, Google Sheets
Data & Sheets

Workflow Overview Zoom Attendance Evaluator with Follow-up is an n8n automation workflow that automatically evaluates Zoom meeting attendance and sends follow-up emails to no-shows and early leavers w

Zoom, Item Lists, HTTP Request +3