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 →
{
"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, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>');\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.
gmailOAuth2openAiApitelegramApi
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 →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
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
Tags: Logistics, Supply Chain, Warehouse Operations, Paperless processes, Quality Management
💥 Automate YouTube thumbnail creation from video links -vide. Uses telegramTrigger, httpRequest, googleDrive, gmail. Event-driven trigger; 25 nodes.
💥 Automate YouTube thumbnail creation from video links -vide. Uses telegramTrigger, httpRequest, googleDrive, gmail. Event-driven trigger; 25 nodes.
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