This workflow corresponds to n8n.io template #4878 — we link there as the canonical source.
This workflow follows the GitHub → 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 →
{
"id": "BnXe0TTG2ep3jJvY",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "github-forked-repo-stats",
"tags": [
{
"id": "ByjgsuAI1ZOmR1Cv",
"name": "github",
"createdAt": "2025-06-10T19:03:59.044Z",
"updatedAt": "2025-06-10T19:03:59.044Z"
}
],
"nodes": [
{
"id": "d27cc197-fa61-421b-8422-8c4e5f716efe",
"name": "When clicking \u2018Execute workflow\u2019",
"type": "n8n-nodes-base.manualTrigger",
"position": [
380,
-260
],
"parameters": {},
"typeVersion": 1
},
{
"id": "5d55b266-a8b8-450f-903c-b04f47a1d708",
"name": "Loop Over Items",
"type": "n8n-nodes-base.splitInBatches",
"position": [
1160,
-80
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "c160d1cf-67a0-4e5a-9a0d-f6c78ad18154",
"name": "get-repo-details",
"type": "n8n-nodes-base.github",
"position": [
1380,
-80
],
"parameters": {
"owner": {
"__rl": true,
"mode": "name",
"value": "={{ $json.owner.login }}"
},
"resource": "repository",
"operation": "get",
"repository": {
"__rl": true,
"mode": "url",
"value": "={{ $json.html_url }}"
}
},
"credentials": {
"githubApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.1
},
{
"id": "7580ab52-4cfc-490f-9f3e-021bba782db4",
"name": "Telegram",
"type": "n8n-nodes-base.telegram",
"position": [
1740,
-480
],
"parameters": {
"text": "={{ $json.telegramMessage }}",
"chatId": "123456789",
"additionalFields": {
"appendAttribution": false
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "f0170087-277f-4f32-bc45-305dd56211df",
"name": "Telegram Trigger",
"type": "n8n-nodes-base.telegramTrigger",
"position": [
-40,
-40
],
"parameters": {
"updates": [
"message"
],
"additionalFields": {}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "f22d2c47-5d0a-4574-8293-e7ddc9a326ac",
"name": "Prepare Upstream URL",
"type": "n8n-nodes-base.code",
"position": [
1600,
-80
],
"parameters": {
"jsCode": "const items = [];\n\nconsole.log(`Code Node (Prepare Upstream URL): Received ${$input.all().length} input items.`);\n\nfor (const item of $input.all()) {\n const forkRepo = item.json;\n\n console.log(`Processing repo: ${forkRepo.full_name || 'N/A'}`);\n\n if (forkRepo && forkRepo.fork && forkRepo.parent) {\n const upstreamRepo = forkRepo.parent;\n\n const upstreamOwner = upstreamRepo.owner.login;\n const upstreamName = upstreamRepo.name;\n const upstreamDefaultBranch = upstreamRepo.default_branch;\n\n const forkOwner = forkRepo.owner.login;\n const forkName = forkRepo.name;\n const forkDefaultBranch = forkRepo.default_branch;\n\n const compareUrl = `https://api.github.com/repos/${upstreamOwner}/${upstreamName}/compare/${upstreamDefaultBranch}...${forkOwner}:${forkDefaultBranch}`;\n\n const newItem = {\n json: {\n // This is the URL the HTTP Request node will use\n compareApiUrl: compareUrl,\n\n // <<<< CRITICAL: Embed ALL original data here in a nested object >>>>\n // This relies on the HTTP Request node passing this nested object through\n original_repo_data: {\n repoName: forkRepo.full_name,\n upstreamUrl: upstreamRepo.html_url,\n forkDefaultBranch: forkDefaultBranch,\n upstreamDefaultBranch: upstreamDefaultBranch,\n upstreamRepoName: upstreamName,\n upstreamOwner: upstreamOwner,\n }\n // <<<< END CRITICAL >>>>\n }\n };\n\n items.push(newItem);\n console.log(`--> Prepared item for: ${forkRepo.full_name}. Compare URL: ${compareUrl}`);\n\n } else {\n let skipReason = \"Not a fork or missing parent\";\n if (forkRepo) {\n if (!forkRepo.fork) skipReason = \"Not marked as a fork\";\n if (!forkRepo.parent) skipReason = \"Missing parent (upstream) repository\";\n }\n console.log(`Skipping repo: ${forkRepo.full_name || 'N/A'} - Reason: ${skipReason}`);\n }\n}\n\nconsole.log(`Code Node (Prepare Upstream URL): Returning ${items.length} items.`);\n\nreturn items;"
},
"typeVersion": 2
},
{
"id": "52d91917-e7d0-4a5a-b73b-d7c6f4041603",
"name": "Compare Branches API Call",
"type": "n8n-nodes-base.httpRequest",
"position": [
1820,
-80
],
"parameters": {
"url": "={{ $json.compareApiUrl }}",
"options": {},
"sendHeaders": true,
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/vnd.github.v3+json"
}
]
}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "f08f4f87-d7a6-49d4-99a2-04790a98b58d",
"name": "Process Comparison Result",
"type": "n8n-nodes-base.code",
"position": [
2040,
-80
],
"parameters": {
"jsCode": "const items = [];\n\nconsole.log(`Code Node (Process Combined Result): Received ${$input.all().length} input items.`);\n\nfor (const item of $input.all()) {\n // Original repo data is picked up from 2 nodes earlier\n const originalRepoData = $('Prepare Upstream URL').first().json.original_repo_data\n\n // API response data is now in item.json\n const apiResponse = item.json;\n console.log(\"apiResponse\",apiResponse);\n\n\n // Debugging: Check if data is available\n if (typeof apiResponse.behind_by === 'undefined' || typeof apiResponse.ahead_by === 'undefined') {\n console.error(`ERROR: Missing or malformed data after merge! Original:`, originalRepoData, `API:`, apiResponse);\n // Push an error item or skip\n items.push({\n json: {\n repoName: originalRepoData?.repoName || 'N/A', // Use optional chaining for safety\n status: \"ERROR_PROCESSING_MERGE\",\n commitsBehind: -1,\n commitsAhead: -1\n }\n });\n continue;\n }\n\n const commitsBehind = apiResponse.behind_by;\n const commitsAhead = apiResponse.ahead_by;\n const status = commitsBehind > 0 ? \"BEHIND\" : (commitsAhead > 0 ? \"AHEAD\" : \"UP-TO-DATE\");\n\n\n\n const processedItem = {\n json: {\n repoName: originalRepoData.repoName,\n upstreamUrl: originalRepoData.upstreamUrl,\n forkDefaultBranch: originalRepoData.forkDefaultBranch,\n upstreamDefaultBranch: originalRepoData.upstreamDefaultBranch,\n upstreamRepoName: originalRepoData.upstreamRepoName,\n upstreamOwner: originalRepoData.upstreamOwner,\n status: status,\n commitsBehind: commitsBehind,\n commitsAhead: commitsAhead\n }\n };\n items.push(processedItem);\n console.log(`--> Processed ${processedItem.json.repoName}: Status=${processedItem.json.status}, Behind=${processedItem.json.commitsBehind}, Ahead=${processedItem.json.commitsAhead}`);\n}\nconsole.log(`Code Node (Process Combined Result): Returning ${items.length} items.`);\nreturn items;"
},
"typeVersion": 2
},
{
"id": "19d0624b-1773-41db-8a37-bbee490b7f50",
"name": "Combine Results",
"type": "n8n-nodes-base.merge",
"position": [
2260,
0
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "5f4fd86e-9c37-4bb3-a55f-2db724ed9bd4",
"name": "Format for Telegram",
"type": "n8n-nodes-base.code",
"position": [
1520,
-480
],
"parameters": {
"jsCode": "const items = $input.all();\nlet telegramMessage = `*GitHub Fork Status Report - ${new Date().toLocaleDateString()}*\\n\\n`;\n\n// Initialize counters for summary\nlet reposBehind = 0;\nlet reposAhead = 0;\nlet reposUpToDate = 0;\n\n// ... (previous code)\n\n// Build the table header\ntelegramMessage += `| Ahead | Behind | Repository |\\n`;\ntelegramMessage += `|------------|------------|-----------------------|\\n`;\n\n// Populate table rows\nfor (const item of items) {\n const data = item.json;\n const repoName = data.repoName;\n const commitsAhead = data.commitsAhead;\n const commitsBehind = data.commitsBehind;\n\n // Update counters for summary\n if (commitsBehind > 0) {\n reposBehind++;\n } else if (commitsAhead > 0) {\n reposAhead++;\n } else {\n reposUpToDate++;\n continue;\n }\n\n // --- CRITICAL FIX START ---\n // Ensure you use BACKTICKS ( ` ) for the string literal\n // And standard JavaScript template literal syntax ${variableName}\n\n // Escape characters in the repo name for Markdown if necessary, but usually not for links like this\n // For general text: `repoName.replace(/[-_.*[\\]()~`>#+\\-=|{}.!]/g, '\\\\$&')`\n // But for links, usually the raw name is fine within `[]`\n const escapedRepoName = repoName.replace(/_/g, '\\\\_').replace(/\\*/g, '\\\\*').replace(/\\[/g, '\\\\[').replace(/\\]/g, '\\\\]').replace(/\\(/g, '\\\\(').replace(/\\)/g, '\\\\)').replace(/~/g, '\\\\~').replace(/`/g, '\\\\`').replace(/>/g, '\\\\>').replace(/#/g, '\\\\#').replace(/\\+/g, '\\\\+').replace(/-/g, '\\\\-').replace(/=/g, '\\\\=').replace(/\\|/g, '\\\\|').replace(/{/g, '\\\\{').replace(/}/g, '\\\\}').replace(/\\./g, '\\\\.').replace(/!/g, '\\\\!');\n\n\n // Use [text](url) format for Telegram MarkdownV2 links\n const repoLink = `[${escapedRepoName}](https://github.com/${repoName})`; // Changed from <url|text>\n // --- CRITICAL FIX END ---\n // Format commitsAhead and commitsBehind to be 4 characters wide, right-aligned\n const formattedAhead = String(commitsAhead).padStart(5, '0');\n const formattedBehind = String(commitsBehind).padStart(5, '0');\n telegramMessage += `| ${formattedAhead} | ${formattedBehind} | ${repoLink} |\\n`;\n}\n\n// ... (rest of the code)\n\n\nreturn [{ json: { telegramMessage: telegramMessage } }];"
},
"typeVersion": 2
},
{
"id": "ea7c4abd-2748-4e9e-8e44-eefb60757db6",
"name": "Code",
"type": "n8n-nodes-base.code",
"position": [
180,
-40
],
"parameters": {
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.repos = processCommand(item.json.message.text);\n}\n\nreturn $input.all();\n\nfunction processCommand(input) {\n const parts = input.trim().split(\" \"); // Split by space and remove extra spaces\n const command = parts[0];\n const number = parseInt(parts[1], 10); // Convert second part to an integer\n\n let repos = 0;\n\n if (command === \"/forkcheck\") {\n if (Number.isInteger(number) && number > 0) {\n repos = number;\n } else {\n repos = 1000; // Default value if blank or 0\n }\n console.log(`Repos set to: ${repos}`);\n } else {\n console.log(\"Invalid command.\");\n }\nreturn repos;\n}\n\n"
},
"typeVersion": 2
},
{
"id": "d9aebe90-f406-4152-bf15-dc73aeacdb6d",
"name": "If",
"type": "n8n-nodes-base.if",
"position": [
380,
-40
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "ae98c5c9-a1df-4458-862f-32555165bae1",
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $json.repos }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.2
},
{
"id": "d926e485-2d76-4c10-a231-5d5e4cc6b1ae",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-200,
-400
],
"parameters": {
"width": 800,
"height": 640,
"content": "## Triggers - *Set your triggers here*\n\n\n\n## Default\n- Manual\n- Telegram bot with command /forkcheck\n\n## Usage\n- /forkcheck = 1000 repositoriesmax\n- /forkcheck n = n repositories\n- any other command is rejected\n"
},
"typeVersion": 1
},
{
"id": "53fde103-cbeb-4932-9027-f779c93b441c",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
620,
-400
],
"parameters": {
"color": 5,
"width": 420,
"height": 640,
"content": "## Filter repositories\n\nSelect only forked repositories\nRequired argument: Count to select repositories"
},
"typeVersion": 1
},
{
"id": "45b0716e-993f-478a-af4f-ead51f5132c5",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
1060,
-260
],
"parameters": {
"width": 1360,
"height": 500,
"content": "## Processing logic"
},
"typeVersion": 1
},
{
"id": "b7349c8b-d4cb-4636-a788-673dfc4fff1a",
"name": "Get repos",
"type": "n8n-nodes-base.github",
"position": [
660,
-80
],
"parameters": {
"limit": "={{ $json.repos }}",
"owner": {
"__rl": true,
"mode": "list",
"value": "mk-in",
"cachedResultUrl": "https://github.com/mk-in",
"cachedResultName": "mk-in"
},
"resource": "user"
},
"credentials": {
"githubApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.1
},
{
"id": "c85f5822-2b50-4f1d-b479-47ff8c4ef6e6",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
1060,
-580
],
"parameters": {
"color": 3,
"width": 1360,
"height": 300,
"content": "## Summarize and send"
},
"typeVersion": 1
},
{
"id": "e5c4395c-8598-4da5-87a5-f8e5395ada44",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-200,
-580
],
"parameters": {
"width": 1240,
"content": "## GitHub Fork Status Monitor\n\nThis workflow helps you keep an eye on your GitHub forks, notifying you when they fall behind or pull ahead of their upstream repositories."
},
"typeVersion": 1
},
{
"id": "bd807482-d2c7-4cb8-aab6-854e816ab25a",
"name": "Filter forked repositories only",
"type": "n8n-nodes-base.filter",
"position": [
880,
-80
],
"parameters": {
"options": {
"ignoreCase": false
},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "651ce98d-bf86-474a-b376-05fb5233fbee",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.fork }}",
"rightValue": "true"
}
]
},
"looseTypeValidation": true
},
"typeVersion": 2.2
}
],
"active": true,
"settings": {
"executionOrder": "v1"
},
"versionId": "a9ced36c-710d-4306-a51d-50b419cf8c86",
"connections": {
"If": {
"main": [
[
{
"node": "Get repos",
"type": "main",
"index": 0
}
]
]
},
"Code": {
"main": [
[
{
"node": "If",
"type": "main",
"index": 0
}
]
]
},
"Get repos": {
"main": [
[
{
"node": "Filter forked repositories only",
"type": "main",
"index": 0
}
]
]
},
"Combine Results": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Items": {
"main": [
[
{
"node": "Format for Telegram",
"type": "main",
"index": 0
}
],
[
{
"node": "get-repo-details",
"type": "main",
"index": 0
}
]
]
},
"Telegram Trigger": {
"main": [
[
{
"node": "Code",
"type": "main",
"index": 0
}
]
]
},
"get-repo-details": {
"main": [
[
{
"node": "Prepare Upstream URL",
"type": "main",
"index": 0
}
]
]
},
"Format for Telegram": {
"main": [
[
{
"node": "Telegram",
"type": "main",
"index": 0
}
]
]
},
"Prepare Upstream URL": {
"main": [
[
{
"node": "Compare Branches API Call",
"type": "main",
"index": 0
}
]
]
},
"Compare Branches API Call": {
"main": [
[
{
"node": "Process Comparison Result",
"type": "main",
"index": 0
}
]
]
},
"Process Comparison Result": {
"main": [
[
{
"node": "Combine Results",
"type": "main",
"index": 0
}
]
]
},
"Filter forked repositories only": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"When clicking \u2018Execute workflow\u2019": {
"main": [
[
{
"node": "Get repos",
"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.
githubApihttpHeaderAuthtelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow helps you keep an eye on your GitHub forks, notifying you when they fall behind or pull ahead of their upstream repositories.
Source: https://n8n.io/workflows/4878/ — 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.
N8N Complete Final. Uses telegramTrigger, dataTable, telegram, mqtt. Event-driven trigger; 58 nodes.
TextMain. Uses telegramTrigger, stopAndError, telegram, httpRequest. Event-driven trigger; 56 nodes.
Pede Ai. Uses httpRequest, telegram, postgres, telegramTrigger. Event-driven trigger; 53 nodes.
📄 Documentation: Notion Guide
Telegram Wait. Uses stickyNote, httpRequest, redis, noOp. Event-driven trigger; 36 nodes.