AutomationFlowsEmail & Gmail › Distribute Policies and Meeting Minutes Then Collect Acknowledgments with…

Distribute Policies and Meeting Minutes Then Collect Acknowledgments with…

Original n8n title: Distribute Policies and Meeting Minutes Then Collect Acknowledgments with Gmail, Data Tables and Google Sheets

Distribute policies and meeting minutes then collect acknowledgments with Gmail, Data Tables and Google Sheets. Uses crypto, dataTable, gmail, googleSheets. Webhook trigger; 24 nodes.

Webhook trigger★★★★☆ complexity24 nodesCryptoData TableGmailGoogle Sheets
Email & Gmail Trigger: Webhook Nodes: 24 Complexity: ★★★★☆ Added:

This workflow follows the Datatable → Gmail 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
{
  "name": "Distribute policies and meeting minutes then collect acknowledgments with Gmail, Data Tables and Google Sheets",
  "nodes": [
    {
      "parameters": {
        "multipleMethods": true,
        "path": "policy-ack-tracker",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "b2d648e5-be8e-49c2-922c-1816cb15fa03",
      "name": "Receive Tracker Request",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "cfg-public-base-url",
              "name": "publicBaseUrl",
              "value": "<__PLACEHOLDER_VALUE__PUBLIC_BASE_URL__>",
              "type": "string"
            },
            {
              "id": "cfg-hmac-secret",
              "name": "hmacSecret",
              "value": "<__PLACEHOLDER_VALUE__HMAC_SECRET__>",
              "type": "string"
            },
            {
              "id": "cfg-api-shared-secret",
              "name": "apiSharedSecret",
              "value": "<__PLACEHOLDER_VALUE__API_SHARED_SECRET__>",
              "type": "string"
            },
            {
              "id": "cfg-ack-log-sheet-id",
              "name": "ackLogSheetId",
              "value": "<__PLACEHOLDER_VALUE__ACK_LOG_SHEET_ID__>",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "aabb5ad5-8c49-49d0-bc3f-34783bd93769",
      "name": "Workflow Configuration",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        272,
        0
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "combinator": "and",
                "conditions": [
                  {
                    "leftValue": "={{ $json.body?.action ?? $json.query?.action ?? \"\" }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "distribute"
                  },
                  {
                    "leftValue": "={{ $json.body?.secret ?? \"\" }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "={{ $json.apiSharedSecret }}"
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "distribute"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "combinator": "and",
                "conditions": [
                  {
                    "leftValue": "={{ $json.body?.action ?? $json.query?.action ?? \"\" }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "ack"
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "ack"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "combinator": "and",
                "conditions": [
                  {
                    "leftValue": "={{ $json.body?.action ?? $json.query?.action ?? \"\" }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "status"
                  },
                  {
                    "leftValue": "={{ $json.query?.secret ?? \"\" }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "={{ $json.apiSharedSecret }}"
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "status"
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra",
          "renameFallbackOutput": "Rejected"
        }
      },
      "id": "dc2d0116-4db8-4052-97b3-5999ea94b9f7",
      "name": "Route and Authorize Request",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        528,
        0
      ]
    },
    {
      "parameters": {
        "fieldToSplitOut": "body.audience",
        "include": "allOtherFields",
        "options": {
          "destinationFieldName": "recipientEmail"
        }
      },
      "id": "5851c4bd-40e9-4944-a659-0f0543e613af",
      "name": "Fan Out Audience",
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        1184,
        -16
      ]
    },
    {
      "parameters": {
        "action": "hmac",
        "type": "SHA256",
        "value": "={{ $json.body.docId }}{{ $json.recipientEmail }}",
        "dataPropertyName": "ackToken",
        "secret": "={{ $json.hmacSecret }}"
      },
      "id": "ca864ed0-aeaf-456f-8a03-7e1ac61fa0d8",
      "name": "Sign Recipient Token",
      "type": "n8n-nodes-base.crypto",
      "typeVersion": 1,
      "position": [
        1440,
        -16
      ]
    },
    {
      "parameters": {
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "Policy Ack Ledger",
          "cachedResultName": null
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "docId": "={{ $json.body.docId }}",
            "docTitle": "={{ $json.body.docTitle }}",
            "docLink": "={{ $json.body.docLink }}",
            "recipientEmail": "={{ $json.recipientEmail }}",
            "token": "={{ $json.ackToken }}",
            "sentAt": "={{ $now.toISO() }}",
            "ackStatus": "pending"
          },
          "schema": [
            {
              "id": "docId",
              "displayName": "docId",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "docTitle",
              "displayName": "docTitle",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "docLink",
              "displayName": "docLink",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "recipientEmail",
              "displayName": "recipientEmail",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "token",
              "displayName": "token",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "sentAt",
              "displayName": "sentAt",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "dateTime",
              "canBeUsedToMatch": false
            },
            {
              "id": "ackStatus",
              "displayName": "ackStatus",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            }
          ]
        },
        "options": {}
      },
      "id": "1456a8b7-9a17-48c2-a709-2cb3ef629711",
      "name": "Record Ledger Row",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        1712,
        -16
      ]
    },
    {
      "parameters": {
        "sendTo": "={{ $json.recipientEmail }}",
        "subject": "=Please acknowledge: {{ $json.docTitle }}",
        "message": "=<p>Hello,</p><p>You have been asked to read and acknowledge <strong>{{ $json.docTitle }}</strong>.</p><p><a href=\"{{ $json.docLink }}\">Open the document</a></p><p>After reading it, click your personal acknowledgment link:</p><p><a href=\"{{ $(\"Workflow Configuration\").first().json.publicBaseUrl }}/webhook/policy-ack-tracker?action=ack&docId={{ encodeURIComponent($json.docId) }}&email={{ encodeURIComponent($json.recipientEmail) }}&token={{ $json.token }}\">Acknowledge {{ $json.docTitle }}</a></p><p>This link is unique to you. Please do not forward it.</p>",
        "options": {
          "appendAttribution": false
        }
      },
      "id": "cd656a31-9f9e-4861-a8e5-f85b45be4a77",
      "name": "Email Acknowledgment Link",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        1984,
        -16
      ],
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { \"status\": \"distributed\", \"docId\": $(\"Workflow Configuration\").first().json.body.docId, \"recipients\": $(\"Fan Out Audience\").all().length } }}",
        "options": {}
      },
      "id": "5287c6a7-a8b4-4f65-83a4-6f1b9a743773",
      "name": "Respond Distribution Summary",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        2240,
        -16
      ],
      "executeOnce": true
    },
    {
      "parameters": {
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "Policy Ack Ledger",
          "cachedResultName": null
        },
        "matchType": "allConditions",
        "filters": {
          "conditions": [
            {
              "keyName": "docId",
              "keyValue": "={{ $json.query?.docId ?? \"\" }}"
            },
            {
              "keyName": "recipientEmail",
              "keyValue": "={{ $json.query?.email ?? \"\" }}"
            }
          ]
        },
        "returnAll": true
      },
      "id": "1945c1fa-0b57-459d-bce9-d6024825b195",
      "name": "Look Up Ledger Row",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        2576,
        16
      ],
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "combinator": "or",
                "conditions": [
                  {
                    "leftValue": "={{ $json.token ?? \"\" }}",
                    "operator": {
                      "type": "string",
                      "operation": "empty"
                    }
                  },
                  {
                    "leftValue": "={{ $json.token ?? \"\" }}",
                    "operator": {
                      "type": "string",
                      "operation": "notEquals"
                    },
                    "rightValue": "={{ $(\"Workflow Configuration\").first().json.query?.token ?? \"\" }}"
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "invalid"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "combinator": "and",
                "conditions": [
                  {
                    "leftValue": "={{ $(\"Workflow Configuration\").first().json.query?.confirm ?? \"\" }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "yes"
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "confirm"
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra",
          "renameFallbackOutput": "Landing"
        }
      },
      "id": "0b734bd6-d8bb-4ece-a373-9bb17aed4132",
      "name": "Classify Ack Request",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        2832,
        0
      ]
    },
    {
      "parameters": {
        "respondWith": "text",
        "responseBody": "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Request rejected</title></head><body style=\"font-family:Arial,sans-serif;max-width:560px;margin:40px auto;padding:0 20px;\"><h2>Request rejected</h2><p>This request could not be processed. The acknowledgment link may be invalid or revoked, or the shared secret is missing or wrong.</p><p>If you followed a link from an email, contact the sender for a fresh one.</p></body></html>",
        "options": {
          "responseCode": 403,
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/html; charset=utf-8"
              }
            ]
          }
        }
      },
      "id": "9bed38a6-9cd9-469a-b754-2b892043cecf",
      "name": "Respond Rejected Request",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        816,
        80
      ]
    },
    {
      "parameters": {
        "operation": "update",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "Policy Ack Ledger",
          "cachedResultName": null
        },
        "matchType": "allConditions",
        "filters": {
          "conditions": [
            {
              "keyValue": "={{ $json.id }}"
            }
          ]
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "acknowledgedAt": "={{ $now.toISO() }}",
            "ackStatus": "acknowledged"
          },
          "schema": [
            {
              "id": "acknowledgedAt",
              "displayName": "acknowledgedAt",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "dateTime",
              "canBeUsedToMatch": false
            },
            {
              "id": "ackStatus",
              "displayName": "ackStatus",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            }
          ]
        },
        "options": {}
      },
      "id": "8bea1ea1-1472-4b37-aa16-be568d8933bf",
      "name": "Record Acknowledgment",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        3104,
        16
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $(\"Workflow Configuration\").first().json.ackLogSheetId }}"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Ack Log"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "docId": "={{ $json.docId }}",
            "docTitle": "={{ $json.docTitle }}",
            "recipientEmail": "={{ $json.recipientEmail }}",
            "acknowledgedAt": "={{ $json.acknowledgedAt }}",
            "ackStatus": "={{ $json.ackStatus }}"
          },
          "schema": [
            {
              "id": "docId",
              "displayName": "docId",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "docTitle",
              "displayName": "docTitle",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "recipientEmail",
              "displayName": "recipientEmail",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "acknowledgedAt",
              "displayName": "acknowledgedAt",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "ackStatus",
              "displayName": "ackStatus",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            }
          ]
        },
        "options": {}
      },
      "id": "44eebd43-c5c8-4eec-820b-bbfce498ddc8",
      "name": "Mirror Ack To Sheets Log",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        3376,
        16
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "respondWith": "text",
        "responseBody": "=<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Acknowledgment recorded</title></head><body style=\"font-family:Arial,sans-serif;max-width:560px;margin:40px auto;padding:0 20px;\"><h2>Thank you</h2><p>Your acknowledgment of <strong>{{ $(\"Look Up Ledger Row\").first().json.docTitle }}</strong> was recorded at {{ $now.toFormat(\"yyyy-MM-dd HH:mm\") }}.</p><p>You can close this page.</p></body></html>",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/html; charset=utf-8"
              }
            ]
          }
        }
      },
      "id": "20451574-dd7d-479c-b6e8-42fd310bd792",
      "name": "Respond Confirmation Page",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        3648,
        16
      ]
    },
    {
      "parameters": {
        "respondWith": "text",
        "responseBody": "=<!DOCTYPE html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>Confirm acknowledgment</title></head><body style=\"font-family:Arial,sans-serif;max-width:560px;margin:40px auto;padding:0 20px;\"><h2>Confirm your acknowledgment</h2><p>Document: <strong>{{ $json.docTitle }}</strong></p><p>Recipient: {{ $json.recipientEmail }}</p><p>Nothing has been recorded yet. Clicking the button below records your acknowledgment with a timestamp.</p><p><a style=\"display:inline-block;background:#1a7f37;color:#ffffff;padding:12px 24px;border-radius:6px;text-decoration:none;font-weight:bold;\" href=\"{{ $(\"Workflow Configuration\").first().json.publicBaseUrl }}/webhook/policy-ack-tracker?action=ack&docId={{ encodeURIComponent($json.docId) }}&email={{ encodeURIComponent($json.recipientEmail) }}&token={{ $json.token }}&confirm=yes\">I have read this document</a></p><p style=\"color:#666666;\">If you did not expect this page, close it. Nothing was recorded.</p></body></html>",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/html; charset=utf-8"
              }
            ]
          }
        }
      },
      "id": "cad76c79-17eb-4692-9341-6eb8b5b41741",
      "name": "Respond Landing Page",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        3104,
        192
      ]
    },
    {
      "parameters": {
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "Policy Ack Ledger",
          "cachedResultName": null
        },
        "returnAll": true
      },
      "id": "48542ca1-d2da-49d0-b8e1-5ccc983c5e3b",
      "name": "Fetch Ledger Rows",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        4000,
        64
      ],
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "var rows = $input.all().map(function (i) { return i.json; }).filter(function (r) { return r && r.docId; });\nvar pending = rows.filter(function (r) { return r.ackStatus !== 'acknowledged'; });\nvar acked = rows.length - pending.length;\nvar byDoc = {};\npending.forEach(function (r) {\n  if (!byDoc[r.docId]) { byDoc[r.docId] = { title: r.docTitle || r.docId, people: [] }; }\n  byDoc[r.docId].people.push({ email: r.recipientEmail, sentAt: r.sentAt });\n});\nvar sections = '';\nvar docIds = Object.keys(byDoc);\nif (docIds.length === 0) {\n  sections = '<p><strong>Everyone has acknowledged every tracked document.</strong></p>';\n} else {\n  docIds.forEach(function (key) {\n    var doc = byDoc[key];\n    var body = doc.people.map(function (p) {\n      var sent = p.sentAt ? String(p.sentAt).slice(0, 10) : '';\n      return '<tr><td>' + p.email + '</td><td>' + sent + '</td></tr>';\n    }).join('');\n    sections += '<h3>' + doc.title + ' (' + key + ')</h3>' +\n      '<table border=1 cellpadding=8 cellspacing=0>' +\n      '<tr><th>Outstanding recipient</th><th>Sent on</th></tr>' + body + '</table>';\n  });\n}\nvar html = '<!DOCTYPE html><html><head><title>Acknowledgment status</title></head><body>' +\n  '<h2>Acknowledgment status</h2>' +\n  '<p>' + acked + ' of ' + rows.length + ' tracked acknowledgments are complete. ' + pending.length + ' outstanding.</p>' +\n  sections + '</body></html>';\nreturn [{ json: { html: html } }];"
      },
      "id": "f6378319-8eac-4cb8-a8aa-19d1f38cc35b",
      "name": "Build Chase Table Page",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4272,
        64
      ]
    },
    {
      "parameters": {
        "respondWith": "text",
        "responseBody": "={{ $json.html }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/html; charset=utf-8"
              }
            ]
          }
        }
      },
      "id": "76b15062-3a62-4240-a4a7-a8f3bc762e37",
      "name": "Respond Status Page",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        4544,
        64
      ]
    },
    {
      "parameters": {
        "content": "# Policy and Minutes Acknowledgment Tracker\n\nDistribution and compliance without an e-sign vendor. One webhook drives three actions: distribute a document to an audience list, serve per-person tokenized acknowledgment links, and report exactly who has not acknowledged each document. Every acknowledgment lives in the Policy Ack Ledger Data Table and is mirrored to a Google Sheets log.\n\n## How it works\n1. POST the webhook with body action=distribute plus secret, docId, docTitle, docLink, and audience (array of email addresses). The workflow signs token = HMAC-SHA256(docId + recipientEmail), inserts one pending ledger row per person, and emails each person a personal acknowledgment link.\n2. The link opens action=ack. The first visit only validates the token and serves a landing page. Nothing is recorded on that first request, which keeps email link scanners from creating false acknowledgments.\n3. The Confirm button on the landing page calls back with confirm=yes. Only this second request stamps acknowledgedAt on the ledger row and appends the ack to the Sheets log.\n4. GET action=status with the shared secret responds with an HTML chase table naming everyone still outstanding, grouped by document.\n5. Revoking a link is one row: delete a person's ledger row and their token stops validating.\n\n## Setup\n1. In the Workflow Configuration node, replace every placeholder:\n   PUBLIC_BASE_URL: the instance base URL, for example https://n8n.example.com. There is no reliable runtime expression for the public URL, so it lives here.\n   HMAC_SECRET: a long random string that signs acknowledgment tokens.\n   API_SHARED_SECRET: the secret that distribute and status callers must present.\n   ACK_LOG_SHEET_ID: the Google Sheets spreadsheet id for the shareable ack log.\n2. In that spreadsheet, create a tab named Ack Log with headers docId, docTitle, recipientEmail, acknowledgedAt, ackStatus.\n3. Confirm the Gmail and Google Sheets credentials on their nodes.\n4. Publish, then distribute with a POST body like: {\"action\": \"distribute\", \"secret\": \"...\", \"docId\": \"POL-7\", \"docTitle\": \"Remote Work Policy v3\", \"docLink\": \"https://...\", \"audience\": [\"a@example.com\", \"b@example.com\"]}.",
        "height": 740,
        "width": 1250
      },
      "id": "1acdee8a-6733-478c-98a6-3be7172e4d63",
      "name": "Overview And Setup",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -80,
        -1168
      ]
    },
    {
      "parameters": {
        "content": "## 1. Intake and routing\nOne webhook accepts GET and POST on the same path. Workflow Configuration is the single home for every environment value. The Switch routes on the action parameter and also enforces the shared secret for distribute and status inside the routing rules, so a wrong or missing secret falls through to the rejected response. Nobody can trigger a distribution or read the status table with the bare URL.",
        "height": 548,
        "width": 1098,
        "color": 7
      },
      "id": "5623649c-d6e3-4f7f-87e6-61318b6be001",
      "name": "Section Intake And Routing",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -64,
        -256
      ]
    },
    {
      "parameters": {
        "content": "## 2. action=distribute\nSplits the audience array into one item per person, signs token = HMAC-SHA256(docId + recipientEmail) with the Crypto node, inserts a pending ledger row per recipient, then emails each person their personal acknowledgment link. The token is stateless-verifiable and revocable: the expected value is stored on the ledger row, so deleting the row kills the link. The Gmail node continues on error so one bad address does not stop the rest of the roster. The response reports how many links went out.",
        "height": 468,
        "width": 1326,
        "color": 7
      },
      "id": "53c7b935-b023-4269-8432-1a7b69c24af7",
      "name": "Section Distribute Action",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1104,
        -256
      ]
    },
    {
      "parameters": {
        "content": "## 3. action=ack, two step by design\nThe first GET only looks up the ledger row and validates the token, then serves a landing page. Recording happens only when the person clicks Confirm, which calls the webhook again with confirm=yes. That second request stamps acknowledgedAt on the ledger row and mirrors the ack to the Google Sheets log. Invalid, revoked, or mismatched tokens get the rejected page and nothing is written.",
        "height": 580,
        "width": 1374,
        "color": 7
      },
      "id": "b3953d49-608e-4273-8611-39f84e5e0537",
      "name": "Section Acknowledge Action",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2496,
        -208
      ]
    },
    {
      "parameters": {
        "content": "## 4. action=status\nRequires the same shared secret as distribute, otherwise anyone with the URL could enumerate who ignored which policy. Reads the whole ledger and responds with an HTML chase table naming exactly who has not acknowledged each document, grouped by docId, with a completion count. Share it only with the coordinator.",
        "height": 420,
        "width": 802,
        "color": 7
      },
      "id": "35f16380-ede9-4dba-9d8f-c5064188468d",
      "name": "Section Status Action",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        3936,
        -176
      ]
    },
    {
      "parameters": {
        "content": "## Warning: never record on the first GET\nEmail security scanners like Outlook SafeLinks auto-visit every link in an email. If the first GET recorded the acknowledgment, a scanner would silently acknowledge on behalf of people who never opened the page, and the compliance trail would be fiction. That is why the landing page records nothing: only the explicit Confirm click, the second request with confirm=yes, writes the timestamp.",
        "height": 228,
        "width": 900,
        "color": 3
      },
      "id": "7eb5e1bd-f1da-4e30-87a9-d69e086c185b",
      "name": "Warning Never Record On First Get",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1248,
        -656
      ]
    }
  ],
  "connections": {
    "Receive Tracker Request": {
      "main": [
        [
          {
            "node": "Workflow Configuration",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Workflow Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Workflow Configuration": {
      "main": [
        [
          {
            "node": "Route and Authorize Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route and Authorize Request": {
      "main": [
        [
          {
            "node": "Fan Out Audience",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Look Up Ledger Row",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Fetch Ledger Rows",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond Rejected Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fan Out Audience": {
      "main": [
        [
          {
            "node": "Sign Recipient Token",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sign Recipient Token": {
      "main": [
        [
          {
            "node": "Record Ledger Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Record Ledger Row": {
      "main": [
        [
          {
            "node": "Email Acknowledgment Link",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email Acknowledgment Link": {
      "main": [
        [
          {
            "node": "Respond Distribution Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Look Up Ledger Row": {
      "main": [
        [
          {
            "node": "Classify Ack Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Ack Request": {
      "main": [
        [
          {
            "node": "Respond Rejected Request",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Record Acknowledgment",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond Landing Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Record Acknowledgment": {
      "main": [
        [
          {
            "node": "Mirror Ack To Sheets Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mirror Ack To Sheets Log": {
      "main": [
        [
          {
            "node": "Respond Confirmation Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Ledger Rows": {
      "main": [
        [
          {
            "node": "Build Chase Table Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Chase Table Page": {
      "main": [
        [
          {
            "node": "Respond Status Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "tags": []
}

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.

Pro

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

About this workflow

Distribute policies and meeting minutes then collect acknowledgments with Gmail, Data Tables and Google Sheets. Uses crypto, dataTable, gmail, googleSheets. Webhook trigger; 24 nodes.

Source: https://github.com/exekyute/n8n-exekyute-templates/blob/main/pending-review/n8n-policy-ack-tracker/workflow.json — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

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

Email & Gmail

Automate WhatsApp communication for recruitment agencies with an interactive, structured customer experience. This workflow handles pricing inquiries, request submissions, tracking, complaints, and hu

HTTP Request, Google Sheets, Gmail +1
Email & Gmail

This workflow automates HR onboarding by capturing new hires via a webhook, saving them to Google Sheets, emailing a Slack invite via Gmail, and notifying managers in Slack, then listening for Slack t

Google Sheets, Gmail, Slack +1
Email & Gmail

Invoice Dunning (idempotent, stop-on-paid). Uses dataTable, gmail. Webhook trigger; 67 nodes.

Data Table, Gmail
Email & Gmail

Advanced AI Powered Document Parsing & Text Extraction with Llama Parse. Uses gmail, gmailTrigger, httpRequest, googleSheets. Webhook trigger; 54 nodes.

Gmail, Gmail Trigger, HTTP Request +3
Email & Gmail

Code. Uses googleSheets, gmail, supabase, stickyNote. Webhook trigger; 51 nodes.

Google Sheets, Gmail, Supabase +1