AI Automation & Agents  

Build a Free Daily News Digest Using RSS + Telegram

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

  • Create a simple list with names and URLs. Start with 3–6 high-signal feeds.

  • Note any feeds that publish more than 50 items per day. You will cap them.

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

    • Set your local time for delivery (e.g., 07:30).

  • 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)

    • Global sort by time.

    • Apply MAX_TOTAL.

    • Build one or more HTML chunks ≤4096 chars.

    • Output chunks[] plus a newCheckpoints{feedUrl:lastGuid} map.

  • 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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
}

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 13F

  • 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 = 2O ≈ 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.