Automate Monthly Azure Cost Emails
Build a Logic App that emails month-over-month Azure cost comparisons automatically.
Introduction
30 Second Summary
Every team that runs workloads in the cloud dreads the moment a monthly bill arrives with unexpected spikes. Knowing how this month compares to last month is the fastest way to catch runaway costs before they snowball.
In this project, you will build an automated workflow that queries your Azure spending for two consecutive months, calculates the difference and percentage change, and emails a styled comparison report to stakeholders every month.
What You'll Build
A Logic Apps workflow that calls the Cost Management Query API on a monthly schedule and delivers a formatted HTML cost comparison email via Office 365 Outlook.
By the end of this project, you'll have:
- A Consumption-tier Logic App with a monthly Recurrence trigger and system-assigned Managed Identity authenticated to your subscription.
- Two HTTP actions querying last month's and the prior month's cost data from the Cost Management REST API.
- A styled HTML email showing previous month cost, current month cost, dollar difference, and percentage change.
- Secret Mission: extend the email with a top-5 services cost breakdown table built from grouped API queries.
Are there any prerequisites?
You need an active Azure subscription with at least two months of existing cost data (resource usage). You also need an Office 365 account with an Exchange Online mailbox for sending emails.
Create the Logic App and Configure Managed Identity
Your Logic App needs a way to authenticate with the Azure Cost Management API without storing credentials. Managed Identity solves this by giving your Logic App its own identity in Microsoft Entra ID, which Azure manages automatically.
In this step, you will create the Logic App resource, enable its system-assigned managed identity, and grant that identity read-only access to your subscription's cost data.
In this step, get ready to:
- Create a Consumption-tier Logic App resource in the Azure Portal.
- Enable the system-assigned managed identity and note the Object ID.
- Assign the Cost Management Reader role to the identity at the subscription scope.
Create the Logic App (Consumption) resource
The Consumption plan is ideal here because you only need one workflow that runs once a month. Azure charges per execution, so a single monthly run costs effectively nothing.
- In the Azure portal search box at the top, type logic app and select Logic apps from the results.
- On the Logic apps page toolbar, click Add.
- On the Create Logic App page, select Consumption (Multi-tenant), then click Select.
- On the Basics tab, select your Subscription.
- For Resource Group, select an existing resource group or click Create new to create one.
- In the Logic App name field, enter la-cost-comparison.
- For Region, select the region closest to you.
- Leave Enable log analytics set to No.
- Click Review + create.
- Review your settings, then click Create.
- Once deployment completes, click Go to resource.
What is a Consumption Logic App?
A Consumption Logic App runs in multi-tenant Azure Logic Apps and supports a single workflow. You pay per action execution, and the first 4,000 actions per month are free.
Since this workflow runs only once a month with roughly 10 actions, you will stay well within the free tier.
Don't see the Consumption option?
Make sure you selected Add on the Logic apps page (not Create from the general portal menu). The Create Logic App page should show both Consumption and Standard plans. If you only see Standard, scroll up to confirm you are on the correct page.
help me find the Consumption plan option when creating a Logic App
Enable the system-assigned managed identity
A system-assigned managed identity is tied directly to your Logic App resource. When the Logic App is deleted, the identity is deleted too. This is the simplest option for a single-purpose workflow.
- In the Logic App's left menu, under Settings, click Identity.
- On the Identity page, under the System assigned tab, set Status to On.
- Click Save.
- A confirmation dialog appears asking if you want to enable the system-assigned managed identity. Click Yes.
- After a moment, an Object (principal) ID appears on the page. Copy this value and save it somewhere (you will need it in the next substep).
What is this Object ID?
The Object (principal) ID uniquely identifies your Logic App's identity in Microsoft Entra ID. You will use it in the next substep to find and select the identity when assigning a role.
Assign the Cost Management Reader role
The Cost Management Reader role grants read-only access to cost data. It does not allow any changes to your subscription or resources.
- In the Azure portal search box at the top, type Subscriptions and select Subscriptions from the results.
- Click your subscription name to open it.
- In the left menu, click Access control (IAM).
- Click Add, then select Add role assignment.
- On the Role tab, type Cost Management Reader in the search box.
- Select Cost Management Reader from the results, then click Next.
- On the Members tab, select Managed identity.
- Click + Select members.
- In the Select managed identities pane, use the Managed identity dropdown to choose Logic app (Consumption).
- Select your la-cost-comparison Logic App from the list, then click Select.
- Click Review + assign.
- Review the assignment (Role: Cost Management Reader, Scope: your subscription, Member: la-cost-comparison), then click Review + assign again to confirm.
Why Cost Management Reader and not Contributor?
The principle of least privilege says you should only grant the minimum permissions needed. Your Logic App only reads cost data. It never writes, deletes, or modifies any resources.
Cost Management Reader provides exactly that: read-only query access to cost and usage data at the subscription scope.
After a few moments, the role assignment takes effect. You can verify it by clicking the Role assignments tab on the Access control (IAM) page and searching for la-cost-comparison.
Add role assignment button is greyed out?
You need Owner or User Access Administrator permissions on the subscription to assign roles. If the button is disabled, ask your subscription Owner to either assign you User Access Administrator or to perform this role assignment on your behalf.
help me troubleshoot why I cannot assign roles in Azure IAM
Your Logic App now has its own identity and permission to query cost data. Next up, you will configure the monthly trigger and make your first API call to retrieve last month's costs.
Configure the Recurrence Trigger and Query Last Month's Cost
Your Logic App has an identity and the permissions it needs to read cost data. Now it needs a workflow that actually does something.
In this step, you will build the core of the automation: a monthly trigger that fires on the 3rd of each month, and an HTTP action that calls the Cost Management Query API to retrieve last month's total spend. By the end of this step, your Logic App will successfully query Azure for real cost data.
In this step, get ready to:
- Configure a monthly Recurrence trigger that fires on the 3rd of the month.
- Add an HTTP action that calls the Cost Management Query API with Managed Identity authentication.
- Extract the returned cost value into a variable and verify it with a test run.
Open the Logic App Designer and add the Recurrence trigger
Every Logic App workflow starts with a trigger. For a monthly cost report, you need a schedule-based trigger that fires reliably after costs have finalized.
- In the Azure portal, navigate to your la-cost-comparison Logic App resource.
- In the left menu, click Logic App Designer (under Development Tools).
- On the designer canvas, click Add a trigger.
- In the search box, type Schedule and select the Recurrence trigger from the results.
- Set Interval to 1.
- Set Frequency to Month.
- Open the Add new parameter list and check Start time.
- In the Start time field, enter 2026-07-03T08:00:00Z.
Why the 3rd of the month at 08:00 UTC?
Azure cost data takes up to 24 hours to finalize after the month ends. Scheduling on the 3rd gives Azure enough buffer to produce accurate totals for the previous month.
The Z suffix on the start time indicates UTC. Because no time zone is selected, you must include this suffix so Logic Apps treats the time correctly.
What is a Recurrence trigger?
A Recurrence trigger fires your workflow on a repeating schedule you define. It runs independently of any incoming data or events. Here it ensures your cost report is generated automatically every month without manual intervention.
Add the subscription ID variable and HTTP action to query costs
Before calling the API, you need to store your subscription ID in a variable so it can be reused across multiple actions later. Then you will add an HTTP action that POSTs to the Cost Management Query API.
- Below the Recurrence trigger, click the + icon and select Add an action.
- Search for Variable and select the Initialize variable action.
- Rename this action to Initialize_SubscriptionId by clicking the action title.
- Set Name to SubscriptionId.
- Set Type to String.
- Set Value to your Azure subscription ID (find it at the top of your subscription's Overview page in the portal).
Find your subscription ID
In the Azure portal, click Subscriptions in the left navigation or search bar. Your subscription ID is the GUID listed next to your subscription name. Copy it and paste it into the Value field.
- Add another action below Initialize_SubscriptionId by clicking the + icon and selecting Add an action.
- Search for HTTP and select the built-in HTTP action (not the connector).
- Rename this action to Query_Last_Month_Cost.
- Set Method to POST.
- Set the URI field to the following value (click in the field and use the dynamic content picker to insert the SubscriptionId variable):
https://management.azure.com/subscriptions/@{variables('SubscriptionId')}/providers/Microsoft.CostManagement/query?api-version=2025-03-01
- In the Body field, paste the following JSON payload:
{
"type": "Usage",
"timeframe": "TheLastMonth",
"dataset": {
"granularity": "None",
"aggregation": {
"totalCost": {
"name": "PreTaxCost",
"function": "Sum"
}
}
}
}
What does this request body do?
- `"type": "Usage"` tells the API to query actual consumption data.
- `"timeframe": "TheLastMonth"` returns the most recently completed calendar month.
- `"granularity": "None"` returns a single aggregated total instead of daily breakdowns.
- `"aggregation"` sums the PreTaxCost column across all resources in the subscription.
- Open the Advanced parameters list in the HTTP action and select Authentication.
- Set Authentication type to Managed identity.
- Set Managed identity to System-assigned managed identity.
- Set Audience to https://management.azure.com.
Why Managed Identity instead of an API key?
Managed Identity lets Azure handle token management automatically. The Logic App proves its identity to the Cost Management API without you storing or rotating any secrets. The Audience value tells Azure which resource to request a token for.
Extract the cost value and run a test
The API returns cost data in a nested JSON structure. You need to pull the actual dollar amount out of the response and store it in a variable for later calculations.
- Add another action below Query_Last_Month_Cost by clicking the + icon and selecting Add an action.
- Search for Variable and select Initialize variable.
- Rename the action to Initialize_CurrentMonthCost.
- Set Name to CurrentMonthCost.
- Set Type to Float.
- Click in the Value field, then open the Expression tab in the dynamic content panel.
- Paste the following expression and click OK:
body('Query_Last_Month_Cost')?['properties']?['rows']?[0]?[0]
What does this expression do?
The Cost Management API returns results in a properties.rows array. Each row is itself an array where index [0] holds the cost value and index [1] holds the currency code.
This expression navigates to the first row ([0]) and grabs the first element ([0]) which is the total cost as a float.
- On the designer toolbar, click Save.
- Click Run Trigger > Run to execute the workflow manually.
- Wait for the run to complete, then click the run entry in the Run History list to open the run details.
Each action should display a green checkmark. Click on Query_Last_Month_Cost and confirm the status code shows 200. Click on Initialize_CurrentMonthCost and confirm the Value output is a numeric cost figure.
Seeing a 401 or 403 error on the HTTP action?
- Verify that the system-assigned managed identity is toggled On under Identity in the Logic App's left menu.
- Confirm the Cost Management Reader role assignment exists on your subscription's Access control (IAM) page with your Logic App's managed identity listed as a member.
- Check that the Audience field in the HTTP action is exactly https://management.azure.com with no trailing slash.
- Role assignments can take up to 5 minutes to propagate. If you just assigned the role, wait and re-run.
Still stuck? Help me troubleshoot a 401/403 error on my Logic App HTTP action calling the Cost Management API with Managed Identity.
✔️ Awesome, I've got everything!
Great work. Make sure you have saved the workflow before continuing to the next step.
ⓧ I'd like to double check the full workflow
Here is the full workflow JSON definition you should have at this point. You can compare this against your workflow by clicking Code view on the designer toolbar.
{
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"contentVersion": "1.0.0.0",
"triggers": {
"Recurrence": {
"type": "Recurrence",
"recurrence": {
"frequency": "Month",
"interval": 1,
"startTime": "2026-07-03T08:00:00Z"
}
}
},
"actions": {
"Initialize_SubscriptionId": {
"type": "InitializeVariable",
"inputs": {
"variables": [
{
"name": "SubscriptionId",
"type": "string",
"value": "your-subscription-id-here"
}
]
},
"runAfter": {}
},
"Query_Last_Month_Cost": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "https://management.azure.com/subscriptions/@{variables('SubscriptionId')}/providers/Microsoft.CostManagement/query?api-version=2025-03-01",
"body": {
"type": "Usage",
"timeframe": "TheLastMonth",
"dataset": {
"granularity": "None",
"aggregation": {
"totalCost": {
"name": "PreTaxCost",
"function": "Sum"
}
}
}
},
"authentication": {
"type": "ManagedServiceIdentity",
"audience": "https://management.azure.com"
}
},
"runAfter": {
"Initialize_SubscriptionId": ["Succeeded"]
}
},
"Initialize_CurrentMonthCost": {
"type": "InitializeVariable",
"inputs": {
"variables": [
{
"name": "CurrentMonthCost",
"type": "float",
"value": "@body('Query_Last_Month_Cost')?['properties']?['rows']?[0]?[0]"
}
]
},
"runAfter": {
"Query_Last_Month_Cost": ["Succeeded"]
}
}
},
"parameters": {}
}
}
Your Logic App can now pull live cost data from Azure on a schedule. Next, you will query the month before that so you can calculate the difference and build a comparison email.
Query the Prior Month and Compute the Comparison
You have last month's total cost stored in a variable. To produce a meaningful report, you need the month before that as a comparison baseline.
In this step, you will use Logic Apps expression functions to calculate the prior month's date range, query the Cost Management API for that period, and then compute the dollar difference and percentage change between the two months.
In this step, get ready to:
- Compute the prior month's start and end dates using Compose actions.
- Query the prior month's cost from the Cost Management Query API.
- Calculate the dollar difference and percentage change between the two months.
Calculate the prior month's date range
The Cost Management API's TheLastMonth timeframe only returns the most recently completed month. To fetch the month before that, you need to supply explicit start and end dates using a Custom timeframe.
- In the Logic App Designer, click the + (plus sign) below the Initialize_CurrentMonthCost action.
- Select Add an action.
- Search for Compose and select the Data Operations - Compose action.
- Rename the action to Compose_PriorMonthStart by clicking the action title and typing the new name.
- Click in the Inputs box, then select the function icon (fx) to open the expression editor.
- Enter the following expression and click Add:
formatDateTime(subtractFromTime(startOfMonth(utcNow()), 2, 'Month'), 'yyyy-MM-ddT00:00:00Z')
What does this expression do?
This expression chains three functions together to produce a date string.
- startOfMonth(utcNow()) gets the first day of the current month.
- subtractFromTime(..., 2, 'Month') goes back two months from that date, landing on the first day of the prior month.
- formatDateTime(..., 'yyyy-MM-ddT00:00:00Z') formats the result as an ISO 8601 timestamp the API expects.
- Add another Compose action directly after Compose_PriorMonthStart.
- Rename it to Compose_PriorMonthEnd.
- Click in the Inputs box, open the expression editor (fx), and enter this expression:
formatDateTime(subtractFromTime(subtractFromTime(startOfMonth(utcNow()), 1, 'Month'), 1, 'Day'), 'yyyy-MM-ddT23:59:59Z')
What does this expression do?
This calculates the last day of the prior month.
- subtractFromTime(startOfMonth(utcNow()), 1, 'Month') gets the first day of last month.
- subtractFromTime(..., 1, 'Day') goes back one day from that, which is the last day of the month before.
- The timestamp ends at 23:59:59Z to capture the entire final day.
- Click Add to confirm the expression.
Expression editor showing an error?
- Make sure you have matching parentheses. Count that every opening ( has a closing ).
- Confirm the function names are lowercase: formatDateTime, subtractFromTime, startOfMonth, utcNow. They are case-sensitive.
- Check that string parameters like 'Month' and 'Day' use single quotes, not double quotes.
Help me fix my Logic Apps Compose expression for date arithmetic
Query the prior month's cost
With the date range calculated, you can now query the Cost Management API for the prior month using a Custom timeframe and the Compose outputs as the date boundaries.
- Add a new HTTP action after Compose_PriorMonthEnd.
- Rename it to Query_Prior_Month_Cost.
- Set Method to POST.
- Set the URI to the same Cost Management endpoint you used in the first HTTP action:
https://management.azure.com/subscriptions/@{variables('SubscriptionId')}/providers/Microsoft.CostManagement/query?api-version=2025-03-01
- In the Body field, enter the following JSON payload. For the from and to values, you will use expressions referencing the Compose outputs:
{
"type": "Usage",
"timeframe": "Custom",
"timePeriod": {
"from": "@{outputs('Compose_PriorMonthStart')}",
"to": "@{outputs('Compose_PriorMonthEnd')}"
},
"dataset": {
"granularity": "None",
"aggregation": {
"totalCost": {
"name": "PreTaxCost",
"function": "Sum"
}
}
}
}
How do I enter the dynamic references in the body?
You have two options for the from and to values.
- Option 1: Type the JSON directly. Place your cursor in the value position, click the lightning icon to open the Dynamic content pane, and select the output of Compose_PriorMonthStart or Compose_PriorMonthEnd.
- Option 2: Type the full expression inline as shown. The @{outputs('Compose_PriorMonthStart')} syntax tells Logic Apps to replace it with the Compose action's output at runtime.
- Expand the Authentication section at the bottom of the HTTP action.
- Set Authentication type to Managed Identity.
- Set Managed Identity to System-assigned managed identity.
- Set Audience to https://management.azure.com.
Now add a variable to store the result from this second query.
- Add an Initialize variable action after Query_Prior_Month_Cost.
- Set Name to PreviousMonthCost.
- Set Type to Float.
- Click in the Value field, open the expression editor (fx), and enter:
body('Query_Prior_Month_Cost')?['properties']?['rows']?[0]?[0]
- Click Add to confirm the expression.
Why rows[0][0] again?
The Cost Management API returns query results as a rows array. With granularity: "None" and a single aggregation, the response contains one row. The first element (index 0) in that row is the total cost as a float.
HTTP action returning a non-200 status?
- Verify the URI uses the same subscription ID variable as your first HTTP action.
- Make sure the body JSON is valid. An extra comma or missing bracket will cause a 400 Bad Request.
- Confirm the Audience field is exactly https://management.azure.com with no trailing slash.
Help me debug my Logic Apps HTTP action calling the Cost Management API
Compute the difference and percentage change
With both months' totals stored, you can now calculate the comparison metrics that will appear in the email.
- Add a Compose action after Initialize_PreviousMonthCost.
- Rename it to Compose_Difference.
- Open the expression editor (fx) in the Inputs box and enter:
sub(variables('CurrentMonthCost'), variables('PreviousMonthCost'))
- Click Add to confirm.
What does this expression do?
The sub() function subtracts the second argument from the first. A positive result means spending increased. A negative result means spending decreased.
- Add one more Compose action after Compose_Difference.
- Rename it to Compose_PercentageChange.
- Open the expression editor and enter this expression:
if(equals(variables('PreviousMonthCost'), 0), 0, mul(div(sub(variables('CurrentMonthCost'), variables('PreviousMonthCost')), variables('PreviousMonthCost')), 100))
- Click Add to confirm.
What does this expression do?
This computes the percentage change using the standard formula: ((current - previous) / previous) * 100.
The if(equals(..., 0), 0, ...) wrapper protects against division by zero. If the previous month had no cost, the result defaults to 0 instead of throwing an error.
- Click Save in the designer toolbar.
- Click Run Trigger and select Run to execute the workflow manually.
- Once the run completes, open Run History and click the most recent run.
- Verify that both HTTP actions show a green checkmark with status 200.
- Click Compose_Difference and Compose_PercentageChange to confirm they each show a numeric output.
Compose outputs showing null or an error?
- Click on Initialize_PreviousMonthCost in the run details and verify it holds a numeric value, not null.
- If the value is null, the prior month query likely returned no rows. Confirm your Azure subscription has cost data for at least two completed months.
- Double-check that variable names in your expressions match exactly: CurrentMonthCost and PreviousMonthCost. Variable names are case-sensitive.
Help me debug my Logic Apps Compose actions that compute cost difference
Your workflow can now fetch two months of cost data and calculate the comparison metrics. Next up, you will compose a styled HTML email and send it to stakeholders with these numbers.
Build and Send the HTML Comparison Email
Your Logic App now knows exactly how much each month cost and how those numbers compare. The missing piece is delivering that insight to stakeholders in a format they can read at a glance.
In this step, you will compose a styled HTML email with month-over-month cost data and wire up Office 365 Outlook to send it automatically.
In this step, get ready to:
- Add Compose actions for month names and a styled HTML email body.
- Configure the Office 365 Outlook Send an email (V2) action.
- Save, manually trigger, and verify the email arrives.
Add Compose actions for month names and the HTML email body
The email needs human-readable month labels like "June 2026" instead of raw dates. You will also compose the full HTML that renders the comparison table in the email client.
- In the Logic App Designer, click the plus sign (+) below the Compose_PercentageChange action and select Add an action.
- Search for Compose and select the Data Operations - Compose action.
- Rename the action to Compose_CurrentMonthName.
- In the Inputs field, switch to expression mode and enter the following expression:
formatDateTime(subtractFromTime(startOfMonth(utcNow()), 1, 'Month'), 'MMMM yyyy')
- Click OK to save the expression.
- Add another Compose action below it and rename it to Compose_PriorMonthName.
- In its Inputs field, switch to expression mode and enter:
formatDateTime(subtractFromTime(startOfMonth(utcNow()), 2, 'Month'), 'MMMM yyyy')
- Click OK to save the expression.
What do these expressions do?
The formatDateTime function formats a timestamp using the 'MMMM yyyy' pattern, which produces the full month name and four-digit year (e.g., "June 2026").
Subtracting 1 month from the start of the current month gives last month's timestamp. Subtracting 2 months gives the month before that. These labels appear in the email table so stakeholders know which months they are comparing.
Now add the HTML email body. This is the largest Compose action in the workflow.
- Add another Compose action below Compose_PriorMonthName and rename it to Compose_EmailBody.
- In its Inputs field, paste the following HTML string exactly as shown:
<html><body style='font-family:Segoe UI,Arial,sans-serif;'><h2 style='color:#0078d4;'>Monthly Azure Cost Report</h2><table style='border-collapse:collapse;width:100%;max-width:500px;'><tr style='background-color:#0078d4;color:white;'><th style='padding:12px;text-align:left;'>Metric</th><th style='padding:12px;text-align:right;'>Value</th></tr><tr style='background-color:#f2f2f2;'><td style='padding:10px;'>@{outputs('Compose_PriorMonthName')} Cost</td><td style='padding:10px;text-align:right;'>$@{formatNumber(variables('PreviousMonthCost'), 'N2')}</td></tr><tr><td style='padding:10px;'>@{outputs('Compose_CurrentMonthName')} Cost</td><td style='padding:10px;text-align:right;'>$@{formatNumber(variables('CurrentMonthCost'), 'N2')}</td></tr><tr style='background-color:#f2f2f2;'><td style='padding:10px;'>Difference ($)</td><td style='padding:10px;text-align:right;font-weight:bold;color:@{if(greaterOrEquals(outputs('Compose_Difference'), 0), "#d32f2f", "#388e3c")};'>@{if(greaterOrEquals(outputs('Compose_Difference'), 0), '+', '')}$@{formatNumber(outputs('Compose_Difference'), 'N2')}</td></tr><tr><td style='padding:10px;'>Change (%)</td><td style='padding:10px;text-align:right;font-weight:bold;color:@{if(greaterOrEquals(outputs('Compose_PercentageChange'), 0), "#d32f2f", "#388e3c")};'>@{if(greaterOrEquals(outputs('Compose_PercentageChange'), 0), '+', '')}@{formatNumber(outputs('Compose_PercentageChange'), 'N1')}%</td></tr></table><p style='color:#666;font-size:12px;margin-top:20px;'>This report is generated automatically by Azure Logic Apps.</p></body></html>
What does this HTML do?
The HTML builds a styled table with inline CSS that renders correctly in most email clients. It has four data rows: prior month cost, current month cost, dollar difference, and percentage change.
The @{...} expressions dynamically insert computed values at runtime. The colour logic uses red (#d32f2f) for cost increases and green (#388e3c) for decreases, making spend changes immediately visible.
Paste carefully
The entire HTML string must be pasted as a single unbroken value in the Inputs field. Do not add line breaks. Logic Apps will resolve the embedded expressions at runtime.
Add the Office 365 Outlook Send an email (V2) action
With the email body composed, you need an action that actually delivers it. The Office 365 Outlook connector sends email from your work or school account.
- Click the plus sign (+) below the Compose_EmailBody action and select Add an action.
- In the search box, type Office 365 Outlook Send an email and select Send an email (V2) from the Office 365 Outlook connector.
- A sign-in prompt appears. Click Sign in and authenticate with your Office 365 work or school account to create the API connection.
Why do I need to sign in?
The Office 365 Outlook connector is a managed connector that requires an authenticated connection to send email on your behalf. Signing in grants the Logic App permission to send through your mailbox. This connection is saved in your resource group and reused on every future run.
- Once the connection is created, configure the action parameters:
- In the To field, enter the stakeholder email address (for testing, use your own email).
- In the Subject field, switch to expression mode and enter:
Azure Cost Report - @{outputs('Compose_CurrentMonthName')}
- In the Body field, click into the field, open the Dynamic content panel, and select the output from Compose_EmailBody.
- Expand Advanced parameters and set Is HTML to Yes.
What does Is HTML do?
Setting Is HTML to Yes tells the Office 365 connector to render the Body as HTML rather than plain text. Without this, recipients would see raw HTML tags instead of a formatted table.
Not seeing Office 365 Outlook in the connector list?
Make sure you are searching under the Shared connector tab, not Built-in. The Office 365 Outlook connector is a Microsoft-managed shared connector. If you have a personal Microsoft account instead of a work/school account, use the Outlook.com connector instead.
Help me find the Office 365 Outlook connector
Save, trigger, and verify the email
The workflow is complete. Time to run it end-to-end and confirm the comparison email arrives in your inbox.
- On the designer toolbar, click Save.
- On the designer toolbar, click Run Trigger and select Run from the dropdown menu.
What does Run Trigger do?
Clicking Run Trigger manually fires the Recurrence trigger regardless of schedule. This lets you test the full workflow without waiting until the 3rd of next month.
- Wait 15-30 seconds for the run to complete.
- In the left menu of your Logic App, click Overview.
- Under Runs history, click the most recent run to open its details.
- Confirm that every action shows a green checkmark (Succeeded status).
- Open your email inbox and look for a message with the subject line starting with Azure Cost Report -.
You should see a formatted HTML table showing the prior month cost, current month cost, dollar difference with a + or - sign, and percentage change. Cost increases appear in red and decreases in green.
Run shows a failed action?
Click the failed action in Run History to expand its inputs and outputs. Common issues include:
- An expression typo in the EmailBody Compose action. Check for missing @{ opening brackets or unmatched quotes.
- The Office 365 connection expired. Re-authorize by opening the Send_Email action and clicking Change connection to sign in again.
- A 401 or 403 on the HTTP actions means the Managed Identity lost its role assignment. Revisit the IAM blade on your subscription and verify the Cost Management Reader role is still assigned.
Help me debug my failed Logic App run
✔️ Awesome, I've got everything!
Great. Make sure you have saved your Logic App after confirming the successful run.
ⓧ I'd like to double check the full code
Switch to Code view in the Logic App Designer to see the full workflow JSON. Compare it against this complete definition:
{
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"contentVersion": "1.0.0.0",
"triggers": {
"Recurrence": {
"type": "Recurrence",
"recurrence": {
"frequency": "Month",
"interval": 1,
"startTime": "2026-07-03T08:00:00Z"
}
}
},
"actions": {
"Initialize_SubscriptionId": {
"type": "InitializeVariable",
"inputs": {
"variables": [{"name": "SubscriptionId", "type": "string", "value": "your-subscription-id-here"}]
},
"runAfter": {}
},
"Query_Last_Month_Cost": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "https://management.azure.com/subscriptions/@{variables('SubscriptionId')}/providers/Microsoft.CostManagement/query?api-version=2025-03-01",
"body": {"type": "Usage", "timeframe": "TheLastMonth", "dataset": {"granularity": "None", "aggregation": {"totalCost": {"name": "PreTaxCost", "function": "Sum"}}}},
"authentication": {"type": "ManagedServiceIdentity", "audience": "https://management.azure.com"}
},
"runAfter": {"Initialize_SubscriptionId": ["Succeeded"]}
},
"Initialize_CurrentMonthCost": {
"type": "InitializeVariable",
"inputs": {"variables": [{"name": "CurrentMonthCost", "type": "float", "value": "@body('Query_Last_Month_Cost')?['properties']?['rows']?[0]?[0]"}]},
"runAfter": {"Query_Last_Month_Cost": ["Succeeded"]}
},
"Compose_PriorMonthStart": {
"type": "Compose",
"inputs": "@formatDateTime(subtractFromTime(startOfMonth(utcNow()), 2, 'Month'), 'yyyy-MM-ddT00:00:00Z')",
"runAfter": {"Initialize_CurrentMonthCost": ["Succeeded"]}
},
"Compose_PriorMonthEnd": {
"type": "Compose",
"inputs": "@formatDateTime(subtractFromTime(subtractFromTime(startOfMonth(utcNow()), 1, 'Month'), 1, 'Day'), 'yyyy-MM-ddT23:59:59Z')",
"runAfter": {"Compose_PriorMonthStart": ["Succeeded"]}
},
"Query_Prior_Month_Cost": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "https://management.azure.com/subscriptions/@{variables('SubscriptionId')}/providers/Microsoft.CostManagement/query?api-version=2025-03-01",
"body": {"type": "Usage", "timeframe": "Custom", "timePeriod": {"from": "@{outputs('Compose_PriorMonthStart')}", "to": "@{outputs('Compose_PriorMonthEnd')}"}, "dataset": {"granularity": "None", "aggregation": {"totalCost": {"name": "PreTaxCost", "function": "Sum"}}}},
"authentication": {"type": "ManagedServiceIdentity", "audience": "https://management.azure.com"}
},
"runAfter": {"Compose_PriorMonthEnd": ["Succeeded"]}
},
"Initialize_PreviousMonthCost": {
"type": "InitializeVariable",
"inputs": {"variables": [{"name": "PreviousMonthCost", "type": "float", "value": "@body('Query_Prior_Month_Cost')?['properties']?['rows']?[0]?[0]"}]},
"runAfter": {"Query_Prior_Month_Cost": ["Succeeded"]}
},
"Compose_Difference": {
"type": "Compose",
"inputs": "@sub(variables('CurrentMonthCost'), variables('PreviousMonthCost'))",
"runAfter": {"Initialize_PreviousMonthCost": ["Succeeded"]}
},
"Compose_PercentageChange": {
"type": "Compose",
"inputs": "@if(equals(variables('PreviousMonthCost'), 0), 0, mul(div(sub(variables('CurrentMonthCost'), variables('PreviousMonthCost')), variables('PreviousMonthCost')), 100))",
"runAfter": {"Compose_Difference": ["Succeeded"]}
},
"Compose_CurrentMonthName": {
"type": "Compose",
"inputs": "@formatDateTime(subtractFromTime(startOfMonth(utcNow()), 1, 'Month'), 'MMMM yyyy')",
"runAfter": {"Compose_PercentageChange": ["Succeeded"]}
},
"Compose_PriorMonthName": {
"type": "Compose",
"inputs": "@formatDateTime(subtractFromTime(startOfMonth(utcNow()), 2, 'Month'), 'MMMM yyyy')",
"runAfter": {"Compose_CurrentMonthName": ["Succeeded"]}
},
"Compose_EmailBody": {
"type": "Compose",
"inputs": "<html><body style='font-family:Segoe UI,Arial,sans-serif;'><h2 style='color:#0078d4;'>Monthly Azure Cost Report</h2><table style='border-collapse:collapse;width:100%;max-width:500px;'><tr style='background-color:#0078d4;color:white;'><th style='padding:12px;text-align:left;'>Metric</th><th style='padding:12px;text-align:right;'>Value</th></tr><tr style='background-color:#f2f2f2;'><td style='padding:10px;'>@{outputs('Compose_PriorMonthName')} Cost</td><td style='padding:10px;text-align:right;'>$@{formatNumber(variables('PreviousMonthCost'), 'N2')}</td></tr><tr><td style='padding:10px;'>@{outputs('Compose_CurrentMonthName')} Cost</td><td style='padding:10px;text-align:right;'>$@{formatNumber(variables('CurrentMonthCost'), 'N2')}</td></tr><tr style='background-color:#f2f2f2;'><td style='padding:10px;'>Difference ($)</td><td style='padding:10px;text-align:right;font-weight:bold;color:@{if(greaterOrEquals(outputs('Compose_Difference'), 0), \"#d32f2f\", \"#388e3c\")};'>@{if(greaterOrEquals(outputs('Compose_Difference'), 0), '+', '')}$@{formatNumber(outputs('Compose_Difference'), 'N2')}</td></tr><tr><td style='padding:10px;'>Change (%)</td><td style='padding:10px;text-align:right;font-weight:bold;color:@{if(greaterOrEquals(outputs('Compose_PercentageChange'), 0), \"#d32f2f\", \"#388e3c\")};'>@{if(greaterOrEquals(outputs('Compose_PercentageChange'), 0), '+', '')}@{formatNumber(outputs('Compose_PercentageChange'), 'N1')}%</td></tr></table><p style='color:#666;font-size:12px;margin-top:20px;'>This report is generated automatically by Azure Logic Apps.</p></body></html>",
"runAfter": {"Compose_PriorMonthName": ["Succeeded"]}
},
"Send_Email": {
"type": "ApiConnection",
"inputs": {
"host": {"connection": {"name": "@parameters('$connections')['office365']['connectionId']"}},
"method": "post",
"path": "/v2/Mail",
"body": {"To": "stakeholder@yourcompany.com", "Subject": "Azure Cost Report - @{outputs('Compose_CurrentMonthName')}", "Body": "@{outputs('Compose_EmailBody')}", "IsHtml": true}
},
"runAfter": {"Compose_EmailBody": ["Succeeded"]}
}
}
}
}
Your cost comparison pipeline is fully automated. Every month, stakeholders will receive a clear, colour-coded snapshot of Azure spend changes without lifting a finger.
Secret mission
Add Top 5 Services Breakdown
Your cost comparison email tells stakeholders the overall trend, but it does not show which services are driving the change. In this secret mission, you will add a grouped API query, sort the results, and build a dynamic HTML table that lists the top 5 Azure services by cost directly in the email.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
This Logic App runs on a Consumption plan and fires only once per month (~10 actions). It sits well within the 4,000 free actions/month included in the plan, so ongoing cost is effectively $0.
Resources you used:
- Logic App la-cost-comparison (Consumption tier).
- Cost Management Reader role assignment on your subscription.
- Office 365 Outlook API connection in your resource group.
Keep everything running
No action needed. Choose this if you want the monthly cost comparison email to keep arriving in your inbox.
- The Logic App will fire on the 3rd of every month. Cost is negligible (well within the free tier).
- The Cost Management Reader role assignment and the Office 365 API connection remain in place.
Pause - I'll come back to this later
Shut down the recurring trigger so no actions fire. Your Logic App definition stays intact for when you return.
- In the Azure portal, navigate to your la-cost-comparison Logic App resource.
- On the Overview blade, click Disable.
- The Logic App will no longer trigger on its schedule. No charges accrue while disabled.
- To resume later, return to the Overview blade and click Enable.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each step below to fully clean up.
- Navigate to your la-cost-comparison Logic App resource in the Azure portal.
- On the Overview blade, click Delete. Confirm the deletion when prompted.
- Navigate to your subscription's Access control (IAM) page.
- Click the Role assignments tab. Find the Cost Management Reader assignment for la-cost-comparison and click Remove. Confirm the removal.
- Navigate to your resource group. Locate the office365 API connection resource. Click on it, then click Delete and confirm.
- If the resource group was created solely for this project, you can delete the entire resource group from its Overview page by clicking Delete resource group.
That's a Wrap!
That's a Wrap!
Nice work! You've just built a fully automated FinOps pipeline that delivers month-over-month Azure cost insights directly to stakeholder inboxes.
You've learned how to:
- Call the Azure Cost Management Query API from a Logic App using Managed Identity authentication on a monthly schedule.
- Use Logic Apps expressions to compute date ranges, cost differences, and percentage changes between billing periods.
- Compose and send a styled HTML email via Office 365 Outlook with a formatted month-over-month cost comparison table.
- Secret Mission: Extended the email with a top-5 services cost breakdown using grouped API queries and dynamic HTML generation.
Ready to quiz yourself?