{
  "name": "Make operations budget monitor",
  "nodes": [
    {
      "parameters": {
        "content": "## Make operations forecast: see the overspend before the plan empties\n\nA Make scenario that loops once too often burns operations quietly, and the first\nsignal is usually the month the plan runs out mid-week.\n\nThis reads your organisation's usage every six hours, projects the month from the run\nrate so far, and alerts while there is still time to fix the scenario.\n\n**Setup**\n1. Put your Make organisation id in the **Fetch Organisation Usage** URL, and check the\n   zone (eu1, eu2, us1) matches your account.\n2. Add a Make API token as a header credential on that node.\n3. Set your monthly operations allowance and the warning threshold in\n   **Project The Bill**.\n4. Replace the webhook URL in **Alert**.",
        "height": 382,
        "width": 1400
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -880
      ],
      "id": "stage-overview",
      "name": "Sticky Overview"
    },
    {
      "parameters": {
        "content": "### 1. Every 6 Hours\n\nFour reads a day is enough to catch a runaway scenario the same day it starts, without\npolling Make for no reason.",
        "height": 298,
        "width": 280
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -418
      ],
      "id": "stage-0",
      "name": "Sticky Every 6 Hours"
    },
    {
      "parameters": {
        "content": "### 2. Fetch Organisation Usage\n\nReads operations consumed so far this period from the Make API.\n\nReplace `YOUR_ORG_ID` and confirm the zone in the hostname. The org id is in the URL\nwhen you open your Make dashboard.",
        "height": 298,
        "width": 280
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        320,
        -418
      ],
      "id": "stage-1",
      "name": "Sticky Fetch Organisation Usage"
    },
    {
      "parameters": {
        "content": "### 3. Project The Bill\n\nTakes consumption so far, divides by the days elapsed, and multiplies by the days in\nthe period.\n\nSet your allowance and the threshold here. Anything above the threshold sets a flag the\nnext node reads.",
        "height": 298,
        "width": 280
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        640,
        -418
      ],
      "id": "stage-2",
      "name": "Sticky Project The Bill"
    },
    {
      "parameters": {
        "content": "### 4. Over Threshold?\n\nSplits on the projection. Only a projected overspend continues to the alert.\n\nKeeping the quiet path explicit means you can see in the executions list that the check\nran and found nothing wrong.",
        "height": 298,
        "width": 280
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        960,
        -418
      ],
      "id": "stage-3",
      "name": "Sticky Over Threshold?"
    },
    {
      "parameters": {
        "content": "### 5. Alert and Log And Stop\n\n**Alert** posts the projection, the allowance and the days remaining, so the message\nsays how bad it is.\n\n**Log And Stop** is the healthy path: it does nothing on purpose.",
        "height": 298,
        "width": 280
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1280,
        -418
      ],
      "id": "stage-4",
      "name": "Sticky Alert"
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 6
            }
          ]
        }
      },
      "id": "every-6-hours",
      "name": "Every 6 Hours",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "url": "https://eu2.make.com/api/v2/organizations/YOUR_ORG_ID/usage",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "fetch-organisation-usage",
      "name": "Fetch Organisation Usage",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        320,
        0
      ],
      "notes": "Swap eu2 for the region your account is on. Header auth: Authorization = Token YOUR_API_TOKEN.",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Project the operations bill before it lands.\n// Make counts an operation per module run, so a scenario that loops silently\n// spends the plan without failing anything. This turns usage into a forecast.\n\nconst usage = $input.first().json;\n\n// Adjust these two to match the plan you are on.\nconst PLAN_OPERATIONS = 10000;       // operations included per billing cycle\nconst CYCLE_DAY = 1;                 // day of month the cycle resets\n\nconst now = new Date();\nconst cycleStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), CYCLE_DAY));\nif (cycleStart > now) cycleStart.setUTCMonth(cycleStart.getUTCMonth() - 1);\nconst cycleEnd = new Date(cycleStart);\ncycleEnd.setUTCMonth(cycleEnd.getUTCMonth() + 1);\n\nconst dayMs = 86400000;\nconst elapsedDays = Math.max(0.5, (now - cycleStart) / dayMs);\nconst totalDays = (cycleEnd - cycleStart) / dayMs;\n\n// The API shape differs by Make region and version; take the first number we\n// recognise rather than assuming one field name.\nconst used = Number(\n  usage.operations ?? usage.operationsCount ?? usage.usage?.operations ?? 0,\n);\n\nconst perDay = used / elapsedDays;\nconst projected = Math.round(perDay * totalDays);\nconst percentUsed = Math.round((used / PLAN_OPERATIONS) * 100);\nconst percentProjected = Math.round((projected / PLAN_OPERATIONS) * 100);\nconst daysUntilExhausted =\n  perDay > 0 ? Math.floor((PLAN_OPERATIONS - used) / perDay) : null;\n\n// Alert on the forecast, not the balance. By the time the balance is alarming\n// the scenarios have already stopped.\nconst shouldAlert = percentProjected >= 90 || percentUsed >= 80;\n\nreturn [{\n  json: {\n    used,\n    planOperations: PLAN_OPERATIONS,\n    percentUsed,\n    perDay: Math.round(perDay),\n    projected,\n    percentProjected,\n    daysUntilExhausted,\n    cycleEnds: cycleEnd.toISOString().slice(0, 10),\n    shouldAlert,\n    summary: `Make operations: ${used.toLocaleString()} of ${PLAN_OPERATIONS.toLocaleString()} used (${percentUsed}%). ` +\n      `Burning ${Math.round(perDay).toLocaleString()}/day, projected ${projected.toLocaleString()} (${percentProjected}%) by ${cycleEnd.toISOString().slice(0,10)}. ` +\n      (daysUntilExhausted !== null && daysUntilExhausted < totalDays\n        ? `Runs out in about ${daysUntilExhausted} days.`\n        : `Inside the plan.`),\n  },\n}];"
      },
      "id": "project-the-bill",
      "name": "Project The Bill",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        640,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "={{ $json.shouldAlert }}-true",
              "leftValue": "={{ $json.shouldAlert }}",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "rightValue": ""
            }
          ],
          "combinator": "and"
        }
      },
      "id": "over-threshold?",
      "name": "Over Threshold?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        960,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "YOUR_SLACK_OR_TEAMS_WEBHOOK_URL",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ text: $json.summary }) }}",
        "options": {}
      },
      "id": "alert",
      "name": "Alert",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1280,
        0
      ],
      "notes": "Any webhook that takes a JSON body with a text field. Telegram, Slack and Teams all do.",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {},
      "id": "log-and-stop",
      "name": "Log And Stop",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1280,
        200
      ],
      "notes": "Inside budget. The run still records the projection, so the execution list becomes a usage history."
    }
  ],
  "connections": {
    "Every 6 Hours": {
      "main": [
        [
          {
            "node": "Fetch Organisation Usage",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Organisation Usage": {
      "main": [
        [
          {
            "node": "Project The Bill",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Project The Bill": {
      "main": [
        [
          {
            "node": "Over Threshold?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Over Threshold?": {
      "main": [
        [
          {
            "node": "Alert",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log And Stop",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "instanceId": "lurtoagency.com/fix/make-scenario-operation-limits"
  }
}
