AutomationFlowsAI & RAG › Send Weekly Github Digests to Telegram with Qwen via Openrouter

Send Weekly Github Digests to Telegram with Qwen via Openrouter

ByDo Thanh Vinh @dothanhvinh on n8n.io

This workflow runs weekly or on demand via Telegram commands (/report, /issues, /prs, /status) to fetch all your GitHub repositories and events, summarizes the last 7 days using an OpenRouter-hosted Qwen model, and sends a multi-part engineering digest to your Telegram chat. No…

Cron / scheduled trigger★★★★☆ complexityAI-powered22 nodesChain LlmOpenAI ChatTelegramHTTP RequestTelegram Trigger
AI & RAG Trigger: Cron / scheduled Nodes: 22 Complexity: ★★★★☆ AI nodes: yes Added:

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

This workflow follows the Chainllm → 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": "EFhluI7YvJE6Ewnq",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "AI-Powered GitHub Weekly Engineering Digest to Telegram",
  "tags": [],
  "nodes": [
    {
      "id": "0983d211-d755-43a0-ad1e-58dfe849cdd5",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2800,
        -1120
      ],
      "parameters": {
        "width": 608,
        "height": 2224,
        "content": "## AI-Powered GitHub Weekly Engineering Digest to Telegram\n\nEvery week (or on-demand via Telegram command), this workflow pulls GitHub activity across all your repositories, uses AI to generate a multi-part engineering digest, and delivers it as Telegram messages.\n\nNo dashboards. No BI tools. No manual reporting. Your GitHub data stays on your own n8n instance.\n\n### How it works\n\n1. **Weekly Schedule + Telegram Trigger** \u2014 Fires every week on schedule, or instantly when you chat a command like `/report`, `/issues`, `/prs`, or `/status` in Telegram.\n2. **Fetch GitHub Repositories** \u2014 Pulls all repos from your GitHub account using a Fine-grained Personal Access Token (read-only, via Header Auth credential). Paginated automatically (100 repos per page).\n3. **Retrieve GitHub Events** \u2014 Calls the GitHub Events API for each repo. Processes all repos in sequence. If a repo returns no events or an error, downstream nodes handle it gracefully.\n4. **Code Filter by Date** \u2014 Groups raw events by repository, filters to a 7-day window, and counts activity by type: commits, issues, PRs, releases. Tracks contributors per repo.\n5. **Aggregate Repositories** \u2014 Merges per-repo stats into a single payload. Calculates totals, identifies the most active repo, flags stale repos, and detects API errors.\n6. **Route by Activity Status** \u2014 Switch node with 3 outputs: (1) Has Activity \u2192 generate report, (2) All Failed \u2192 send API error alert, (3) No Activity \u2192 silent stop.\n7. **Build Report Components** \u2014 Sends aggregated stats to Qwen 3 via OpenRouter. The LLM reads the command mode (`/report`, `/issues`, `/prs`, `/status`) and generates the appropriate output format.\n8. **Format Telegram Messages** \u2014 Parses the LLM output, splits multi-part reports by `|||` delimiter, validates character limits (3800 chars per message), and appends a branded footer.\n9. **Create Fallback Report** \u2014 Error branch from the LLM. If the model fails (timeout, rate limit, unavailable), formats raw stats into a readable report without AI.\n10. **Create API Error Message** \u2014 Error branch from Route by Activity Status. If all repos failed to fetch, builds an error message listing the failures.\n11. **Send Messages to Telegram + Send Error Alert** \u2014 Delivers the final output to the user's Telegram chat.\n\n### Telegram Commands\n\n| Command | Output |\n|---|---|\n| `/report` | Full 4-part weekly digest (2-4 messages) |\n| `/issues` | Short open issues report (1 message) |\n| `/prs` | Pull request status (1 message) |\n| `/status` | One-message health check with emoji status per repo |\n\nNon-command messages are silently ignored \u2014 the bot only responds when you explicitly request a report.\n\n### Error handling\n\n- GitHub API fails for one repo \u2192 skip that repo, continue with others\n- GitHub API fails for all repos \u2192 send error alert via Telegram\n- LLM fails \u2192 fallback report from raw stats (no AI)\n- No activity across all repos \u2192 silent stop, no message sent\n\n### Setup (10\u201315 min)\n\n- [ ] Create a GitHub Fine-grained Personal Access Token: Settings \u2192 Developer settings \u2192 Fine-grained tokens. Permissions: Contents (Read-only). Scope it to the repositories you want to track, or grant access to all repos.\n- [ ] In n8n, create a Header Auth credential for GitHub: Name = `Authorization`, Value = `Bearer ghp_xxxxxxxxxxxx`.\n- [ ] Create a Telegram Bot via @BotFather. Copy the token.\n- [ ] In n8n, create a Telegram Bot API credential using the token.\n- [ ] Get your Telegram chat ID: send a message to your bot, then visit `https://api.telegram.org/bot<TOKEN>/getUpdates` to find the `chat.id`.\n- [ ] Create an OpenRouter account at openrouter.ai. Generate an API key.\n- [ ] In n8n, create an OpenAI API credential: Base URL = `https://openrouter.ai/api/v1`, API Key = your OpenRouter key.\n- [ ] Edit \"Fetch GitHub Repositories\": verify the Header Auth credential is set.\n- [ ] Edit \"Retrieve GitHub Events\": verify the Header Auth credential is set.\n- [ ] Edit \"OpenAI Report Model\": verify the OpenAI API credential is set.\n- [ ] Edit \"Send Messages to Telegram\" and \"Send Error Alert\": set `chatId` to your Telegram chat ID.\n- [ ] Activate the workflow. Test by sending `/report` to your bot on Telegram.\n\n### Customization\n\n- **Change schedule**: Edit the Weekly Digest Trigger node. Default: weekly at 9 AM. Change to daily (`0 9 * * *`) or Friday 5pm (`0 17 * * 5`).\n- **Filter repos**: The workflow fetches all repos by default. To exclude specific repos, add a filter step after \"Fetch GitHub Repositories\" using a Code node.\n- **Change the AI model**: Edit \"OpenAI Report Model\" to swap Qwen 3 for GPT-4o, Claude, Gemini, or any OpenRouter-supported model.\n- **Change output channel**: Replace Telegram nodes with Discord, Slack, or Email nodes. The report format works across all platforms.\n- **Change report depth**: Edit the prompt in \"Build Report Components\" to request more or fewer message parts.\n- **Single message mode**: Remove the `|||` split logic in \"Format Telegram Messages\" and concatenate all parts.\n\n### What's next\n\nThis is a foundation. Here's where it can grow:\n- **Multi-channel delivery**: Send the same report to Telegram + Email + Discord simultaneously.\n- **Social media auto-post**: Repurpose the weekly digest into a LinkedIn or Twitter post with a Telegram approval button.\n- **Obsidian sync**: Push each report as a .md file to an Obsidian vault via Git.\n- **Unified weekly report**: Aggregate GitHub + Gmail + Google Calendar + WakaTime + Notion tasks into a single digest.\n- **Approval flow**: Telegram inline keyboard \u2014 \"Post to social media? [Yes] [Skip]\" \u2014 before auto-posting.\n\n### Requirements\n\n- n8n instance (self-hosted or cloud)\n- GitHub Fine-grained Personal Access Token (Contents: Read-only)\n- Telegram Bot token + chat ID\n- OpenRouter API key (or any OpenAI-compatible endpoint)"
      },
      "typeVersion": 1
    },
    {
      "id": "87937a48-a1a6-45dd-b91d-8a8f25606a51",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2144,
        -800
      ],
      "parameters": {
        "color": 7,
        "width": 528,
        "height": 720,
        "content": "## Initialize Workflow\n\nTwo entry points that feed into the same pipeline.\n\n**Weekly Digest Trigger** \u2014 Schedule Trigger node that fires every week at 9 AM UTC. Edit the schedule rule to change the frequency or time. Common alternatives: daily (`0 9 * * *`), Monday+Thursday (`0 9 * * 1,4`), Friday 5pm (`0 17 * * 5`). Time is UTC \u2014 adjust the hour to match your timezone.\n\n**Telegram Message Trigger** \u2014 Telegram Trigger node that listens for all incoming messages. Does not filter at the trigger level \u2014 all messages pass through. Filtering happens in the next node.\n\nCredential needed: Telegram Bot API (same token across all Telegram nodes in this workflow).\n\n**Parse Telegram Command** \u2014 Code node that extracts the command from the incoming Telegram message. Only messages starting with `/` are processed. Four valid commands: `report`, `issues`, `prs`, `status`. Messages that don't start with `/` or use an unrecognized command return an empty array \u2014 the workflow stops silently. No spam, no error message.\n\nBoth triggers feed into the same Fetch GitHub Repositories node. A Telegram command triggers an instant report. The schedule triggers a weekly report. Same logic, same output."
      },
      "typeVersion": 1
    },
    {
      "id": "02ca113d-e7fe-4c2b-b2a2-531bf300be46",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1568,
        -1024
      ],
      "parameters": {
        "color": 7,
        "width": 544,
        "height": 1024,
        "content": "## Fetch GitHub Data\n\nTwo HTTP Request nodes that pull repository and event data from the GitHub API. Both use the same Header Auth credential \u2014 no hardcoded tokens.\n\n**Fetch GitHub Repositories** \u2014 Calls `GET /user/repos` with pagination (100 per page, sorted by last updated). Returns all repositories owned by the authenticated user. Archived repos are not filtered here \u2014 downstream nodes handle stale detection.\n\nHTTP Request configuration:\n- URL: `https://api.github.com/user/repos`\n- Authentication: Predefined credential (Header Auth)\n- Query: `per_page=100`, `sort=updated`, `direction=desc`\n- Headers: `Accept: application/vnd.github+json`, `X-GitHub-Api-Version: 2022-11-28`\n- Response format: JSON\n\nCredential needed: Header Auth with Name=`Authorization`, Value=`Bearer ghp_xxxxxxxxxxxx` (Fine-grained PAT, Contents: Read-only).\n\n**Retrieve GitHub Events** \u2014 Calls `GET /repos/{owner}/{repo}/events` for each repository returned by the previous node. The URL uses `{{ $json.full_name }}` from the repo list to build the endpoint dynamically. Returns up to 100 recent events per repo.\n\nHTTP Request configuration:\n- URL: `https://api.github.com/repos/{{ $json.full_name }}/events`\n- Authentication: Predefined credential (Header Auth)\n- Query: `per_page=100`\n- Headers: `Accept: application/vnd.github+json`, `X-GitHub-Api-Version: 2022-11-28`\n- Response format: JSON\n\nCredential needed: Same Header Auth credential as above.\n\nIf a repo returns a 404 (token lacks access) or any other error, the error is captured downstream in the Filter node \u2014 this node does not crash the workflow."
      },
      "typeVersion": 1
    },
    {
      "id": "92d17134-6452-4c70-b3c9-3d3190bdeaa9",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -992,
        -1120
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 1056,
        "content": "## Process GitHub Events\n\nTwo code nodes that transform raw GitHub API responses into structured stats.\n\n**Code Filter by Date** \u2014 Takes the raw event arrays from the HTTP Request output and groups them by repository name. Filters events to a 7-day window (computed at runtime, not dependent on upstream node output format). Counts activity by event type: PushEvent (commits), IssuesEvent, PullRequestEvent, ReleaseEvent. Tracks contributors per repo by GitHub username. If a repo has no events, it gets zero-filled stats with `is_stale: true`. The output is one item per repository.\n\nKey logic:\n- Flattens array responses from HTTP Request (handles both array-of-arrays and flat event objects)\n- Groups by `ev.repo.name` to associate events with the correct repository\n- Matches against the repo list from Fetch GitHub Repositories to ensure all repos get an entry\n- Repos with 0 events still get an output item (zero-filled stats)\n\n**Aggregate Repositories** \u2014 Merges all per-repo stats into a single report payload. Calculates totals across all repos (commits, issues, PRs, releases, events). Identifies the most active repo and its share of total activity. Flags stale repos and repos with API errors. Computes `has_activity` (any events at all), `has_errors` (any fetch failures), and `all_failed` (every repo failed). These boolean flags drive the Route by Activity Status switch node downstream.\n\nOutput structure:\n- `repos`: array of per-repo stats\n- `totals`: aggregated counts\n- `contributors`: top 10 contributors sorted by activity\n- `most_active_repo`: repo name with highest commit count\n- `activity_percent`: percentage of total activity from most active repo\n- `has_activity`: boolean \u2014 true if any repo had events\n- `has_errors`: boolean \u2014 true if any repo had a fetch error\n- `all_failed`: boolean \u2014 true if every repo had a fetch error\n- `period`: since/until ISO timestamps"
      },
      "typeVersion": 1
    },
    {
      "id": "aecbce08-fcad-407e-8a9e-a8751cd84af9",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -480,
        -848
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 1040,
        "content": "## Determine Report Path\n\nSwitch node that routes the workflow based on aggregated activity status. Three visible outputs \u2014 no hidden logic.\n\n**Route by Activity Status** \u2014 Switch node with 2 rules + 1 fallback:\n\n| Output | Condition | Route |\n|---|---|---|\n| Has Activity | `has_activity` equals \"true\" | \u2192 Build Report Components (generate AI digest) |\n| All Failed | `all_failed` equals \"true\" | \u2192 Create API Error Message (send error alert) |\n| Fallback | Neither condition matched | \u2192 Workflow stops silently (no activity, no errors) |\n\nBoth conditions use `String($json.has_activity)` and `String($json.all_failed)` with string comparison to avoid boolean type mismatch errors.\n\n**Create API Error Message** \u2014 Code node that builds a user-friendly error message listing all repos that failed to fetch, with the specific error from each. Only fires when all repos have `fetch_error` set \u2014 meaning every GitHub API call failed.\n\n**Send Error Alert** \u2014 Telegram node that delivers the API error message. Uses the same chat ID as the main report sender.\n\nThis routing ensures:\n- Normal weeks with activity \u2192 AI-generated report\n- Total GitHub API failure \u2192 clear error message with actionable info\n- Quiet weeks (no activity, no errors) \u2192 silent stop, no spam"
      },
      "typeVersion": 1
    },
    {
      "id": "cd8741dc-0cf4-4c95-8742-022ee3a917c1",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        48,
        -1024
      ],
      "parameters": {
        "color": 7,
        "width": 704,
        "height": 1088,
        "content": "## Generate and Format Report\n\nThree nodes that turn aggregated stats into Telegram-ready messages using AI, plus a fallback for when the LLM is unavailable.\n\n**Build Report Components** \u2014 Chain LLM node that sends the aggregated stats to Qwen 3 via OpenRouter. The prompt includes all repo data, contributor rankings, and activity totals. It also reads the command from the Parse Telegram Command node to determine output format:\n- `/report` \u2192 4-part weekly digest separated by `|||`\n- `/issues` \u2192 Short issue list (1 message)\n- `/prs` \u2192 PR status (1 message)\n- `/status` \u2192 Health check with emoji per repo (1 message)\n\nThe `onError: continueErrorOutput` setting ensures that if the LLM call fails (timeout, rate limit, model unavailable), the error branch fires instead of crashing the workflow.\n\nCredential needed: OpenAI API credential (Base URL: `https://openrouter.ai/api/v1`, Key: your OpenRouter API key). Model: `qwen/qwen3-235b-a22b-2507` \u2014 can be swapped for any OpenRouter-supported model.\n\n**OpenAI Report Model** \u2014 Sub-node providing the language model to Build Report Components. Change the model name here to switch to GPT-4o, Claude, Gemini, or any other model available on OpenRouter.\n\n**Format Telegram Messages** \u2014 Code node that parses the LLM output. Strips Qwen 3 thinking tags (`&lt;think&gt;...&lt;/think&gt;`), splits multi-part reports by `|||` delimiter, falls back to double-newline splitting if `|||` was not found, enforces 3800-character limit per message (Telegram caps at 4096), and appends a branded footer to the last message only.\n\n**Create Fallback Report** \u2014 Error branch from Build Report Components. If the LLM call fails entirely, this node formats raw stats into a readable report without AI. Adds a note: \"AI summary unavailable \u2014 showing raw stats.\" The report still gets delivered \u2014 the user is never left without feedback.\n\nBoth Format Telegram Messages and Create Fallback Report converge at the Send Messages to Telegram node downstream."
      },
      "typeVersion": 1
    },
    {
      "id": "618c0688-c898-41b0-9fbf-2612641f470e",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        816,
        -768
      ],
      "parameters": {
        "color": 7,
        "width": 352,
        "height": 688,
        "content": "## Send Telegram Notifications\n\nTwo Telegram nodes that deliver all output types to the user.\n\n**Send Messages to Telegram** \u2014 Telegram node that sends the formatted report messages (AI-generated or fallback) to the user's chat. Both Format Telegram Messages and Create Fallback Report converge here \u2014 every successful report path ends with this node.\n\nSet `chatId` to your Telegram chat ID. `appendAttribution` is turned off to keep messages clean. If the chat ID is invalid or the bot was blocked by the user, the error is logged but does not crash the workflow.\n\n**Send Error Alert** \u2014 Telegram node that sends API error messages when all repositories fail to fetch. Set `chatId` to the same value as Send Messages to Telegram.\n\nBoth nodes use the same Telegram Bot API credential as the trigger node.\n\nCredential needed: Telegram Bot API (same token as Telegram Message Trigger)."
      },
      "typeVersion": 1
    },
    {
      "id": "07f1dda9-4509-4858-82ab-3c489f992e70",
      "name": "Weekly Digest Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -2064,
        -304
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [],
              "triggerAtHour": 9
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "029cde06-4a2f-44b5-973c-5d8ece6b3ff7",
      "name": "Code Filter by Date",
      "type": "n8n-nodes-base.code",
      "position": [
        -944,
        -240
      ],
      "parameters": {
        "jsCode": "var buildItems = $('Fetch GitHub Repositories').all();\nvar inputItems = $input.all();\n\n// Flatten: handle array response t\u1eeb HTTP Request\nvar events = [];\ninputItems.forEach(function(item) {\n  var data = item.json;\n  if (Array.isArray(data)) {\n    events = events.concat(data);\n  } else if (data && typeof data === 'object' && data.type) {\n    events.push(data);\n  }\n});\n\nvar now = new Date();\nvar since = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);\nvar until = now;\n\n\n// Group events theo repo name\nvar repoGroups = {};\nevents.forEach(function(ev) {\n  var repoName = (ev.repo && ev.repo.name) ? ev.repo.name : 'unknown';\n  if (!repoGroups[repoName]) repoGroups[repoName] = [];\n  repoGroups[repoName].push(ev);\n});\n\nvar allRepoNames = buildItems.map(function(item) { return item.json.full_name; });\nvar results = [];\n\nallRepoNames.forEach(function(repoName) {\n  var repoEvents = repoGroups[repoName] || [];\n\n  if (repoEvents.length === 0) {\n    results.push({\n      json: {\n        repo: repoName,\n        fetch_error: null,\n        stats: {\n          total_events: 0, pushes: 0, issues: 0, pull_requests: 0,\n          releases: 0, commits: 0, contributors: [],\n          is_stale: true, dominant_contributor: null\n        }\n      }\n    });\n    return;\n  }\n\n  var filtered = repoEvents.filter(function(ev) {\n    if (!ev.created_at) return false;\n    var evDate = new Date(ev.created_at);\n    return evDate >= since && evDate <= until;\n  });\n\n  var pushes = 0, issues = 0, prs = 0, releases = 0, commits = 0;\n  var contributors = {};\n\n  filtered.forEach(function(ev) {\n    var actor = (ev.actor && ev.actor.login) ? ev.actor.login : 'unknown';\n    if (!contributors[actor]) contributors[actor] = 0;\n\n    if (ev.type === 'PushEvent') {\n      pushes++;\n      var cc = (ev.payload && ev.payload.commits) ? ev.payload.commits.length : 0;\n      commits += cc;\n      contributors[actor] += cc;\n    } else if (ev.type === 'IssuesEvent') {\n      issues++;\n      contributors[actor]++;\n    } else if (ev.type === 'PullRequestEvent') {\n      prs++;\n      contributors[actor]++;\n    } else if (ev.type === 'ReleaseEvent') {\n      releases++;\n    } else {\n      contributors[actor]++;\n    }\n  });\n\n  var contributorList = Object.keys(contributors).map(function(name) {\n    return { name: name, count: contributors[name] };\n  }).sort(function(a, b) { return b.count - a.count; });\n\n  results.push({\n    json: {\n      repo: repoName,\n      fetch_error: null,\n      stats: {\n        total_events: filtered.length,\n        pushes: pushes,\n        issues: issues,\n        pull_requests: prs,\n        releases: releases,\n        commits: commits,\n        contributors: contributorList,\n        is_stale: filtered.length === 0,\n        dominant_contributor: contributorList.length > 0 ? contributorList[0] : null\n      }\n    }\n  });\n});\n\nreturn results;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "b7f792bf-52f4-4a5c-9cde-786cefa80909",
      "name": "Aggregate Repositories",
      "type": "n8n-nodes-base.code",
      "position": [
        -688,
        -240
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\n\nvar repos = items.map(function(item) {\n  return {\n    repo: item.json.repo,\n    stats: item.json.stats,\n    fetch_error: item.json.fetch_error\n  };\n});\n\nvar totals = { commits: 0, issues: 0, pull_requests: 0, releases: 0, events: 0 };\nvar allContributors = {};\n\nrepos.forEach(function(r) {\n  totals.commits += r.stats.commits;\n  totals.issues += r.stats.issues;\n  totals.pull_requests += r.stats.pull_requests;\n  totals.releases += r.stats.releases;\n  totals.events += r.stats.total_events;\n\n  r.stats.contributors.forEach(function(c) {\n    if (!allContributors[c.name]) allContributors[c.name] = 0;\n    allContributors[c.name] += c.count;\n  });\n});\n\nvar contributorList = Object.keys(allContributors).map(function(name) {\n  return { name: name, count: allContributors[name] };\n}).sort(function(a, b) { return b.count - a.count; });\n\nvar activeRepos = repos.filter(function(r) { return !r.fetch_error && !r.stats.is_stale; });\nvar mostActive = activeRepos.length > 0\n  ? activeRepos.reduce(function(max, r) { return r.stats.commits > max.stats.commits ? r : max; }, activeRepos[0])\n  : null;\n\nvar activityPercent = mostActive && totals.commits > 0\n  ? Math.round(mostActive.stats.commits / totals.commits * 100)\n  : 0;\n\nvar staleRepos = repos.filter(function(r) { return r.stats.is_stale || r.fetch_error; });\nvar hasActivity = totals.events > 0;\n\nvar now = new Date();\nvar sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);\n\nreturn [{\n  json: {\n    repos: repos,\n    totals: totals,\n    contributors: contributorList.slice(0, 10),\n    most_active_repo: mostActive ? mostActive.repo : null,\n    activity_percent: activityPercent,\n    stale_repos: staleRepos.map(function(r) { return r.repo; }),\n    has_activity: hasActivity,\n    has_errors: repos.some(function(r) { return r.fetch_error; }),\n    all_failed: repos.every(function(r) { return r.fetch_error; }),\n    period: {\n      since: sevenDaysAgo.toISOString(),\n      until: now.toISOString()\n    }\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "2ef6e682-f453-4d77-be00-2b1062292876",
      "name": "Build Report Components",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "onError": "continueErrorOutput",
      "position": [
        96,
        -256
      ],
      "parameters": {
        "text": "=You are a technical project manager writing a GitHub activity report for a solo developer or small team.\n\nREQUEST MODE: {{ $('Parse Telegram Command').first().json.command }}\n\n {{ $json.period.until }}\n\nACTIVITY: {{ $json.totals.commits }} commits, {{ $json.totals.issues }} issues, {{ $json.totals.pull_requests }} PRs, {{ $json.totals.releases }} releases across {{ $json.repos.length }} repos.\n\nTOP CONTRIBUTORS: {{ $json.contributors.slice(0,5).map(c => c.name + ' (' + c.count + ')').join(', ') }}\n\nMOST ACTIVE REPO: {{ $json.most_active_repo }} ({{ $json.activity_percent }}% of total activity)\n\nREPOS:\n{{ $json.repos.map(r => '- ' + r.repo + ': ' + r.stats.commits + ' commits, ' + r.stats.issues + ' issues, ' + r.stats.pull_requests + ' PRs, ' + r.stats.releases + ' releases' + (r.stats.is_stale ? ' [STALE]' : '') + (r.fetch_error ? ' [ERROR]' : '')).join('\\n') }}\n\nFollow the instructions for the REQUEST MODE listed above:\n\n=== MODE: issues ===\nGenerate a SHORT issue report only (1 part, no ||| separator):\n- Title: \ud83d\udccb Open Issues Report \u2014 DD Mon \u2013 DD Mon YYYY\n- List all issues across repos with title, repo, state (open/closed), and age in days\n- Highlight issues older than 3 days with \u26a0\ufe0f\n- Group by repo\n- Skip commits, PRs, releases, contributors, insights, priorities\n- Keep it under 2000 characters\n\n=== MODE: prs ===\nGenerate a SHORT PR status report only (1 part, no ||| separator):\n- Title: \ud83d\udd00 Pull Request Status \u2014 DD Mon \u2013 DD Mon YYYY\n- List all open PRs with title, repo, author, age in days\n- List recently merged PRs (last 3 days) separately\n- If no PRs this week, say \"No pull requests this week\"\n- Skip commits, issues, contributors, insights, priorities\n- Keep it under 2000 characters\n\n=== MODE: status ===\nGenerate a ONE-MESSAGE health check (1 part, no ||| separator):\n- Title: \ud83e\ude7a Repository Health Check\n- Each repo: 1 line with emoji status\n  \ud83d\udfe2 Active (has events this week)\n  \ud83d\udfe1 Low activity (1-2 events)\n  \ud83d\udd34 Stale (0 events)\n  \u274c Error (fetch_error)\n- Total events this week across all repos\n- One-line recommendation if any repo needs attention\n- Keep it under 1000 characters\n\n=== MODE: report ===\nGenerate the full 4-part weekly digest. Separate each part with ||| (three pipes, no spaces around them).\n\nPart 1 \u2014 HEADER + OVERVIEW:\n- Title: \ud83d\udcca GitHub Weekly Digest\n- Show date range formatted as DD Mon \u2013 DD Mon YYYY\n- 2-3 sentence overview of the week activity\n- \ud83d\udd25 HIGHLIGHTS: Surface repo with more than 60% of total commits, any contributor dominating more than 70% of activity, any releases published\n- \u26a0\ufe0f NEEDS ATTENTION: Flag stale repos with 0 events, repos with API errors, repos where issues outnumber commits\n\nPart 2 \u2014 MOST ACTIVE REPO DETAIL:\n- Title: \ud83d\udd39 repo-name \u2014 X commits, X issues, X PRs\n- Activity breakdown with context\n- Key contributors this week\n- Trend: active / stable / stale / needs attention\n\nPart 3 \u2014 REMAINING REPOS:\n- Same format as Part 2, one block per remaining repo\n- If a repo had 0 events, note it may need maintenance\n- If a repo had an API error, note it\n\nPart 4 \u2014 INSIGHTS + PRIORITIES:\n- \ud83d\udcc8 WEEKLY INSIGHTS \u2014 activity concentration, growth trend, any risks\n- \ud83d\udc65 CONTRIBUTORS \u2014 top contributors and what they worked on\n- \ud83d\udd2e NEXT WEEK \u2014 3-4 prioritized action items based on the data\n\nRules:\n- Only output content matching the REQUEST MODE above\n- Each part under 3500 characters\n- Use emoji section headers\n- Be specific with numbers\n- Do NOT invent data not provided above\n- Write for a busy developer who wants to skim in 30 seconds\n",
        "batching": {},
        "promptType": "define"
      },
      "typeVersion": 1.9
    },
    {
      "id": "0590b8e1-0064-4b83-a923-00b2f76cd8b3",
      "name": "OpenAI Report Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        160,
        -80
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "qwen/qwen3-235b-a22b-2507",
          "cachedResultName": "qwen/qwen3-235b-a22b-2507"
        },
        "options": {},
        "builtInTools": {}
      },
      "typeVersion": 1.3
    },
    {
      "id": "5e081f92-ddb5-4cf5-ad61-025c316a4dda",
      "name": "Format Telegram Messages",
      "type": "n8n-nodes-base.code",
      "position": [
        608,
        -352
      ],
      "parameters": {
        "jsCode": "const llmOutput = $input.first().json;\n\nvar raw = llmOutput.output || llmOutput.text || '';\n\nraw = raw.replace(/&lt;think&gt;[\\s\\S]*?<\\/think>/gi, '').trim();\n\nvar parts = raw.split('|||').map(function(p) { return p.trim(); }).filter(function(p) { return p.length > 0; });\n\nif (parts.length <= 1 && raw.length > 500) {\n  parts = raw.split(/\\n{3,}/).map(function(p) { return p.trim(); }).filter(function(p) { return p.length > 0; });\n}\n\nif (parts.length === 0) {\n  parts = [raw || 'Weekly digest generated but content was empty.'];\n}\n\nvar messages = parts.map(function(part) {\n  var msg = part;\n  if (msg.length > 3800) {\n    msg = msg.substring(0, 3797) + '...';\n  }\n  return msg;\n});\n\nif (messages.length > 0) {\n  messages[messages.length - 1] += '\\n\\n\u2014\\nGitHub Activity Digest | Powered by n8n';\n}\n\nreturn messages.map(function(msg) { return { json: { message_text: msg } }; });"
      },
      "typeVersion": 2
    },
    {
      "id": "8e685c61-6797-470d-93f6-0ca14103faa9",
      "name": "Create Fallback Report",
      "type": "n8n-nodes-base.code",
      "position": [
        608,
        -112
      ],
      "parameters": {
        "jsCode": "const data = $('Aggregate Repositories').first().json;\n\nvar since = new Date(data.period.since);\nvar until = new Date(data.period.until);\nvar months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];\nvar start = months[since.getMonth()] + ' ' + since.getDate();\nvar end = months[until.getMonth()] + ' ' + until.getDate() + ', ' + until.getFullYear();\n\nvar msg = '\ud83d\udcca GitHub Weekly Digest\\n';\nmsg += start + ' \u2013 ' + end + '\\n\\n';\nmsg += '\u26a0\ufe0f AI summary unavailable \u2014 showing raw stats.\\n\\n';\n\nmsg += 'TOTAL: ' + data.totals.commits + ' commits, ';\nmsg += data.totals.issues + ' issues, ';\nmsg += data.totals.pull_requests + ' PRs across ';\nmsg += data.repos.length + ' repos\\n';\nmsg += 'Contributors: ' + data.contributors.map(function(c) { return c.name; }).join(', ') + '\\n\\n';\n\ndata.repos.forEach(function(r) {\n  msg += '\ud83d\udd39 ' + r.repo + '\\n';\n  msg += '  Commits: ' + r.stats.commits + ' | Issues: ' + r.stats.issues + ' | PRs: ' + r.stats.pull_requests + '\\n';\n  if (r.stats.is_stale) msg += '  \u26a0\ufe0f No activity this week\\n';\n  if (r.fetch_error) msg += '  \u274c Error: ' + r.fetch_error + '\\n';\n  msg += '\\n';\n});\n\nmsg += '\u2014\\nGitHub Activity Digest | Powered by n8n';\n\nif (msg.length > 3800) msg = msg.substring(0, 3797) + '...';\n\nreturn [{ json: { message_text: msg } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "ad6949fa-81f0-473d-8cf4-b6a235f8976f",
      "name": "Send Messages to Telegram",
      "type": "n8n-nodes-base.telegram",
      "position": [
        880,
        -240
      ],
      "parameters": {
        "text": "={{ $json.message_text }}",
        "chatId": "YOUR_CHAT_ID",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "d7b13e83-ba80-41a5-970e-e0a5d5f23454",
      "name": "Create API Error Message",
      "type": "n8n-nodes-base.code",
      "position": [
        -416,
        16
      ],
      "parameters": {
        "jsCode": "const data = $input.first().json;\nvar errorLines = data.repos\n  .filter(function(r) { return r.fetch_error; })\n  .map(function(r) { return '- ' + r.repo + ': ' + r.fetch_error; });\n\nvar msg = '\u26a0\ufe0f GitHub Activity Digest\\n\\n';\nmsg += 'All repositories failed to fetch this week.\\n\\n';\nmsg += 'Errors:\\n' + errorLines.join('\\n') + '\\n\\n';\nmsg += 'Please check your GitHub token and repository access.';\n\nreturn [{ json: { message_text: msg } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "d00f18de-ca4f-444d-8af0-05b7124913de",
      "name": "Send Error Alert",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -160,
        16
      ],
      "parameters": {
        "text": "={{ $json.message_text }}",
        "chatId": "YOUR_CHAT_ID",
        "additionalFields": {}
      },
      "typeVersion": 1.2
    },
    {
      "id": "4310d964-20a8-436a-af65-acde2fb05f4d",
      "name": "Route by Activity Status",
      "type": "n8n-nodes-base.switch",
      "position": [
        -400,
        -240
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Has Activity",
              "conditions": {
                "options": {
                  "version": 3,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "2c6d0fe8-4212-4151-ab59-acb62bbb17e3",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ String($json.has_activity) }}",
                    "rightValue": "true"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "All Failed",
              "conditions": {
                "options": {
                  "version": 3,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "513159d3-d55f-4430-824a-9d74c1b60871",
                    "operator": {
                      "name": "filter.operator.equals",
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ String($json.all_failed) }}",
                    "rightValue": "true"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.4
    },
    {
      "id": "57f7678d-ca6f-4a82-93b2-e95b603fdacf",
      "name": "Fetch GitHub Repositories",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1472,
        -240
      ],
      "parameters": {
        "url": "https://api.github.com/user/repos",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendQuery": true,
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "per_page",
              "value": "100"
            },
            {
              "name": "sort",
              "value": "updated"
            },
            {
              "name": "direction",
              "value": "desc"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/vnd.github+json"
            },
            {
              "name": "X-GitHub-Api-Version",
              "value": "2022-11-28"
            }
          ]
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "7a214afa-e765-4bdb-a38a-f2a18d463902",
      "name": "Telegram Message Trigger",
      "type": "n8n-nodes-base.telegramTrigger",
      "position": [
        -2080,
        -96
      ],
      "parameters": {
        "updates": [
          "message"
        ],
        "additionalFields": {}
      },
      "typeVersion": 1.2
    },
    {
      "id": "f5a7d61a-b6cf-4275-8f6f-add33eb1550d",
      "name": "Parse Telegram Command",
      "type": "n8n-nodes-base.code",
      "position": [
        -1760,
        -96
      ],
      "parameters": {
        "jsCode": "var text = $input.first().json.message.text || '';\nvar chat_id = $input.first().json.message.chat.id;\n\nif (!text.startsWith('/')) {\n  return [];\n}\n\nvar command = text.split(' ')[0].replace('/', '').toLowerCase();\n\nvar valid = ['report', 'issues', 'prs', 'status'];\nif (valid.indexOf(command) === -1) {\n  return [];\n}\n\nreturn [{\n  json: {\n    command: command,\n    chat_id: chat_id\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "6c530d7c-f0e9-4860-83bc-10f7c405bd2a",
      "name": "Retrieve GitHub Events",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1200,
        -240
      ],
      "parameters": {
        "url": "=https://api.github.com/repos/{{ $json.full_name }}/events\n",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendQuery": true,
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "per_page",
              "value": "100"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/vnd.github+json"
            },
            {
              "name": "X-GitHub-Api-Version",
              "value": "2022-11-28"
            }
          ]
        }
      },
      "typeVersion": 4.4
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "f2c23d8d-ff0c-47e9-aa49-606f2cff0550",
  "connections": {
    "Code Filter by Date": {
      "main": [
        [
          {
            "node": "Aggregate Repositories",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Report Model": {
      "ai_languageModel": [
        [
          {
            "node": "Build Report Components",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Weekly Digest Trigger": {
      "main": [
        [
          {
            "node": "Fetch GitHub Repositories",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Repositories": {
      "main": [
        [
          {
            "node": "Route by Activity Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Fallback Report": {
      "main": [
        [
          {
            "node": "Send Messages to Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Telegram Command": {
      "main": [
        [
          {
            "node": "Fetch GitHub Repositories",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retrieve GitHub Events": {
      "main": [
        [
          {
            "node": "Code Filter by Date",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Report Components": {
      "main": [
        [
          {
            "node": "Format Telegram Messages",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Create Fallback Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create API Error Message": {
      "main": [
        [
          {
            "node": "Send Error Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Telegram Messages": {
      "main": [
        [
          {
            "node": "Send Messages to Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Activity Status": {
      "main": [
        [
          {
            "node": "Build Report Components",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Create API Error Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Telegram Message Trigger": {
      "main": [
        [
          {
            "node": "Parse Telegram Command",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch GitHub Repositories": {
      "main": [
        [
          {
            "node": "Retrieve GitHub Events",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

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

About this workflow

This workflow runs weekly or on demand via Telegram commands (/report, /issues, /prs, /status) to fetch all your GitHub repositories and events, summarizes the last 7 days using an OpenRouter-hosted Qwen model, and sends a multi-part engineering digest to your Telegram chat. No…

Source: https://n8n.io/workflows/15898/ — 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

Chanchito_PROD. Uses googleGemini, postgres, telegram, httpRequest. Scheduled trigger; 94 nodes.

Google Gemini, Postgres, Telegram +4
AI & RAG

A Telegram bot that converts natural-language work descriptions into detailed cost estimates using AI parsing, vector search, and the open-source DDC CWICR database with 55,000+ construction work item

HTTP Request, Telegram, Telegram Trigger +6
AI & RAG

Bitlab-Chatbot. Uses telegramTrigger, telegram, snowflake, httpRequest. Event-driven trigger; 87 nodes.

Telegram Trigger, Telegram, Snowflake +13
AI & RAG

System Architecture Two integrated N8N workflows providing automated US stock portfolio management through Telegram:

Output Parser Autofixing, OpenAI Chat, Perplexity +10
AI & RAG

leads. Uses supabase, gmail, formTrigger, httpRequest. Webhook trigger; 62 nodes.

Supabase, Gmail, Form Trigger +13