{
  "name": "Monitor Azure subscription resources with cost and usage tracking",
  "tags": [],
  "nodes": [
    {
      "id": "sticky-main-overview",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        240,
        240
      ],
      "parameters": {
        "color": 4,
        "width": 480,
        "height": 460,
        "content": "## How it works\n\nThis workflow connects to your Azure subscription via OAuth2 and queries two main APIs. It uses Azure Resource Graph to get all resources (VMs, databases, storage, etc.) and Cost Management API for spending data in your billing period.\n\nIt merges the data to find top-cost resources, calculates totals by type, and makes reports in text, HTML, and Excel with top 10 expensive items and breakdowns.\n\n## Setup steps\n\n1. **Create Service Principal**: In Azure AD, make an App Registration with 'Reader' and 'Cost Management Reader' roles. Note Client ID, Secret, Tenant ID.\n\n2. **Configure OAuth2 Credentials**: In n8n, set up OAuth2 with Client Credentials grant, URL 'https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token', scope 'https://management.azure.com/.default'.\n\n3. **Update Configuration**: In 'Set Configuration', put your subscription ID and tenant ID. Adjust dates as needed."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-config",
      "name": "Sticky Note - Config",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        800,
        240
      ],
      "parameters": {
        "color": 7,
        "width": 340,
        "height": 180,
        "content": "## Configuration\n\nSet your subscription ID, tenant ID, and billing period dates. The workflow defaults to the current month but can be customized for any date range."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-data-collection",
      "name": "Sticky Note - Data Collection",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1240,
        240
      ],
      "parameters": {
        "color": 7,
        "width": 340,
        "height": 180,
        "content": "## Data Collection\n\nQueries Azure Resource Graph for all resources and Cost Management API for spending data. Both calls execute in parallel for faster results."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-processing",
      "name": "Sticky Note - Processing",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1920,
        240
      ],
      "parameters": {
        "color": 7,
        "width": 340,
        "height": 180,
        "content": "## Processing & Reporting\n\nMerges resource data with cost information, calculates totals, identifies top expensive resources, and generates formatted text and HTML reports."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-outputs",
      "name": "Sticky Note - Outputs",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2640,
        240
      ],
      "parameters": {
        "color": 7,
        "width": 340,
        "height": 180,
        "content": "## Output Options\n\nExport to Excel, send to Power BI dashboards, or return JSON via webhook. Enable the nodes you need (currently disabled)."
      },
      "typeVersion": 1
    },
    {
      "id": "manual-trigger",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        820,
        680
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "set-configuration",
      "name": "Set Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        1040,
        680
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "subscription-id",
              "name": "subscriptionId",
              "type": "string",
              "value": "=YOUR_SUBSCRIPTION_ID"
            },
            {
              "id": "tenant-id",
              "name": "tenantId",
              "type": "string",
              "value": "=YOUR_TENANT_ID"
            },
            {
              "id": "billing-period",
              "name": "billingPeriod",
              "type": "string",
              "value": "={{ $now.format('yyyyMM') }}"
            },
            {
              "id": "start-date",
              "name": "startDate",
              "type": "string",
              "value": "={{ $now.startOf('month').format('yyyy-MM-dd') }}"
            },
            {
              "id": "end-date",
              "name": "endDate",
              "type": "string",
              "value": "={{ $now.endOf('day').format('yyyy-MM-dd') }}"
            }
          ]
        }
      },
      "typeVersion": 3.3
    },
    {
      "id": "query-azure-resources",
      "name": "Query Azure Resources",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1480,
        680
      ],
      "parameters": {
        "url": "=https://management.azure.com/subscriptions/{{ $json.subscriptionId }}/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01",
        "options": {},
        "jsonBody": "={\n  \"query\": \"Resources | project name, type, location, resourceGroup, tags, sku, properties, id\",\n  \"subscriptions\": [\"{{ $json.subscriptionId }}\"]\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "oAuth2Api"
      },
      "credentials": {
        "oAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "get-cost-data",
      "name": "Get Cost Data",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1480,
        880
      ],
      "parameters": {
        "url": "=https://management.azure.com/subscriptions/{{ $('Set Configuration').item.json.subscriptionId }}/providers/Microsoft.CostManagement/query?api-version=2023-11-01",
        "options": {},
        "jsonBody": "={\n  \"type\": \"ActualCost\",\n  \"timeframe\": \"Custom\",\n  \"timePeriod\": {\n    \"from\": \"{{ $('Set Configuration').item.json.startDate }}\",\n    \"to\": \"{{ $('Set Configuration').item.json.endDate }}\"\n  },\n  \"dataset\": {\n    \"granularity\": \"Daily\",\n    \"aggregation\": {\n      \"totalCost\": {\n        \"name\": \"Cost\",\n        \"function\": \"Sum\"\n      },\n      \"totalCostUSD\": {\n        \"name\": \"CostUSD\",\n        \"function\": \"Sum\"\n      }\n    },\n    \"grouping\": [\n      {\n        \"type\": \"Dimension\",\n        \"name\": \"ResourceId\"\n      },\n      {\n        \"type\": \"Dimension\",\n        \"name\": \"ServiceName\"\n      },\n      {\n        \"type\": \"Dimension\",\n        \"name\": \"ResourceType\"\n      }\n    ]\n  }\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "oAuth2Api"
      },
      "credentials": {
        "oAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "merge-and-process-data",
      "name": "Merge and Process Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1920,
        780
      ],
      "parameters": {
        "jsCode": "const resourcesData = $input.first().json.data;\nconst resources = resourcesData.rows.map(row => {\n  const columns = resourcesData.columns.map(col => col.name);\n  let resource = {};\n  columns.forEach((col, idx) => {\n    resource[col] = row[idx];\n  });\n  return resource;\n});\n\nconst costData = $input.last().json.properties;\nconst costs = costData.rows.map(row => {\n  const columns = costData.columns.map(col => col.name);\n  let cost = {};\n  columns.forEach((col, idx) => {\n    cost[col] = row[idx];\n  });\n  return cost;\n});\n\nconst merged = resources.map(resource => {\n  const resourceCosts = costs.filter(cost => \n    cost.ResourceId && cost.ResourceId.toLowerCase().includes(resource.name.toLowerCase())\n  );\n  \n  const totalCost = resourceCosts.reduce((sum, cost) => sum + (cost.Cost || 0), 0);\n  \n  return {\n    resourceName: resource.name,\n    resourceType: resource.type,\n    resourceGroup: resource.resourceGroup,\n    location: resource.location,\n    sku: resource.sku,\n    tags: resource.tags,\n    cost: totalCost.toFixed(2),\n    costDetails: resourceCosts\n  };\n});\n\nmerged.sort((a, b) => parseFloat(b.cost) - parseFloat(a.cost));\n\nconst totalCost = merged.reduce((sum, r) => sum + parseFloat(r.cost), 0);\nconst resourceCount = merged.length;\nconst topCostResources = merged.slice(0, 10);\n\nconst costByType = {};\nmerged.forEach(r => {\n  const type = r.resourceType;\n  if (!costByType[type]) {\n    costByType[type] = { count: 0, cost: 0 };\n  }\n  costByType[type].count++;\n  costByType[type].cost += parseFloat(r.cost);\n});\n\nconst summary = {\n  totalCost: totalCost.toFixed(2),\n  resourceCount: resourceCount,\n  period: `${$('Set Configuration').item.json.startDate} to ${$('Set Configuration').item.json.endDate}`,\n  topCostResources: topCostResources.map(r => ({\n    name: r.resourceName,\n    type: r.resourceType,\n    cost: r.cost\n  })),\n  costByType: Object.entries(costByType)\n    .map(([type, data]) => ({\n      type,\n      count: data.count,\n      cost: data.cost.toFixed(2)\n    }))\n    .sort((a, b) => parseFloat(b.cost) - parseFloat(a.cost))\n};\n\nreturn [{\n  json: {\n    summary,\n    allResources: merged\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "format-report",
      "name": "Format Report",
      "type": "n8n-nodes-base.code",
      "position": [
        2160,
        780
      ],
      "parameters": {
        "jsCode": "const data = $input.first().json;\nconst summary = data.summary;\n\nlet report = `# Azure Resource & Cost Report\\n\\n`;\nreport += `**Period:** ${summary.period}\\n`;\nreport += `**Total Cost:** $${summary.totalCost}\\n`;\nreport += `**Total Resources:** ${summary.resourceCount}\\n\\n`;\nreport += `## Top 10 Most Expensive Resources\\n\\n`;\nsummary.topCostResources.forEach((r, idx) => {\n  report += `${idx + 1}. **${r.name}** (${r.type}) - $${r.cost}\\n`;\n});\nreport += `\\n## Cost by Resource Type\\n\\n`;\nsummary.costByType.slice(0, 10).forEach((t, idx) => {\n  report += `${idx + 1}. **${t.type}** - ${t.count} resources - $${t.cost}\\n`;\n});\nreport += `\\n---\\n_Generated by n8n on ${new Date().toLocaleString()}_`;\n\nlet htmlReport = `\n<html>\n<head>\n  <style>\n    body { font-family: Arial, sans-serif; margin: 20px; }\n    h1 { color: #0078D4; }\n    h2 { color: #333; margin-top: 30px; }\n    table { border-collapse: collapse; width: 100%; margin-top: 10px; }\n    th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }\n    th { background-color: #0078D4; color: white; }\n    tr:nth-child(even) { background-color: #f2f2f2; }\n    .summary { background-color: #e7f3ff; padding: 15px; border-radius: 5px; margin: 20px 0; }\n  </style>\n</head>\n<body>\n  <h1>Azure Resource & Cost Report</h1>\n  <div class=\"summary\">\n    <p><strong>Period:</strong> ${summary.period}</p>\n    <p><strong>Total Cost:</strong> $${summary.totalCost}</p>\n    <p><strong>Total Resources:</strong> ${summary.resourceCount}</p>\n  </div>\n  <h2>Top 10 Most Expensive Resources</h2>\n  <table>\n    <tr><th>#</th><th>Resource Name</th><th>Type</th><th>Cost</th></tr>\n`;\nsummary.topCostResources.forEach((r, idx) => {\n  htmlReport += `    <tr><td>${idx + 1}</td><td>${r.name}</td><td>${r.type}</td><td>$${r.cost}</td></tr>\\n`;\n});\nhtmlReport += `\n  </table>\n  <h2>Cost by Resource Type</h2>\n  <table>\n    <tr><th>#</th><th>Resource Type</th><th>Count</th><th>Total Cost</th></tr>\n`;\nsummary.costByType.slice(0, 10).forEach((t, idx) => {\n  htmlReport += `    <tr><td>${idx + 1}</td><td>${t.type}</td><td>${t.count}</td><td>$${t.cost}</td></tr>\\n`;\n});\nhtmlReport += `\n  </table>\n  <p style=\"margin-top: 30px; color: #666; font-size: 12px;\">Generated by n8n on ${new Date().toLocaleString()}</p>\n</body>\n</html>\n`;\n\nreturn [{\n  json: {\n    textReport: report,\n    htmlReport: htmlReport,\n    summary: summary,\n    allData: data\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "check-if-data-exists",
      "name": "Check If Data Exists",
      "type": "n8n-nodes-base.if",
      "position": [
        2400,
        780
      ],
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{ $json.summary.totalCost }}",
              "operation": "isNotEmpty"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "output-report",
      "name": "Output Report",
      "type": "n8n-nodes-base.set",
      "position": [
        2640,
        680
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "output",
              "name": "report",
              "type": "string",
              "value": "={{ $json.textReport }}"
            },
            {
              "id": "html",
              "name": "htmlReport",
              "type": "string",
              "value": "={{ $json.htmlReport }}"
            },
            {
              "id": "total-cost",
              "name": "totalCost",
              "type": "string",
              "value": "={{ $json.summary.totalCost }}"
            },
            {
              "id": "resource-count",
              "name": "resourceCount",
              "type": "number",
              "value": "={{ $json.summary.resourceCount }}"
            }
          ]
        }
      },
      "typeVersion": 3.3
    },
    {
      "id": "export-to-excel",
      "name": "Export to Excel",
      "type": "n8n-nodes-base.spreadsheetFile",
      "disabled": true,
      "position": [
        2880,
        580
      ],
      "parameters": {
        "json": "={{ $json.allData.allResources }}",
        "mode": "jsonToSpreadsheet",
        "options": {
          "fileName": "azure-cost-report-{{ $now.format('yyyy-MM-dd') }}.xlsx",
          "headerRow": true
        },
        "operation": "toJson"
      },
      "typeVersion": 1
    },
    {
      "id": "prepare-powerbi-data",
      "name": "Prepare Power BI Data",
      "type": "n8n-nodes-base.set",
      "disabled": true,
      "position": [
        2880,
        720
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "summary",
              "name": "summary",
              "type": "object",
              "value": "={{ $json.allData.summary }}"
            },
            {
              "id": "resources",
              "name": "resources",
              "type": "array",
              "value": "={{ $json.allData.allResources }}"
            },
            {
              "id": "timestamp",
              "name": "timestamp",
              "type": "string",
              "value": "={{ $now.toISO() }}"
            },
            {
              "id": "reportType",
              "name": "reportType",
              "type": "string",
              "value": "azure-cost-analysis"
            }
          ]
        }
      },
      "typeVersion": 3.3
    },
    {
      "id": "send-to-powerbi",
      "name": "Send to Power BI",
      "type": "n8n-nodes-base.httpRequest",
      "disabled": true,
      "position": [
        3120,
        720
      ],
      "parameters": {
        "url": "https://api.powerbi.com/beta/YOUR_WORKSPACE/datasets/YOUR_DATASET/rows?key=YOUR_TOKEN_HERE",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ $json }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "respond-to-webhook",
      "name": "Respond to Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "disabled": true,
      "position": [
        2880,
        860
      ],
      "parameters": {
        "options": {
          "responseCode": 200,
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              },
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        },
        "respondWith": "json",
        "responseBody": "={{ { \"status\": \"success\", \"data\": { \"summary\": $json.summary, \"totalCost\": $json.totalCost, \"resourceCount\": $json.resourceCount, \"report\": $json.report }, \"timestamp\": $now.toISO() } }}"
      },
      "typeVersion": 1
    },
    {
      "id": "no-data-found",
      "name": "No Data Found",
      "type": "n8n-nodes-base.set",
      "position": [
        2640,
        880
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "error",
              "name": "error",
              "type": "string",
              "value": "No cost data found for the specified period"
            }
          ]
        }
      },
      "typeVersion": 3.3
    }
  ],
  "settings": {
    "executionOrder": "v1"
  },
  "updatedAt": "2026-01-19T00:00:00.000Z",
  "versionId": "3",
  "staticData": null,
  "connections": {
    "Format Report": {
      "main": [
        [
          {
            "node": "Check If Data Exists",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Cost Data": {
      "main": [
        [
          {
            "node": "Merge and Process Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Output Report": {
      "main": [
        [
          {
            "node": "Export to Excel",
            "type": "main",
            "index": 0
          },
          {
            "node": "Prepare Power BI Data",
            "type": "main",
            "index": 0
          },
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Set Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Configuration": {
      "main": [
        [
          {
            "node": "Query Azure Resources",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get Cost Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check If Data Exists": {
      "main": [
        [
          {
            "node": "Output Report",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No Data Found",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Power BI Data": {
      "main": [
        [
          {
            "node": "Send to Power BI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query Azure Resources": {
      "main": [
        [
          {
            "node": "Merge and Process Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge and Process Data": {
      "main": [
        [
          {
            "node": "Format Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "triggerCount": 0
}