This workflow corresponds to n8n.io template #12802 — 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 →
{
"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
}
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.
oAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Monitor Azure subscription resources with cost and usage tracking
Source: https://n8n.io/workflows/12802/ — 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.
Working With Excel Spreadsheet Files Xls Xlsx. Uses stickyNote, readBinaryFile, manualTrigger, writeBinaryFile. Event-driven trigger; 24 nodes.
This workflow will help guide you through obtaining a spreadsheet file, reading it, making a change then saving it to local or cloud storage.
Download invoices from Poland's KSeF (Krajowy System e-Faktur) and export them as an XLSX spreadsheet. Handles the full v2 authentication flow automatically.
Upload Bulk Records From Csv Airtable Interfaces. Uses airtable, httpRequest, stickyNote, spreadsheetFile. Event-driven trigger; 17 nodes.
This workflow is a supporting automation to a common Airtable situation, that as of this writing, has no direct solution but has great demand.