AutomationFlowsAI & RAG › On-demand Email Newsletter Summaries From Gmail to Telegram with Gpt-4.1-mini

On-demand Email Newsletter Summaries From Gmail to Telegram with Gpt-4.1-mini

ByVlad Arbatov @vladzima on n8n.io

Send 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…

Event trigger★★★★☆ complexityAI-powered16 nodesGmailTelegram TriggerTelegramOpenAI
AI & RAG Trigger: Event Nodes: 16 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #7254 — 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": "nCr0DSvP0NQHbFEn",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "email \u2192 tg public",
  "tags": [
    {
      "id": "TIMIlK5hNxVuDAlg",
      "name": "public",
      "createdAt": "2025-08-11T13:46:00.810Z",
      "updatedAt": "2025-08-11T13:46:00.810Z"
    }
  ],
  "nodes": [
    {
      "id": "54461eef-b4f1-4e29-aa87-674842f889e4",
      "name": "Get many messages",
      "type": "n8n-nodes-base.gmail",
      "position": [
        384,
        64
      ],
      "parameters": {
        "filters": {
          "q": "=(from:____@____.com) OR (from:____@____.com) OR (from:____@____.com -\"____\") after:{{ $json.dateString }}"
        },
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "2673b554-b1b8-422e-8075-1cedbd5d6235",
      "name": "Telegram Trigger",
      "type": "n8n-nodes-base.telegramTrigger",
      "position": [
        -64,
        64
      ],
      "parameters": {
        "updates": [
          "message"
        ],
        "additionalFields": {
          "chatIds": "your_tg_id"
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "cff69481-a8da-4ea1-b78a-3e6d94b54493",
      "name": "Loop Over Items",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        608,
        64
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "37f59023-026e-44f5-b2b2-f0e37fe8c6d8",
      "name": "Get a message",
      "type": "n8n-nodes-base.gmail",
      "position": [
        832,
        64
      ],
      "parameters": {
        "simple": false,
        "options": {},
        "messageId": "={{ $json.id }}",
        "operation": "get"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "16d90be1-d6fb-487e-a836-643021da12e1",
      "name": "Get days",
      "type": "n8n-nodes-base.code",
      "position": [
        160,
        64
      ],
      "parameters": {
        "jsCode": "// input: daysAgo (number of days)\nconst daysAgo = parseInt($json[\"message\"][\"text\"], 10);\nconst date = new Date();\ndate.setDate(date.getDate() - daysAgo);\nconst yyyy = date.getFullYear();\nconst mm = String(date.getMonth() + 1).padStart(2, '0');\nconst dd = String(date.getDate()).padStart(2, '0');\nreturn [{ dateString: `${yyyy}/${mm}/${dd}` }];"
      },
      "typeVersion": 2
    },
    {
      "id": "79126930-ced7-40e2-947b-466e9b1417fa",
      "name": "Get message data",
      "type": "n8n-nodes-base.code",
      "position": [
        1056,
        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": "7a00feb1-4fb2-43a3-8ea3-6c023e138a4c",
      "name": "Merge",
      "type": "n8n-nodes-base.code",
      "position": [
        832,
        -128
      ],
      "parameters": {
        "jsCode": "/**\n * This code is intended for use in the \"Code\" node in n8n.\n * It merges the 'topics' arrays from multiple 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 item (item) in the incoming array (items),\n// extracts the 'topics' array and immediately flattens all 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 of the items.\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 at the output of the node for further use.\nreturn [{\n  json: {\n    topics: allTopics\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "222dd34e-19b9-46aa-98ba-8f014457fbd4",
      "name": "Create TG message",
      "type": "n8n-nodes-base.code",
      "position": [
        1056,
        -128
      ],
      "parameters": {
        "jsCode": "const topics = $json.topics;\nconst list = topics.map((t, idx) =>\n  `${idx + 1}. *${t.title}*\\n\\n${t.descr}\\n\\n${t.subject}\\n\u2192 ${t.from} - ${t.date}`\n).join('\\n\\n');\n\nreturn [{ json: { message: `${list}` } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "de2c745d-e575-4670-ac3a-fbdd6e5ba6a2",
      "name": "Sanitize",
      "type": "n8n-nodes-base.code",
      "position": [
        1568,
        -128
      ],
      "parameters": {
        "jsCode": "/**\n * This code is intended for use in the \"Code\" node in n8n.\n * It prepares text for sending to Telegram in 'HTML' mode.\n * 1. Fixes \"broken\" formatting (*...* and _..._) by adding a closing symbol if their count is odd.\n * 2. Converts *text* to <b>text</b> and _text_ to <i>text</i>, even if the text spans multiple lines.\n * 3. Escapes basic HTML characters (<, >, &) for safety.\n *\n * IMPORTANT: In the Telegram node, Parse Mode must be set to HTML.\n *\n * The code processes EACH incoming item in the array.\n */\n\nconst correctedItems = items.map(item => {\n  // FIXED: Use the 'text' key that comes from the \"Split\" node.\n  let text = item.json.text;\n\n  if (!text) {\n    return item;\n  }\n\n  // --- STEP 1: Fix unbalanced formatting characters ---\n  const fixUnbalanced = (str, char) => {\n    // Use new RegExp to create a dynamic regular expression\n    const count = (str.match(new RegExp(`\\\\${char}`, 'g')) || []).length;\n    if (count % 2 !== 0) {\n      return str + char;\n    }\n    return str;\n  };\n\n  text = fixUnbalanced(text, '*');\n  text = fixUnbalanced(text, '_');\n\n  // --- STEP 2: Convert to safe HTML ---\n  // First, escape basic characters in the entire text so they don't conflict with tags.\n  let safeText = text\n    .replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;');\n\n  // Now replace Markdown with HTML tags.\n  // Use the 's' (dotAll) flag so '.' also matches newline characters.\n  // This is important for your multi-line italics.\n  safeText = safeText\n    .replace(/\\*(.*?)\\*/gs, '<b>$1</b>')\n    .replace(/_(.*?)_/gs, '<i>$1</i>');\n\n\n  // Update the 'text' field in the current item.\n  // In the Telegram node, use {{ $json.text }}\n  item.json.text = safeText;\n\n  // Return the modified item.\n  return item;\n});\n\n// Return the full array of processed items.\nreturn correctedItems;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "57984a3f-ccb0-4e14-bac8-4d83ad89bb44",
      "name": "Split",
      "type": "n8n-nodes-base.code",
      "position": [
        1280,
        -128
      ],
      "parameters": {
        "jsCode": "const CHUNK = 3500;\nconst txt = $json.message;\nconst parts = txt.match(/[\\s\\S]{1,3500}/g) || [];\nreturn parts.map(p => ({ json: { text: p } }));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "f1b7cbb8-7ae7-4699-97c2-354a0b82684b",
      "name": "Clean",
      "type": "n8n-nodes-base.code",
      "position": [
        1280,
        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) 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 format \"MM.DD\".\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": "a9eb6447-cc67-4364-9994-bcdb37824980",
      "name": "Send a message",
      "type": "n8n-nodes-base.telegram",
      "position": [
        1856,
        -128
      ],
      "parameters": {
        "text": "={{ $json.text }}\n",
        "chatId": "your_tg_id",
        "additionalFields": {
          "parse_mode": "HTML",
          "appendAttribution": false,
          "disable_web_page_preview": true
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "452b5f0c-db00-4cfe-9229-c61162476228",
      "name": "Message a model",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        1504,
        136
      ],
      "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
    },
    {
      "id": "fc2f7ecd-5a40-4f15-8a4f-055280b762e3",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -64,
        -224
      ],
      "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": "e9af2dfe-5a7b-4a55-8d78-39d82048a7b4",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        592,
        16
      ],
      "parameters": {
        "color": 7,
        "width": 1168,
        "height": 272,
        "content": "## Iterates over each message"
      },
      "typeVersion": 1
    },
    {
      "id": "27bbab62-1efb-4384-9d22-829a6515e855",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        816,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 880,
        "height": 192,
        "content": "## Clean up the text and forms the final message"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "56e514d0-6916-476f-992d-246d29f02f11",
  "connections": {
    "Clean": {
      "main": [
        [
          {
            "node": "Message a model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Create TG message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split": {
      "main": [
        [
          {
            "node": "Sanitize",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get days": {
      "main": [
        [
          {
            "node": "Get many messages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sanitize": {
      "main": [
        [
          {
            "node": "Send a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get a message": {
      "main": [
        [
          {
            "node": "Get message data",
            "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
          }
        ]
      ]
    },
    "Telegram Trigger": {
      "main": [
        [
          {
            "node": "Get days",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create TG message": {
      "main": [
        [
          {
            "node": "Split",
            "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

Send 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…

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

Send a target niche and location via Telegram message Workflow discovers businesses via Google Maps API AI enriches contacts with email and LinkedIn data via Serper GPT-4o scores and qualifies each le

Telegram Trigger, OpenAI, Google Sheets +3
AI & RAG

Tags: Logistics, Supply Chain, Warehouse Operations, Paperless processes, Quality Management

Telegram Trigger, Telegram, OpenAI +1
AI & RAG

💥 Automate YouTube thumbnail creation from video links -vide. Uses telegramTrigger, httpRequest, googleDrive, gmail. Event-driven trigger; 25 nodes.

Telegram Trigger, HTTP Request, Google Drive +6
AI & RAG

💥 Automate YouTube thumbnail creation from video links -vide. Uses telegramTrigger, httpRequest, googleDrive, gmail. Event-driven trigger; 25 nodes.

Telegram Trigger, HTTP Request, Google Drive +6
AI & RAG

This n8n template demonstrates how to capture Telegram voice messages, transcribe them into text using AssemblyAI, analyze the transcript with AI for summary and sentiment insights, and finally delive

Telegram, HTTP Request, OpenAI +2