How I Deployed Flows to Every Developer Environment in My Tenant

 



Power Platform Automation

How I Deployed Flows to Every Developer Environment in My Tenant

One script. One login. Zero portal clicks. A story about wrestling with Microsoft APIs so you don't have to.

Kerolos
February 2026
12 min read
↓ scroll

If you manage a Power Platform tenant with multiple developer environments, you know the pain. Environments get created for testing, POCs, and hackathons—then forgotten. Nobody knows what's alive, what's orphaned, or what's silently eating up capacity.

I wanted a way to prove that every developer environment is reachable and functional. Not with a fancy monitoring solution. Not with a third-party tool. Just a dead-simple scheduled Power Automate flow that runs daily at 8 AM, does absolutely nothing meaningful, and terminates with success.

If it runs, the environment is alive. If it doesn't, something's wrong. That's it.

The Goal

One script. One login. Deploy a heartbeat flow to every developer environment in the tenant, always create a fresh copy with a UTC timestamp, and export a clean report with environment names, URLs, owners, and flow links.

01

The Approach

Here's what the script needed to do:

  1. Authenticate once — no multiple login prompts, no module conflicts
  2. Discover every environment in the tenant via the BAP admin API
  3. Filter to Developer environments only
  4. Deploy a 2-action flow (Recurrence → Terminate Success) to each
  5. Always deploy fresh — even if a flow already exists, create a new one with a UTC timestamp
  6. Export a report with environment name, URL, owner, flow URL, and deployment status
02

Challenge #1: The Authentication Nightmare

This was the hardest part. The Power Platform PowerShell modules have a messy auth story:

  • Microsoft.PowerApps.Administration.PowerShell has its own internal auth that doesn't expose tokens
  • The Flow REST API needs a Bearer token for https://service.flow.microsoft.com/
  • Az.Accounts, MSAL.PS, and the admin module all fight over Microsoft.Identity.Client DLL versions

The solution? Skip all PowerShell modules for auth entirely. Use a raw OAuth2 device code flow with Azure PowerShell's well-known client ID. One login prompt, then silently exchange the refresh token for additional resource tokens.

Key Insight

Authenticate once for the Flow API, then use the refresh token to silently get a BAP admin API token. Zero module dependencies for auth. Zero DLL conflicts.

The Device Code Token Function

Get-DeviceCodeToken.ps1
function Get-DeviceCodeToken {
    param([string]$Resource)
    $deviceCode = Invoke-RestMethod `
        -Uri "https://login.microsoftonline.com/common/oauth2/devicecode" `
        -Method POST -Body @{ client_id = $clientId; resource = $Resource }
    Write-Host $deviceCode.message
    # Poll until user completes login
    while ($true) {
        Start-Sleep -Seconds 3
        try { return (Invoke-RestMethod -Uri "...oauth2/token" -Body @{
            grant_type = "urn:ietf:params:oauth:grant-type:device_code"
            client_id = $clientId; code = $deviceCode.device_code
        }) } catch { continue }
    }
}

Silent Token Exchange (No Second Login)

Silent refresh token exchange
# Get Flow API token (interactive - only prompt)
$flowTokenResponse = Get-DeviceCodeToken -Resource "https://service.flow.microsoft.com/"

# Silently get BAP admin token using refresh token
$adminToken = (Invoke-RestMethod -Uri "...oauth2/token" -Body @{
    grant_type    = "refresh_token"
    client_id     = $clientId
    refresh_token = $flowTokenResponse.refresh_token
    resource      = "https://api.bap.microsoft.com/"
}).access_token
03

Challenge #2: The Environment Discovery Gotcha

I initially used Get-AdminPowerAppEnvironment and filtered on EnvironmentType. It returned 4 environments but the filter matched zero. Why?

Because the BAP admin API returns the type under environmentSkunot environmentType (which comes back empty).

A debug dump revealed the truth:

Debug output — spot the difference
# What I was filtering on (empty!):
[prod101]   environmentType=''   environmentSku='Developer'
[microsoft] environmentType=''   environmentSku='Default'
[DEV2]      environmentType=''   environmentSku='Developer'

# The fix:
$devEnvironments = $allEnvironments | Where-Object {
    $_.properties.environmentSku -eq 'Developer'
}
Lesson Learned

Always dump the raw API response structure before writing filters. The Microsoft docs and the actual API response don't always agree.

04

Challenge #3: The 400 Bad Request Mystery

Even after getting auth right and discovering environments, the flow creation API returned 400 Bad Request on every attempt. Three things were broken:

  • PowerShell hashtable → ConvertTo-Json was mangling $schema and nested empty objects
  • Missing required empty fields — the API silently requires connectionReferencesparameters, and outputs even when empty
  • Encoding issues — Invoke-RestMethod wasn't sending clean UTF-8

The Fix: Raw JSON + UTF-8 Bytes

