This workflow corresponds to n8n.io template #12141 — we link there as the canonical source.
This workflow follows the Googlegemini → HTTP Request 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": "qWVPl30dEBIDO7HE",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Daily News Digest: RSS & Youtube to Telegram (Text + Audio) with Gemini & OpenAI",
"tags": [],
"nodes": [
{
"id": "a6b1fbdc-2d05-49b4-bf26-3c1dd6cf2d92",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-496,
-96
],
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 7
}
]
}
},
"typeVersion": 1.3
},
{
"id": "069ba213-9d37-4262-b79f-de447bdcf064",
"name": "RSS Feed Read (TechCrunch)",
"type": "n8n-nodes-base.rssFeedRead",
"onError": "continueRegularOutput",
"position": [
-208,
-384
],
"parameters": {
"url": "https://techcrunch.com/feed/",
"options": {
"ignoreSSL": true
}
},
"retryOnFail": false,
"typeVersion": 1.2
},
{
"id": "4be9bbdd-8e45-4eab-bd19-a9d640730edc",
"name": "RSS Feed Read (The Verge)",
"type": "n8n-nodes-base.rssFeedRead",
"onError": "continueRegularOutput",
"position": [
-208,
-192
],
"parameters": {
"url": "https://www.theverge.com/rss/index.xml",
"options": {
"ignoreSSL": true
}
},
"retryOnFail": false,
"typeVersion": 1.2
},
{
"id": "d699416f-d446-4c93-8cb6-cd3a82aec355",
"name": "Merge - Tech (Append)",
"type": "n8n-nodes-base.merge",
"position": [
48,
-288
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "276f62c2-69a7-4f30-8fc0-c96d187720c5",
"name": "Code - Clean & Dedup (newsContext)",
"type": "n8n-nodes-base.code",
"position": [
928,
-96
],
"parameters": {
"jsCode": "// ===== CONFIG =====\nconst ONLY_LAST_HOURS = 36; // Filter news from the last X hours (set null/0 if not filtering)\nconst MAX_ITEMS = 60; // total number of items included in context\nconst MAX_TOTAL_CHARS = 28000; // Block token overshoot (optional)\nconst MAX_TITLE_CHARS = 160;\nconst MAX_DESC_CHARS = 260;\n\n// Mix ratio by source type (RSS vs YouTube)\n// Example 70/30 => MAX_ITEMS=60 => RSS ~42, YouTube ~18 (if there's a shortage, it will be automatically compensated)\nconst MIX_RATIO = {\n rss: 0.7,\n youtube: 0.3,\n};\n\n// Remove posts that are in the form of deals/prices/where to buy... (less clutter and less length)\nconst EXCLUDE_IF_TEXT_MATCH = [\n /\\bwhere to buy\\b/i,\n /\\bdeal\\b/i,\n /\\bsale\\b/i,\n /\\bpromo code\\b/i,\n /\\$\\d/i,\n /\\bblack friday\\b/i,\n /\\bcyber monday\\b/i,\n];\n\n// The Verge's signature phrase is often inserted.\nconst STRIP_PHRASES = [\n /read the full story at the verge\\.?/gi,\n /read our review\\.?/gi,\n];\n\n// ==================\n\nfunction decodeEntities(str) {\n str = String(str ?? '');\n\n const map = {\n ' ': ' ',\n '&': '&',\n '"': '\"',\n ''': \"'\",\n ''': \"'\",\n '<': '<',\n '>': '>',\n '’': '\u2019',\n '‘': '\u2018',\n '“': '\u201c',\n '”': '\u201d',\n '—': '\u2014',\n '–': '\u2013',\n '…': '\u2026',\n };\n\n str = str.replace(\n / |&|"|'|'|<|>|’|‘|“|”|—|–|…/g,\n (m) => map[m] ?? m\n );\n\n // numeric entities: ’ / ’\n str = str.replace(/&#(\\d+);/g, (_, n) => {\n const code = parseInt(n, 10);\n return Number.isFinite(code) ? String.fromCodePoint(code) : _;\n });\n str = str.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => {\n const code = parseInt(h, 16);\n return Number.isFinite(code) ? String.fromCodePoint(code) : _;\n });\n\n return str;\n}\n\nfunction stripHtml(s) {\n return decodeEntities(String(s ?? ''))\n .replace(/<[^>]*>/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction clip(s, n) {\n s = String(s ?? '').trim();\n if (!n || s.length <= n) return s;\n return s.slice(0, n - 1).trimEnd() + '\u2026';\n}\n\nfunction normTitle(s) {\n return stripHtml(s).toLowerCase().replace(/\\s+/g, ' ').trim();\n}\n\nfunction inferPublication(link, feedTitle) {\n const l = String(link ?? '');\n const ft = String(feedTitle ?? '');\n if (/techcrunch\\.com/i.test(l)) return 'TechCrunch';\n if (/theverge\\.com/i.test(l)) return 'The Verge';\n if (/vnexpress\\.net/i.test(l)) return 'VNExpress';\n if (/youtube\\.com|youtu\\.be/i.test(l)) return 'YouTube';\n if (/trends\\.google\\.com/i.test(l)) return 'Google Trends';\n if (ft) return stripHtml(ft);\n return 'RSS';\n}\n\nfunction inferSourceType(j) {\n // Prioritize fields set from upstream.\n if (j?.sourceType) return String(j.sourceType);\n\n const link = String(j?.link || j?.url || '');\n if (/youtube\\.com|youtu\\.be/i.test(link)) return 'youtube';\n if (/trends\\.google\\.com/i.test(link)) return 'trends';\n\n return 'rss';\n}\n\nfunction parseDateMaybe(v) {\n if (!v) return null;\n const d = new Date(v);\n return Number.isNaN(d.getTime()) ? null : d;\n}\n\nfunction matchesAny(reList, text) {\n const t = String(text ?? '');\n return reList.some((re) => re.test(t));\n}\n\nfunction getTs(a) {\n const d = a.publishedAt ? new Date(a.publishedAt) : null;\n const t = d && !Number.isNaN(d.getTime()) ? d.getTime() : 0;\n return t;\n}\n\n// time cutoff\nconst now = new Date();\nconst cutoff = ONLY_LAST_HOURS ? new Date(now.getTime() - ONLY_LAST_HOURS * 3600 * 1000) : null;\n\n// main\nconst seen = new Set();\nconst candidates = [];\n\nlet droppedDup = 0;\nlet droppedByTime = 0;\nlet droppedByExclude = 0;\n\nfor (const it of $input.all()) {\n const j = it.json || {};\n\n const title = stripHtml(j.title || j.Title || '');\n if (!title) continue;\n\n const link = stripHtml(j.link || j.url || '');\n const descRaw = stripHtml(j.description || j.content || j.snippet || '');\n const author = stripHtml(j.creator || j.author || j.dcCreator || '');\n\n // PublishedAt can be pubDate/isoDate/publishedAt...\n const publishedAtRaw =\n j.isoDate || j.pubDate || j.publishedAt || j.publishTime || j.date || '';\n const publishedAt = parseDateMaybe(publishedAtRaw);\n\n if (cutoff && publishedAt && publishedAt < cutoff) {\n droppedByTime++;\n continue;\n }\n\n // Filter out \"deals/junk\" based on aggregated text.\n const haystack = `${title}\\n${descRaw}\\n${link}`;\n if (matchesAny(EXCLUDE_IF_TEXT_MATCH, haystack)) {\n droppedByExclude++;\n continue;\n }\n\n // key dedup\n const key = link ? `L:${link}` : `T:${normTitle(title)}`;\n if (seen.has(key)) {\n droppedDup++;\n continue;\n }\n seen.add(key);\n\n // clean desc\n let desc = descRaw;\n for (const re of STRIP_PHRASES) desc = desc.replace(re, '').trim();\n desc = clip(desc, MAX_DESC_CHARS);\n\n const publication = inferPublication(link, j.feedTitle);\n const sourceType = inferSourceType(j);\n const titleClipped = clip(title, MAX_TITLE_CHARS);\n\n candidates.push({\n title: titleClipped,\n description: desc,\n link,\n publication,\n author,\n publishedAt: publishedAt\n ? publishedAt.toISOString()\n : (publishedAtRaw ? String(publishedAtRaw) : ''),\n sourceType,\n });\n}\n\n// ===== MIX by ratio (70/30) =====\n\n// Group by sourceType\nconst rssList = [];\nconst ytList = [];\nconst otherList = [];\n\nfor (const a of candidates) {\n if (a.sourceType === 'youtube') ytList.push(a);\n else if (a.sourceType === 'rss') rssList.push(a);\n else otherList.push(a);\n}\n\n// Sort each group from newest to first (this is up to you; if you want to keep the input order, remove the sort).\nrssList.sort((a, b) => getTs(b) - getTs(a));\nytList.sort((a, b) => getTs(b) - getTs(a));\notherList.sort((a, b) => getTs(b) - getTs(a));\n\n// Quotas\nlet rssQuota = Math.round(MAX_ITEMS * (MIX_RATIO.rss ?? 0.7));\nrssQuota = Math.max(0, Math.min(MAX_ITEMS, rssQuota));\nlet ytQuota = MAX_ITEMS - rssQuota;\n\n// Pick quota\nlet pickedRss = rssList.slice(0, rssQuota);\nlet pickedYt = ytList.slice(0, ytQuota);\n\n// Fill n\u1ebfu 1 b\u00ean thi\u1ebfu\nif (pickedYt.length < ytQuota) {\n const need = ytQuota - pickedYt.length;\n pickedRss = pickedRss.concat(rssList.slice(rssQuota, rssQuota + need));\n}\nif (pickedRss.length < rssQuota) {\n const need = rssQuota - pickedRss.length;\n pickedYt = pickedYt.concat(ytList.slice(ytQuota, ytQuota + need));\n}\n\n// Alternate mixing according to the 70/30 pattern (7 RSS, 3 YT).\nconst w = 10;\nconst wR = Math.max(0, Math.min(10, Math.round((MIX_RATIO.rss ?? 0.7) * w)));\nconst wY = w - wR;\nconst pattern = [\n ...Array(wR).fill('rss'),\n ...Array(wY).fill('youtube'),\n];\n\nconst queueR = [...pickedRss];\nconst queueY = [...pickedYt];\n\nconst articles = [];\nlet pi = 0;\n\nwhile (articles.length < MAX_ITEMS && (queueR.length || queueY.length)) {\n const want = pattern[pi % pattern.length];\n pi++;\n\n if (want === 'youtube' && queueY.length) articles.push(queueY.shift());\n else if (want === 'rss' && queueR.length) articles.push(queueR.shift());\n else if (queueR.length) articles.push(queueR.shift());\n else if (queueY.length) articles.push(queueY.shift());\n}\n\n// (Optional) If still missing, fill with otherList\nif (articles.length < MAX_ITEMS && otherList.length) {\n articles.push(...otherList.slice(0, MAX_ITEMS - articles.length));\n}\n\n// ===== build newsContext (for Gemini) =====\nlet newsContext = '';\nfor (const a of articles) {\n // format: [Publication | Author | sourceType] Title: Desc (Link)\n // (You can remove sourceType from the head if you don't want it to be visible)\n const headParts = [a.publication];\n if (a.author) headParts.push(a.author);\n if (a.sourceType) headParts.push(a.sourceType);\n\n const head = `[${headParts.join(' | ')}]`;\n\n const line =\n `${head} ${a.title}` +\n `${a.description ? `: ${a.description}` : ''}` +\n `${a.link ? ` (${a.link})` : ''}\\n`;\n\n if ((newsContext.length + line.length) > MAX_TOTAL_CHARS) break;\n newsContext += line;\n}\n\nreturn [{\n json: {\n newsContext: newsContext.trim(),\n articles,\n stats: {\n inputCount: $input.all().length,\n candidatesCount: candidates.length,\n rssCount: rssList.length,\n youtubeCount: ytList.length,\n outputCount: articles.length,\n droppedDup,\n droppedByTime,\n droppedByExclude,\n cutoffISO: cutoff ? cutoff.toISOString() : null,\n ratio: MIX_RATIO,\n quota: {\n rssQuota,\n ytQuota,\n pickedRss: pickedRss.length,\n pickedYt: pickedYt.length,\n }\n }\n }\n}];"
},
"typeVersion": 2
},
{
"id": "fab7cfbf-1b3f-4579-9111-239af2f8da4e",
"name": "YouTube - Search (Latest 24h)",
"type": "n8n-nodes-base.youTube",
"position": [
-208,
224
],
"parameters": {
"limit": 15,
"filters": {
"q": "AI, Tech News, Artificial Intelligence",
"regionCode": "US",
"publishedAfter": "={{ $now.setZone('America/New_York').minus({ hours: 24 }).toISO() }}"
},
"options": {
"order": "date"
},
"resource": "video"
},
"credentials": {
"youTubeOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "2531e648-ab2a-4dc7-b130-7f30872843ac",
"name": "Merge - Add YouTube (Append)",
"type": "n8n-nodes-base.merge",
"position": [
720,
-96
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "cef6ff91-7995-4e39-8a65-1ec9b38a3b1a",
"name": "Code - YouTube Normalize + Filter",
"type": "n8n-nodes-base.code",
"position": [
272,
224
],
"parameters": {
"jsCode": "// ====== CONFIG ======\nconst ONLY_LAST_HOURS = 24;\n\n// Keyword \"Positive\" (Must match at least one)\nconst INCLUDE_REGEX = [\n /\\bai\\b/i,\n /\\bartificial intelligence\\b/i,\n /\\btech\\b/i,\n /\\bllm\\b/i,\n /\\bchatgpt\\b/i,\n /\\bgemini\\b/i,\n /\\bopenai\\b/i,\n /\\bautomation\\b/i,\n /\\brobotics\\b/i,\n /\\bmachine learning\\b/i,\n];\n\n// Keyword \"Negative\" (If matched, exclude item)\nconst EXCLUDE_REGEX = [\n /\\bcrypto\\b/i,\n /\\bbitcoin\\b/i,\n /\\bcoin\\b/i,\n /\\binvestment\\b/i,\n /\\bprice prediction\\b/i,\n /\\bxrp\\b/i,\n /\\bgiveaway\\b/i,\n /\\blive stream\\b/i, // Often low quality or just looping music\n];\n\n// ======================================\n\nconst now = new Date();\nconst cutoff = new Date(now.getTime() - ONLY_LAST_HOURS * 60 * 60 * 1000);\n\nfunction pickThumb(snippet) {\n return (\n snippet?.thumbnails?.high?.url ||\n snippet?.thumbnails?.medium?.url ||\n snippet?.thumbnails?.default?.url ||\n ''\n );\n}\n\nfunction matchAny(regexList, text) {\n return regexList.some((re) => re.test(text));\n}\n\nconst out = [];\n\nfor (const item of $input.all()) {\n const j = item.json;\n\n const videoId = j?.id?.videoId || '';\n const snippet = j?.snippet || {};\n\n const title = snippet?.title || '';\n const description = snippet?.description || '';\n const channelTitle = snippet?.channelTitle || '';\n const publishedAt = snippet?.publishedAt || snippet?.publishTime || '';\n\n if (!videoId || !title) continue;\n\n // Time filter (if publishedAt parse is possible)\n if (publishedAt) {\n const d = new Date(publishedAt);\n if (!Number.isNaN(d.getTime()) && d < cutoff) continue;\n }\n\n // Content filter\n const haystack = `${title}\\n${description}\\n${channelTitle}`;\n if (!matchAny(INCLUDE_REGEX, haystack)) continue;\n if (matchAny(EXCLUDE_REGEX, haystack)) continue;\n\n out.push({\n json: {\n title,\n link: `https://www.youtube.com/watch?v=${videoId}`,\n description,\n source: `YouTube - ${channelTitle || 'Unknown'}`,\n publishedAt,\n thumbnail: pickThumb(snippet),\n channelTitle,\n videoId,\n }\n });\n}\n\nreturn out;"
},
"typeVersion": 2
},
{
"id": "1c33a22a-aa9f-4cd2-bc01-6c033e15ea05",
"name": "Set - Mark RSS",
"type": "n8n-nodes-base.set",
"position": [
480,
-112
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "2ef4eb06-d6fb-4ccb-a5c7-24e10fd4734e",
"name": "sourceType",
"type": "string",
"value": "=rss"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "27f323b2-af06-4321-a498-fbe4e46fe432",
"name": "Set - Mark YouTube",
"type": "n8n-nodes-base.set",
"position": [
496,
224
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "7fddd203-3144-4d14-af88-6608b0b5778c",
"name": "sourceType",
"type": "string",
"value": "=youtube"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "a84e7155-1591-4000-94a2-2982a116f3c3",
"name": "Google Gemini Chat (AI Analysis)",
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"position": [
1168,
-96
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "models/gemini-2.5-flash",
"cachedResultName": "models/gemini-2.5-flash"
},
"options": {
"temperature": 0.3,
"systemMessage": "=You are a professional News Editor. You must follow instructions exactly."
},
"messages": {
"values": [
{
"content": "=Return ONLY valid JSON. No markdown fences. No extra text.\nRead this context:\n{{$json.newsContext}}\n\nTasks:\n1) Select top 10 most important stories (mix of Tech & Global News).\n2) Write a morning briefing in English.\n\nOutput strict JSON (no markdown fences, no extra text):\n{\n \"title\": \"{{$now.setZone('America/New_York').toFormat('yyyy-LL-dd')}} Morning Briefing\",\n \"telegram_body\": \"Use ONLY HTML: <b>, <i>, <a href='url'>. NO Markdown symbols (*, _, [). Keep under 4000 chars.\",\n \"audio_script\": \"Natural, energetic English text for speech. No special chars like < > [ ] * _. Avoid URLs.\"\n}\n\nRules:\n- telegram_body must be valid Telegram HTML.\n- Use bullet points with '\u2022' not '-' if you want lists.\n- Always include links via <a href='...'>...</a>."
}
]
},
"simplify": false,
"jsonOutput": true
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "b5e21b3a-2f50-4437-aeb9-297d685319e1",
"name": "Code - Robust Parser (Gemini JSON)",
"type": "n8n-nodes-base.code",
"position": [
1488,
-96
],
"parameters": {
"jsCode": "const item = $input.first().json;\n\n// 1) Get raw text from Gemini\nconst raw = (item?.candidates?.[0]?.content?.parts || [])\n .map(p => p?.text ?? '')\n .join('')\n .trim();\n\nif (!raw) throw new Error('Gemini output error: candidates[0].content.parts[].text not found');\n\n// 2) Cut JSON objects (to avoid any extra text).\nconst first = raw.indexOf('{');\nconst last = raw.lastIndexOf('}');\nif (first === -1 || last === -1 || last <= first) {\n throw new Error('Not found JSON object in output Gemini');\n}\nconst jsonStr = raw.slice(first, last + 1);\n\n// 3) parse\nlet parsed;\ntry {\n parsed = JSON.parse(jsonStr);\n} catch (e) {\n throw new Error('JSON.parse failed. JSON string was:\\n' + jsonStr);\n}\n\n// 4) CLEAN HTML FOR TELEGRAM\nlet telegram_body = String(parsed.telegram_body ?? '').trim();\n\n// Fix 1: Replace the tags <br>, <br/>, <br /> with the newline character \\n\ntelegram_body = telegram_body.replace(/<br\\s*\\/?>/gi, '\\n');\n\n// Fix 2: Replace the <p> tag with a blank line and </p> with a line break (as a precaution).\ntelegram_body = telegram_body.replace(/<p>/gi, '').replace(/<\\/p>/gi, '\\n');\n\n// Fix 3: Normalize href (change ' to \" if applicable)\ntelegram_body = telegram_body.replace(/href='([^']+)'/g, 'href=\"$1\"');\n\nconst audio_script = String(parsed.audio_script ?? '').trim();\nconst title = String(parsed.title ?? '').trim();\n\nif (!telegram_body) throw new Error('Missing telegram_body');\nif (!audio_script) throw new Error('Missing audio_script');\n\nreturn [{\n json: { title, telegram_body, audio_script }\n}];"
},
"typeVersion": 2
},
{
"id": "acecf9dc-e3f6-4e55-b69c-9f45b6b6dbcd",
"name": "Telegram - Send Briefing Text",
"type": "n8n-nodes-base.telegram",
"position": [
2016,
-96
],
"parameters": {
"text": "={{ $json.telegram_body }}",
"chatId": "INPUT YOUR CHAT ID WITH BOT",
"additionalFields": {
"parse_mode": "HTML"
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "a8477a99-b8a2-412b-8413-2ab509743f06",
"name": "HTTP - OpenAI TTS (speech)",
"type": "n8n-nodes-base.httpRequest",
"position": [
1248,
224
],
"parameters": {
"url": "https://api.openai.com/v1/audio/speech",
"method": "POST",
"options": {
"response": {
"response": {
"responseFormat": "file",
"outputPropertyName": "audio"
}
}
},
"jsonBody": "={{ $json.tts }}",
"sendBody": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "openAiApi"
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.3
},
{
"id": "98377e3d-ec07-4b77-ad1a-fc7dc3106ffe",
"name": "Telegram - Send Briefing Audio",
"type": "n8n-nodes-base.telegram",
"position": [
2016,
224
],
"parameters": {
"chatId": "INPUT YOUR CHAT ID WITH BOT",
"operation": "sendAudio",
"binaryData": true,
"additionalFields": {
"caption": "={{ $json.title }}"
},
"binaryPropertyName": "audio"
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "e6b67eae-8d2f-45e4-aab3-c7f3e1d29dfc",
"name": "Code - Fix Audio Meta (filename + mime)",
"type": "n8n-nodes-base.code",
"position": [
1744,
224
],
"parameters": {
"jsCode": "const item = $input.first();\nif (!item.binary?.audio) {\n throw new Error('Not found binary.audio from node HTTP TTS');\n}\n\nconst desiredNameFromJson = item.json.fileName || item.json.filename || item.json.audioFileName;\nlet desiredName = desiredNameFromJson || 'morning_briefing.mp3';\n\n// Make sure to add the .mp3 extension.\nif (!desiredName.toLowerCase().endsWith('.mp3')) desiredName += '.mp3';\n\n// Reset metadata for the binary.\nitem.binary.audio.fileName = desiredName;\n\n// mimeType: OpenAI usually returns audio/mpeg (correct). Telegram is also OK.\n// But if you want \"standard mp3\", then audio/mpeg is still the best option.\nitem.binary.audio.mimeType = 'audio/mpeg';\n\n// (Optional) some flows/nodes can read file extensions.\nitem.binary.audio.fileExtension = 'mp3';\n\nreturn [item];"
},
"typeVersion": 2
},
{
"id": "62b3256d-d14b-43c8-a9d5-a0daa724d841",
"name": "Code - Build TTS Payload (OpenAI)",
"type": "n8n-nodes-base.code",
"position": [
1744,
32
],
"parameters": {
"jsCode": "const { title, telegram_body, audio_script } = $input.first().json;\n\n// Guard + API-based restrictions (safe)\nlet input = String(audio_script || '').trim();\nif (!input) throw new Error('audio_script r\u1ed7ng');\n\n// OpenAI TTS input is limited to 4096 characters -> temporarily truncated to avoid failure.\nif (input.length > 4096) {\n input = input.slice(0, 4050).trimEnd() + '...';\n}\n\nconst now = new Date();\nconst yyyy = now.getFullYear();\nconst mm = String(now.getMonth() + 1).padStart(2, '0');\nconst dd = String(now.getDate()).padStart(2, '0');\n\nreturn [{\n json: {\n title,\n telegram_body,\n fileName: `morning_briefing_${yyyy}-${mm}-${dd}.mp3`,\n tts: {\n model: 'gpt-4o-mini-tts',\n voice: 'alloy',\n input,\n instructions: 'Clear, professional English broadcast voice.',\n response_format: 'mp3',\n speed: 1.05,\n },\n }\n}];"
},
"typeVersion": 2
},
{
"id": "6fb4d594-8bbb-42b8-aff2-24e74dec3f96",
"name": "RSS Feed Read (BBC World)",
"type": "n8n-nodes-base.rssFeedRead",
"onError": "continueRegularOutput",
"position": [
-208,
32
],
"parameters": {
"url": "http://feeds.bbci.co.uk/news/world/rss.xml",
"options": {
"ignoreSSL": true
}
},
"retryOnFail": false,
"typeVersion": 1.2
},
{
"id": "d4caa68b-01fd-4c6c-8c0c-841c49ab99db",
"name": "Merge - Add BBC World (Append)",
"type": "n8n-nodes-base.merge",
"position": [
272,
-112
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "c8afa7c8-7490-4e81-9a81-0192884b1459",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1088,
-304
],
"parameters": {
"width": 512,
"height": 704,
"content": "## Daily News Digest: Text & Audio Briefing\nThis workflow automates your morning news routine by aggregating content from RSS feeds and YouTube, curating it with AI, and delivering a text summary plus a podcast-style audio file to Telegram.\n\n### How it works\n1. **Aggregates:** Fetches latest news from your chosen RSS sources (e.g., BBC, TechCrunch) and YouTube video transcripts.\n2. **Processes:** Cleans HTML, deduplicates stories, and filters irrelevant content via Code.\n3. **Analyzes:** Google Gemini acts as an Editor-in-Chief to select the most important stories and rewrite them into a briefing.\n4. **Delivers:** Generates a professional audio file using OpenAI TTS and sends it to Telegram along with an HTML-formatted text summary.\n\n### Setup steps\n1. **Credentials:** Configure credentials for **Google Gemini**, **OpenAI**, **Telegram API**, and **YouTube Data API**.\n2. **Environment Variables:** Define `TELEGRAM_CHAT_ID` in your n8n variables (or hardcode it in the Telegram nodes for testing).\n3. **Run:** Activate the Schedule Trigger to run daily at 7:00 AM \n\n### Customization tips\n* **Input Sources:** The provided RSS feeds are examples. Feel free to replace them with your favorite news sources in the **Input Section**.\n* **AI Logic:** Open the **Google Gemini** node to adjust the prompt. You can change the number of stories (e.g., from 10 to 15), the summary length, or the personality of the audio script.\n* **YouTube Filters:** Modify keywords in the **YouTube - Search (Latest 24h)** node to track specific topics like \"AI\", \"Finance\", or \"Sports\"."
},
"typeVersion": 1
},
{
"id": "3eb9ba99-6666-4969-8237-e3bf9b1d24e5",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-544,
-480
],
"parameters": {
"color": 7,
"width": 496,
"height": 896,
"content": "## 1. Input Sources\nFetches data from multiple sources.\n*Tip: Replace these RSS URLs and YouTube keywords to match your personal interests.*"
},
"typeVersion": 1
},
{
"id": "f7c33d4b-2d2a-45c5-94db-db784daff802",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
16,
-400
],
"parameters": {
"color": 7,
"width": 1040,
"height": 816,
"content": "## 2. Data Preparation\nCleans HTML tags, deduplicates repeated articles, and mixes sources based on a defined ratio."
},
"typeVersion": 1
},
{
"id": "48ba11b1-9cc6-4aba-8f30-846bafd213de",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
1120,
-224
],
"parameters": {
"color": 7,
"width": 512,
"height": 640,
"content": "## 3. AI Analysis\nGemini curates the top stories.\n*Tip: Edit the Prompt in the Gemini node to change the item count (e.g., 10 vs 20) or max character limit.*"
},
"typeVersion": 1
},
{
"id": "9a59bebf-0f1b-479f-a594-e8301d51fd42",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
1696,
-224
],
"parameters": {
"color": 7,
"width": 512,
"height": 640,
"content": "## 4. Audio Generation & Delivery\nConverts the script to speech (MP3) via OpenAI and delivers the full briefing to Telegram."
},
"typeVersion": 1
},
{
"id": "623a44a1-7ef5-4465-b789-a9f34d54fbab",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
1968,
80
],
"parameters": {
"color": 3,
"width": 208,
"height": 128,
"content": "### \u26a0\ufe0f Check Chat ID\nEnsure `TELEGRAM_CHAT_ID` is set in your Global Variables, or the message will fail to send."
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"timezone": "Asia/Ho_Chi_Minh",
"callerPolicy": "workflowsFromSameOwner",
"timeSavedMode": "fixed",
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "4f4ad20c-a577-4d6a-8cac-3c76b4b517c5",
"connections": {
"Set - Mark RSS": {
"main": [
[
{
"node": "Merge - Add YouTube (Append)",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "RSS Feed Read (TechCrunch)",
"type": "main",
"index": 0
},
{
"node": "RSS Feed Read (The Verge)",
"type": "main",
"index": 0
},
{
"node": "RSS Feed Read (BBC World)",
"type": "main",
"index": 0
},
{
"node": "YouTube - Search (Latest 24h)",
"type": "main",
"index": 0
}
]
]
},
"Set - Mark YouTube": {
"main": [
[
{
"node": "Merge - Add YouTube (Append)",
"type": "main",
"index": 1
}
]
]
},
"Merge - Tech (Append)": {
"main": [
[
{
"node": "Merge - Add BBC World (Append)",
"type": "main",
"index": 0
}
]
]
},
"RSS Feed Read (BBC World)": {
"main": [
[
{
"node": "Merge - Add BBC World (Append)",
"type": "main",
"index": 1
}
]
]
},
"RSS Feed Read (The Verge)": {
"main": [
[
{
"node": "Merge - Tech (Append)",
"type": "main",
"index": 1
}
]
]
},
"HTTP - OpenAI TTS (speech)": {
"main": [
[
{
"node": "Code - Fix Audio Meta (filename + mime)",
"type": "main",
"index": 0
}
]
]
},
"RSS Feed Read (TechCrunch)": {
"main": [
[
{
"node": "Merge - Tech (Append)",
"type": "main",
"index": 0
}
]
]
},
"Merge - Add YouTube (Append)": {
"main": [
[
{
"node": "Code - Clean & Dedup (newsContext)",
"type": "main",
"index": 0
}
]
]
},
"Telegram - Send Briefing Text": {
"main": [
[]
]
},
"YouTube - Search (Latest 24h)": {
"main": [
[
{
"node": "Code - YouTube Normalize + Filter",
"type": "main",
"index": 0
}
]
]
},
"Merge - Add BBC World (Append)": {
"main": [
[
{
"node": "Set - Mark RSS",
"type": "main",
"index": 0
}
]
]
},
"Google Gemini Chat (AI Analysis)": {
"main": [
[
{
"node": "Code - Robust Parser (Gemini JSON)",
"type": "main",
"index": 0
}
]
]
},
"Code - Build TTS Payload (OpenAI)": {
"main": [
[
{
"node": "HTTP - OpenAI TTS (speech)",
"type": "main",
"index": 0
}
]
]
},
"Code - YouTube Normalize + Filter": {
"main": [
[
{
"node": "Set - Mark YouTube",
"type": "main",
"index": 0
}
]
]
},
"Code - Clean & Dedup (newsContext)": {
"main": [
[
{
"node": "Google Gemini Chat (AI Analysis)",
"type": "main",
"index": 0
}
]
]
},
"Code - Robust Parser (Gemini JSON)": {
"main": [
[
{
"node": "Code - Build TTS Payload (OpenAI)",
"type": "main",
"index": 0
},
{
"node": "Telegram - Send Briefing Text",
"type": "main",
"index": 0
}
]
]
},
"Code - Fix Audio Meta (filename + mime)": {
"main": [
[
{
"node": "Telegram - Send Briefing Audio",
"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.
googlePalmApiopenAiApitelegramApiyouTubeOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Start your day with a personalized news podcast delivered directly to your Telegram. This workflow helps you stay informed without scrolling through endless feeds. It automatically collects news from your favorite websites and YouTube channels, filters out the noise, and uses AI…
Source: https://n8n.io/workflows/12141/ — 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.
This workflow creates a daily “n8n News Radar” briefing: Pulls the latest n8n ecosystem updates from Blog, Community, GitHub Releases, and Reddit. Filters to the last 24 hours + keyword relevance. Use
AI Institutional Stock Valuation Engine with Risk Scoring & Scenario Targets
Overview This is a production-grade, fully automated stock analysis system built entirely in n8n. It combines institutional-level financial analysis, dual AI model consensus, and a self-improving back
A professional AI equity analysis automation built on n8n that transforms structured financial data and real-time news into disciplined, risk-adjusted price targets and actionable BUY/HOLD/SELL signal
Takes a product image from Google Sheets, adds frozen effect with Gemini, generates ASMR video with Veo3, writes captions with GPT-4o, and posts to 4 platforms automatically. Schedule trigger picks fi