AutomationFlowsData & Sheets › Track Github Trending Repositories with Scrapeops & Google Sheets

Track Github Trending Repositories with Scrapeops & Google Sheets

ByIan Kerins @iankerins on n8n.io

This n8n template tracks GitHub Trending repositories (daily/weekly/monthly), parses the trending page into structured data (rank, repo name, stars, language, etc.), and stores results in Google Sheets with automatic deduping. It’s designed for teams who want a simple “trending…

Cron / scheduled trigger★★★★☆ complexity18 nodes@Scrapeops/N8N Nodes ScrapeopsGoogle Sheets
Data & Sheets Trigger: Cron / scheduled Nodes: 18 Complexity: ★★★★☆ Added:

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

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": "VKCnlRbfmd3GmvXZ",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "GitHub Trending Dev Tools",
  "tags": [
    {
      "id": "EwNZ2taImfwXNIV6",
      "name": "github",
      "createdAt": "2025-12-11T12:51:23.257Z",
      "updatedAt": "2025-12-11T12:51:23.257Z"
    },
    {
      "id": "851ehcNzkj5GLim3",
      "name": "scraping",
      "createdAt": "2025-12-11T12:52:06.933Z",
      "updatedAt": "2025-12-11T12:52:06.933Z"
    },
    {
      "id": "lZKSh2IoxHklnOUw",
      "name": "ScrapeOps",
      "createdAt": "2025-10-20T20:27:13.410Z",
      "updatedAt": "2025-10-20T20:27:13.410Z"
    },
    {
      "id": "VtV62IGgHutlusaN",
      "name": "devtools",
      "createdAt": "2025-12-11T12:52:38.192Z",
      "updatedAt": "2025-12-11T12:52:38.192Z"
    },
    {
      "id": "yh7XX4GObvd3np2S",
      "name": "google sheets",
      "createdAt": "2025-11-06T11:10:29.979Z",
      "updatedAt": "2025-11-06T11:10:29.979Z"
    },
    {
      "id": "5DcXvqnjGQotjqKa",
      "name": "trending",
      "createdAt": "2025-12-11T12:52:53.810Z",
      "updatedAt": "2025-12-11T12:52:53.810Z"
    }
  ],
  "nodes": [
    {
      "id": "f0f306fd-4fe7-46db-8761-7980eef55bdb",
      "name": "Cron (daily/weekly)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -1104,
        -80
      ],
      "parameters": {
        "rule": {
          "interval": [
            {}
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "ab4b695b-de79-4729-9463-8b59c047be2c",
      "name": "Build URLs",
      "type": "n8n-nodes-base.code",
      "position": [
        -656,
        -80
      ],
      "parameters": {
        "jsCode": "// Build URLs (n8n Code node)\n\nconst input = items[0]?.json ?? {};\n\n// since: daily | weekly | monthly\nconst since = String(input.since || 'daily').toLowerCase().trim();\n\n// languages_csv example: \"any,JavaScript,TypeScript,Python,Go,Rust,C++,Java\"\nconst languagesCsv = String(input.languages_csv || 'any').trim();\n\n// Normalize \"since\"\nconst allowedSince = new Set(['daily', 'weekly', 'monthly']);\nconst sinceFinal = allowedSince.has(since) ? since : 'daily';\n\n// Parse languages\nlet langs = languagesCsv\n  .split(',')\n  .map(s => s.trim())\n  .filter(Boolean);\n\n// remove duplicates\nlangs = [...new Set(langs.map(l => l.toLowerCase()))];\n\n// map normalized language -> GitHub path slug\nconst langSlugMap = {\n  'any': null,\n  'javascript': 'javascript',\n  'typescript': 'typescript',\n  'python': 'python',\n  'go': 'go',\n  'rust': 'rust',\n  'c++': 'c%2B%2B',     // URL encoded\n  'cpp': 'c%2B%2B',     // allow cpp too\n  'java': 'java',\n};\n\nconst nowIso = new Date().toISOString();\n\n// week_id like 2025-W50\nfunction getWeekId(d = new Date()) {\n  const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));\n  const dayNum = date.getUTCDay() || 7;\n  date.setUTCDate(date.getUTCDate() + 4 - dayNum);\n  const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));\n  const weekNo = Math.ceil((((date - yearStart) / 86400000) + 1) / 7);\n  return `${date.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`;\n}\n\nconst weekId = getWeekId(new Date());\n\n// Build URLs\nconst urls = [];\nfor (const l of langs) {\n  const slug = langSlugMap[l];\n  // skip unknown languages safely\n  if (slug === undefined) continue;\n\n  const base = slug ? `https://github.com/trending/${slug}` : `https://github.com/trending`;\n  const url = `${base}?since=${encodeURIComponent(sinceFinal)}`;\n\n  urls.push({\n    url,\n    source_url: url,\n    page_index: urls.length,\n    since: sinceFinal,\n    captured_at: nowIso,\n    week_id: weekId,\n    // keep your sheet config if you want:\n    sheet_id: input.sheet_id,\n    raw_sheet_name: input.raw_sheet_name || 'trending_raw',\n    brief_sheet_name: input.brief_sheet_name || 'weekly_brief',\n  });\n}\n\nreturn urls.map(x => ({ json: x }));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "fbb4a5fc-1222-48c0-a9dd-2efd06472d66",
      "name": "Split URLs (loop)",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -400,
        -80
      ],
      "parameters": {
        "options": {},
        "batchSize": 1
      },
      "typeVersion": 2
    },
    {
      "id": "e00a10e3-0650-467f-bda5-1ebd855d1779",
      "name": "ScrapeOps: Fetch HTML",
      "type": "@scrapeops/n8n-nodes-scrapeops.ScrapeOps",
      "position": [
        -176,
        -80
      ],
      "parameters": {
        "url": "={{$json.url}}",
        "advancedOptions": {}
      },
      "credentials": {
        "scrapeOpsApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "2d61a915-3d69-4fff-a179-150362f26a3c",
      "name": "Polite Delay",
      "type": "n8n-nodes-base.wait",
      "position": [
        64,
        -80
      ],
      "parameters": {
        "unit": "seconds",
        "amount": 2
      },
      "typeVersion": 1
    },
    {
      "id": "3d681415-dd9a-454d-a108-dc1c1ec9cdb8",
      "name": "Extract Trending Repos",
      "type": "n8n-nodes-base.code",
      "position": [
        320,
        -80
      ],
      "parameters": {
        "jsCode": "const html = $json.data || $json.body || $json || '';\nconst upstream = $items('Split URLs (loop)')[0]?.json || {};\n\nconst articles = [...String(html).matchAll(/<article[\\s\\S]*?<\\/article>/g)];\nlet rank = 0;\n\nconst cleanText = (s) => String(s || '').replace(/<[^>]+>/g,' ').replace(/\\s+/g,' ').trim();\n\nfunction extractTightDescription(block) {\n  const preferred = block.match(/<p[^>]*class=\"[^\"]*\\bcol-9\\b[^\"]*\"[^>]*>([\\s\\S]*?)<\\/p>/i);\n  if (preferred) return cleanText(preferred[1]);\n\n  const afterTitle = block.match(/<h2[\\s\\S]*?<\\/h2>[\\s\\S]*?<p[^>]*>([\\s\\S]*?)<\\/p>/i);\n  if (afterTitle) return cleanText(afterTitle[1]);\n\n  const anyP = block.match(/<p[^>]*>([\\s\\S]*?)<\\/p>/i);\n  return anyP ? cleanText(anyP[1]) : '';\n}\n\nfunction extractCountFromAnchor(block, hrefSuffix) {\n  const aMatch = block.match(new RegExp(`(<a[^>]*href=\\\"\\\\/[^\"]+\\\\/${hrefSuffix}\\\"[^>]*>[\\\\s\\\\S]*?<\\\\/a>)`, 'i'));\n  if (!aMatch) return '';\n  const text = cleanText(aMatch[1]);\n  const numMatch = text.match(/([0-9][0-9.,]*\\s*[kKmM]?)/);\n  return numMatch ? numMatch[1].replace(/\\s+/g,'') : '';\n}\n\nconst extracted = [];\nfor (const m of articles) {\n  const block = m[0];\n\n  const h2RepoMatch = block.match(/<h2[^>]*>[\\s\\S]*?<a[^>]*href=\\\"\\/([^\\/\\s]+)\\/([^\\/\"]+)\\\"/i);\n  if (!h2RepoMatch) continue;\n\n  const owner = h2RepoMatch[1].trim();\n  const repo = h2RepoMatch[2].trim();\n  if (!owner || !repo) continue;\n  if (owner.toLowerCase() === 'sponsors') continue;\n\n  rank += 1;\n\n  const full_name = `${owner}/${repo}`;\n  const repo_url = `https://github.com/${full_name}`;\n  const description = extractTightDescription(block);\n\n  const langMatch = block.match(/itemprop=\\\"programmingLanguage\\\"[^>]*>([^<]+)/i);\n  const language = langMatch ? cleanText(langMatch[1]) : '';\n\n  const stars_total_raw = extractCountFromAnchor(block, 'stargazers');\n  const forks_total_raw = extractCountFromAnchor(block, 'forks');\n\n  const starsInPeriodMatch = block.match(/([0-9.,kKmM]+)\\s+stars\\s+this\\s+week/i) || block.match(/([0-9.,kKmM]+)\\s+stars\\s+today/i) || block.match(/([0-9.,kKmM]+)\\s+stars\\s+this\\s+month/i);\n\n  extracted.push({\n    ...upstream,\n    rank_on_page: rank,\n    owner,\n    repo,\n    full_name,\n    repo_url,\n    description,\n    language,\n    stars_total_raw,\n    forks_total_raw,\n    stars_in_period_raw: starsInPeriodMatch?.[1] || ''\n  });\n}\n\nreturn extracted.map(r => ({ json: r }));"
      },
      "typeVersion": 2
    },
    {
      "id": "6df21227-e87a-4db6-96a1-efe0b60e5b58",
      "name": "Normalize + Enrich-from-page",
      "type": "n8n-nodes-base.code",
      "position": [
        576,
        -80
      ],
      "parameters": {
        "jsCode": "function parseNumber(s) {\n  if (!s) return 0;\n  const t = String(s).trim().toLowerCase();\n  if (t.endsWith('k')) return Math.round(parseFloat(t.slice(0, -1)) * 1000);\n  if (t.endsWith('m')) return Math.round(parseFloat(t.slice(0, -1)) * 1000000);\n  return parseInt(t.replace(/,/g, ''), 10) || 0;\n}\n\nreturn items.map((item) => {\n  const j = item.json || {};\n\n  const upstreamKeywords = (j.keywords || []).map(k => String(k).toLowerCase());\n  const langSet = new Set((j.languages || []).map(l => String(l).toLowerCase()));\n\n  const stars_total = parseNumber(j.stars_total_raw);\n  const forks_total = parseNumber(j.forks_total_raw);\n  const stars_in_period = parseNumber(j.stars_in_period_raw);\n\n  const text = `${j.full_name || ''} ${j.description || ''}`.toLowerCase();\n  const matches = upstreamKeywords.filter(k => k && text.includes(k));\n  const uniqueMatches = [...new Set(matches)];\n  const is_dev_tool = uniqueMatches.length > 0;\n\n  let keyword_bonus = 0;\n  if (is_dev_tool) keyword_bonus = Math.min(20, 10 + Math.max(0, (uniqueMatches.length - 1) * 2));\n  const language_bonus = (j.language && langSet.has(String(j.language).toLowerCase())) ? 3 : 0;\n  const score = (stars_in_period * 2) + keyword_bonus + language_bonus;\n\n  const dedupe_key = `${j.since}|${j.full_name}`;\n\n\n  return {\n    json: {\n      captured_at: j.captured_at,\n      week_id: j.week_id,\n      since: j.since,\n      source_url: j.source_url,\n      rank_on_page: j.rank_on_page,\n      full_name: j.full_name,\n      repo_url: j.repo_url,\n      owner: j.owner,\n      repo: j.repo,\n      description: j.description,\n      language: j.language,\n      stars_total,\n      forks_total,\n      stars_in_period,\n      keyword_matches: uniqueMatches.join(', '),\n      is_dev_tool,\n      score,\n      dedupe_key,\n      sheet_id: j.sheet_id,\n      raw_sheet_name: j.raw_sheet_name,\n      brief_sheet_name: j.brief_sheet_name\n    }\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "db2a227b-66d9-4c1f-8a25-21dba32aa701",
      "name": "Write to Google Sheets (raw tab)",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        864,
        -80
      ],
      "parameters": {
        "columns": {
          "value": {
            "repo": "={{$json.repo}}",
            "owner": "={{$json.owner}}",
            "score": "={{$json.score}}",
            "since": "={{$json.since}}",
            "week_id": "={{$json.week_id}}",
            "language": "={{$json.language}}",
            "repo_url": "={{$json.repo_url}}",
            "full_name": "={{$json.full_name}}",
            "dedupe_key": "={{$json.dedupe_key}}",
            "source_url": "={{$json.source_url}}",
            "captured_at": "={{$json.captured_at}}",
            "description": "={{$json.description}}",
            "forks_total": "={{$json.forks_total}}",
            "stars_total": "={{$json.stars_total}}",
            "rank_on_page": "={{$json.rank_on_page}}",
            "stars_in_period": "={{$json.stars_in_period}}"
          },
          "schema": [
            {
              "id": "captured_at",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "captured_at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "week_id",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "week_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "since",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "since",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "source_url",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "source_url",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "rank_on_page",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "rank_on_page",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "full_name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "full_name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "repo_url",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "repo_url",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "owner",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "owner",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "repo",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "repo",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "description",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "description",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "language",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "language",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "stars_total",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "stars_total",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "forks_total",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "forks_total",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "stars_in_period",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "stars_in_period",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "score",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "score",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "dedupe_key",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "dedupe_key",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "dedupe_key"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "trending_raw"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1GhCbbPilZXMVDox0hQ0Ncqf5-g3AdFFy55Ld30gPD-E"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "d3b522dc-93f3-403a-a923-8c040920710b",
      "name": "Write to Google Sheets (weekly tab)",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        400,
        336
      ],
      "parameters": {
        "columns": {
          "value": {
            "notes": "={{$json.notes}}",
            "week_id": "={{$json.week_id}}",
            "top_repos": "={{$json.top_repos}}",
            "top_themes": "={{$json.top_themes}}",
            "generated_at": "={{$json.generated_at}}"
          },
          "schema": [
            {
              "id": "week_id",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "week_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "generated_at",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "generated_at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "top_repos",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "top_repos",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "top_themes",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "top_themes",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "notes",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "notes",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "message_sent_to",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "message_sent_to",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "link_to_raw_filter",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "link_to_raw_filter",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 723657204,
          "cachedResultUrl": "https://docs.google.com/YOUR_AWS_SECRET_KEY_HERE-g3AdFFy55Ld30gPD-E/edit#gid=723657204",
          "cachedResultName": "weekly_brief"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/YOUR_AWS_SECRET_KEY_HERE-g3AdFFy55Ld30gPD-E/edit?usp=sharing"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "c735eb78-72a1-4183-84fe-44c78746f9f3",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1808,
        -608
      ],
      "parameters": {
        "width": 656,
        "height": 800,
        "content": "# GitHub Trending Tracker (Daily/Weekly/Monthly) \u2192 Google Sheets\n\nThis workflow pulls **GitHub Trending repositories** for a chosen time window (**daily / weekly / monthly**) and writes results to a Google Sheet. It uses **ScrapeOps (community node)** to fetch HTML reliably, then parses the page into structured rows and **upserts** them into Sheets using a `dedupe_key` so you don\u2019t get duplicates.\n\n## How it works\n1) **Set Inputs** defines `since` (daily/weekly/monthly) and optional languages.  \n2) **Build URLs** creates GitHub Trending URLs for the chosen window.  \n3) **ScrapeOps: Fetch HTML** downloads the Trending page HTML.  \n4) **Extract Trending Repos** parses repositories + rank + metadata from HTML.  \n5) **Normalize + Enrich** cleans numbers, calculates score, creates `dedupe_key`.  \n6) **Google Sheets (Append or Update Row)** writes rows and updates existing ones by `dedupe_key`.\n\n## Setup steps\n- Create a free ScrapeOps account & API key: https://scrapeops.io/app/register/n8n  \n- Add ScrapeOps credentials in n8n (community node). Docs: https://scrapeops.io/docs/n8n/overview/  \n- Duplicate the [Google Sheet](https://docs.google.com/spreadsheets/d/1GhCbbPilZXMVDox0hQ0Ncqf5-g3AdFFy55Ld30gPD-E/edit?usp=sharing) with matching tabs and columns for node mapping.\n- In **Write to Google Sheets**, set operation to **Append or Update Row** and match on **dedupe_key**.\n\n## Customization tips\n- Change `since` to daily/weekly/monthly in **Set Inputs**.\n- Add languages in `languages_csv` (e.g. `any,python,go,rust`).\n- Adjust delay and parsing if GitHub\u2019s HTML changes.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "a0cef4cb-1d76-4f67-bf07-cf97c3c281b0",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -928,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 496,
        "height": 304,
        "content": "## Inputs & URL Builder\nSet `since` + languages, then generate GitHub Trending URLs.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "feae26f1-0889-4a52-b705-62a5d55fb2b8",
      "name": "Set Inputs",
      "type": "n8n-nodes-base.set",
      "position": [
        -896,
        -80
      ],
      "parameters": {
        "values": {
          "string": [
            {
              "name": "since",
              "value": "monthly"
            },
            {
              "name": "languages_csv",
              "value": "any"
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 2
    },
    {
      "id": "7c9908e4-a0b1-4a6b-9e65-7fbc91f398e1",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -256,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 496,
        "height": 320,
        "content": "## Scrape Trending Pages\nFetch HTML via ScrapeOps, then wait briefly to be polite.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "c04937d5-93d4-4f1d-8925-5451faecd8d2",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        272,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 496,
        "height": 320,
        "content": "## Parse & Normalize\nExtract repos + rank from HTML and normalize numbers + scoring + dedupe key.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "101bb8bd-8bad-44f8-881f-cdb22756e186",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        800,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 320,
        "height": 320,
        "content": "## Save Results\nUpsert rows into Google Sheets using `dedupe_key` to avoid duplicates.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "bb9fefc0-51ca-4006-b1e9-8321923243c9",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -112,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 736,
        "height": 320,
        "content": "## Weekly Brief (Optional)\nReads raw data and writes a summary into the weekly_brief tab.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "0f912457-142c-4a91-af13-686946f1aad8",
      "name": "Read raw (for weekly)",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -64,
        336
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "trending_raw"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1GhCbbPilZXMVDox0hQ0Ncqf5-g3AdFFy55Ld30gPD-E"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 3,
      "alwaysOutputData": true
    },
    {
      "id": "20904aec-177e-4174-a859-bdce20b32e17",
      "name": "Generate Weekly",
      "type": "n8n-nodes-base.code",
      "position": [
        176,
        336
      ],
      "parameters": {
        "jsCode": "const weekId = ($items('Build URLs')[0]?.json?.week_id) || '';\nlet rows = items.map(x => x.json || {});\nif (rows.length === 1 && Array.isArray(rows[0].data)) rows = rows[0].data;\n\nconst weekRows = rows.filter(r => String(r.week_id || '') === String(weekId));\nconst devRows = weekRows.filter(r => String(r.is_dev_tool).toLowerCase() === 'true');\n\nconst top = [...weekRows].sort((a,b)=> (Number(b.score)||0) - (Number(a.score)||0)).slice(0,10);\n\nconst top_repos = top.map(r => r.repo_url).join('\\n');\nconst top_10_list = top.map((r,i)=> `${i+1}) ${r.full_name} \u2014 stars_in_period=${r.stars_in_period||0}, lang=${r.language||''}, score=${r.score||0}`).join('\\n');\n\nconst themeCounts = {};\nfor (const r of devRows) {\n  const parts = String(r.keyword_matches || '').split(',').map(s=>s.trim()).filter(Boolean);\n  for (const p of parts) themeCounts[p] = (themeCounts[p] || 0) + 1;\n}\nconst top_themes = Object.entries(themeCounts).sort((a,b)=>b[1]-a[1]).slice(0,20).map(([k,v])=>`${k}(${v})`).join(', ');\n\nconst why = top.length\n  ? `Top repo this ${$items('Build URLs')[0]?.json?.since || 'period'}: ${top[0].full_name} (${top[0].language || 'n/a'}) with ${top[0].stars_in_period || 0} stars in period.`\n  : '';\n\nreturn [{\n  json: {\n    week_id: weekId,\n    generated_at: new Date().toISOString(),\n    total_repos_captured: weekRows.length,\n    total_devtools_flagged: devRows.length,\n    top_10_list,\n    top_repos,\n    top_themes,\n    notes: why,\n    message_sent_to: '',\n    link_to_raw_filter: ''\n  }\n}];"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "d8af9578-5963-468b-94d8-788fd478ff2d",
  "connections": {
    "Build URLs": {
      "main": [
        [
          {
            "node": "Split URLs (loop)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Inputs": {
      "main": [
        [
          {
            "node": "Build URLs",
            "type": "main",
            "index": 0
          },
          {
            "node": "Read raw (for weekly)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Polite Delay": {
      "main": [
        [
          {
            "node": "Extract Trending Repos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Weekly": {
      "main": [
        [
          {
            "node": "Write to Google Sheets (weekly tab)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split URLs (loop)": {
      "main": [
        [
          {
            "node": "ScrapeOps: Fetch HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Cron (daily/weekly)": {
      "main": [
        [
          {
            "node": "Set Inputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read raw (for weekly)": {
      "main": [
        [
          {
            "node": "Generate Weekly",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ScrapeOps: Fetch HTML": {
      "main": [
        [
          {
            "node": "Polite Delay",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Trending Repos": {
      "main": [
        [
          {
            "node": "Normalize + Enrich-from-page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize + Enrich-from-page": {
      "main": [
        [
          {
            "node": "Write to Google Sheets (raw tab)",
            "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

This n8n template tracks GitHub Trending repositories (daily/weekly/monthly), parses the trending page into structured data (rank, repo name, stars, language, etc.), and stores results in Google Sheets with automatic deduping. It’s designed for teams who want a simple “trending…

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

More Data & Sheets workflows → · Browse all categories →

Related workflows

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

Data & Sheets

This n8n template automates daily monitoring of AppSumo lifetime deals. Using ScrapeOps Proxy with JavaScript rendering to reliably fetch pages and a structured parsing pipeline, the workflow tracks n

@Scrapeops/N8N Nodes Scrapeops, Google Sheets
Data & Sheets

This workflow automates video distribution to 9 social platforms simultaneously using Blotato's API. It includes both a scheduled publisher (checks Google Sheets for videos marked "Ready") and a subwo

Google Sheets, HTTP Request, Form Trigger +2
Data & Sheets

YogiAI. Uses googleSheets, googleSheetsTool, httpRequest, stopAndError. Scheduled trigger; 61 nodes.

Google Sheets, Google Sheets Tool, HTTP Request +1
Data & Sheets

This workflow monitors Google Calendar for events indicating that a customer will visit the company today or the next day, retrieves the required details, and sends reminder notifications to the relev

Google Calendar, Google Sheets, HTTP Request +1
Data & Sheets

Useful if a team is working within a single instance and you want to be notified of what workflows have changed since you last visited them. Another use-case might be monitoring your managed instances

Google Sheets, Execute Workflow Trigger, n8n