AutomationFlowsAI & RAG › Create Daily Newsletter Digests From Gmail Using Gpt-4.1-mini

Create Daily Newsletter Digests From Gmail Using Gpt-4.1-mini

ByVlad Arbatov @vladzima on n8n.io

Every day at a set time, this workflow fetches yesterday’s newsletters from Gmail, summarizes each email into concise topics with an LLM, merges all topics, renders a clean HTML digest, and emails it to your inbox. Triggers on a daily schedule (default 16:00, server time)…

Cron / scheduled trigger★★★★☆ complexityAI-powered13 nodesGmailOpenAI
AI & RAG Trigger: Cron / scheduled Nodes: 13 Complexity: ★★★★☆ AI nodes: yes Added:

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

This workflow follows the Gmail → OpenAI 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": "4LGHvpa2PjTNwGc1",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "email \u2192 email public",
  "tags": [
    {
      "id": "TIMIlK5hNxVuDAlg",
      "name": "public",
      "createdAt": "2025-08-11T13:46:00.810Z",
      "updatedAt": "2025-08-11T13:46:00.810Z"
    }
  ],
  "nodes": [
    {
      "id": "8636c298-0e5a-494d-9bd1-beace2be380c",
      "name": "Get many messages",
      "type": "n8n-nodes-base.gmail",
      "position": [
        368,
        64
      ],
      "parameters": {
        "filters": {
          "q": "=(from:____@____.com) OR (from:____@____.com) OR (from:____@____.com -\"____\") after:{{ $now.minus({ days: 1 }).toFormat('yyyy/MM/dd') }}"
        },
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "d77aad78-85fa-4dd1-9227-6636b023dc04",
      "name": "Loop Over Items",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        592,
        64
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "3f688cd5-ac4b-4965-a9d4-2220cee11440",
      "name": "Get a message",
      "type": "n8n-nodes-base.gmail",
      "position": [
        816,
        64
      ],
      "parameters": {
        "simple": false,
        "options": {},
        "messageId": "={{ $json.id }}",
        "operation": "get"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "104254b9-51ea-4655-9e49-38dd3b200929",
      "name": "Get message data",
      "type": "n8n-nodes-base.code",
      "position": [
        1040,
        64
      ],
      "parameters": {
        "jsCode": "function extractHtml(payload) {\n  if (!payload) return '';\n  if (payload.body && payload.body.data) {\n    return Buffer.from(payload.body.data, 'base64').toString('utf-8');\n  }\n  function findHtmlPart(parts) {\n    for (const part of parts) {\n      if (part.mimeType === 'text/html' && part.body && part.body.data) {\n        return Buffer.from(part.body.data, 'base64').toString('utf-8');\n      }\n      if (part.parts) {\n        const result = findHtmlPart(part.parts);\n        if (result) return result;\n      }\n    }\n    return '';\n  }\n  if (payload.parts) {\n    const html = findHtmlPart(payload.parts);\n    if (html) return html;\n  }\n  return '';\n}\n\nlet html = $json.html || '';\nif (!html && $json.payload) {\n  html = extractHtml($json.payload);\n}\nif (!html && $json.text) {\n  html = $json.text;\n}\n\n// Clean 'from': keep only the sender's name\nlet from = $json.from?.text || $json.from || '';\nconst match = from.match(/^\\\"?([^\\\"<]+)\\\"?\\s*<[^>]+>$/);\nif (match) {\n  from = match[1].trim();\n}\n\n// Convert date to DD.MM.YYYY format\nlet date = $json.date || '';\nlet formattedDate = date;\nif (date) {\n  const d = new Date(date);\n  const day = String(d.getDate()).padStart(2, '0');\n  const month = String(d.getMonth() + 1).padStart(2, '0');\n  const year = d.getFullYear();\n  formattedDate = `${day}.${month}.${year}`;\n}\n\nconst subject = $json.subject || '';\n\nreturn [{\n  html,\n  subject,\n  from,\n  date: formattedDate\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "31e5402b-7146-4955-ae7b-ee4dd3ecc583",
      "name": "Merge",
      "type": "n8n-nodes-base.code",
      "position": [
        816,
        -128
      ],
      "parameters": {
        "jsCode": "/**\n * This code is intended for use in the \"Code\" node in n8n.\n * It merges the 'topics' arrays from several incoming items into a single array.\n *\n * Incoming data (items) have the following structure:\n * [\n * { json: { message: { content: { topics: [...] } } } },\n * { json: { message: { content: { topics: [...] } } } },\n * ...\n * ]\n */\n\n// We use the flatMap method, which is a combination of map and flat.\n// It iterates over each element (item) in the incoming array (items),\n// extracts the 'topics' array and immediately flattens all collected arrays into one.\nconst allTopics = items.flatMap(item => {\n  // We use optional chaining (?.) for safe access to the nested property.\n  // This prevents an error if 'message' or 'content' is missing in any element.\n  // If the path is not found, return an empty array [] so flatMap can work correctly.\n  return item.json?.message?.content?.topics || [];\n});\n\n// The \"Code\" node must return an array of objects.\n// We return one object containing the 'json' property with our merged 'topics' array.\n// This result will be available on the node's output for further use.\nreturn [{\n  json: {\n    topics: allTopics\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "df46e906-1a21-47c3-9168-900ee58c3730",
      "name": "Clean",
      "type": "n8n-nodes-base.code",
      "position": [
        1264,
        64
      ],
      "parameters": {
        "jsCode": "/**\n * This code is intended for use in the \"Code\" node in n8n.\n * Its task is to prepare data from the Gmail node for the next node (LLM).\n * It does not parse HTML; it only passes it along with other fields.\n *\n * Incoming data (items) \u2014 this is the result of the Gmail node.\n * [\n * { json: { html: \"...\", subject: \"...\", from: \"...\", date: \"DD.MM.YYYY\" } }\n * ]\n *\n * The code converts the date from \"DD.MM.YYYY\" to \"MM.DD\" and passes\n * all the necessary fields (html, subject, from, date) to the next step.\n */\n\n// Use .map() to transform each incoming item.\nconst results = items.map(item => {\n  // Extract the required fields from the incoming JSON.\n  const { html, subject, from, date: originalDate } = item.json;\n\n  // Check for required fields.\n  if (!html || !originalDate) {\n    // If there is no data, return null to filter this item later.\n    return null;\n  }\n\n  // 1. Split the date string into components (day, month, year).\n  const [day, month] = originalDate.split('.').slice(0, 2);\n\n  // 2. Form a new string in the \"MM.DD\" format.\n  const newDate = `${month}.${day}`;\n\n  // 3. Return a new object in the structure expected by\n  // the next node (LLM).\n  return {\n    json: {\n      html,\n      subject,\n      from,\n      date: newDate\n    }\n  };\n});\n\n// Filter out empty results and return the array for the next node.\nreturn results.filter(item => item !== null);\n"
      },
      "typeVersion": 2
    },
    {
      "id": "4bd967a6-cd5c-4225-a785-fda4f227979a",
      "name": "Create template",
      "type": "n8n-nodes-base.code",
      "position": [
        1040,
        -128
      ],
      "parameters": {
        "jsCode": "/**\n * This code is intended for use in the \"Code\" node in n8n.\n * It takes an array of topics and creates a single,\n * well-formatted HTML email for sending.\n *\n * This node replaces the nodes \"Formatting\", \"Split\", and \"Sanitizer\".\n */\n\n// Get the array of topics from the previous node.\nconst topics = $json.topics;\n\n// Check if there are topics to process.\nif (!Array.isArray(topics) || topics.length === 0) {\n  return []; // If there are no topics, stop the workflow.\n}\n\n// --- Create HTML markup for each news item ---\nconst topicsHtml = topics.map((topic, index) => {\n  // Escape basic HTML characters in the data to avoid breaking the markup.\n  const escapeHtml = (unsafe) => {\n    if (!unsafe) return '';\n    return unsafe\n         .replace(/&/g, \"&amp;\")\n         .replace(/</g, \"&lt;\")\n         .replace(/>/g, \"&gt;\")\n         .replace(/\\\"/g, \"&quot;\")\n         .replace(/'/g, \"&#039;\");\n  };\n\n  const title = escapeHtml(topic.title);\n  const descr = escapeHtml(topic.descr).replace(/\\n/g, '<br>'); // Preserve line breaks in the description\n  const subject = escapeHtml(topic.subject);\n  const from = escapeHtml(topic.from);\n  const date = escapeHtml(topic.date);\n\n  // Build a nice HTML block for a single news item.\n  return `\n    <div style=\"margin-bottom: 24px; padding-bottom: 16px; border-bottom: 1px solid #eeeeee;\">\n      <h3 style=\"margin: 0 0 8px 0; font-size: 18px; color: #1a1a1a;\">\n        ${index + 1}. ${title}\n      </h3>\n      <p style=\"margin: 0 0 12px 0; font-size: 16px; color: #333333; line-height: 1.5;\">\n        ${descr}\n      </p>\n      <p style=\"margin: 0; font-size: 14px; color: #777777; font-style: italic;\">\n        ${subject}<br>\n        \u2192 ${from} - ${date}\n      </p>\n    </div>\n  `;\n}).join(''); // Join all HTML blocks into a single string.\n\n// --- Wrap everything in a full HTML document with styles ---\nconst finalHtml = `\n  <!DOCTYPE html>\n  <html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>\u0412\u0430\u0448 \u043d\u043e\u0432\u043e\u0441\u0442\u043d\u043e\u0439 \u0434\u0430\u0439\u0434\u0436\u0435\u0441\u0442</title>\n  </head>\n  <body style=\"margin: 0; padding: 0; background-color: #f7f7f7; font-family: Arial, sans-serif;\">\n    <table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\n      <tr>\n        <td align=\"center\" style=\"padding: 20px;\">\n          <table width=\"600\" border=\"0\" cellpadding=\"20\" cellspacing=\"0\" style=\"background-color: #ffffff; border-radius: 8px;\">\n            <tr>\n              <td>\n                <h1 style=\"text-align: center; color: #1a1a1a;\">\u041d\u043e\u0432\u043e\u0441\u0442\u043d\u043e\u0439 \u0434\u0430\u0439\u0434\u0436\u0435\u0441\u0442</h1>\n                ${topicsHtml}\n              </td>\n            </tr>\n          </table>\n        </td>\n      </tr>\n    </table>\n  </body>\n  </html>\n`;\n\n// Return the result. In the \"Send Email\" node use {{ $json.htmlBody }}\nreturn [{ json: { htmlBody: finalHtml } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "57cc3b99-7a91-492f-8d5c-cbf0da079d16",
      "name": "Send a message",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1264,
        -128
      ],
      "parameters": {
        "sendTo": "user@example.com",
        "message": "={{ $json.htmlBody }}",
        "options": {},
        "subject": "Your-subject"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "363983e4-7be4-4319-8fdb-dd8ff39200e5",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        144,
        64
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 16
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "06729a53-d537-4a80-8aed-b1032b858390",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        144,
        -208
      ],
      "parameters": {
        "color": 4,
        "width": 320,
        "height": 240,
        "content": "## Try this out!\nSend a number to your Telegram bot (e.g., 2) and get a neatly formatted digest of all Gmail newsletters received since that date. Each email is summarized by an LLM into concise topics, merged into a single Telegram message, automatically split into chunks to fit Telegram limits, and safely formatted as HTML."
      },
      "typeVersion": 1
    },
    {
      "id": "00d59259-5d92-458d-9b8e-bc7bf0f58979",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        576,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 1152,
        "height": 256,
        "content": "## Iterates over each message"
      },
      "typeVersion": 1
    },
    {
      "id": "351eb40c-ba0a-4ea6-9f14-d7eabbf956e7",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        800,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 656,
        "height": 192,
        "content": "## Clean up the text and forms the final message"
      },
      "typeVersion": 1
    },
    {
      "id": "bddb5d4d-629a-4bfd-bf26-b60451949144",
      "name": "Message a model",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        1488,
        144
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-mini",
          "cachedResultName": "GPT-4.1-MINI"
        },
        "options": {},
        "messages": {
          "values": [
            {
              "content": "=You are a specialist in analyzing email newsletters. Create a brief summary of the email in JSON format:\n\n`{ \"topics\": [ { \"title\": ..., \"descr\": ..., \"subject\": ..., \"from\": ..., \"date\": ... } ] }`\n\nFor each news item within a block, create a separate topic with a brief one-sentence description, even if they are listed or under a single headline. Do not combine them into one topic.\n\nHowever, if the email is from ____, combine each block (for example, \"____\") by listing the topics in the description.\n\nIf the email is from ____, ignore the sections \"____\" and \"____\".\n\nAll values in your JSON must be in ____ language, except for the subject. The subject\u2014{{ $json.subject }}\u2014must remain untranslated.\n\nHere is the subject: {{ $json.subject }}\nHere is the from: {{ $json.from }}\nHere is the date: {{ $json.date }}\n\nHere is the email:\n{{ $json.html }}"
            }
          ]
        },
        "jsonOutput": true
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.8
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "53fdca83-a1b5-4866-bed8-0b3322de6bc5",
  "connections": {
    "Clean": {
      "main": [
        [
          {
            "node": "Message a model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Create template",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get a message": {
      "main": [
        [
          {
            "node": "Get message data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create template": {
      "main": [
        [
          {
            "node": "Send a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Message a model": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get message data": {
      "main": [
        [
          {
            "node": "Clean",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Get many messages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get many messages": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "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

Every day at a set time, this workflow fetches yesterday’s newsletters from Gmail, summarizes each email into concise topics with an LLM, merges all topics, renders a clean HTML digest, and emails it to your inbox. Triggers on a daily schedule (default 16:00, server time)…

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

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

Personalized Outreach & Follow-Up - Phase 2. Uses googleSheets, openAi, gmail, gmailTrigger. Scheduled trigger; 59 nodes.

Google Sheets, OpenAI, Gmail +2
AI & RAG

A scheduled process aggregates content from eight distinct data sources and standardizes all inputs into a unified format. AI models perform sentiment scoring, detect conspiracy or misinformation sign

HTTP Request, OpenAI, Postgres +2
AI & RAG

This workflow monitors filesystem sync and backup jobs by validating their execution logs, not by running or inspecting the jobs themselves.

Google Cloud Storage, Gmail, GitHub +2
AI & RAG

This advanced workflow automates brand monitoring and media coverage tracking for musicians, bands, and music labels. The system uses multiple search queries (dorky) to discover mentions across the we

Google Sheets, Gmail, @Brave/N8N Nodes Brave Search +1
AI & RAG

Stop wasting billable hours on manual time-tracking. AutoTimesheet Pro uses AI to collect emails, meetings, and GitHub work, then writes a clean timesheet straight into Google Sheets. Perfect for deve

Google Calendar, Gmail, GitHub +3