This workflow follows the Emailsend → Postgres 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 →
{
"name": "News Digest Bot - Multi-User (Postgres)",
"nodes": [
{
"parameters": {
"pollTimes": {
"item": [
{
"mode": "everyMinute"
}
]
},
"additionalFields": {}
},
"id": "telegram-trigger",
"name": "Telegram Polling Trigger",
"type": "n8n-nodes-base.telegramTrigger",
"typeVersion": 1.1,
"position": [
240,
300
],
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const msg = $input.first().json;\nconst update = msg;\nlet chatId, userId, text, firstName, isCallback, callbackData, messageId;\n\nif (update.callback_query) {\n isCallback = true;\n chatId = update.callback_query.message.chat.id;\n userId = update.callback_query.from.id;\n firstName = update.callback_query.from.first_name;\n callbackData = update.callback_query.data;\n messageId = update.callback_query.message.message_id;\n text = callbackData;\n} else if (update.message) {\n isCallback = false;\n chatId = update.message.chat.id;\n userId = update.message.from.id;\n firstName = update.message.from.first_name;\n text = update.message.text || '';\n messageId = update.message.message_id;\n callbackData = null;\n} else {\n return [];\n}\n\nreturn [{ json: {\n chatId: String(chatId),\n userId: String(userId),\n firstName,\n text: text.trim(),\n isCallback,\n callbackData,\n messageId,\n raw: update\n}}];"
},
"id": "parse-update",
"name": "Parse Telegram Update",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
460,
300
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.text }}",
"rightValue": "/start",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": false
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.text }}",
"rightValue": "/help",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": false
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.text }}",
"rightValue": "/newtopic",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": false
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.text }}",
"rightValue": "/mytopics",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": false
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.text }}",
"rightValue": "/digest",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": false
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.text }}",
"rightValue": "/addfeed",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": false
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.text }}",
"rightValue": "/settings",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": false
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.text }}",
"rightValue": "/unsubscribe",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": false
}
]
},
"options": {}
},
"id": "router-commands",
"name": "Router Comandi",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [
680,
300
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO users (user_id, chat_id, first_name)\nVALUES ($1, $2, $3)\nON CONFLICT (user_id) DO UPDATE SET\n chat_id = EXCLUDED.chat_id,\n first_name = EXCLUDED.first_name,\n updated_at = NOW();",
"queryParams": "={{ [$('Parse Telegram Update').item.json.userId, $('Parse Telegram Update').item.json.chatId, $('Parse Telegram Update').item.json.firstName] }}"
},
"id": "upsert-user",
"name": "Upsert User",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
900,
180
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"chatId": "={{ $('Parse Telegram Update').item.json.chatId }}",
"text": "\ud83d\udc4b Benvenuto *{{ $('Parse Telegram Update').item.json.firstName }}*!\\n\\nSono il tuo *News Digest Bot*. Raccoglio notizie su argomenti personalizzati e te le invio via Telegram o email.\\n\\n\ud83d\udccb *Comandi disponibili:*\\n/newtopic \u2014 Crea un nuovo argomento\\n/mytopics \u2014 Vedi i tuoi argomenti attivi\\n/digest \u2014 Richiedi un digest al volo\\n/addfeed \u2014 Aggiungi un feed RSS personalizzato\\n/settings \u2014 Modifica email e scheduling\\n/help \u2014 Guida completa\\n\\n\ud83d\ude80 Inizia con /newtopic per creare il tuo primo argomento!",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"id": "send-welcome",
"name": "Send Welcome",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1120,
60
],
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"chatId": "={{ $('Parse Telegram Update').item.json.chatId }}",
"text": "\ud83d\udcd6 *Guida completa News Digest Bot*\\n\\n*Creare un argomento (/newtopic)*\\nTi chieder\u00f2 di inviare un messaggio nel formato guidato con:\\n\u2014 Nome argomento\\n\u2014 Keywords di ricerca\\n\u2014 Frequenza (none/daily/weekly)\\n\u2014 Orario e giorno\\n\u2014 Canale (telegram/email/both)\\n\\n*Argomenti AI speciali:*\\nSe crei un topic con keywords AI, LLM, machine learning ecc., attivo automaticamente i feed di HuggingFace, OpenAI Blog, The Batch, Anthropic, GitHub Trending.\\n\\n*Digest on-demand (/digest)*\\nRicevi subito le ultime notizie su un tuo topic.\\n\\n*Aggiungere feed RSS (/addfeed)*\\nPuoi aggiungere feed RSS custom a qualsiasi topic.\\n\\n*Impostazioni (/settings)*\\nModifica email, scheduling, elimina topic.",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"id": "send-help",
"name": "Send Help",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1120,
180
],
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"chatId": "={{ $('Parse Telegram Update').item.json.chatId }}",
"text": "\ud83c\udd95 *Creazione Nuovo Topic*\\n\\nInviami un messaggio con questo formato esatto:\\n\\n```\\nNOME: [nome argomento]\\nKEYWORDS: [keyword1, keyword2, keyword3]\\nSCHEDULE: [none/daily/weekly]\\nORA: [HH:MM]\\nGIORNO: [lunedi/martedi/mercoledi/giovedi/venerdi/sabato/domenica]\\nDELIVERY: [telegram/email/both]\\nEMAIL: [tua@email.com]\\n```\\n\\n*Esempio:*\\n```\\nNOME: Intelligenza Artificiale\\nKEYWORDS: AI, LLM, machine learning, GPT, Claude\\nSCHEDULE: daily\\nORA: 08:00\\nGIORNO: lunedi\\nDELIVERY: both\\nEMAIL: mario@gmail.com\\n```\\n\\n_GIORNO ed EMAIL sono opzionali in base a SCHEDULE e DELIVERY scelti._",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"id": "ask-topic-form",
"name": "Ask Topic Form",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1120,
300
],
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT t.id, t.name, t.keywords, t.schedule, t.schedule_time, t.schedule_day, t.delivery, u.chat_id, u.email\nFROM topics t\nJOIN users u ON t.user_id = u.user_id\nWHERE t.user_id = $1 AND t.active = TRUE\nORDER BY t.created_at DESC;",
"queryParams": "={{ [$('Parse Telegram Update').item.json.userId] }}"
},
"id": "get-my-topics",
"name": "Get My Topics",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
900,
420
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const topics = $input.all();\nconst chatId = $('Parse Telegram Update').first().json.chatId;\n\nif (!topics || topics.length === 0 || !topics[0].json.id) {\n return [{ json: {\n chatId,\n text: '\ud83d\udced Non hai ancora nessun argomento.\\n\\nUsa /newtopic per crearne uno!',\n replyMarkup: null\n }}];\n}\n\nlet msg = '\ud83d\udccb *I tuoi argomenti attivi:*\\n\\n';\nconst buttons = [];\n\ntopics.forEach((t, i) => {\n const topic = t.json;\n const schedule = topic.schedule === 'none' ? 'Solo on-demand' :\n topic.schedule === 'daily' ? `Giornaliero alle ${topic.schedule_time}` :\n `Settimanale (${topic.schedule_day}) alle ${topic.schedule_time}`;\n const delivery = topic.delivery === 'both' ? 'Telegram + Email' : topic.delivery;\n msg += `*${i+1}. ${topic.name}*\\n`;\n msg += ` \ud83d\udd11 ${topic.keywords}\\n`;\n msg += ` \u23f0 ${schedule} \u00b7 \ud83d\udce8 ${delivery}\\n\\n`;\n buttons.push([{ text: `\ud83d\udcf0 Digest \"${topic.name}\"`, callback_data: `digest:${topic.id}` }]);\n});\n\nmsg += '_Clicca su un argomento per un digest immediato._';\n\nreturn [{ json: {\n chatId,\n text: msg,\n replyMarkup: JSON.stringify({ inline_keyboard: buttons })\n}}];"
},
"id": "format-topics-list",
"name": "Format Topics List",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1120,
420
]
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.text }}",
"additionalFields": {
"parse_mode": "Markdown",
"reply_markup": "={{ $json.replyMarkup }}"
}
},
"id": "send-topics-list",
"name": "Send Topics List",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1340,
420
],
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT t.id, t.name FROM topics t WHERE t.user_id = $1 AND t.active = TRUE ORDER BY t.name;",
"queryParams": "={{ [$('Parse Telegram Update').item.json.userId] }}"
},
"id": "get-topics-for-digest",
"name": "Get Topics for Digest Picker",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
900,
540
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const topics = $input.all();\nconst chatId = $('Parse Telegram Update').first().json.chatId;\n\nif (!topics || topics.length === 0 || !topics[0].json.id) {\n return [{ json: {\n chatId,\n text: '\ud83d\udced Nessun argomento trovato. Crea prima un topic con /newtopic',\n buttons: null\n }}];\n}\n\nconst buttons = topics.map(t => ([{\n text: t.json.name,\n callback_data: `digest:${t.json.id}`\n}]));\n\nreturn [{ json: {\n chatId,\n text: '\ud83d\udcf0 Su quale argomento vuoi un digest immediato?',\n buttons: JSON.stringify({ inline_keyboard: buttons })\n}}];"
},
"id": "ask-which-digest",
"name": "Ask Which Digest",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1120,
540
]
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.text }}",
"additionalFields": {
"reply_markup": "={{ $json.buttons }}"
}
},
"id": "send-digest-picker",
"name": "Send Digest Picker",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1340,
540
],
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const data = $input.first().json;\nconst isCallback = data.isCallback;\nconst callbackData = data.callbackData || '';\nconst chatId = data.chatId;\n\nif (isCallback && callbackData.startsWith('digest:')) {\n const topicId = callbackData.split(':')[1];\n return [{ json: { ...data, action: 'digest', value: topicId } }];\n}\n\nif (isCallback && callbackData.startsWith('confirm_unsubscribe:')) {\n return [{ json: { ...data, action: 'confirm_unsubscribe', value: 'all' } }];\n}\n\nif (isCallback && callbackData.startsWith('cancel:')) {\n return [{ json: { ...data, action: 'cancel', value: callbackData.split(':')[1] } }];\n}\n\n// Testo libero: potrebbe essere la risposta al form /newtopic\nif (!isCallback && data.text && data.text.includes('NOME:') && data.text.includes('KEYWORDS:')) {\n return [{ json: { ...data, action: 'submit_topic_form', value: data.text } }];\n}\n\nreturn [{ json: { ...data, action: 'unknown', value: data.text } }];"
},
"id": "parse-callback",
"name": "Parse Callback / Free Text",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1120,
660
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "digest",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": false
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "submit_topic_form",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": false
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "confirm_unsubscribe",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": false
}
]
},
"options": {}
},
"id": "router-callbacks",
"name": "Router Callbacks",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [
1340,
660
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT\n t.id, t.name, t.keywords, t.schedule, t.schedule_time,\n t.schedule_day, t.delivery, t.user_id,\n u.chat_id, u.email,\n STRING_AGG(cf.feed_url, '|||') AS custom_feeds\nFROM topics t\nJOIN users u ON t.user_id = u.user_id\nLEFT JOIN custom_feeds cf ON cf.topic_id = t.id\nWHERE t.id = $1\nGROUP BY t.id, t.name, t.keywords, t.schedule, t.schedule_time,\n t.schedule_day, t.delivery, t.user_id, u.chat_id, u.email;",
"queryParams": "={{ [$json.value] }}"
},
"id": "get-topic-by-id",
"name": "Get Topic by ID",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1560,
540
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const topic = $input.first().json;\nconst keywords = (topic.keywords || '').toLowerCase();\nconst name = (topic.name || '').toLowerCase();\n\nconst AI_KEYWORDS = ['ai','artificial intelligence','intelligenza artificiale',\n 'llm','machine learning','deep learning','neural','gpt','claude',\n 'gemini','openai','anthropic','huggingface','diffusion','transformer'];\n\nconst isAI = AI_KEYWORDS.some(k => keywords.includes(k) || name.includes(k));\n\nconst AI_FEEDS = [\n { url: 'https://huggingface.co/blog/feed.xml', name: 'HuggingFace Blog' },\n { url: 'https://openai.com/blog/rss.xml', name: 'OpenAI Blog' },\n { url: 'https://www.deeplearning.ai/the-batch/feed/', name: 'The Batch' },\n { url: 'https://www.anthropic.com/rss.xml', name: 'Anthropic News' },\n { url: 'https://github-trending-rss.vercel.app/repositories?language=python', name: 'GitHub Trending Python' },\n { url: 'https://github-trending-rss.vercel.app/repositories', name: 'GitHub Trending' },\n { url: 'https://www.technologyreview.com/feed/', name: 'MIT Tech Review' },\n { url: 'https://feeds.arstechnica.com/arstechnica/technology-lab', name: 'Ars Technica AI' }\n];\n\nconst GENERIC_FEEDS = [\n { url: 'https://feeds.arstechnica.com/arstechnica/technology-lab', name: 'Ars Technica Tech' },\n { url: 'https://feeds.feedburner.com/oreilly/radar', name: \"O'Reilly Radar\" },\n { url: 'https://rss.nytimes.com/services/xml/rss/nyt/Technology.xml', name: 'NYT Technology' }\n];\n\nconst customFeedsRaw = topic.custom_feeds || '';\nconst customFeeds = customFeedsRaw\n ? customFeedsRaw.split('|||').filter(u => u.trim()).map(url => ({ url: url.trim(), name: 'Custom Feed' }))\n : [];\n\nconst feedsToUse = isAI\n ? [...AI_FEEDS, ...customFeeds]\n : [...GENERIC_FEEDS, ...customFeeds];\n\nreturn [{ json: {\n ...topic,\n isAI,\n feedsToUse,\n searchQuery: topic.keywords\n}}];"
},
"id": "prepare-sources",
"name": "Prepare Sources",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1780,
540
]
},
{
"parameters": {
"fieldToSplitOut": "feedsToUse",
"options": {}
},
"id": "split-feeds",
"name": "Split Feeds",
"type": "n8n-nodes-base.splitOut",
"typeVersion": 1,
"position": [
2000,
540
]
},
{
"parameters": {
"url": "={{ $json.feedsToUse.url }}",
"options": {
"timeout": 10000
}
},
"id": "fetch-rss",
"name": "Fetch RSS Feed",
"type": "n8n-nodes-base.rssFeedRead",
"typeVersion": 1,
"position": [
2220,
540
],
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const items = $input.all();\nconst MAX_ARTICLES = 20;\nconst DAYS_BACK = 7;\nconst cutoff = new Date();\ncutoff.setDate(cutoff.getDate() - DAYS_BACK);\n\nconst articles = [];\nitems.forEach(item => {\n if (!item.json.title) return;\n const pub = new Date(item.json.pubDate || item.json.isoDate || item.json.date || Date.now());\n if (pub >= cutoff || !item.json.pubDate) {\n articles.push({\n title: item.json.title || 'Senza titolo',\n link: item.json.link || item.json.url || '',\n summary: (item.json.contentSnippet || item.json.summary || item.json.content || '').substring(0, 400),\n date: item.json.pubDate || item.json.isoDate || 'N/D',\n source: item.json.feedTitle || 'RSS'\n });\n }\n});\n\n// Deduplication per URL\nconst seen = new Set();\nconst unique = articles.filter(a => {\n if (!a.link || seen.has(a.link)) return false;\n seen.add(a.link);\n return true;\n});\n\nunique.sort((a, b) => new Date(b.date) - new Date(a.date));\nconst selected = unique.slice(0, MAX_ARTICLES);\n\nconst topicCtx = $('Prepare Sources').first().json;\n\nreturn [{ json: {\n articles: selected,\n topicName: topicCtx.name,\n keywords: topicCtx.keywords,\n chatId: topicCtx.chat_id,\n email: topicCtx.email,\n delivery: topicCtx.delivery,\n topicId: topicCtx.id,\n userId: topicCtx.user_id,\n articleCount: selected.length\n}}];"
},
"id": "aggregate-articles",
"name": "Aggregate & Deduplicate",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2440,
540
]
},
{
"parameters": {
"jsCode": "const data = $input.first().json;\nconst articles = data.articles;\n\nif (!articles || articles.length === 0) {\n return [{ json: {\n ...data,\n skipLLM: true,\n digestText: `\ud83d\udced *Nessuna notizia recente trovata per \"${data.topicName}\".*\\n\\nProva ad espandere le keywords o aggiungi feed RSS con /addfeed.`\n }}];\n}\n\nconst articlesText = articles.map((a, i) =>\n `${i+1}. **${a.title}** (${a.source}, ${a.date})\\n URL: ${a.link}\\n ${a.summary}`\n).join('\\n\\n');\n\nconst prompt = `Sei un giornalista specializzato che produce newsletter di qualit\u00e0.\nL'utente segue l'argomento: \"${data.topicName}\" con keywords: ${data.keywords}.\n\nHai a disposizione questi articoli recenti da fonti autorevoli:\n\n${articlesText}\n\nGenera un digest in italiano, stile newsletter professionale:\n\n\ud83d\uddde\ufe0f **DIGEST: ${data.topicName}**\n\ud83d\udcc5 ${new Date().toLocaleDateString('it-IT')}\n\n## Panoramica\n[2-3 frasi che sintetizzano il momento attuale dell'argomento]\n\n## Notizie Principali\n[Per ogni notizia rilevante (max 5): titolo linkato, 2-3 righe di analisi, impatto]\n\n## Da Tenere d'Occhio\n[1-2 trend emergenti o sviluppi interessanti]\n\n## Risorse\n[Lista link pi\u00f9 importanti con titolo breve]\n\nTono: informativo, preciso, coinvolgente. Solo notizie pertinenti alle keywords. Usa Markdown con grassetti ed emoji per leggibilit\u00e0 su Telegram.`;\n\nreturn [{ json: { ...data, skipLLM: false, llmPrompt: prompt } }];"
},
"id": "build-llm-prompt",
"name": "Build LLM Prompt",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2660,
540
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "cond_bool",
"leftValue": "={{ $json.skipLLM }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
"id": "check-skip-llm",
"name": "Skip LLM?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
2880,
540
]
},
{
"parameters": {
"model": "gpt-4.1",
"messages": {
"values": [
{
"content": "={{ $json.llmPrompt }}"
}
]
},
"options": {
"maxTokens": 2000,
"temperature": 0.4
}
},
"id": "llm-generate-digest",
"name": "LLM Generate Digest",
"type": "n8n-nodes-github-copilot-models.githubCopilotModels",
"typeVersion": 1,
"position": [
3100,
420
],
"credentials": {
"githubCopilotModelsApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const llmOut = $input.first().json;\nconst src = $('Build LLM Prompt').first().json;\n\nconst digestText =\n llmOut.text ||\n llmOut.message ||\n llmOut.content ||\n (llmOut.choices?.[0]?.message?.content) ||\n 'Errore nella generazione del digest.';\n\nreturn [{ json: { ...src, digestText: digestText.trim() } }];"
},
"id": "extract-llm-output",
"name": "Extract LLM Output",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3320,
420
]
},
{
"parameters": {
"jsCode": "return [{ json: $input.first().json }];"
},
"id": "merge-digest-paths",
"name": "Merge Digest Paths",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3320,
620
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "cond_str",
"leftValue": "={{ $json.delivery }}",
"rightValue": "telegram",
"operator": {
"type": "string",
"operation": "contains"
}
}
],
"combinator": "and"
}
},
"id": "check-telegram-delivery",
"name": "Deliver via Telegram?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
3540,
540
]
},
{
"parameters": {
"jsCode": "const data = $input.first().json;\nconst text = data.digestText;\nconst LIMIT = 4000;\n\nif (text.length <= LIMIT) {\n return [{ json: { ...data, chunk: text } }];\n}\n\nconst chunks = [];\nlet i = 0;\nwhile (i < text.length) {\n let end = i + LIMIT;\n if (end < text.length) {\n const nl = text.lastIndexOf('\\n', end);\n if (nl > i) end = nl;\n }\n chunks.push(text.substring(i, end));\n i = end;\n}\n\nreturn chunks.map(chunk => ({ json: { ...data, chunk } }));"
},
"id": "split-telegram-chunks",
"name": "Split for Telegram",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3760,
420
]
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.chunk }}",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"id": "send-digest-telegram",
"name": "Send Digest via Telegram",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
3980,
420
],
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "cond_str",
"leftValue": "={{ $json.delivery }}",
"rightValue": "email",
"operator": {
"type": "string",
"operation": "contains"
}
},
{
"id": "cond_str",
"leftValue": "={{ $json.email }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "isNotEmpty"
}
}
],
"combinator": "and"
}
},
"id": "check-email-delivery",
"name": "Deliver via Email?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
3760,
620
]
},
{
"parameters": {
"fromEmail": "YOUR_SENDER@gmail.com",
"toEmail": "={{ $json.email }}",
"subject": "\ud83d\udcf0 News Digest: {{ $json.topicName }} \u2014 {{ $now.format('DD/MM/YYYY') }}",
"emailType": "html",
"message": "={{ '<html><body style=\"font-family:Arial,sans-serif;max-width:700px;margin:0 auto;padding:20px;background:#f9f9f9\"><div style=\"background:#fff;padding:24px;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.08)\"><h2 style=\"color:#2c3e50;border-bottom:2px solid #3498db;padding-bottom:8px\">\ud83d\udcf0 News Digest: ' + $json.topicName + '</h2>' + $json.digestText.replace(/\\n/g,'<br>').replace(/\\*\\*(.*?)\\*\\*/g,'<strong>$1</strong>').replace(/\\*(.*?)\\*/g,'<em>$1</em>') + '<hr style=\"margin-top:32px\"><p style=\"color:#aaa;font-size:11px\">Digest generato automaticamente. Per gestire le preferenze usa il bot Telegram.</p></div></body></html>' }}",
"options": {}
},
"id": "send-digest-email",
"name": "Send Digest via Email",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2.1,
"position": [
3980,
620
],
"credentials": {
"smtp": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO digest_log (user_id, topic_id, delivery_method)\nVALUES ($1, $2, $3);",
"queryParams": "={{ [$('Prepare Sources').first().json.user_id, $('Prepare Sources').first().json.id, $('Prepare Sources').first().json.delivery] }}"
},
"id": "log-digest-sent",
"name": "Log Digest Sent",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
4200,
540
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 1
}
]
}
},
"id": "scheduler-trigger",
"name": "Scheduler (Hourly Check)",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
240,
900
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT\n t.id, t.name, t.keywords, t.schedule, t.schedule_time,\n t.schedule_day, t.delivery, t.user_id,\n u.chat_id, u.email,\n STRING_AGG(cf.feed_url, '|||') AS custom_feeds\nFROM topics t\nJOIN users u ON t.user_id = u.user_id\nLEFT JOIN custom_feeds cf ON cf.topic_id = t.id\nWHERE\n t.active = TRUE\n AND t.schedule != 'none'\n AND (\n (\n t.schedule = 'daily'\n AND to_char(NOW() AT TIME ZONE 'Europe/Rome', 'HH24') = SPLIT_PART(t.schedule_time, ':', 1)\n AND EXTRACT(MINUTE FROM NOW() AT TIME ZONE 'Europe/Rome') BETWEEN 0 AND 5\n ) OR (\n t.schedule = 'weekly'\n AND lower(to_char(NOW() AT TIME ZONE 'Europe/Rome', 'day')) LIKE lower(t.schedule_day) || '%'\n AND to_char(NOW() AT TIME ZONE 'Europe/Rome', 'HH24') = SPLIT_PART(t.schedule_time, ':', 1)\n AND EXTRACT(MINUTE FROM NOW() AT TIME ZONE 'Europe/Rome') BETWEEN 0 AND 5\n )\n )\n AND NOT EXISTS (\n SELECT 1 FROM digest_log dl\n WHERE dl.topic_id = t.id\n AND dl.sent_at > NOW() - INTERVAL '60 minutes'\n )\nGROUP BY t.id, t.name, t.keywords, t.schedule, t.schedule_time,\n t.schedule_day, t.delivery, t.user_id, u.chat_id, u.email;"
},
"id": "get-scheduled-topics",
"name": "Get Topics Due for Digest",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
460,
900
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "cond_num",
"leftValue": "={{ $input.all().filter(i => i.json.id).length }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "larger"
}
}
],
"combinator": "and"
}
},
"id": "check-has-scheduled",
"name": "Has Topics to Process?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
680,
900
]
},
{
"parameters": {
"fieldToSplitOut": "id",
"options": {}
},
"id": "split-scheduled-topics",
"name": "Split Scheduled Topics",
"type": "n8n-nodes-base.splitOut",
"typeVersion": 1,
"position": [
900,
840
]
},
{
"parameters": {
"jsCode": "const data = $input.first().json;\nconst text = data.text || '';\nconst chatId = data.chatId;\nconst userId = data.userId;\n\nconst extract = (field) => {\n const regex = new RegExp(`${field}:\\\\s*(.+)`, 'i');\n const match = text.match(regex);\n return match ? match[1].trim() : null;\n};\n\nconst nome = extract('NOME');\nconst keywords = extract('KEYWORDS');\nconst schedule = (extract('SCHEDULE') || 'none').toLowerCase();\nconst ora = extract('ORA') || '08:00';\nconst giorno = (extract('GIORNO') || 'lunedi').toLowerCase();\nconst delivery = (extract('DELIVERY') || 'telegram').toLowerCase();\nconst email = extract('EMAIL');\n\nif (!nome || !keywords) {\n return [{ json: {\n chatId, error: true,\n message: '\u274c Formato non valido. Includi almeno NOME e KEYWORDS. Riprova con /newtopic'\n }}];\n}\n\nreturn [{ json: { chatId, userId, nome, keywords, schedule, ora, giorno, delivery, email, error: false } }];"
},
"id": "parse-topic-form",
"name": "Parse Topic Form",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1560,
660
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "cond_bool",
"leftValue": "={{ $json.error }}",
"rightValue": false,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
"id": "check-form-valid",
"name": "Form Valid?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1780,
660
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO users (user_id, chat_id, email)\nVALUES ($1, $2, $3)\nON CONFLICT (user_id) DO UPDATE SET\n email = COALESCE(EXCLUDED.email, users.email),\n updated_at = NOW();\n\nINSERT INTO topics (user_id, name, keywords, schedule, schedule_time, schedule_day, delivery)\nVALUES ($1, $4, $5, $6, $7, $8, $9);",
"queryParams": "={{ [$json.userId, $json.chatId, $json.email, $json.nome, $json.keywords, $json.schedule, $json.ora, $json.giorno, $json.delivery] }}"
},
"id": "save-new-topic",
"name": "Save New Topic",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
2000,
600
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"chatId": "={{ $('Parse Topic Form').first().json.chatId }}",
"text": "\u2705 *Topic creato con successo!*\\n\\n\ud83d\udccc *{{ $('Parse Topic Form').first().json.nome }}*\\n\ud83d\udd11 Keywords: `{{ $('Parse Topic Form').first().json.keywords }}`\\n\u23f0 Schedule: {{ $('Parse Topic Form').first().json.schedule }}\\n\ud83d\udce8 Delivery: {{ $('Parse Topic Form').first().json.delivery }}\\n\\nUsa /digest per ricevere subito un digest!\\nVuoi aggiungere feed RSS? Usa /addfeed",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"id": "confirm-topic-created",
"name": "Confirm Topic Created",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2220,
600
],
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.message }}",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"id": "send-form-error",
"name": "Send Form Error",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2000,
740
],
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "UPDATE topics SET active = FALSE, updated_at = NOW() WHERE user_id = $1;",
"queryParams": "={{ [$('Parse Callback / Free Text').first().json.userId] }}"
},
"id": "do-unsubscribe",
"name": "Unsubscribe All",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1560,
780
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"chatId": "={{ $('Parse Telegram Update').item.json.chatId }}",
"text": "\u2705 Tutti i digest schedulati sono stati disattivati.\\nI tuoi topic sono conservati. Puoi riattivarli con /settings.",
"additionalFields": {}
},
"id": "confirm-unsubscribe",
"name": "Confirm Unsubscribe",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1780,
780
],
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"chatId": "={{ $('Parse Telegram Update').item.json.chatId }}",
"text": "\u2699\ufe0f *Impostazioni account*\\n\\nCosa vuoi fare?",
"additionalFields": {
"parse_mode": "Markdown",
"reply_markup": "{\"inline_keyboard\":[[{\"text\":\"\ud83d\uddd1\ufe0f Disattiva tutti i digest\",\"callback_data\":\"confirm_unsubscribe:all\"}],[{\"text\":\"\ud83d\udccb I miei topic\",\"callback_data\":\"cmd:mytopics\"}]]}"
}
},
"id": "send-settings-menu",
"name": "Send Settings Menu",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1120,
780
],
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"chatId": "={{ $('Parse Telegram Update').item.json.chatId }}",
"text": "\ud83d\udce1 *Aggiungi Feed RSS Personalizzato*\\n\\nInvia un messaggio nel formato:\\n\\n```\\nFEED_URL: https://example.com/feed.xml\\nFEED_NAME: Nome del feed\\nTOPIC_ID: [id del topic, usa /mytopics per vederlo]\\n```",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"id": "ask-feed-url",
"name": "Ask Feed URL",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1120,
900
],
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"chatId": "={{ $('Parse Telegram Update').item.json.chatId }}",
"text": "\u2753 Comando non riconosciuto.\\n\\nUsa /help per vedere tutti i comandi disponibili.",
"additionalFields": {}
},
"id": "send-unknown",
"name": "Send Unknown Command",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1560,
900
],
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
}
],
"connections": {
"Telegram Polling Trigger": {
"main": [
[
{
"node": "Parse Telegram Update",
"type": "main",
"index": 0
}
]
]
},
"Parse Telegram Update": {
"main": [
[
{
"node": "Router Comandi",
"type": "main",
"index": 0
}
]
]
},
"Router Comandi": {
"main": [
[
{
"node": "Upsert User",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Help",
"type": "main",
"index": 0
}
],
[
{
"node": "Ask Topic Form",
"type": "main",
"index": 0
}
],
[
{
"node": "Get My Topics",
"type": "main",
"index": 0
}
],
[
{
"node": "Get Topics for Digest Picker",
"type": "main",
"index": 0
}
],
[
{
"node": "Ask Feed URL",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Settings Menu",
"type": "main",
"index": 0
}
],
[
{
"node": "Unsubscribe All",
"type": "main",
"index": 0
}
],
[
{
"node": "Parse Callback / Free Text",
"type": "main",
"index": 0
}
]
]
},
"Upsert User": {
"main": [
[
{
"node": "Send Welcome",
"type": "main",
"index": 0
}
]
]
},
"Get My Topics": {
"main": [
[
{
"node": "Format Topics List",
"type": "main",
"index": 0
}
]
]
},
"Format Topics List": {
"main": [
[
{
"node": "Send Topics List",
"type": "main",
"index": 0
}
]
]
},
"Get Topics for Digest Picker": {
"main": [
[
{
"node": "Ask Which Digest",
"type": "main",
"index": 0
}
]
]
},
"Ask Which Digest": {
"main": [
[
{
"node": "Send Digest Picker",
"type": "main",
"index": 0
}
]
]
},
"Send Digest Picker": {
"main": [
[
{
"node": "Get Topic by ID",
"type": "main",
"index": 0
}
]
]
},
"Parse Callback / Free Text": {
"main": [
[
{
"node": "Router Callbacks",
"type": "main",
"index": 0
}
]
]
},
"Router Callbacks": {
"main": [
[
{
"node": "Get Topic by ID",
"type": "main",
"index": 0
}
],
[
{
"node": "Parse Topic Form",
"type": "main",
"index": 0
}
],
[
{
"node": "Unsubscribe All",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Unknown Command",
"type": "main",
"index": 0
}
]
]
},
"Get Topic by ID": {
"main": [
[
{
"node": "Prepare Sources",
"type": "main",
"index": 0
}
]
]
},
"Prepare Sources": {
"main": [
[
{
"node": "Split Feeds",
"type": "main",
"index": 0
}
]
]
},
"Split Feeds": {
"main": [
[
{
"node": "Fetch RSS Feed",
"type": "main",
"index": 0
}
]
]
},
"Fetch RSS Feed": {
"main": [
[
{
"node": "Aggregate & Deduplicate",
"type": "main",
"index": 0
}
]
]
},
"Aggregate & Deduplicate": {
"main": [
[
{
"node": "Build LLM Prompt",
"type": "main",
"index": 0
}
]
]
},
"Build LLM Prompt": {
"main": [
[
{
"node": "Skip LLM?",
"type": "main",
"index": 0
}
]
]
},
"Skip LLM?": {
"main": [
[
{
"node": "Merge Digest Paths",
"type": "main",
"index": 0
}
],
[
{
"node": "LLM Generate Digest",
"type": "main",
"index": 0
}
]
]
},
"LLM Generate Digest": {
"main": [
[
{
"node": "Extract LLM Output",
"type": "main",
"index": 0
}
]
]
},
"Extract LLM Output": {
"main": [
[
{
"node": "Merge Digest Paths",
"type": "main",
"index": 0
}
]
]
},
"Merge Digest Paths": {
"main": [
[
{
"node": "Deliver via Telegram?",
"type": "main",
"index": 0
}
]
]
},
"Deliver via Telegram?": {
"main": [
[
{
"node": "Split for Telegram",
"type": "main",
"index": 0
}
],
[
{
"node": "Deliver via Email?",
"type": "main",
"index": 0
}
]
]
},
"Split for Telegram": {
"main": [
[
{
"node": "Send Digest via Telegram",
"type": "main",
"index": 0
}
]
]
},
"Send Digest via Telegram": {
"main": [
[
{
"node": "Deliver via Email?",
"type": "main",
"index": 0
}
]
]
},
"Deliver via Email?": {
"main": [
[
{
"node": "Send Digest via Email",
"type": "main",
"index": 0
}
],
[
{
"node": "Log Digest Sent",
"type": "main",
"index": 0
}
]
]
},
"Send Digest via Email": {
"main": [
[
{
"node": "Log Digest Sent",
"type": "main",
"index": 0
}
]
]
},
"Scheduler (Hourly Check)": {
"main": [
[
{
"node": "Get Topics Due for Digest",
"type": "main",
"index": 0
}
]
]
},
"Get Topics Due for Digest": {
"main": [
[
{
"node": "Has Topics to Process?",
"type": "main",
"index": 0
}
]
]
},
"Has Topics to Process?": {
"main": [
[
{
"node": "Split Scheduled Topics",
"type": "main",
"index": 0
}
],
[]
]
},
"Split Scheduled Topics": {
"main": [
[
{
"node": "Prepare Sources",
"type": "main",
"index": 0
}
]
]
},
"Parse Topic Form": {
"main": [
[
{
"node": "Form Valid?",
"type": "main",
"index": 0
}
]
]
},
"Form Valid?": {
"main": [
[
{
"node": "Save New Topic",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Form Error",
"type": "main",
"index": 0
}
]
]
},
"Save New Topic": {
"main": [
[
{
"node": "Confirm Topic Created",
"type": "main",
"index": 0
}
]
]
},
"Unsubscribe All": {
"main": [
[
{
"node": "Confirm Unsubscribe",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"saveManualExecutions": true,
"callerPolicy": "workflowsFromSameOwner"
},
"tags": [
"news",
"telegram",
"digest",
"postgres"
],
"versionId": "2.0.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.
githubCopilotModelsApipostgressmtptelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
News Digest Bot - Multi-User (Postgres). Uses telegramTrigger, postgres, telegram, rssFeedRead. Event-driven trigger; 45 nodes.
Source: https://github.com/Mingo07-dev/n8n_workflows/blob/f95457653d23a057208d3b788a0b648db80f0259/news_digest_workflow_v3.json — 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.
Pede Ai. Uses httpRequest, telegram, postgres, telegramTrigger. Event-driven trigger; 53 nodes.
03 - Manual Entry. Uses telegramTrigger, telegram, postgres. Event-driven trigger; 16 nodes.
TextMain. Uses telegramTrigger, stopAndError, telegram, httpRequest. Event-driven trigger; 56 nodes.
📄 Documentation: Notion Guide
Telegram Wait. Uses stickyNote, httpRequest, redis, noOp. Event-driven trigger; 36 nodes.