AutomationFlowsAI & RAG › Send 24-hour Hacker News Trends to Telegram with Openrouter AI Translation

Send 24-hour Hacker News Trends to Telegram with Openrouter AI Translation

ByCryoZeroLabs @cryozero on n8n.io

Stay ahead of the curve with this smart Hacker News monitor. Unlike simple RSS feeds, this workflow uses a custom "Gravity Score" algorithm to identify rising trends and filter out noise, pushing a clean, summarized digest to your Telegram. 🧠 Smart Algorithms: Gravity Score:…

Cron / scheduled trigger★★★★☆ complexityAI-powered13 nodesTelegramOpenRouter ChatAgentHTTP Request
AI & RAG Trigger: Cron / scheduled Nodes: 13 Complexity: ★★★★☆ AI nodes: yes Added:
Send 24-hour Hacker News Trends to Telegram with Openrouter AI Translation — n8n workflow card showing Telegram, OpenRouter Chat, Agent integration

This workflow corresponds to n8n.io template #12748 — we link there as the canonical source.

This workflow follows the Agent → 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 →

Download .json
{
  "id": "n8PjlpfC2Fudq3QA",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Hacker News 24H Trend to Telegram & AI Translator",
  "tags": [
    {
      "id": "tQPxiOWM4dsxfpPF",
      "name": "hacker news",
      "createdAt": "2026-01-07T16:53:06.456Z",
      "updatedAt": "2026-01-07T16:53:06.456Z"
    }
  ],
  "nodes": [
    {
      "id": "57574ba6-49e8-4d19-b7d0-1ea4a7b747af",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        352,
        -224
      ],
      "parameters": {
        "color": 5,
        "width": 592,
        "height": 352,
        "content": "# Hacker News Trend Tracking\rTrack popular via https://hn.algolia.com/api\r\r## Sliding Window\rTimer (Trigger): Run every 4 hours. Lookback: Fetch data from the past 24 hours.\r\r- Define a rule\u2014\"Posts published less than 1 hour ago and with extremely low interaction (<3 points) are considered noise and should be discarded directly.\"\r- If a post receives 20 likes in just 10 minutes (extremely explosive), we should keep it; but if it only gets 1 like after 10 minutes (normal situation), we temporarily ignore it and wait for the next scheduled task (e.g., 4 hours later) to evaluate it when it \"matures.\""
      },
      "typeVersion": 1
    },
    {
      "id": "6a607aac-b15f-46d8-901c-a53d9099a08d",
      "name": "Filter",
      "type": "n8n-nodes-base.filter",
      "position": [
        1200,
        176
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f3c26f60-2368-4461-9f2e-3072acb1f7b1",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $json.analysis.velocity_score }}",
              "rightValue": 1
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "f0fbb4ac-164c-483c-b0eb-2bfedab2e2a0",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        368,
        176
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 4
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "ba782266-418a-4d19-90bf-255f0f955cb9",
      "name": "Send a text message",
      "type": "n8n-nodes-base.telegram",
      "position": [
        2096,
        176
      ],
      "parameters": {
        "text": "={{ $json.text }}",
        "chatId": "YourChatID",
        "additionalFields": {
          "parse_mode": "HTML"
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "fd96243b-229e-436d-91ea-b31438f175a8",
      "name": "OpenRouter Chat Model1",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        1248,
        416
      ],
      "parameters": {
        "model": "google/gemini-2.5-flash-lite",
        "options": {}
      },
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "7eaefa3f-577d-4809-895b-49d03e23495a",
      "name": "Algolia parameters",
      "type": "n8n-nodes-base.code",
      "position": [
        576,
        176
      ],
      "parameters": {
        "jsCode": "// @see https://hn.algolia.com/api\n\nconst now = DateTime.now();\n// Observation window (past X hours)\nconst lookBackHours = 24;\nconst startTime = Math.floor(now.minus({hours: lookBackHours}).toSeconds())\n\nconst numericFilters = [\n  `created_at_i>${startTime}`,\n  // Filter out noise/spam (posts with zero engagement)\n  \"points>1\",\n];\n\nreturn [{\n  json: {\n    tags: \"(ask_hn,show_hn)\",\n    startTime: now.minus({hours: lookBackHours}),\n    numericFilters: numericFilters.join(\",\"),\n    // Fetch maximum allowed items per page\n    hitsPerPage: 1000\n  }\n}]"
      },
      "typeVersion": 2
    },
    {
      "id": "b346f0d8-f5c1-4161-af9f-75059735f0f4",
      "name": "Recalculate popularity score",
      "type": "n8n-nodes-base.code",
      "position": [
        992,
        176
      ],
      "parameters": {
        "jsCode": "const hits = items[0].json.hits;\n\nconst now = DateTime.now();\n\nconst cleanData = hits.map(item => {\n  const postedTime = DateTime.fromISO(item.created_at);\n  // Calculate post age (in hours)\n  const ageInHours = now.diff(postedTime, 'hours').hours;\n  const points = item.points || 0;\n  const comments = item.num_comments || 0;\n\n  // --- Strategy 1: Infancy Filtering (The Buffer) ---\n  // If the post is less than 1 hour old and has fewer than 5 points, it is still \"baking\"\n  // and lacks analysis value for now. Skip it.\n  if (ageInHours < 1.0 && points < 5) {\n        return null; // Mark for removal\n  }\n\n  // --- Strategy 2: Recalculate Heat Score (Gravity Score) ---\n  const rawScore = points + (comments * 2);\n  const gravity = 1.8;\n  const normalizedScore = (rawScore - 1) / Math.pow(ageInHours + 2, gravity);\n\n  return {\n      ...item,\n      analysis: {\n          age_hours: parseFloat(ageInHours.toFixed(2)),\n          velocity_score: parseFloat(normalizedScore.toFixed(4)),\n      }\n  };\n})\n.filter(item => item !== null) // Filter out the new posts marked as null above\n.sort((a, b) => b.analysis.velocity_score - a.analysis.velocity_score);\n\nreturn cleanData.map(item => ({ json: item }));"
      },
      "typeVersion": 2
    },
    {
      "id": "105d6b96-622a-4956-bf77-77c69b814ef0",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1376,
        352
      ],
      "parameters": {
        "color": 6,
        "width": 272,
        "content": "### \ud83d\udc46 Replace with your target language\nMust be Spanish, French, Chinese, Japanese..."
      },
      "typeVersion": 1
    },
    {
      "id": "19c8577a-7191-45c5-b6b8-60a8be70ed5d",
      "name": "Translate",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "onError": "continueRegularOutput",
      "position": [
        1376,
        176
      ],
      "parameters": {
        "text": "=title: {{ JSON.stringify($json.title) }}\nstory_text: {{ JSON.stringify($json.story_text) }}",
        "options": {
          "systemMessage": "You are a professional news media translation API.\n\nYour Task:\nTranslate the 'title' and 'story_text' into Chinese.\nIf 'story_text' is empty, keep the original text.\n\nHTML Handling:\n1. Remove all HTML tags.\n2. If an HTML tag is a link (<a href=\"...\">), convert it to Markdown format: [text](url).\n\nOutput Rules:\nReturn ONLY a valid JSON object. Do not include markdown formatting (like ```json) or any conversational text.\n\nExample:\nInput:\ntitle: \"hi\"\nstory_text: \"hello world\"\n\nOutput:\n{\n  \"title\": \"\u55e8\",\n  \"story_text\": \"\u4f60\u597d\u4e16\u754c\"\n}"
        },
        "promptType": "define"
      },
      "typeVersion": 3.1
    },
    {
      "id": "aeefe4c7-1603-4e57-b7a0-d373dcddf67d",
      "name": "Request Algolia",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        784,
        176
      ],
      "parameters": {
        "url": "http://hn.algolia.com/api/v1/search_by_date",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "tags",
              "value": "={{ $json.tags }}"
            },
            {
              "name": "numericFilters",
              "value": "={{ $json.numericFilters }}"
            },
            {
              "name": "hitsPerPage",
              "value": "={{ $json.hitsPerPage }}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "48997584-e40b-4a2d-bcaa-f1de24a6ad6b",
      "name": "Merge translation results",
      "type": "n8n-nodes-base.code",
      "position": [
        1680,
        176
      ],
      "parameters": {
        "jsCode": "// Retrieve all original data from the 'Filter' node (since the AI Agent node replaces the original input data)\n// Note: Ensure the upstream node is named 'Filter'. If you renamed it, update the reference here accordingly.\nconst originalItems = $('Filter').all();\n\nreturn items.map((item, index) => {\n  // 1. Get the output text from the AI Agent\n  const aiOutputString = item.json.output || \"\";\n\n  // 2. Sanitize the data (remove Markdown formatting like ```json ... ``` if added by the AI)\n  const cleanJsonString = aiOutputString.replace(/```json/g, '').replace(/```/g, '').trim();\n\n  let translatedData = {};\n\n  // 3. Attempt to parse the JSON string\n  try {\n    translatedData = JSON.parse(cleanJsonString);\n  } catch (error) {\n    // If parsing fails, retain the raw text for debugging purposes\n    translatedData = {\n      translation_error: true,\n      raw_ai_response: aiOutputString\n    };\n  }\n\n  // 4. Merge original data with the translated data\n  const originalData = originalItems[index] ? originalItems[index].json : {};\n\n  return {\n    json: {\n      ...originalData,   // Preserve original fields (e.g., url, objectID)\n      ...translatedData  // Add new translated fields (e.g., title_cn, story_cn)\n    }\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "28dba153-23d5-4c12-a786-605cb236040d",
      "name": "Combine message templates",
      "type": "n8n-nodes-base.code",
      "position": [
        1872,
        176
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\n\n// --- Helper Functions ---\n\n// HTML escape (prevents < > & errors)\nfunction escapeHtml(text) {\n    if (!text) return \"\";\n    return text\n        .replace(/&/g, \"&amp;\")\n        .replace(/</g, \"&lt;\")\n        .replace(/>/g, \"&gt;\");\n}\n\n// Clean and truncate text\nfunction cleanAndTruncate(story_text = \"\") {\n    if (!story_text) return \"\";\n    \n    // 1. Convert HTML tags to newlines or empty strings\n    let cleaned = story_text\n        .replace(/<p>/g, \"\\n\")      // p tag to newline\n        .replace(/<br>/g, \"\\n\")     // br tag to newline\n        .replace(/<[^>]+>/g, \"\")    // Remove all other HTML tags\n        .replace(/&quot;/g, '\"')\n        .replace(/&#x27;/g, \"'\")\n        .replace(/&amp;/g, \"&\");\n\n    // 2. Truncation logic (limit to 120 characters)\n    if (cleaned.length > 120) {\n        cleaned = cleaned.substring(0, 120) + \"...\";\n    }\n\n    // 3. Escape output\n    return escapeHtml(cleaned);\n}\n\n// --- Main Logic ---\n\nconst count = items.length;\nlet message = \"\";\n\n// 1. Header Information\nmessage += `<b>\ud83d\udea8 Hacker News 24H Trending Alert</b>\\n`;\nmessage += `\ud83d\udcc9 Top stories captured: <b>${count}</b>\\n`; \nmessage += `\\n`; // Extra empty line to separate the header\n\nconst contents = [];\n\nfor (const item of items) {\n    const data = item.json;\n    let content = \"\";\n\n    // A. Title (with link)\n    const safeTitle = escapeHtml(data.title);\n    const hnLink = `https://news.ycombinator.com/item?id=${data.objectID}`;\n    content += `\ud83d\udd25 <a href=\"${hnLink}\">${safeTitle}</a>\\n`;\n\n    // B. Data row (aligned using Code style)\n    const score = parseFloat(data.analysis.velocity_score || 0).toFixed(2);\n    const age = parseFloat(data.analysis.age_hours || 0).toFixed(1);\n    \n    content += `<code>\ud83d\udcc8 ${score} | \ud83d\udcac ${data.num_comments} | \u2b50\ufe0f ${data.points}</code>\\n`;\n    content += `\ud83d\udd52 Posted ${age} hours ago\\n`;\n\n    // C. Content preview (only shown if story_text exists and is not empty)\n    if (data.story_text && data.story_text.trim().length > 0) {\n        content += `<blockquote>${cleanAndTruncate(data.story_text)}</blockquote>`;\n    }\n    \n    contents.push(content);\n}\n\n// --- Combine Output ---\nconst separator = \"\\n\";\n\nreturn {\n    json: {\n        // If no stories, do not join, just output header\n        text: message + (contents.length > 0 ? contents.join(separator) : \"\")\n    }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "ff3c82ac-4846-4744-96a3-0d70c20008be",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2096,
        336
      ],
      "parameters": {
        "color": 6,
        "width": 448,
        "height": 208,
        "content": "### \ud83d\udc46 Replace this with your ChatID\n1. **Credentials**: Create a bot via @BotFather and save the Token in the credentials.\n2. **Chat ID**:\n   - For **Personal**: Search @userinfobot to get your ID.\n   - For **Channel**: Add your bot as an **Administrator** to the channel. The Chat ID is usually the channel link suffix (e.g., -100xxxxxxx).\n3. **Important**: Ensure the bot has permission to post messages!"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "timezone": "Etc/UTC",
    "callerPolicy": "workflowsFromSameOwner",
    "timeSavedMode": "fixed",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "44526615-7197-46a5-bbdf-91512e5059ee",
  "connections": {
    "Filter": {
      "main": [
        [
          {
            "node": "Translate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Translate": {
      "main": [
        [
          {
            "node": "Merge translation results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Request Algolia": {
      "main": [
        [
          {
            "node": "Recalculate popularity score",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Algolia parameters",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Algolia parameters": {
      "main": [
        [
          {
            "node": "Request Algolia",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model1": {
      "ai_languageModel": [
        [
          {
            "node": "Translate",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Combine message templates": {
      "main": [
        [
          {
            "node": "Send a text message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge translation results": {
      "main": [
        [
          {
            "node": "Combine message templates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Recalculate popularity score": {
      "main": [
        [
          {
            "node": "Filter",
            "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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Stay ahead of the curve with this smart Hacker News monitor. Unlike simple RSS feeds, this workflow uses a custom "Gravity Score" algorithm to identify rising trends and filter out noise, pushing a clean, summarized digest to your Telegram. 🧠 Smart Algorithms: Gravity Score:…

Source: https://n8n.io/workflows/12748/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

This workflow is for beauty salons who want consistent, high‑quality social media content without writing every post manually. It also suits agencies and automation builders who manage multiple beauty

Telegram, Google Sheets Trigger, Agent +26
AI & RAG

Who Is This For?

Telegram, Google Sheets Trigger, Lm Chat Mistral Cloud +17
AI & RAG

This cutting-edge n8n workflow is a comprehensive automation solution designed to streamline various Instagram operations. It combines an intelligent AI chatbot for direct message management, automate

Agent, OpenRouter Chat, Output Parser Structured +4
AI & RAG

AI powered Automated Crypto Insights with Chart-img and BrowserAI

HTTP Request, Output Parser Structured, OpenRouter Chat +2
AI & RAG

This workflow automatically generates stock market insights for selected tickers (e.g. GAZP, SBER, LKOH) using historical data, technical indicators, and an AI model. The results are then sent to Tele

Agent, OpenRouter Chat, Telegram Trigger +5