AutomationFlowsEmail & Gmail › Verify Email Content Against Google Sheets and Gmail Logs

Verify Email Content Against Google Sheets and Gmail Logs

BySasikala Jayamani @sasikalajayamani on n8n.io

Input: Google Sheets provides “Expected Content” rows (one per block/section). HTML Parse: A JS/HTML step extracts Actual Content from the email’s HTML (from Gmail or any provided HTML source). Merge: Expected and Actual items are merged into aligned pairs for comparison.…

Event trigger★★★★☆ complexity9 nodesGmailGoogle Sheets
Email & Gmail Trigger: Event Nodes: 9 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #13530 — 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 →

Download .json
{
  "id": "D1FrvXZykBqu21rH",
  "name": "My workflow",
  "tags": [],
  "nodes": [
    {
      "id": "55bef248-1f8f-4c7a-85b5-51fcc9232c19",
      "name": "When clicking \u2018Execute workflow\u2019",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -672,
        1136
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "74bb545a-30c1-4822-b6e9-f7640d45b912",
      "name": "Get many messages",
      "type": "n8n-nodes-base.gmail",
      "position": [
        -464,
        1136
      ],
      "parameters": {
        "simple": false,
        "filters": {
          "sender": "user@example.com",
          "readStatus": "unread",
          "includeSpamTrash": true
        },
        "options": {},
        "operation": "getAll"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "7d6616c4-6535-406c-b705-08a241a339ef",
      "name": "Extracting Preheader From HTML",
      "type": "n8n-nodes-base.html",
      "position": [
        0,
        1248
      ],
      "parameters": {
        "options": {},
        "operation": "extractHtmlContent",
        "dataPropertyName": "html",
        "extractionValues": {
          "values": [
            {
              "key": "preheader",
              "cssSelector": "div#emailPreHeader"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "6396dbe1-4a71-4692-85c5-ba429a3e70aa",
      "name": "Combine HTML, Expected Content and Preheader",
      "type": "n8n-nodes-base.merge",
      "position": [
        320,
        1120
      ],
      "parameters": {
        "numberInputs": 3
      },
      "typeVersion": 3.2
    },
    {
      "id": "620a25d1-d472-4bc9-8991-7fcfaf3fc427",
      "name": "Extract Actual Content and Results",
      "type": "n8n-nodes-base.code",
      "position": [
        608,
        1120
      ],
      "parameters": {
        "jsCode": "function cleanHtml(str) {\n  return str\n    .replace(/<style[\\s\\S]*?<\\/style>/gi, \"\")\n    .replace(/<head[\\s\\S]*?<\\/head>/gi, \"\")\n    .replace(/<footer[\\s\\S]*?<\\/footer>/gi, \"\")\n    .replace(/<[^>]+>/g, \" \")\n    .replace(/\\s+/g, \" \")\n    .trim();\n}\n\nfunction decodeEntities(str) {\n  return str\n    .replace(/&#x9;/g, \" \")\n    .replace(/&amp;/g, \"&\")\n    .replace(/&nbsp;/g, \" \")\n    .trim();\n}\n\n// Expected values from Excel\nconst excelRows = items.filter(i => i.json[\"Expected\"]);\n\n// Gmail node output\nconst gmail = items.find(i => i.json.subject || i.json.from || i.json.textAsHtml || i.json.text)?.json || {};\nconst fromAddress = gmail.from?.value?.[0]?.address || \"\";\nconst replyToAddress = gmail.replyTo?.value?.[0]?.address || \"\";\nconst subjectLine = (gmail.subject || \"\").replace(/^TEST\\s*\\|\\s*/i, \"\").trim();\n\n// Extract HTML Content node output (preheader)\nconst preheaderItem = items.find(i => i.json.preheader)?.json || {};\nconst preheader = preheaderItem.preheader || \"\";\n\n// Body Copy\nlet bodyCopy = \"\";\nif (gmail.textAsHtml) {\n  bodyCopy = cleanHtml(gmail.textAsHtml);\n  bodyCopy = decodeEntities(bodyCopy);\n} else if (gmail.text) {\n  bodyCopy = decodeEntities(gmail.text.trim());\n}\n\n// Validation logic\nconst results = excelRows.map(row => {\n  const expected = String(row.json[\"Expected\"] || \"\");\n  let actualValue = \"\";\n\n  if (expected.includes(\"@\")) {\n    actualValue = fromAddress || replyToAddress;\n  } else if (expected.toLowerCase().includes(\"subject\")) {\n    actualValue = subjectLine;\n  } else if (expected.toLowerCase().includes(\"preheader\")) {\n    actualValue = preheader;\n  } else {\n    actualValue = bodyCopy;\n  }\n\n  const match = expected.trim() === String(actualValue).trim() ? \"MATCH\" : \"MISMATCH\";\n  return { Expected: expected, Actual: actualValue, Result: match };\n});\n\nreturn results.map(r => ({ json: r }));"
      },
      "typeVersion": 2
    },
    {
      "id": "34805684-af93-454c-850f-9c8fc3aa0c2b",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1584,
        848
      ],
      "parameters": {
        "width": 832,
        "height": 624,
        "content": "## Main Overview \u2013 Email Marketing Automation QA Workflow\n\nThis workflow automates the end\u2011to\u2011end content verification process for email QA by comparing expected content values stored in Google Sheets with the actual values extracted from the email HTML. It eliminates manual checks for key email elements\u2014including From Address, Subject Line, Preheader, Body Copy, and Footer Content\u2014and generates a clear Result (Pass/Fail) for each item. This ensures consistency, accuracy, and significantly reduces QA turnaround time.\n\n**How it works**\n1. Load all expected content values (From, Subject, Preheader, Body, Footer) from Google Sheets.\n2. Extract actual content from the email HTML using HTML parsing + JavaScript logic.\n3. Merge expected and actual entries using a unique key (such as Section ID).\n4. Compare Expected vs Actual values to determine the Result (Pass/Fail).\n5. Write the Actual Content + Result back into Google Sheets, forming a structured QA output.\n\n**Setup steps**\n* Connect Google Sheets (Expected Content Source).\n* Configure Gmail / HTML Source to pull the email HTML.\n* Update JS extraction logic to match your email template structure.\n* Ensure proper row matching via Section ID / Labels."
      },
      "typeVersion": 1
    },
    {
      "id": "c8074a02-05b1-4924-a07f-727dc816120e",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -112,
        880
      ],
      "parameters": {
        "color": 7,
        "width": 1168,
        "height": 592,
        "content": "## Content Check \nReads expected email elements from Google Sheets\u2014From address, Subject line, Preheader, Body copy, and Footer content. Extracts actual values from HTML, merges inputs, compares expected vs actual to generate the Result, and logs Actual Content + Result back to Google Sheets."
      },
      "typeVersion": 1
    },
    {
      "id": "58537ad7-fc2a-4625-8e51-e81d2b290ed7",
      "name": "Log Content Checks to Excel",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        848,
        1120
      ],
      "parameters": {
        "columns": {
          "value": {
            "Label": "={{ $json.Label }}",
            "Actual": "={{ $json.Actual }}",
            "Result": "={{ $json.Result }}",
            "Expected": "={{ $json.Expected }}"
          },
          "schema": [
            {
              "id": "Label",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Label",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Expected",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Expected",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Actual",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Actual",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Result",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Result",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Mismatch Reason",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Mismatch Reason",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "Expected"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": "=// Parameters provided by user in node config\nconst sheetId = this.getNodeParameter(\"sheetId\", 0) || \"YOUR_SPREADSHEET_ID\";\nconst sheetName = this.getNodeParameter(\"sheetName\", 0) || \"Sheet1\";\n\nconst { google } = require(\"googleapis\");\nconst sheets = google.sheets({ version: \"v4\", auth });\n\nconst response = await sheets.spreadsheets.values.get({\n  spreadsheetId: sheetId,\n  range: sheetName,\n});\n\nreturn response.data.values.map(row => ({ json: { row } }));"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "=https://docs.google.com/spreadsheets/d/<SPREADSHEET_ID>/edit"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "5b8662c9-8e5b-493e-bd66-85d840b44acd",
      "name": "Load Expected Content",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        16,
        1008
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "=// Parameters provided by user in node config\nconst sheetId = this.getNodeParameter(\"sheetId\", 0) || \"YOUR_SPREADSHEET_ID\";\nconst sheetName = this.getNodeParameter(\"sheetName\", 0) || \"Sheet1\";\n\nconst { google } = require(\"googleapis\");\nconst sheets = google.sheets({ version: \"v4\", auth });\n\nconst response = await sheets.spreadsheets.values.get({\n  spreadsheetId: sheetId,\n  range: sheetName,\n});\n\nreturn response.data.values.map(row => ({ json: { row } }));"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "=https://docs.google.com/spreadsheets/d/<SPREADSHEET_ID>/edit"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "2e8681fb-5313-4c55-bbd2-1ce6e27a64ad",
  "connections": {
    "Get many messages": {
      "main": [
        [
          {
            "node": "Extracting Preheader From HTML",
            "type": "main",
            "index": 0
          },
          {
            "node": "Combine HTML, Expected Content and Preheader",
            "type": "main",
            "index": 1
          },
          {
            "node": "Load Expected Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Expected Content": {
      "main": [
        [
          {
            "node": "Combine HTML, Expected Content and Preheader",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extracting Preheader From HTML": {
      "main": [
        [
          {
            "node": "Combine HTML, Expected Content and Preheader",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "Extract Actual Content and Results": {
      "main": [
        [
          {
            "node": "Log Content Checks to Excel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking \u2018Execute workflow\u2019": {
      "main": [
        [
          {
            "node": "Get many messages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine HTML, Expected Content and Preheader": {
      "main": [
        [
          {
            "node": "Extract Actual Content and Results",
            "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.

Pro

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

About this workflow

Input: Google Sheets provides “Expected Content” rows (one per block/section). HTML Parse: A JS/HTML step extracts Actual Content from the email’s HTML (from Gmail or any provided HTML source). Merge: Expected and Actual items are merged into aligned pairs for comparison.…

Source: https://n8n.io/workflows/13530/ — 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

Stop losing valuable leads to missed meetings with this No-Show Follow-Up & Rescheduling automation! Whenever a lead is marked as a “no-show” in your CRM or calendar, this workflow automatically sends

Google Calendar, Gmail, Google Sheets
Email & Gmail

This workflow automatically detects bounced or invalid email addresses from your Gmail inbox and updates their status in Google Sheets. It fetches bounce notifications, extracts failed email addresses

Google Sheets, Slack, Gmail
Email & Gmail

The goal is to reduce inbox noise and automatically organize repetitive types of emails so that imprtant messages remain visible while unsolicited or promotional emails are handled automatically. When

Gmail, Gmail Trigger, Google Sheets
Email & Gmail

This workflow streamlines accounts receivable management by automatically monitoring invoices in Google Sheets and sending scheduled payment reminders. It is designed for businesses using Gmail and Go

Form Trigger, Gmail, Google Sheets +1
Email & Gmail

Never miss a qualified LinkedIn Sales Navigator message again. This automation monitors your Gmail for LinkedIn notifications, cross-references senders with your lead database in Google Sheets, and in

Gmail, Google Sheets