{
  "id": "4728tv9kqR5xbk8t",
  "name": "Monitor GitHub Actions Usage and Prevent Budget Overruns with Slack Alerts",
  "tags": [],
  "nodes": [
    {
      "id": "6a1b5a6d-2eed-4305-bfa8-2f70c6cbcbb1",
      "name": "Main \u2014 GitHub Actions Usage Monitor",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1248,
        32
      ],
      "parameters": {
        "width": 360,
        "height": 640,
        "content": "## \ud83d\udcca GitHub Actions Usage & Cost Monitor\n\n### \ud83d\udc64 Who's it for\nPlatform engineers, DevOps leads, and FinOps teams who pay for GitHub Actions minutes and want a daily health-check before the bill arrives.\n\n### \u2699\ufe0f What it does\n1. Runs daily via schedule.\n2. Reads your repo list from config.\n3. Fetches recent workflow runs aggregated by workflow name.\n4. Pulls org-level billing data from GitHub's Actions billing API.\n5. Calculates budget burn rate: % of monthly budget, projected full-month spend, estimated cost.\n6. Posts a daily summary to Slack.\n7. If usage exceeds your threshold %, sends an urgent budget alert.\n\n### \ud83d\udee0\ufe0f How to set up\n\u2022 Attach **GitHub** (OAuth2 or PAT with `admin:org` read) and **Slack** credentials.\n\u2022 Edit **Configuration**: repo list, monthly budget, alert threshold, cost per minute.\n\u2022 For personal accounts, swap the billing API URL to `/users/{user}/settings/billing/actions`."
      },
      "typeVersion": 1
    },
    {
      "id": "dd405d1f-72b3-4111-bbb1-a4710dbf8872",
      "name": "Section \u2014 Collect",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -864,
        336
      ],
      "parameters": {
        "width": 916,
        "height": 464,
        "content": "## \ud83d\udce5 **Collect**"
      },
      "typeVersion": 1
    },
    {
      "id": "30e6cb52-0094-4397-832b-b0059151c88a",
      "name": "Section \u2014 Analyze & Calculate",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        64,
        336
      ],
      "parameters": {
        "width": 632,
        "height": 464,
        "content": "## \ud83d\udcc8 **Analyze & Calculate**"
      },
      "typeVersion": 1
    },
    {
      "id": "008742ae-b5d8-44d1-9caa-cd0d8cbec2a6",
      "name": "Section \u2014 Report & Alert",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        704,
        336
      ],
      "parameters": {
        "width": 692,
        "height": 464,
        "content": "## \ud83d\udcac **Report & Alert**"
      },
      "typeVersion": 1
    },
    {
      "id": "11a1de92-2836-4138-ac92-67a439bb87c4",
      "name": "Credentials Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1248,
        688
      ],
      "parameters": {
        "width": 360,
        "height": 100,
        "content": "\ud83d\udd11 **Credentials needed**\n\u2022 GitHub token: `repo` + `admin:org` (read)\n\u2022 Slack Bot (`chat:write`)"
      },
      "typeVersion": 1
    },
    {
      "id": "9f04cbff-034d-4922-8209-08484f5b8687",
      "name": "Personal Account Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        80,
        656
      ],
      "parameters": {
        "color": 5,
        "width": 220,
        "height": 128,
        "content": "\u2139\ufe0f **Personal accounts**\nEdit the `Fetch Org Billing Data` node URL to:\n`/users/{{owner}}/settings/billing/actions`"
      },
      "typeVersion": 1
    },
    {
      "id": "54faec29-5758-4afe-aded-4ae740ec2a85",
      "name": "Daily Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -800,
        496
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 23
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "d5281acd-12dd-46c4-975e-6c4050e019c0",
      "name": "Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        -576,
        496
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "repoOwner",
              "name": "repoOwner",
              "type": "string",
              "value": "your-org-or-username"
            },
            {
              "id": "monitorRepos",
              "name": "monitorRepos",
              "type": "string",
              "value": "repo-one,repo-two,repo-three"
            },
            {
              "id": "budgetMinutesPerMonth",
              "name": "budgetMinutesPerMonth",
              "type": "number",
              "value": 2000
            },
            {
              "id": "alertThresholdPercent",
              "name": "alertThresholdPercent",
              "type": "number",
              "value": 80
            },
            {
              "id": "slackChannel",
              "name": "slackChannel",
              "type": "string",
              "value": "#devops-finops"
            },
            {
              "id": "costPerMinute",
              "name": "costPerMinute",
              "type": "number",
              "value": 0.008
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "52a25cfb-67a2-4741-aea9-931906a3e948",
      "name": "Expand Repo List",
      "type": "n8n-nodes-base.code",
      "position": [
        -352,
        496
      ],
      "parameters": {
        "jsCode": "const config = $('Configuration').item.json;\nconst repos = config.monitorRepos.split(',').map(r => r.trim());\nreturn repos.map(repo => ({ repoName: repo, repoOwner: config.repoOwner }));"
      },
      "typeVersion": 2
    },
    {
      "id": "0d6731ab-c702-4fb9-af22-31e132afc6c7",
      "name": "Fetch Recent Workflow Runs",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueErrorOutput",
      "position": [
        -128,
        496
      ],
      "parameters": {
        "url": "=https://api.github.com/repos/{{ $json.repoOwner }}/{{ $json.repoName }}/actions/runs?per_page=100&created=>{{ new Date(Date.now() - 24*3600*1000).toISOString() }}",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "githubApi"
      },
      "credentials": {
        "githubApi": {
          "name": "<your credential>"
        },
        "githubOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "07210c31-686e-4967-b2b4-a0ef922b1581",
      "name": "Aggregate Workflow Stats",
      "type": "n8n-nodes-base.code",
      "position": [
        96,
        496
      ],
      "parameters": {
        "jsCode": "const allStats = {};\nconst result = {\n  reposScanned: 0,\n  totalRuns: 0,\n  totalCompleted: 0,\n  totalFailed: 0,\n  workflowStats: []\n};\n\nfor (const item of $input.all()) {\n  result.reposScanned++;\n  const runs = item.json.workflow_runs || [];\n  for (const run of runs) {\n    const wfName = run.name || run.workflow_id || 'unknown';\n    if (!allStats[wfName]) {\n      allStats[wfName] = {\n        workflowName: wfName,\n        totalRuns: 0,\n        completedRuns: 0,\n        failedRuns: 0\n      };\n    }\n    const entry = allStats[wfName];\n    entry.totalRuns++;\n    result.totalRuns++;\n    if (run.status === 'completed') {\n      entry.completedRuns++;\n      result.totalCompleted++;\n      if (run.conclusion === 'failure') {\n        entry.failedRuns++;\n        result.totalFailed++;\n      }\n    }\n  }\n}\n\nresult.workflowStats = Object.values(allStats);\nreturn result;"
      },
      "typeVersion": 2
    },
    {
      "id": "5de1175a-9c6e-441f-a389-61c15ef7b2b6",
      "name": "Fetch Org Billing Data",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        320,
        496
      ],
      "parameters": {
        "url": "=https://api.github.com/orgs/{{ $('Configuration').item.json.repoOwner }}/settings/billing/actions",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "githubOAuth2Api"
      },
      "credentials": {
        "githubOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4,
      "alwaysOutputData": false
    },
    {
      "id": "7efb319a-b0bf-4b4b-a1be-d8d437d3470e",
      "name": "Calculate Budget Burn Rate",
      "type": "n8n-nodes-base.code",
      "position": [
        544,
        496
      ],
      "parameters": {
        "jsCode": "const billing = $input.first().json;\nconst config = $('Configuration').item.json;\n\nconst totalMinutesUsed = billing.total_minutes_used_minutes || 0;\nconst includableMinutes = billing.included_minutes || 0;\nconst billableMinutes = totalMinutesUsed > includableMinutes ? (totalMinutesUsed - includableMinutes) : 0;\nconst estimatedCost = billableMinutes * config.costPerMinute;\nconst budgetLimit = config.budgetMinutesPerMonth;\n\nconst now = new Date();\nconst dayOfMonth = now.getDate();\nconst daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();\nconst monthFraction = dayOfMonth / daysInMonth;\n\nconst expectedFullMonth = totalMinutesUsed / monthFraction;\nconst percentOfBudget = (totalMinutesUsed / budgetLimit) * 100;\nconst isOverBudget = percentOfBudget >= config.alertThresholdPercent;\nconst isOnTrackToExceed = expectedFullMonth > budgetLimit;\n\nreturn {\n  reportDate: now.toISOString().split('T')[0],\n  totalMinutesUsed,\n  includableMinutes,\n  billableMinutes,\n  estimatedCost,\n  budgetLimit,\n  percentOfBudget: Math.round(percentOfBudget * 10) / 10,\n  expectedFullMonth: Math.round(expectedFullMonth),\n  isOverBudget,\n  isOnTrackToExceed,\n  dayOfMonth,\n  daysInMonth\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "3f56deb9-4915-4f18-a037-78b162ca99a4",
      "name": "Format Usage Report",
      "type": "n8n-nodes-base.code",
      "position": [
        768,
        496
      ],
      "parameters": {
        "jsCode": "const billing = $('Calculate Budget Burn Rate').item.json;\nconst stats = $('Aggregate Workflow Stats').item.json;\n\nlet emoji = billing.isOverBudget ? '\ud83d\udd34' : billing.isOnTrackToExceed ? '\ud83d\udfe1' : '\ud83d\udfe2';\n\nlet message = `*\ud83d\udcca Daily GitHub Actions Usage Report \u2014 ${billing.reportDate}*\\n\\n` +\n  `${emoji} *Budget Status:* ${billing.percentOfBudget}% of monthly budget\\n` +\n  `\u2022 Minutes used: *${billing.totalMinutesUsed}* / ${billing.budgetLimit}\\n` +\n  `\u2022 Included minutes: ${billing.includableMinutes}\\n` +\n  `\u2022 Billable minutes: ${billing.billableMinutes}\\n` +\n  `\u2022 Est. cost: $${billing.estimatedCost.toFixed(2)}\\n` +\n  `\u2022 Projected full month: *${billing.expectedFullMonth}* min\\n\\n`;\n\nmessage += `*\u26a1 Recent Workflow Runs (last 5 days)*\\n`;\nmessage += `\u2022 Repos scanned: ${stats.reposScanned}\\n`;\nmessage += `\u2022 Total runs: ${stats.totalRuns}\\n`;\nmessage += `\u2022 Completed: ${stats.totalCompleted} | Failed: ${stats.totalFailed}\\n`;\n\nif (stats.workflowStats && stats.workflowStats.length > 0) {\n  message += `\\n*Workflow breakdown:*\\n`;\n  const top = stats.workflowStats.sort((a,b)=>b.totalRuns-a.totalRuns).slice(0,10);\n  for (const wf of top) {\n    const status = wf.failedRuns > 0 ? `\u26a0\ufe0f ${wf.failedRuns} failed` : '\u2705';\n    message += `\u2022 ${wf.workflowName}: ${wf.totalRuns} runs (${status})\\n`;\n  }\n}\n\nmessage += `\\n_Day ${billing.dayOfMonth} of ${billing.daysInMonth}_`;\n\nreturn { message };"
      },
      "typeVersion": 2
    },
    {
      "id": "9d436d6d-e4d7-4049-8584-f744878ca0c5",
      "name": "Post Daily Summary to Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        1216,
        592
      ],
      "parameters": {
        "text": "={{ $json.message }}",
        "otherOptions": {}
      },
      "typeVersion": 2.2
    },
    {
      "id": "761509bf-a88b-4828-b7e6-2fbe41e76c2d",
      "name": "Budget Exceeded Threshold?",
      "type": "n8n-nodes-base.if",
      "position": [
        992,
        496
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "over-budget",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $('Calculate Budget Burn Rate').item.json.isOverBudget }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "711b0b43-611f-43a8-8a10-b97fba64c860",
      "name": "Send Urgent Budget Warning",
      "type": "n8n-nodes-base.slack",
      "position": [
        1216,
        400
      ],
      "parameters": {
        "text": "=\ud83d\udea8 *ACTIONS BUDGET ALERT*\\n\\nGitHub Actions has used *{{ $('Calculate Budget Burn Rate').item.json.percentOfBudget }}%* of the monthly budget!\\n\\n\u2022 Used: {{ $('Calculate Budget Burn Rate').item.json.totalMinutesUsed }} min\\n\u2022 Budget: {{ $('Calculate Budget Burn Rate').item.json.budgetLimit }} min\\n\u2022 Est. cost: ${{ $('Calculate Budget Burn Rate').item.json.estimatedCost.toFixed(2) }}\\n\\n\u26a0\ufe0f Action needed \u2014 review usage to avoid overage!",
        "otherOptions": {}
      },
      "typeVersion": 2.2
    },
    {
      "id": "ae8b6247-6795-447e-8e75-3a585314e493",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -208,
        656
      ],
      "parameters": {
        "color": 5,
        "height": 128,
        "content": "\u26a0\ufe0f**Error Path**\nWhen a repo has not actions for the given timeframe, it exists via the Error path\n"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "a0fa0452-62f3-40b2-a7b2-0845fe811a9c",
  "nodeGroups": [],
  "connections": {
    "Configuration": {
      "main": [
        [
          {
            "node": "Expand Repo List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Expand Repo List": {
      "main": [
        [
          {
            "node": "Fetch Recent Workflow Runs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Usage Report": {
      "main": [
        [
          {
            "node": "Budget Exceeded Threshold?",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Daily Schedule Trigger": {
      "main": [
        [
          {
            "node": "Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Org Billing Data": {
      "main": [
        [
          {
            "node": "Calculate Budget Burn Rate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Workflow Stats": {
      "main": [
        [
          {
            "node": "Fetch Org Billing Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Budget Exceeded Threshold?": {
      "main": [
        [
          {
            "node": "Send Urgent Budget Warning",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Post Daily Summary to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Budget Burn Rate": {
      "main": [
        [
          {
            "node": "Format Usage Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Recent Workflow Runs": {
      "main": [
        [
          {
            "node": "Aggregate Workflow Stats",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}