{
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "nodes": [
    {
      "id": "2892543e-bad2-4a9f-92ba-2931f2abdfc8",
      "name": "When clicking \u2018Execute workflow\u2019",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -752,
        704
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "85c30688-a106-4f39-9b16-e33641adeef6",
      "name": "Google Gemini Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        1872,
        1056
      ],
      "parameters": {
        "options": {},
        "modelName": "models/gemini-2.5-pro"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "97612572-3562-4156-8379-c9e48e1fac89",
      "name": "Clean HTML Tags, newlines and trim text1",
      "type": "n8n-nodes-base.code",
      "position": [
        1456,
        688
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Get the current item's HTML\nlet html = $('Get Website Content').item.json.data || '';\n\n// 1: Remove Scripts and Styles\nhtml = html.replace(/<script[\\s\\S]*?>[\\s\\S]*?<\\/script>/gi, \"\");\nhtml = html.replace(/<style[\\s\\S]*?>[\\s\\S]*?<\\/style>/gi, \"\");\n\n// 2: Remove Comments\nhtml = html.replace(/<!--[\\s\\S]*?-->/g, \"\");\n\n// 3: Remove all other HTML tags\nhtml = html.replace(/<[^>]+>/g, \" \");\n\n// 4: Decode HTML entities\nhtml = html\n  .replace(/&nbsp;/g, \" \")\n  .replace(/&amp;/g, \"&\")\n  .replace(/&quot;/g, '\"')\n  .replace(/&lt;/g, \"<\")\n  .replace(/&gt;/g, \">\");\n\n// 5: Clean up excessive whitespace\n// First: Convert carriage returns and tabs to newlines/spaces\nhtml = html.replace(/\\r/g, \"\\n\").replace(/\\t/g, \" \");\n\n// Collapse everything (e.g., \"\\n \\n \\n\") into a single \"\\n\".\nhtml = html.replace(/(\\n\\s*)+/g, \"\\n\");\n\n// Collapse multiple horizontal spaces into one\nhtml = html.replace(/ +/g, \" \");\n\n\n// 6: Overwrite the original 'data' field\nconst limit = $json.trim_length;\n$json.data = html.trim().substring(0, limit);\n\n// 7: Remove the configuration field from the output for cleanness\ndelete $json.trim_length;\n\n// Return the cleaned item\nreturn $json;"
      },
      "typeVersion": 2
    },
    {
      "id": "8fed5c46-98fd-4676-b82d-30d63318eeb3",
      "name": "Aggregate Google Alerts Content",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        64,
        704
      ],
      "parameters": {
        "include": "specifiedFields",
        "options": {},
        "aggregate": "aggregateAllItemData",
        "fieldsToInclude": "html"
      },
      "typeVersion": 1
    },
    {
      "id": "c7620438-272b-407c-b470-da0bdf9888a7",
      "name": "Create HTML template and sort by topic",
      "type": "n8n-nodes-base.code",
      "position": [
        3280,
        672
      ],
      "parameters": {
        "jsCode": "// --- 1: RETRIEVE DATA ---\n// Your data is nested inside an \"output\" array within the first item.\nconst inputNode = $input.first();\nconst items = inputNode ? inputNode.json.output : [];\n\n// Safety check\nif (!items || !Array.isArray(items) || items.length === 0) {\n  return [{ json: { htmlEmailBody: '<p>No items were found to create a report.</p>' } }];\n}\n\n// --- 2: BUILD THE HTML TABLE ---\n\n// Style definitions\nconst tableAttributes = `width=\"100%\" border=\"1\" cellspacing=\"0\" cellpadding=\"12\"`;\nconst tableStyle = `font-family: Arial, sans-serif; font-size: 14px; border: 1px solid #cccccc; border-collapse: collapse;`;\nconst thStyle = `background-color: #f2f2f2; font-weight: bold; text-align: left; vertical-align: top; padding: 12px; border: 1px solid #cccccc;`;\nconst tdStyle = `text-align: left; vertical-align: top; padding: 12px; border: 1px solid #cccccc;`;\nconst linkStyle = `color: #1a0dab; text-decoration: none; word-break: break-all;`;\nconst evenRowStyle = `background-color: #f9f9f9;`;\n\n// Helper function to clean strings \nconst cleanString = (str) => {\n  if (typeof str !== 'string') return str;\n  // 1. Remove leading and trailing quotes\n  // 2. Fix escaped quotes (e.g. \\\" becomes \")\n  return str.replace(/^\"|\"$/g, '').replace(/\\\\\"/g, '\"');\n};\n\n// Helper to format the link\nconst formatLinkCell = (linkString) => {\n  if (!linkString) return 'N/A';\n  \n  let cleanLink = cleanString(linkString);\n  \n  // Basic validation to ensure it looks like a URL\n  if (!cleanLink.startsWith('http')) return cleanLink;\n\n  return `<a href=\"${cleanLink}\" target=\"_blank\" style=\"${linkStyle}\">${cleanLink}</a>`;\n};\n\n// Start the HTML string\nlet html = `\n<table ${tableAttributes} style=\"${tableStyle}\">\n  <thead>\n    <tr>\n      <th style=\"${thStyle}\">Summary</th>\n      <th style=\"${thStyle}\">Topic</th>\n      <th style=\"${thStyle}\">Link</th>\n    </tr>\n  </thead>\n  <tbody>\n`;\n\n\n// --- 3: SORT ITEMS BY TOPIC (A-Z) ---\nitems.sort((a, b) => {\n  // Get topics or default to empty string\n  const topicA = cleanString(a.topic || '').toLowerCase();\n  const topicB = cleanString(b.topic || '').toLowerCase();\n  \n  // Compare them alphabetically\n  return topicA.localeCompare(topicB);\n});\n// YOUR_AWS_SECRET_KEY_HERE==\n\n// --- 4: LOOP THROUGH ITEMS ---\nfor (const [index, item] of items.entries()) {\n  const isEvenRow = index % 2 === 0;\n  const trStyle = isEvenRow ? evenRowStyle : '';\n\n  // 'item' is the direct object \n  // (e.g. { summary: \"...\", topic: \"...\" })\n  \n  const summary = cleanString(item.summary ?? 'N/A');\n  const topic = cleanString(item.topic ?? 'N/A');\n  const rawLink = item.link ?? '';\n\n  const linkCellHtml = formatLinkCell(rawLink);\n\n  html += `\n    <tr style=\"${trStyle}\">\n      <td style=\"${tdStyle}\">${summary}</td>\n      <td style=\"${tdStyle}\">${topic}</td>\n      <td style=\"${tdStyle}\">${linkCellHtml}</td>\n    </tr>\n  `;\n}\n\n// Finish the HTML\nhtml += `\n  </tbody>\n</table>\n`;\n\n// --- 5: RETURN THE FINAL HTML OBJECT ---\nreturn [{\n  json: {\n    htmlEmailBody: html\n  }\n}];"
      },
      "executeOnce": false,
      "typeVersion": 2
    },
    {
      "id": "86be61f8-86df-49bd-ab96-da11dd309da3",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1520,
        400
      ],
      "parameters": {
        "width": 528,
        "height": 1264,
        "content": "## Automated AI News Digest from Google Alerts\n### Stop drowning in individual notification emails. This workflow collects your Google Alerts, reads the articles for you, and compiles a single, clean AI-summarized briefing.\n\nUse cases: Create daily executive briefings on competitors, track industry trends without reading clickbait, or monitor brand mentions efficiently.\n\n### How it works\n* **Ingestion:** The workflow fetches unread \"Google Alerts\" emails from your Gmail account.\n* **Extraction:** It aggregates the emails and uses a custom Code Node to extract the specific article links and topics, filtering out Google's tracking redirects.\n* **Scraping & Cleaning:** It visits every article link to retrieve the content. A smart \"Cleaning\" node strips away ads, scripts, and unnecessary HTML to ensure high-quality data and reduce AI token costs.\n* **AI Analysis:** Google Gemini reads the cleaned text and generates a concise 2-4 sentence summary for each article.\n* **Reporting:** The workflow compiles all summaries into a professional, responsive HTML table, sorted alphabetically by Topic.\n* **Delivery:** You receive **one** single digest email containing all the news.\n* **Cleanup:** The original Google Alert emails are automatically marked as \"read\" to keep your inbox zero.\n\n### How to use\n* **Setup Trigger:** The template uses a Manual Trigger for testing. For production, replace this with a **Schedule Trigger** (e.g., every morning at 7 AM) or a **Cron Trigger**.\n* **Gmail Configuration:** Ensure the `Get many messages` node is set to filter for `sender:googlealerts-noreply@google.com`.\n* **Prompt Customization:** You can adjust the system prompt in the AI Agent node if you prefer bullet points or a specific language for your summaries.\n\n### Requirements\n* **Google Gemini API Key:** To power the AI summarization (using the `gemini-2.5-pro` or similar model).\n* **Gmail Account (OAuth2):** Required for both reading the alerts and sending the final digest.\n\n### Need Help?\nreach me under [berni@zindel.digital](mailto:berni@zindel.digital?subject=N8N%20Google%20Alerts%20Template)"
      },
      "typeVersion": 1
    },
    {
      "id": "600eef50-d2c1-42c2-8545-2ffee2009542",
      "name": "Sticky Note10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -400,
        368
      ],
      "parameters": {
        "color": 4,
        "width": 1024,
        "height": 544,
        "content": "# 1. Fetch Alerts & Extract Data\n\n**Get Google Alerts node:**\nRetrieves all unread emails from the Google Alerts sender.\n\n**Aggregate Google Alerts Content node:**\nCombines the HTML body of all emails into a single list for batch processing.\n\n**Extract Links and Topics node:**\nParses the HTML to extract article topics and clean the links (removes Google tracking redirects)."
      },
      "typeVersion": 1
    },
    {
      "id": "87cff872-69d3-405b-9164-820c8a7b7999",
      "name": "Get Google Alerts",
      "type": "n8n-nodes-base.gmail",
      "position": [
        -336,
        704
      ],
      "parameters": {
        "simple": false,
        "filters": {
          "sender": "user@example.com",
          "readStatus": "unread"
        },
        "options": {},
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "fd82cfe2-387b-4f46-926c-5858cb22eefa",
      "name": "Get Website Content",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueErrorOutput",
      "position": [
        784,
        704
      ],
      "parameters": {
        "url": "={{$json.link }}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          },
          "allowUnauthorizedCerts": true
        }
      },
      "executeOnce": false,
      "typeVersion": 4.2
    },
    {
      "id": "bd95dbdd-6f52-48ab-a307-ed0be4e95366",
      "name": "Structured Output Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        1984,
        912
      ],
      "parameters": {
        "autoFix": true,
        "jsonSchemaExample": "{\n  \"Summary\": \"<your summary here>\"\n}"
      },
      "typeVersion": 1.3
    },
    {
      "id": "4493b3d0-5d1e-4e84-9667-2551fa2f1067",
      "name": "Create Summary",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "onError": "continueErrorOutput",
      "position": [
        1936,
        688
      ],
      "parameters": {
        "text": "=You are an expert in content analysis and data structuring. Your task is to analyze and summarize the content of the attached website.\n\nInstructions:\n1. Identify the main topic, purpose, and key information of the website.\n2. Write a short, concise summary (approx. 2-4 sentences).\n3. Output the result **exclusively** in valid JSON format.\n4. Do not use markdown code blocks (like ```json), just return the raw JSON object.\n5. Use the exact key \"Summary\".\n\nTarget Format:\n{\"Summary\": \"<your summary here>\"}\n\nHere is the content of the website:\n{{ $json.data }}",
        "options": {},
        "promptType": "define",
        "hasOutputParser": true
      },
      "executeOnce": false,
      "typeVersion": 2.2
    },
    {
      "id": "9a3fb02b-b381-4e25-9359-ef79c03292fd",
      "name": "Map summary, topic and link",
      "type": "n8n-nodes-base.set",
      "position": [
        2624,
        672
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "7e015c4c-6fe0-416e-8d29-7e86f7ce0fd9",
              "name": "summary",
              "type": "string",
              "value": "={{ $json.output.Summary }}"
            },
            {
              "id": "86488ea3-2ebe-4801-9a76-007d867199cd",
              "name": "topic",
              "type": "string",
              "value": "={{$('Extract Links and Topics').item.json.topic }}"
            },
            {
              "id": "dc11d0f5-8d7b-4908-a969-067606121c22",
              "name": "link",
              "type": "string",
              "value": "={{$('Extract Links and Topics').item.json.link }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "61037b80-5104-47a6-b4d8-bcfcaae0e3d6",
      "name": "put all entries into a single list",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        2944,
        672
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "output"
      },
      "typeVersion": 1
    },
    {
      "id": "4dc272e7-658f-46b2-b8aa-f560247eed72",
      "name": "Send Google Alert Summary",
      "type": "n8n-nodes-base.gmail",
      "position": [
        3744,
        672
      ],
      "parameters": {
        "sendTo": "user@example.com",
        "message": "={{ $json.htmlEmailBody }}",
        "options": {},
        "subject": "Google Alert Summary"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "8a8f74b9-b171-4427-913e-c74234dc5e75",
      "name": "Extract Links and Topics",
      "type": "n8n-nodes-base.code",
      "position": [
        464,
        704
      ],
      "parameters": {
        "jsCode": "// --- 1: GET AGGREGATED DATA ---\n// The Aggregate node put all your emails into an array called \"data\"\nconst inputNode = $input.first();\nconst emailList = inputNode.json.data;\n\nif (!emailList || !Array.isArray(emailList) || emailList.length === 0) {\n  return [];\n}\n\n// --- CONSTANTS ---\nconst googlePrefix = 'https://www.google.com/url?rct=j&amp;sa=t&amp;url=';\nconst googleSuffixMarker = '&amp;ct=';\n// Regex: Matches Topic Headers OR Links\nconst pattern = /(<span[^>]*style=\"[^\"]*font-size:22px[^\"]*\"[^>]*>(.*?)<\\/span>)|(href=\"([^\"]*)\")/gi;\n\nconst rawResults = [];\n\n// --- 2: LOOP THROUGH EACH EMAIL ---\nfor (const emailItem of emailList) {\n  const html = emailItem.html;\n\n  // Skip if this specific item has no HTML\n  if (!html) continue;\n\n  // Reset state for this specific email\n  let currentTopic = \"General\"; \n  let match;\n  \n  // Reset Regex index for the new string\n  pattern.lastIndex = 0;\n\n  // --- 3: EXTRACT LINKS FROM CURRENT EMAIL ---\n  while ((match = pattern.exec(html)) !== null) {\n    \n    // CASE A: Found a Topic Header (Group 2)\n    if (match[2]) {\n      currentTopic = match[2]\n        .trim()\n        .replace(/&quot;/g, '\"')\n        .replace(/&amp;/g, '&');\n    } \n    \n    // CASE B: Found a Link (Group 4)\n    else if (match[4]) {\n      const rawLink = match[4];\n\n      if (rawLink.startsWith(googlePrefix)) {\n        // Clean the link\n        let cleanLink = rawLink.replace(googlePrefix, '');\n        const suffixIndex = cleanLink.indexOf(googleSuffixMarker);\n        \n        if (suffixIndex !== -1) {\n          cleanLink = cleanLink.substring(0, suffixIndex);\n        }\n        \n        const decodedLink = decodeURIComponent(cleanLink.replace(/%25/g, '%'));\n        \n        // Add to our master list\n        rawResults.push({\n          topic: currentTopic,\n          link: decodedLink\n        });\n      }\n    }\n  }\n}\n\n// --- STEP 4: DEDUPLICATE GLOBAL LIST ---\nconst uniqueLinks = [...new Map(rawResults.map(item => [item.link, item])).values()];\n\nreturn uniqueLinks;"
      },
      "typeVersion": 2
    },
    {
      "id": "f20d891f-2ecd-4a61-b53c-1db7265f6ee0",
      "name": "Sticky Note11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        720,
        368
      ],
      "parameters": {
        "color": 6,
        "width": 896,
        "height": 544,
        "content": "# 2. Scrape & Clean Content\n\n**Get Website Content node:**\nVisits the extracted article URLs and downloads the raw HTML content.\n\n**Trim Text node:**\nSets a maximum character limit for the content to control AI token usage and costs.\n\n**Clean HTML Tags node:**\nStrips ads, scripts, and formatting to produce clean, concise text for the AI"
      },
      "typeVersion": 1
    },
    {
      "id": "bd365b60-4d93-4d4c-bb79-b1fa28ec7ad1",
      "name": "Trim Text",
      "type": "n8n-nodes-base.set",
      "position": [
        1136,
        688
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "eced0310-9808-4e80-bd2c-ff71c3bcc1a8",
              "name": "trim_length",
              "type": "number",
              "value": 300000
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "73918e8f-d70b-42cf-9afb-4f609c967fd3",
      "name": "Sticky Note12",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1712,
        368
      ],
      "parameters": {
        "color": 6,
        "width": 736,
        "height": 848,
        "content": "# 3. AI Analysis & Summarization\n\n**Structured Output Parser node:**\nEnforces a strict JSON format for the AI's response, ensuring reliable data structure for the report.\n\n**Create Summary node:**\nUses Google Gemini to analyze the cleaned text and generate a concise summary based on your system prompt.\n\n**Pro Tip:**\nCustomize this for your business! For example, if looking for Real Estate, update the prompt to ask: \"Is this property suitable for investment?\" and add a \"rating\" field to the Output Parser to automatically filter for top picks."
      },
      "typeVersion": 1
    },
    {
      "id": "d019f458-fd04-4bd2-b2d2-b26b985bfdc8",
      "name": "Sticky Note13",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2544,
        368
      ],
      "parameters": {
        "color": 5,
        "width": 896,
        "height": 544,
        "content": "# 4. Compile & Format Report\n\n**Map summary node:**\nStandardizes the final data object by merging the AI-generated summary with the original topic and source link.\n\n**Put all entries into single list node:**\nAggregates all processed articles into one master list to prepare for the single digest email.\n\n**Create HTML template node:**\nGenerates a professional, responsive HTML table and automatically sorts all items alphabetically by topic for easy reading."
      },
      "typeVersion": 1
    },
    {
      "id": "9d9e0cbd-21cf-4312-ab84-3f22280a5016",
      "name": "Sticky Note14",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3552,
        368
      ],
      "parameters": {
        "color": 4,
        "width": 496,
        "height": 544,
        "content": "# 5. Delivering\n\n**Send Email node:**\nDelivers the compiled HTML digest to your specified email recipient."
      },
      "typeVersion": 1
    },
    {
      "id": "3b321eb9-58e6-4819-a217-4394f07b3fde",
      "name": "Sticky Note15",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -128,
        944
      ],
      "parameters": {
        "color": 4,
        "width": 496,
        "height": 544,
        "content": "# 6.  Clean inbox\n\n**Mark as read node:**\nMarks the original Google Alert emails as \"read\" in your inbox to ensure they are not processed again."
      },
      "typeVersion": 1
    },
    {
      "id": "4772a8de-b069-49eb-89f3-7ee37dcf1ea9",
      "name": "Mark Alert as read",
      "type": "n8n-nodes-base.gmail",
      "position": [
        48,
        1136
      ],
      "parameters": {
        "messageId": "={{ $('Get Google Alerts').item.json.id }}",
        "operation": "markAsRead"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    }
  ],
  "connections": {
    "Trim Text": {
      "main": [
        [
          {
            "node": "Clean HTML Tags, newlines and trim text1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Summary": {
      "main": [
        [
          {
            "node": "Map summary, topic and link",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Google Alerts": {
      "main": [
        [
          {
            "node": "Aggregate Google Alerts Content",
            "type": "main",
            "index": 0
          },
          {
            "node": "Mark Alert as read",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Website Content": {
      "main": [
        [
          {
            "node": "Trim Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Links and Topics": {
      "main": [
        [
          {
            "node": "Get Website Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Create Summary",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Structured Output Parser",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Structured Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Create Summary",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Send Google Alert Summary": {
      "main": [
        []
      ]
    },
    "Map summary, topic and link": {
      "main": [
        [
          {
            "node": "put all entries into a single list",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Google Alerts Content": {
      "main": [
        [
          {
            "node": "Extract Links and Topics",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "put all entries into a single list": {
      "main": [
        [
          {
            "node": "Create HTML template and sort by topic",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking \u2018Execute workflow\u2019": {
      "main": [
        [
          {
            "node": "Get Google Alerts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create HTML template and sort by topic": {
      "main": [
        [
          {
            "node": "Send Google Alert Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean HTML Tags, newlines and trim text1": {
      "main": [
        [
          {
            "node": "Create Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}