Most Power Automate environments fly blind when flows fail — this lesson teaches you to build a production-grade watchdog system that detects failures across an entire environment, routes intelligent alerts to flow owners via Teams Adaptive Cards, and ships structured telemetry to Azure Application Insights for dashboarding and trend analysis. By the end, you'll have centralized, proactive monitoring that works even when your flows don't.

Imagine this: it's Monday morning, and a critical flow that processes overnight purchase orders has been silently failing since Friday at 6 PM. Three hundred transactions are stuck in limbo, the finance team is scrambling, and nobody got a single alert. You discover the problem when someone checks a SharePoint list that's conspicuously empty. This is not a hypothetical — it's a pattern that plays out in organizations worldwide, because Power Automate's built-in failure visibility is scoped to individual flows and assumes someone is actively watching.
The default monitoring story in Power Automate is adequate for development and personal automation, but it breaks down at enterprise scale. You might have hundreds of flows owned by dozens of teams, and the native run history only surfaces data to users who know where to look and have the right permissions. If a flow fails, the person who owns it might get an email — if they haven't disabled the setting. What you need is a centralized, proactive monitoring system that doesn't rely on anyone checking anything.
By the end of this lesson, you'll have built a production-grade monitoring and alerting architecture: a scheduled flow that polls the Power Automate Management connector for failed runs across your environment, routes intelligent notifications to flow owners and a central operations channel in Teams, and ships structured telemetry to Azure Application Insights for dashboarding, alerting, and long-term analysis. You'll understand not just the mechanics but the design trade-offs — why this architecture beats the alternatives, where it breaks down, and how to harden it for scale.
What you'll learn:
Before building this system, you should be comfortable with several intermediate-to-advanced Power Automate concepts. You should know how to work with expressions and dynamic content, because this lesson uses body(), outputs(), and JSON parsing extensively. If you need a refresher on conditions, loops, and variables, revisit that before continuing.
You'll also need:
Let's be precise about what we're building and why each component exists. A naive approach would be to enable email notifications on every flow and call it done. The problem with that approach is threefold: individual flow owners may not see alerts in time, there's no centralized aggregation, and you get zero structured data for trend analysis.
The monitoring system we're building has three distinct layers:
Detection Layer: A scheduled "watchdog" flow that runs every 15 minutes (or whatever cadence fits your SLA) and uses the Power Automate Management connector to enumerate all flows in an environment, then queries their run history for failures within the last polling window.
Notification Layer: For each failure found, the watchdog decides who should be notified. The primary owner gets a targeted alert — not a generic "something failed" email, but a contextualized message with the flow name, the time of failure, the error details available from the run record, and a direct link to the run history. The operations team gets an aggregated summary to a Teams channel so they have situational awareness without being drowned in per-flow alerts.
Telemetry Layer: Every failure event — and optionally every successful run as a health heartbeat — is shipped to Azure Application Insights as a custom event with structured properties. This is the layer that enables dashboards, trend analysis, mean-time-to-recovery calculations, and proactive threshold alerts (e.g., "alert if more than 10 flows fail in any 30-minute window").
Key insight: Separation of concerns matters here. Your notification logic and your telemetry logic should be independent. If Application Insights is temporarily unavailable, notifications still go out. If you refactor the Teams message format, it doesn't touch your telemetry schema. Build these as parallel branches in your flow, not sequential steps where one failure blocks the other.
Before building the Power Automate flow, provision your Application Insights resource, because you'll need its instrumentation key and connection string during flow construction.
In the Azure portal, navigate to "Create a resource," search for "Application Insights," and click Create. For the resource configuration:
rg-powerautomate-monitoring — keeping this separate from application workloads makes cost tracking and access control cleanerappi-powerautomate-prodOnce provisioned, open the resource and navigate to "Properties" to find the Instrumentation Key and Connection String. Copy both. The instrumentation key looks like a GUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) and is what Application Insights uses to route telemetry to the right resource.
Warning: The instrumentation key is not a secret in the same way that a password is — it's often embedded in client-side JavaScript. However, for server-side telemetry like this, treat it with reasonable care. Store it as an environment variable in your Power Platform solution rather than hardcoding it in the flow. If you're operating in a high-security environment, review the guidance in Integrating Power Automate with Azure Key Vault and Managed Identities for retrieving secrets at runtime.
The endpoint you'll POST telemetry to is the Application Insights Track endpoint:
https://dc.services.visualstudio.com/v2/track
This endpoint accepts a JSON payload with a specific schema. We'll cover that schema in detail when we build the telemetry action.
Create a new flow in the Power Automate portal. Select "Scheduled cloud flow" as the type. Name it something deliberate — MON-FlowFailureWatchdog-Prod follows a convention that makes monitoring flows immediately identifiable in a large environment. Set the recurrence to every 15 minutes with no specific start time.
Tip: For understanding how scheduled triggers work and how to handle edge cases like missed runs during maintenance windows, that linked article covers the recurrence trigger in depth. For a monitoring flow specifically, you want the interval short enough to catch failures quickly but long enough to avoid excessive API call costs. Fifteen minutes means your maximum time-to-alert is 15 minutes plus processing time, which is acceptable for most enterprise scenarios. Mission-critical flows with tight SLAs should use a dedicated monitoring flow on a shorter interval.
The first action after the trigger should be a Initialize variable action to calculate your lookback window. Name the variable varLookbackStart and set its type to String. The value should be an expression that computes the current UTC time minus 15 minutes — your polling window:
addMinutes(utcNow(), -15)
You'll also want a variable to track how many failures were found this cycle — initialize varFailureCount as an Integer with value 0. This will be used later to decide whether to send a summary alert.
Add another Initialize variable action for varFailureDetails, set to type Array with an empty array [] as the initial value. This array will accumulate failure records as the flow iterates through flows and their runs.
Now comes the core detection logic. Add a List flows as admin action from the Power Automate Management connector. This connector is what enables centralized monitoring — it lets one flow inspect the state of all other flows in an environment, provided the connection is made by an account with Environment Admin rights.
Configure the action:
The output of this action is a paginated list of flow definitions. Each item in the value array includes the flow's internal ID, display name, owner information, and current status.
Warning: Environments with hundreds of flows will return paginated results. By default, the List flows action returns 100 items. Enable "Pagination" in the action settings (click the three-dot menu on the action, then Settings) and set a high threshold — 5000 is the maximum and is appropriate unless you have an extraordinarily large environment. Failing to do this means your monitoring only covers the first page of flows and silently misses the rest. This is one of those bugs that's hard to detect because the monitoring system itself appears to work fine.
After the List flows action, add an Apply to each loop over value from the List flows output. Inside the loop, you'll check whether each flow is enabled and worth monitoring. Add a Condition action:
items('Apply_to_each')?['properties']?['state']StartedThe state field of Started indicates an enabled flow. Disabled or suspended flows will have other state values and don't need run-history checks — they're not running, so they can't fail.
Inside the "Yes" branch of this condition, add a Get flow run history as admin action:
items('Apply_to_each')?['name'] — this is the flow's GUID identifier, not its display nameThis gives you a list of run records for each flow. Each run record includes:
name — the run GUIDproperties.startTime — ISO 8601 timestampproperties.endTime — ISO 8601 timestamp properties.status — Succeeded, Failed, Cancelled, Runningproperties.error — error details if the run failedNow add another Apply to each nested inside the first, iterating over the run history results. Add a condition to filter for failures within the lookback window. The condition should check two things: status equals Failed, AND the start time is greater than or equal to varLookbackStart.
The combined condition expression for the time comparison:
greaterOrEquals(
items('Apply_to_each_2')?['properties']?['startTime'],
variables('varLookbackStart')
)
And the status check:
equals(
items('Apply_to_each_2')?['properties']?['status'],
'Failed'
)
Use "And" to combine these two conditions. In the "Yes" branch, you've found a genuine failure within the current monitoring window.
When you find a failure, you need to know who to notify. The flow owner is embedded in the flow definition from the outer loop. Extract it with:
items('Apply_to_each')?['properties']?['creator']?['userPrincipalName']
Not all flows will have a creator with a UPN — flows created by service accounts or via ALM deployment pipelines may have incomplete creator data. Add a null-check condition and fall back to a designated operations alias if the UPN is missing.
Build a failure record by appending to varFailureDetails using an Append to array variable action. The item to append should be a Compose action output containing a structured JSON object:
{
"flowId": "@{items('Apply_to_each')?['name']}",
"flowName": "@{items('Apply_to_each')?['properties']?['displayName']}",
"runId": "@{items('Apply_to_each_2')?['name']}",
"failedAt": "@{items('Apply_to_each_2')?['properties']?['startTime']}",
"errorCode": "@{items('Apply_to_each_2')?['properties']?['error']?['code']}",
"errorMessage": "@{items('Apply_to_each_2')?['properties']?['error']?['message']}",
"ownerUPN": "@{items('Apply_to_each')?['properties']?['creator']?['userPrincipalName']}",
"environmentId": "YOUR_ENV_GUID_HERE",
"runHistoryUrl": "https://make.powerautomate.com/environments/YOUR_ENV_GUID_HERE/flows/@{items('Apply_to_each')?['name']}/runs/@{items('Apply_to_each_2')?['name']}"
}
Also increment varFailureCount by 1 using a Increment variable action.
Key insight: The
runHistoryUrlfield is one of the most valuable things you can include in an alert. Rather than telling an owner "your flow failed," you're giving them a one-click path to the exact failed run with full action-by-action detail. This dramatically reduces mean time to diagnosis. If you haven't explored run history in depth yourself, the Using Power Automate Run History and Flow Checker to Debug and Fix Failing Flows article covers how to read that detail and what to look for.
After the outer Apply to each loop completes, you have varFailureDetails populated with every failure found in this polling cycle. Now build the notification logic.
Add a Condition to check whether varFailureCount is greater than 0. Only proceed if there are actual failures — don't send a "nothing failed" heartbeat through this path (you'll handle that separately for Application Insights).
Inside the "Yes" branch, add another Apply to each loop over variables('varFailureDetails'). For each failure record, send a targeted notification.
The cleanest notification format for owner alerts is an Adaptive Card in Microsoft Teams, which allows rich formatting with action buttons. Add a Post an Adaptive Card to a Teams user (V2) action from the Teams connector:
items('Apply_to_each_3')?['ownerUPN']The Adaptive Card JSON for a failure alert might look like this:
{
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{
"type": "TextBlock",
"size": "Medium",
"weight": "Bolder",
"color": "Attention",
"text": "⚠️ Flow Failure Detected"
},
{
"type": "FactSet",
"facts": [
{
"title": "Flow Name",
"value": "@{items('Apply_to_each_3')?['flowName']}"
},
{
"title": "Failed At",
"value": "@{items('Apply_to_each_3')?['failedAt']}"
},
{
"title": "Error",
"value": "@{items('Apply_to_each_3')?['errorMessage']}"
}
]
}
],
"actions": [
{
"type": "Action.OpenUrl",
"title": "View Failed Run",
"url": "@{items('Apply_to_each_3')?['runHistoryUrl']}"
}
]
}
Tip: For a deeper dive on Adaptive Card construction, dynamic content injection, and response handling patterns, see Sending Adaptive Cards and Collecting Responses in Microsoft Teams Channels with Power Automate. That article covers schema validation and how to handle the case where a user's account doesn't have Teams messaging enabled.
Configure the "Post Adaptive Card" action to run in "Continue on error" mode — go to Settings on the action and toggle "Configure run after" to include failed runs. This is critical because if one owner's UPN is invalid or their Teams account is inactive, you don't want the entire notification loop to stop. Also add a Increment variable for a varNotificationErrors counter in the error branch, so you have visibility into notification delivery failures.
Individual owner notifications handle personal accountability. The ops channel summary handles organizational awareness. After the owner notification loop, add a Post message in a chat or channel action targeting your operations Teams channel.
Rather than posting one message per failure (which would flood the channel), post a single aggregated summary. Build the message body using a Compose action that joins the failure details into a readable format using the join expression. However, since Adaptive Cards don't natively support dynamic list rendering from an array variable (without a flow-side transform), the practical approach here is to build an HTML table or a formatted text string using an expression:
join(
select(
variables('varFailureDetails'),
item(),
concat('• ', item()?['flowName'], ' — Failed at ', item()?['failedAt'])
),
decodeUriComponent('%0A')
)
This creates a newline-separated list of failed flow names and timestamps. Post it to the ops channel with the failure count in the subject:
concat('🚨 ', string(variables('varFailureCount')), ' flow failure(s) detected in the last 15 minutes')
Here's an edge case that will bite you in production: if a flow fails and the failure occurred 12 minutes ago, your current logic will detect it in this polling cycle. It will also detect it in the next polling cycle because the failure timestamp is still within the 15-minute window. You'll send the same owner two alerts for the same failure.
There are several ways to handle deduplication:
Option 1: Run GUID tracking in SharePoint. Before sending notifications, check a SharePoint list that stores processed run GUIDs. If the run GUID is already in the list, skip notification. Add the new run GUID after notification. This is reliable but adds latency and a SharePoint dependency.
Option 2: Adjust the lookback window. Instead of using addMinutes(utcNow(), -15) for a 15-minute schedule, use addMinutes(utcNow(), -14) — a slightly shorter window than the schedule interval. This creates a small gap that eliminates overlap at the cost of possibly missing failures that occur exactly in the gap. For most use cases this trade-off is acceptable.
Option 3: Use the flow run's startTime with greater-than logic only. Store the timestamp of the last successful monitoring run in a SharePoint list row or an Azure Table Storage record. On each run, retrieve that timestamp and use it as your lookback start. Update it at the end of each successful monitoring run. This is the most accurate approach and handles schedule drift gracefully.
For production, Option 3 is the right choice. Add a Get item action at the beginning of your watchdog flow to retrieve a "last run time" record from a designated SharePoint list. Use that value as varLookbackStart. At the very end of the flow (after notifications and telemetry are shipped), add an Update item action to record the current run time.
Note: This SharePoint list for tracking monitor state is different from your telemetry store. It's an operational state record with a single row, not a log. Don't conflate these two concerns.
Every failure found in the polling cycle should generate a telemetry event in Application Insights. This is where the monitoring system transitions from reactive (notifications) to analytical (dashboards and trend alerts).
Inside the same loop where you process each failure record, add an HTTP action (requires premium) configured as follows:
https://dc.services.visualstudio.com/v2/trackContent-Type: application/jsonThe Application Insights Track endpoint expects a specific JSON schema. Here's the payload for a custom event:
{
"name": "Microsoft.ApplicationInsights.Event",
"time": "@{items('Apply_to_each_3')?['failedAt']}",
"iKey": "YOUR_INSTRUMENTATION_KEY_HERE",
"tags": {
"ai.cloud.role": "PowerAutomate.Monitoring",
"ai.operation.name": "FlowFailureDetected"
},
"data": {
"baseType": "EventData",
"baseData": {
"ver": 2,
"name": "FlowFailure",
"properties": {
"flowId": "@{items('Apply_to_each_3')?['flowId']}",
"flowName": "@{items('Apply_to_each_3')?['flowName']}",
"runId": "@{items('Apply_to_each_3')?['runId']}",
"ownerUPN": "@{items('Apply_to_each_3')?['ownerUPN']}",
"errorCode": "@{items('Apply_to_each_3')?['errorCode']}",
"errorMessage": "@{items('Apply_to_each_3')?['errorMessage']}",
"environmentId": "@{items('Apply_to_each_3')?['environmentId']}",
"runHistoryUrl": "@{items('Apply_to_each_3')?['runHistoryUrl']}"
},
"measurements": {
"failureCount": 1
}
}
}
}
The iKey field takes the instrumentation key (the GUID format). The time field should use the actual failure timestamp from the run record — not the current time — so that your Application Insights data reflects when the failure actually occurred, not when your watchdog noticed it.
Warning: The Application Insights ingestion endpoint at
dc.services.visualstudio.comhas changed over the years and the exact endpoint may differ based on your Azure region or if you're using a workspace-based resource with its own ingestion URL. Check the "Connection String" field in your Application Insights resource — it contains theIngestionEndpointvalue for your specific instance. Using the wrong endpoint means telemetry is silently dropped with no error.
For the connection string-based endpoint, the URL pattern is:
https://[your-ingestion-endpoint]/v2/track
Extract the ingestion endpoint from the connection string format: InstrumentationKey=xxxx;IngestionEndpoint=https://eastus-8.in.applicationinsights.azure.com/;...
Also emit a health heartbeat event every cycle — even if zero flows failed. This lets you detect if the monitoring system itself has stopped running. Add a separate HTTP action outside the failure loop that posts a MonitoringHeartbeat event with the current run timestamp and a flowsChecked measurement:
{
"name": "Microsoft.ApplicationInsights.Event",
"time": "@{utcNow()}",
"iKey": "YOUR_INSTRUMENTATION_KEY_HERE",
"data": {
"baseType": "EventData",
"baseData": {
"ver": 2,
"name": "MonitoringHeartbeat",
"properties": {
"environmentId": "YOUR_ENV_GUID_HERE",
"pollingCycleStart": "@{variables('varLookbackStart')}"
},
"measurements": {
"failuresDetected": "@{variables('varFailureCount')}",
"flowsChecked": "@{length(body('List_flows_as_admin')?['value'])}"
}
}
}
}
Once telemetry is flowing, you can build dashboards and set up metric alerts directly in Application Insights. In the Azure portal, navigate to your Application Insights resource, then select "Logs" from the left navigation to open the KQL query interface.
Here's a query to see all flow failures over the last 24 hours, grouped by flow name:
customEvents
| where name == "FlowFailure"
| where timestamp > ago(24h)
| extend flowName = tostring(customDimensions.flowName)
| extend errorCode = tostring(customDimensions.errorCode)
| extend ownerUPN = tostring(customDimensions.ownerUPN)
| summarize failureCount = count() by flowName, ownerUPN, errorCode
| order by failureCount desc
To check monitoring heartbeat continuity (detect if the watchdog itself has failed):
customEvents
| where name == "MonitoringHeartbeat"
| where timestamp > ago(2h)
| summarize lastHeartbeat = max(timestamp)
| extend minutesSinceLastHeartbeat = datetime_diff('minute', now(), lastHeartbeat)
| where minutesSinceLastHeartbeat > 20
If this query returns a row, your monitoring system itself has gone quiet — which is a critical finding.
To compute a failure rate trend over time for dashboarding:
customEvents
| where name == "FlowFailure"
| where timestamp > ago(7d)
| summarize failures = count() by bin(timestamp, 1h)
| render timechart
Tip: Pin these KQL queries directly to an Azure Dashboard for at-a-glance observability. In the query results pane, click "Pin to dashboard" and select or create a shared dashboard. You can share that dashboard with your operations team without giving them access to the underlying Power Automate environment. This separation of monitoring from access is a meaningful security improvement over sending everyone admin credentials to check run history manually.
There's a scenario that doesn't seem obvious until you think carefully about it: what happens when the watchdog flow itself fails? If the monitoring system that detects failures is itself failing, you have a blind spot.
The heartbeat telemetry approach partially addresses this — if Application Insights stops receiving heartbeats, you can set a metric alert in Azure Monitor to fire when the MonitoringHeartbeat count drops below expected thresholds. Configure this in Azure Monitor under "Alerts" → "Create alert rule":
MonitoringHeartbeat events in the last 30 minutesThis Azure Monitor alert is your watchdog's watchdog. It operates entirely outside Power Automate, so a Power Platform outage doesn't silence it.
A second consideration: you probably want to exclude the monitoring flow itself from the flows it monitors, to avoid false alarms if it takes slightly longer than expected on heavy polling cycles. Filter it out in the Apply to each loop by checking the display name or flow ID against a hardcoded exclusion list. Alternatively, tag monitoring flows with a specific naming convention and use a condition to skip flows whose display name starts with MON-.
The watchdog flow itself needs robust error handling — because if the monitoring flow fails silently, you've created exactly the problem you were trying to solve. The irony is not lost.
Wrap every major operation (the List flows call, each Get run history call, the HTTP telemetry post) in a Scope action. Scopes let you configure unified error handling for a group of actions. For the run history retrieval inside the Apply to each loop, add a scope and configure the actions after the scope to run on "has failed." In the error branch, append an error record to a varScopeErrors array so you can emit a diagnostic telemetry event at the end.
For the notification dispatch, as mentioned earlier, configure each Teams post action with "Continue on error" and check the action's status code afterward. A 400 or 404 status typically means the recipient UPN is invalid. Log this as a NotificationDeliveryFailure custom event in Application Insights rather than surfacing it as an exception — it's diagnostic data, not a crash.
The Get flow run history as admin call can throw a 403 Forbidden if the admin connection's permissions were revoked or if a flow was deleted between the time you listed flows and the time you tried to get its run history. Both of these are normal operational conditions, not bugs. Configure the scope containing these actions to continue on error, and in the error branch, check result('Scope_GetRunHistory')?[0]?['error']?['code'] — if it's WorkflowNotFound or Forbidden, log it and continue rather than treating it as a monitoring failure.
Key insight: One of the most common mistakes when building monitoring infrastructure is making the monitoring system fragile in its own right. Every action in a watchdog flow should be wrapped in error handling that prioritizes continuing to run over surfacing errors to the user. Errors should be logged to telemetry; they should rarely cause the monitoring flow to terminate early. The only exception is a systemic failure early in the flow (like the admin connection being broken) that would make all subsequent actions meaningless — in that case, fail fast and emit a
MonitoringSystemErrortelemetry event before terminating.
A watchdog flow querying a large environment will bump into Power Automate's API call throttling. The Management connector has rate limits on the Get flow run history as admin action — roughly 60 calls per minute per connection. If you have 200 enabled flows, you'll hit this limit in the nested loop.
Strategies to manage this:
Batching and chunking: Instead of monitoring all flows in one watchdog run, split the flow list into batches by environment or by team/department (if your flows are organized that way). Each batch runs in its own scheduled flow offset by a few minutes. This distributes the API call load across time.
Selective monitoring: Not every flow in your environment is equally critical. Add a monitoring "tier" system — a SharePoint list of flows designated as Tier 1 (mission-critical, poll every 15 minutes), Tier 2 (important, poll every hour), and Tier 3 (nice-to-have, poll daily). Your watchdog reads this list first and only queries run history for registered flows. This dramatically reduces API call volume and focuses your attention.
Parallel execution with concurrency limits: You can use parallel branching to query multiple flows' run histories simultaneously rather than sequentially. Configure the Apply to each loop's concurrency to run 5 iterations in parallel — enough to speed up processing without hitting rate limits. Find this setting in the Apply to each action's settings under "Concurrency Control."
Warning: When enabling concurrency on the Apply to each loop, any actions that write to shared variables (
varFailureCount,varFailureDetails) will encounter race conditions. Switch from variables to a more thread-safe pattern: instead of appending to a shared array, emit telemetry directly from within each iteration without aggregating first. Move the count increment into a separate flow-level Scope that runs sequentially, or accept that the count variable may be inaccurate and rely on the telemetry aggregation in Application Insights for accurate counts.
For environments with 500+ flows, consider whether Power Automate is the right tool for the detection layer at all. The Power Platform Admin APIs are accessible from Azure Logic Apps or Azure Functions with far higher throughput limits, and you can still use Power Automate for the notification dispatch layer — triggered by an HTTP request from the Azure-hosted detection component.
This monitoring system runs with elevated permissions — the connection to the Power Automate Management connector is made by an account with Environment Admin rights. That's a significant privilege that needs protection.
Store the service account credentials (or better, use a service principal with the right Delegated App permissions) in Azure Key Vault and retrieve them at runtime using the Key Vault connector. Never hardcode the admin account's credentials in the flow or connection configuration that's visible to all flow editors.
Limit edit access to the watchdog flow itself. In the flow's "Share" settings, give the operations team "Run-only" access — they can trigger it manually if needed but can't modify the notification routing or telemetry schema. Only the monitoring system owner should have edit rights.
The Application Insights instrumentation key should be stored as an environment variable within your Power Platform solution. This means you can deploy the same watchdog flow to dev, staging, and production environments with different Application Insights resources — without modifying the flow itself.
Review who has access to the Application Insights resource in Azure. The telemetry contains flow names, owner UPNs, and error messages — which could leak information about your automation architecture. Apply Azure RBAC to limit Reader access to the ops team and restrict the Log Analytics workspace from broader organizational access.
Build a complete working version of this monitoring system using the following constraints, which simulate a real enterprise scenario:
Your scenario: You've been asked to set up monitoring for a set of 5 critical flows in your development environment — flows that handle customer onboarding, invoice processing, and weekly reporting. You need to detect failures within 15 minutes, notify owners via Teams DM, post a summary to a channel called #automation-ops, and log telemetry to Application Insights.
Step 1: Provision an Application Insights resource in Azure (use the free tier). Copy the instrumentation key and the ingestion endpoint from the connection string.
Step 2: Create a SharePoint list called FlowMonitorState with a single row containing these columns:
Title: "LastRun"LastRunTimestamp (Single line of text): Set to a datetime value 30 minutes in the past as a starting valueStep 3: Create a SharePoint list called MonitoredFlows with the columns:
Title (the flow's display name)FlowId (the flow's GUID — find this in the flow's URL in the portal)OwnerUPNTier (Choice: Critical, Standard)Populate it with the 5 flows you've identified.
Step 4: Build the watchdog flow. Instead of querying all flows via the Management connector (which requires an Admin connection), use the MonitoredFlows SharePoint list as your source of flows to check. This approach also gives you selective monitoring by default. Use Get flow run history as admin for each flow ID in the list.
Step 5: For each failure found, send yourself a Teams direct message with the Adaptive Card template from this lesson. Post an aggregated summary to your #automation-ops channel.
Step 6: POST each failure as a FlowFailure custom event to Application Insights. POST a heartbeat at the end of the run.
Step 7: In Application Insights Logs, run the failure grouping query and the heartbeat continuity query from this lesson. Verify your events appear correctly.
Stretch goal: Trigger a test failure by manually disabling a flow action or adding a deliberate error to one of your monitored flows, then running it. Verify that your watchdog detects it within 15 minutes, sends the notification, and logs the telemetry event.
"My watchdog detects zero failures even when flows are failing."
First, verify the varLookbackStart calculation is correct and that failed runs actually fall within the window. Go to the watchdog's run history and expand the Compose action where you set the lookback start — does the timestamp look right? Second, verify the Management connector is authenticated with an account that has Environment Admin access. A viewer-level account can list flows but may return empty run history arrays. Third, check pagination — confirm you enabled the pagination setting on the List flows action.
"Owners are getting duplicate notifications for the same failure."
This is the deduplication problem discussed earlier. Implement Option 3 — state tracking with a SharePoint "last run timestamp" — and ensure varLookbackStart never overlaps with the previous polling cycle's window. Also check whether you have multiple watchdog flows running in the same environment.
"Telemetry isn't showing up in Application Insights."
Open the watchdog flow's run history, find an HTTP action that posts telemetry, and check the response status code. A 200 means the data was accepted. A 400 means your JSON payload has a schema error — validate it against the Application Insights schema carefully, particularly the baseType and baseData nesting. A 404 or 500 likely means the endpoint URL is wrong. Double-check the ingestion endpoint from your connection string, not just the generic dc.services.visualstudio.com URL. Note that Application Insights has an ingestion latency of 2-5 minutes — data doesn't appear instantly after a successful POST.
"The watchdog flow itself fails on large environments."
You're hitting API rate limits. Add a Delay action at the bottom of the inner Apply to each loop — a 1-second delay between flow run history requests is usually enough to stay within throttle limits. Alternatively, implement the batching strategy described in the scaling section.
"Teams notifications are delivered but the Adaptive Card renders with missing fields."
The Adaptive Card JSON is processed as a string before being sent. Any dynamic content expression that resolves to null will render as empty in the card. Add null-coalescing expressions to provide default values: coalesce(items('Apply_to_each_3')?['errorMessage'], 'No error message available'). Also validate your Adaptive Card JSON in the Adaptive Card Designer at adaptivecards.io before embedding it in the flow — schema errors produce confusingly empty cards rather than visible error messages.
"The monitoring flow runs too slowly to complete within its schedule window."
If the flow takes longer than 15 minutes, you'll have overlapping executions. Enable concurrency control on the scheduled trigger (in the trigger's settings, set concurrency to 1) to ensure only one instance runs at a time — this prevents the second instance from starting before the first completes. Then investigate the performance bottleneck: usually it's the sequential nested loop with no delay management.
You've built a monitoring and alerting system that transforms Power Automate's built-in failure visibility from a passive, per-flow experience into an active, centralized, telemetry-driven capability. The key architectural decisions we made together:
The system you've built today is a foundation. There are several natural extensions to explore next:
If your organization uses the Center of Excellence (CoE) Toolkit, the Auditing and Governing Power Automate at Scale article shows how the CoE's built-in monitoring capabilities complement rather than replace what you've built — CoE focuses on governance and inventory, while your custom watchdog focuses on real-time operational alerting.
If you're operating across multiple Power Platform environments (dev, UAT, production), the concepts in Deploying and Managing Power Automate Solutions Across Environments will help you deploy this monitoring system as a solution with environment variables, making it portable across environments without code changes.
And if you find that certain high-failure flows need to be architected more defensively in the first place, Master Error Handling and Retry Patterns in Power Automate for Bulletproof Flows covers how to build flows that recover gracefully before they ever need to generate a failure alert.
Monitoring is not a feature you add after automation matures — it's the difference between automation you trust and automation you're afraid of. Build it early, build it well, and your entire organization's confidence in the platform grows with it.