Flow creation — the payload that actually works
# Build JSON as a literal string - no serialization surprises
$flowJson = @"
{
  "properties": {
    "displayName": "$fullFlowName",
    "state": "Started",
    "connectionReferences": {},
    "definition": {
      "$schema": "https://schema.management.azure.com/...",
      "contentVersion": "1.0.0.0",
      "parameters": {},
      "triggers": {
        "Recurrence": {
          "recurrence": { "frequency": "Day", "interval": 1,
            "schedule": { "hours": ["8"], "minutes": ["0"] },
            "timeZone": "Eastern Standard Time" },
          "type": "Recurrence"
        }
      },
      "actions": {
        "Terminate": { "inputs": { "runStatus": "Succeeded" },
          "runAfter": {}, "type": "Terminate" }
      },
      "outputs": {}
    }
  }
}
"@

# Send as explicit UTF-8 bytes via Invoke-WebRequest
$bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($flowJson)
Invoke-WebRequest -Uri $createUrl -Method POST `
    -ContentType 'application/json; charset=utf-8' `
    -Body $bodyBytes -UseBasicParsing
What Worked

The combination of raw JSON string + UTF-8 bytes + Invoke-WebRequest (not Invoke-RestMethod) is what finally got the 200 OK across all environments.

05

The Flow: Beautifully Simple

The deployed flow has exactly two nodes. That's it. That's the whole thing:

Recurrence
Daily @ 8 AM EST
Terminate
Status: Succeeded

Each flow gets a unique name with a UTC timestamp:

Flow naming convention
Daily 8AM Health Check - 2026-02-13_031916Z

Every run of the script creates a fresh flow. You can see exactly when each one was deployed just by reading the name.

06

The Results

3
Environments
3
Deployed
0
Failed
PowerShell 7.5.4 — Terminal Output
╔══════════════════════════════════════════════════════╗ ║ Power Platform - Scheduled Flow Bulk Deployer v4.2 ║ ╚══════════════════════════════════════════════════════╝ [AUTH] Sign in once below. This token handles everything. [AUTH] Flow API token acquired. [AUTH] Admin token acquired (no extra login). ───────────────────────────────────────────── [ENV] prod101 (Developer) [DEPLOY] Creating: Daily 8AM Health Check - 2026-02-13_031916Z [SUCCESS] Flow deployed! ───────────────────────────────────────────── [ENV] prod101 (Developer) [DEPLOY] Creating: Daily 8AM Health Check - 2026-02-13_031916Z [SUCCESS] Flow deployed! ───────────────────────────────────────────── [ENV] DEV2 (Developer) [DEPLOY] Creating: Daily 8AM Health Check - 2026-02-13_031917Z [SUCCESS] Flow deployed! ═══════════════════════════════════════════════ DEPLOYMENT SUMMARY ═══════════════════════════════════════════════ Total Environments : 3 Deployed : 3 Skipped : 0 Failed : 0 ═══════════════════════════════════════════════

Three developer environments. Three flows deployed. One login. Zero manual portal clicks. The HTML report opened automatically with clickable links to every environment and every flow.

07

Lessons Learned

  1. Don't trust PowerShell module auth for cross-API scenarios. Device code flow with refresh token exchange is more reliable and avoids DLL conflicts entirely.
  2. Dump the raw API response before filtering. The BAP API uses environmentSku, not environmentType. Documentation and reality diverge.
  3. Use raw JSON strings for complex API bodies. ConvertTo-Json with nested hashtables and $schema is a recipe for 400 errors.
  4. Always send UTF-8 bytes explicitly. Invoke-WebRequest with [System.Text.Encoding]::UTF8.GetBytes() avoids subtle encoding issues.
  5. The Flow API requires empty objects. connectionReferencesparameters, and outputs must be present even when empty, or you get a cryptic 400.
08

What's Next?

This script is a foundation. Here's where I'm taking it:

  • Run history monitoring — A companion script that checks if each heartbeat flow actually ran today. If it didn't, flag the environment as unhealthy.
  • Scheduled via Azure Automation — Run as a weekly runbook to keep deploying fresh flows with timestamps, creating a breadcrumb trail of environment health.
  • Teams/email alerts — Push the HTML report to a Teams channel or email distribution list automatically.
  • Cleanup automation — Auto-delete heartbeat flows older than 30 days to prevent clutter.
Get the Script

The full script is a single .ps1 file with no module dependencies for auth, handles all edge cases I discovered the hard way, and produces both CSV and HTML reports. If you manage a multi-environment Power Platform tenant, give it a spin.

Download  https://limewire.com/d/K5VPO#O1vSFIsg8H


Comments

Popular posts from this blog

Bridging the Impossible: Connecting Jira On-Prem to Power Automate & Copilot Studio — The Solution Nobody Built Until Now"

How I Automated My Entire SharePoint Tenant with 150 MCP Tools and Claude Desktop

Azure Management MCP Server