Abstract / Overview
Goal. Deliver one clean daily digest to Telegram from selected RSS feeds.
Approach. Use Make.com to poll RSS, deduplicate items, format an HTML digest, and send it through a Telegram bot. Use a Make Data Store or Google Sheet to track last-seen GUIDs. No servers. No cost.
Constraint. “No-cost productivity workflow for content updates.” All steps respect free-tier limits.
Fit. Teams and individuals who want “RSS digest Telegram” delivery and reliable “daily news alerts” without coding.
Outcomes
A single scheduled message with top headlines and links.
Stable deduplication across feeds.
Automatic chunking when messages exceed Telegram limits.
Conceptual Background
RSS basics
Each feed exposes items with title
, link
, pubDate
, and guid
or a stable URL.
Deduplication uses guid
when present. Fallback uses link
.
Telegram constraints
Bot messages accept parse_mode = HTML
or MarkdownV2
.
Single message limit: 4,096 characters. Longer content requires chunking.
Rate safety: throttle to ≤1 message per second per chat.
Make.com building blocks
Scheduler triggers once per day at a fixed time.
HTTP fetches RSS XML.
RSS/XML parser converts XML to items.
Code (JavaScript) merges feeds, removes duplicates, sorts by recency, and formats HTML.
Data Store (or Google Sheet) persists “last seen” checkpoints by feed URL.
Telegram Bot > Send a message posts into a chat or channel.
Design choices
One digest per day. Top N
items per feed.
HTML formatting to keep links clean.
Idempotency at the feed level using last guid
or last pubDate
.
Optional link UTM tagging for analytics.
Step-by-Step Walkthrough
1. Create a Telegram bot and chat
In Telegram, start @BotFather
→ /newbot
→ name it. Copy YOUR_TELEGRAM_BOT_TOKEN
.
Create a private channel or group for the digest. Invite your bot and promote it to admin.
Obtain YOUR_TELEGRAM_CHAT_ID
:
Send any message in the channel.
Call getUpdates
once, or use any ID helper bot. Copy the numeric chat.id
.
2. List your RSS sources
3. Decide state storage
Preferred: Make Data Store named rss_state
with key = feed URL, value = last-seen GUID or ISO date.
Alternative: a Google Sheet with columns FeedURL
, LastGUID
, LastPubDate
.
4. Build the Make.com scenario
Modules in order:
Scheduler
Tools > Set variables
FEEDS
= JSON array of {name, url, max}
.
WINDOW_HOURS
= 24
.
MAX_TOTAL
= global cap (e.g., 25
).
Iterator over FEEDS
.
HTTP > Make a request (GET) for each feed URL.
RSS > Parse RSS or XML > Parse XML to items.
Data Store > Get a record by feed URL (checkpoint).
Code (JavaScript)
Filter new items after last checkpoint.
Sort by pubDate
desc.
Trim to per-feed max
.
Emit normalized items {feed, title, link, guid, isoDate}
.
Array aggregator to merge items from all feeds.
Code (JavaScript)
Telegram Bot > Send a message for each chunk
with parse_mode = HTML
, disable_web_page_preview = true
.
Data Store > Create/Update records with new checkpoints.
Optional: Google Sheets > Append one log row per run.
5. Content formatting rules
Header: Daily Digest – {YYYY-MM-DD}
.
Per item line: • <a href="{link}">{title}</a> <i>({feed})</i>
.
Escape HTML entities.
Avoid heavy formatting. Keep the digest scannable.
6. Test
Run once with a tiny FEEDS
list and MAX_TOTAL=5
.
Inspect Telegram output, links, and time zone alignment.
Verify the Data Store updated and the next run only shows new items.
7. Harden
Add a Sleep of 100–300 ms between Telegram sends.
Wrap HTTP with 3 retries and 5 s timeout.
Fallback: if a feed fails, skip it but keep others.
Log the number of items pulled vs. sent.
Code / JSON Snippets
Feed list variable (copy into “Set variables” as JSON)
Minimal working example.
{
"feeds": [
{ "name": "Tech", "url": "https://example.com/tech/rss", "max": 7 },
{ "name": "World", "url": "https://example.com/world/rss", "max": 5 },
{ "name": "Finance", "url": "https://example.com/markets/rss", "max": 5 }
],
"windowHours": 24,
"maxTotal": 20
}
Make.com Code module: filter and normalize per feed
Input: parsed items[]
, feed
object, checkpoint
value (last GUID or ISO). Output: normalized[]
and newCheckpoint
.
// Inputs: items (array of RSS items), feed (name,url,max), checkpoint (string|null)
// RSS item fields vary by source: try guid > link; date try isoDate > pubDate
const toISO = v => v ? new Date(v).toISOString() : null;
const now = new Date();
const windowHours = parseInt(input.windowHours || 24, 10);
function idOf(item){
return (item.guid && item.guid.trim()) || (item.link && item.link.trim()) || "";
}
function isoOf(item){
return toISO(item.isoDate || item.pubDate || item.pubdate || item.date);
}
const since = checkpoint ? new Date(checkpoint) : new Date(now.getTime() - windowHours*3600*1000);
let cleaned = (input.items || [])
.map(it => ({
feed: input.feed.name,
feedUrl: input.feed.url,
title: (it.title || "").replace(/\s+/g, " ").trim(),
link: it.link || "",
guid: idOf(it),
isoDate: isoOf(it)
}))
.filter(x => x.link && x.title);
if (checkpoint) {
cleaned = cleaned.filter(x => new Date(x.isoDate || 0) > since);
}
cleaned.sort((a,b) => new Date(b.isoDate||0) - new Date(a.isoDate||0));
const limited = cleaned.slice(0, input.feed.max || 5);
const newCheckpoint = limited.length ? (limited[0].guid || limited[0].isoDate || "") : (checkpoint || "");
return { normalized: limited, newCheckpoint };
Make.com Code module: merge, format, and chunk
Input: aggregated allItems[]
, maxTotal
. Output: chunks[]
and checkpoints{}
.
const MAX_TELEGRAM = 4096; // chars
const maxTotal = parseInt(input.maxTotal || 20, 10);
const items = (input.allItems || []).slice().sort((a,b)=> new Date(b.isoDate||0)-new Date(a.isoDate||0)).slice(0, maxTotal);
function esc(s){
return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
}
const dateStr = new Date().toISOString().slice(0,10);
let header = `<b>Daily Digest – ${dateStr}</b>\n`;
let lines = items.map(x => `• <a href="${esc(x.link)}">${esc(x.title)}</a> <i>(${esc(x.feed)})</i>`);
let chunks = [];
let current = header;
for (const line of lines){
// +1 for newline
if (current.length + line.length + 1 > MAX_TELEGRAM){
chunks.push(current);
current = line;
} else {
current += (current === header ? "" : "\n") + line;
}
}
if (current.trim().length) chunks.push(current);
// Compute latest checkpoints per feed
const checkpoints = {};
for (const x of items){
if (!checkpoints[x.feedUrl]) checkpoints[x.feedUrl] = x.guid || x.isoDate || "";
}
return { chunks, checkpoints, count: items.length };
Telegram send example (HTTP fallback)
Use only for testing or if you prefer HTTP modules.
curl -X POST "https://api.telegram.org/botYOUR_TELEGRAM_BOT_TOKEN/sendMessage" \
-d "chat_id=YOUR_TELEGRAM_CHAT_ID" \
-d "parse_mode=HTML" \
--data-urlencode "text=<b>Daily Digest – 2025-08-18</b>%0A• <a href=\"https://example.com/a\">Example A</a> <i>(Tech)</i>"
Sample workflow JSON code
Portable representation you can adapt to Make or n8n.
{
"workflow": {
"name": "rss-to-telegram-daily-digest",
"assumptions": {
"freeTier": true,
"oneDigestPerDay": true,
"messageLimitChars": 4096
},
"nodes": [
{
"id": "schedule_daily",
"type": "trigger.scheduler.daily",
"config": { "time": "07:30", "timezone": "UTC" },
"outputs": ["tick"]
},
{
"id": "vars_feeds",
"type": "tools.variables",
"inputs": ["tick"],
"config": {
"feeds": [
{ "name": "Tech", "url": "https://example.com/tech/rss", "max": 7 },
{ "name": "World", "url": "https://example.com/world/rss", "max": 5 },
{ "name": "Finance", "url": "https://example.com/markets/rss", "max": 5 }
],
"windowHours": 24,
"maxTotal": 20
},
"outputs": ["feed_list"]
},
{
"id": "iterate_feeds",
"type": "control.iterator",
"inputs": ["feed_list.feeds"],
"outputs": ["feed"]
},
{
"id": "http_fetch",
"type": "net.http.get",
"inputs": ["feed.url"],
"config": { "timeout": 5000, "retry": 3 },
"outputs": ["xml"]
},
{
"id": "rss_parse",
"type": "parser.rss",
"inputs": ["xml"],
"outputs": ["items"]
},
{
"id": "store_get",
"type": "db.datastore.get",
"inputs": ["feed.url"],
"config": { "store": "rss_state", "key": "{{feed.url}}" },
"outputs": ["checkpoint"]
},
{
"id": "code_filter",
"type": "function.javascript",
"inputs": ["items", "feed", "checkpoint", "vars_feeds.windowHours"],
"config": { "source": "// per-feed filter/normalize code" },
"outputs": ["normalized", "newCheckpoint"]
},
{
"id": "aggregate_items",
"type": "control.array_aggregate",
"inputs": ["code_filter.normalized"],
"outputs": ["allItems"]
},
{
"id": "code_format",
"type": "function.javascript",
"inputs": ["allItems", "vars_feeds.maxTotal"],
"config": { "source": "// merge + format + chunk code" },
"outputs": ["chunks", "checkpoints"]
},
{
"id": "send_chunks",
"type": "notify.telegram.sendMessage",
"iterate": true,
"inputs": ["code_format.chunks[]"],
"config": {
"botToken": "YOUR_TELEGRAM_BOT_TOKEN",
"chatId": "YOUR_TELEGRAM_CHAT_ID",
"parseMode": "HTML",
"disablePreview": true
}
},
{
"id": "store_set",
"type": "db.datastore.set_many",
"inputs": ["code_format.checkpoints"],
"config": { "store": "rss_state" }
}
]
}
}
Diagram
![diagram]()
Use Cases / Scenarios
Morning brief for founders. Market, tech, and macro in one message.
Classroom or team updates. Department feeds are summarized daily.
Niche topic monitors. Regulatory notices, package releases, or security advisories.
Local news blend. Municipal RSS mixed with traffic and weather feeds.
Competitive watching. Company blogs and newsroom feeds.
Limitations / Considerations
Some sites serve partial content or throttle bots. Respect robots and terms.
RSS item structures vary. Keep parsing generic.
Time zones differ across feeds. Normalize to UTC before sorting.
Telegram preview cards inflate message length. Disable previews for safety.
Free tiers have quotas. Heavy feeds may require capping or batching.
Feeds without guid
can cause subtle duplicates when titles change.
Fixes (common pitfalls with solutions and troubleshooting tips)
No items appear. Verify feed URL returns XML. Test with a browser. Reduce windowHours
for the first run.
Duplicate headlines. Switch checkpoint from pubDate
to guid
or link
. Store a hash of link+title
.
Message is truncated. The bot has a 4,096-character limit. Use chunking code. Disable previews. Lower maxTotal
.
Bot cannot post. Add the bot as an admin to the channel. Confirm chat_id
.
Malformed HTML. Only simple tags are allowed. Use <b>
, <i>
, <a>
. Escape others.
Slow or failing feeds. Add retries and 5–10 s timeouts. Skip failing feeds rather than failing the run.
Timezone drift. Pin the scheduler to your working timezone. Add the timezone label to the header.
State not updating. Ensure Data Store keys use the exact feed URL string. Update after successful sends only.
Budget Calculation
Variables
F
= number of feeds.
I_d
= average new items per feed per day after dedupe.
C
= number of Telegram chunks (≈ ceil((headline_chars×I_total)/4096)
, usually 1–2).
O
= daily Make operations.
Approximate operations per day:
Scheduler 1
Per feed: HTTP 1
+ Parse 1
+ Store read 1
→ 3F
Merge/format code: 2
Telegram sends: C
Store writes: min(F, I_total > 0 ? F : 0)
→ F
in typical runs
So O ≈ 1 + 3F + 2 + C + F = 1 + 4F + 2 + C = 3 + 4F + C
.
Example
F = 5
, C = 2
→ O ≈ 3 + 20 + 2 = 25 ops/day
.
Monthly ≈ 750 ops
. Fits many free plans.
Telegram usage is free for text messages. No extra cost.
Cost controls
Reduce F
or per-feed max
.
Lower WINDOW_HOURS
for niche feeds to reduce noise.
Send on weekdays only with a calendar guard.
Collapse to a single chunk by capping MAX_TOTAL
.
Conclusion
This workflow turns scattered RSS streams into a single, timed Telegram digest. The system is simple, serverless, and free. Make.com handles scheduling, fetch, and logic. A small state store provides stable deduplication. Telegram delivers fast and reliably. You get “daily news alerts” in a compact “RSS digest Telegram” format without switching apps or paying for infrastructure.