When employees leave, accounts get deprovisioned, or credentials rotate, Power Automate flows fail silently across your entire tenant. Learn how to enumerate orphaned flows, programmatically transfer ownership, and rehome connections to durable service principals before the damage happens.

Picture this: your organization's top automation architect just gave two weeks' notice. She owns 47 flows across three production environments. Some of those flows run overnight invoice processing. Others trigger customer notifications every time a Dynamics 365 record changes. Every single one of them is authenticated using connections that live under her personal Microsoft account — and when her account gets deprovisioned in Azure AD, every one of those flows will silently start failing.
You find out on a Tuesday. The account goes away on Friday.
This scenario plays out constantly in enterprise Power Platform environments, and it's not just about departing employees. Mergers introduce new tenants. Reorganizations move people between departments. Service accounts get rotated. IT policies mandate that personal credentials be replaced with managed identities or service principals. The underlying problem is the same in every case: Power Automate flows are deeply tied to the identity of the person who built them, and at scale, that creates a sprawling web of implicit dependencies that nobody fully mapped.
By the end of this lesson, you'll know how to systematically find every flow that's at risk, programmatically transfer ownership, rehome connections to durable service principals, and automate the entire detection-and-remediation cycle so it runs continuously rather than as a fire drill.
What you'll learn:
You should be comfortable with:
Invoke-RestMethodBefore you can fix ownership problems at scale, you need to understand the three distinct ownership layers in Power Automate, because they fail independently and require different remediation approaches.
Layer 1: Flow Owner (Creator) The flow owner is the Azure AD principal recorded at the time the flow was created. The owner's license is what satisfies the per-user entitlement check when the flow runs. If the owner's account is deleted or their license revoked, the flow will be disabled within 24–48 hours, depending on the license enforcement cycle.
Layer 2: Connection Owner Every connection in Power Automate — the SharePoint connection, the Outlook connection, the Dataverse connection — is owned by the Azure AD user whose credentials were used to authenticate it. When a flow action uses a connection, it impersonates that connection owner's identity. The flow owner and the connection owner are frequently different people, especially when flows were built collaboratively or cloned from someone else's work.
Layer 3: Run Context Some connectors, particularly Dataverse, also record a run-as context that determines which user's security role is applied during data operations. This is separate from both flow ownership and connection ownership.
Key insight
A flow can fail in three completely independent ways: the owner's license disappears (flow is disabled), the connection owner's account is deprovisioned (connections become broken), or the run-as principal loses its security role (operations return 403s). You need a remediation strategy for all three layers, not just the first.
When a connection becomes broken, every flow that uses it will fail — not just flows owned by the departing user. This is the most dangerous blast radius: a single shared connection used by 30 flows means 30 simultaneous failures when its owner leaves.
You cannot remediate what you cannot see. The first step is building a complete inventory of flows, their owners, their connections, and the owners of those connections. You'll do this using the Power Platform Admin APIs combined with the PowerShell PAC CLI.
You need an Azure AD app registration with the following permissions:
https://service.powerapps.com/.default (Power Platform service)https://management.azure.com/user_impersonation (ARM, for environment enumeration)Get a token and store it for reuse:
# Authenticate and get access token for Power Platform Admin API
$tenantId = "your-tenant-id"
$clientId = "your-app-registration-client-id"
$clientSecret = "your-client-secret"
$tokenBody = @{
grant_type = "client_credentials"
scope = "https://service.powerapps.com/.default"
client_id = $clientId
client_secret = $clientSecret
}
$tokenResponse = Invoke-RestMethod `
-Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" `
-Method POST `
-Body $tokenBody
$accessToken = $tokenResponse.access_token
$headers = @{ Authorization = "Bearer $accessToken" }
# Get all environments in the tenant
$environmentsUri = "https://api.bap.microsoft.com/providers/Microsoft.BusinessAppPlatform/environments?api-version=2021-04-01&`$top=250"
$environments = Invoke-RestMethod -Uri $environmentsUri -Headers $headers -Method GET
$envList = $environments.value
Write-Host "Found $($envList.Count) environments"
$allFlows = @()
foreach ($env in $envList) {
$envName = $env.name
$envDisplayName = $env.properties.displayName
# Get all flows in this environment (admin endpoint sees all flows, not just owned)
$flowsUri = "https://api.flow.microsoft.com/providers/Microsoft.ProcessSimple/scopes/admin/environments/$envName/flows?api-version=2016-11-01&`$top=250"
try {
$flowsResponse = Invoke-RestMethod -Uri $flowsUri -Headers $headers -Method GET
foreach ($flow in $flowsResponse.value) {
$allFlows += [PSCustomObject]@{
EnvironmentName = $envName
EnvironmentDisplayName = $envDisplayName
FlowId = $flow.name
FlowDisplayName = $flow.properties.displayName
OwnerObjectId = $flow.properties.creator.objectId
OwnerEmail = $flow.properties.creator.email
State = $flow.properties.state
CreatedTime = $flow.properties.createdTime
LastModifiedTime = $flow.properties.lastModifiedTime
DefinitionSummary = $flow.properties.definitionSummary | ConvertTo-Json -Compress
}
}
# Handle pagination
while ($flowsResponse.nextLink) {
$flowsResponse = Invoke-RestMethod -Uri $flowsResponse.nextLink -Headers $headers -Method GET
foreach ($flow in $flowsResponse.value) {
$allFlows += [PSCustomObject]@{
EnvironmentName = $envName
EnvironmentDisplayName = $envDisplayName
FlowId = $flow.name
FlowDisplayName = $flow.properties.displayName
OwnerObjectId = $flow.properties.creator.objectId
OwnerEmail = $flow.properties.creator.email
State = $flow.properties.state
CreatedTime = $flow.properties.createdTime
LastModifiedTime = $flow.properties.lastModifiedTime
}
}
}
}
catch {
Write-Warning "Could not enumerate flows in environment $envDisplayName : $_"
}
}
Write-Host "Total flows discovered: $($allFlows.Count)"
$allFlows | Export-Csv -Path ".\flow-inventory.csv" -NoTypeInformation
Note
The admin flow enumeration endpoint requires either the Power Platform Admin role or the Environment Admin role scoped to each environment. If you're operating with delegated permissions rather than application permissions, you'll need to handle token refresh for long-running enumerations. For large tenants with 500+ environments, refer to Handling Pagination and Throttling When Querying Large Datasets in Power Automate for backoff and continuation strategies.
Now compare your flow owner list against Azure AD to identify accounts that are disabled, deleted, or unlicensed:
# Get unique owner object IDs
$uniqueOwnerIds = $allFlows | Select-Object -ExpandProperty OwnerObjectId -Unique
# Graph API token (separate scope)
$graphTokenBody = @{
grant_type = "client_credentials"
scope = "https://graph.microsoft.com/.default"
client_id = $clientId
client_secret = $clientSecret
}
$graphToken = (Invoke-RestMethod -Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" -Method POST -Body $graphTokenBody).access_token
$graphHeaders = @{ Authorization = "Bearer $graphToken" }
$ownerStatus = @{}
foreach ($ownerId in $uniqueOwnerIds) {
try {
$userUri = "https://graph.microsoft.com/v1.0/users/$ownerId`?`$select=id,displayName,userPrincipalName,accountEnabled,assignedLicenses"
$user = Invoke-RestMethod -Uri $userUri -Headers $graphHeaders -Method GET
$ownerStatus[$ownerId] = [PSCustomObject]@{
ObjectId = $ownerId
DisplayName = $user.displayName
UPN = $user.userPrincipalName
AccountEnabled = $user.accountEnabled
HasLicense = ($user.assignedLicenses.Count -gt 0)
RiskLevel = if (-not $user.accountEnabled) { "Critical" }
elseif ($user.assignedLicenses.Count -eq 0) { "High" }
else { "OK" }
}
}
catch {
# 404 means the user object is completely deleted from AAD
$ownerStatus[$ownerId] = [PSCustomObject]@{
ObjectId = $ownerId
DisplayName = "DELETED"
UPN = "DELETED"
AccountEnabled = $false
HasLicense = $false
RiskLevel = "Critical"
}
}
}
# Join flows with owner status
$riskReport = $allFlows | ForEach-Object {
$ownerInfo = $ownerStatus[$_.OwnerObjectId]
$_ | Add-Member -NotePropertyName "OwnerRiskLevel" -NotePropertyValue $ownerInfo.RiskLevel -PassThru |
Add-Member -NotePropertyName "OwnerEnabled" -NotePropertyValue $ownerInfo.AccountEnabled -PassThru |
Add-Member -NotePropertyName "OwnerHasLicense" -NotePropertyValue $ownerInfo.HasLicense -PassThru
}
$criticalFlows = $riskReport | Where-Object { $_.OwnerRiskLevel -eq "Critical" -and $_.State -eq "Started" }
Write-Host "CRITICAL: $($criticalFlows.Count) active flows with deleted/disabled owners"
$criticalFlows | Export-Csv -Path ".\critical-flows-for-remediation.csv" -NoTypeInformation
Once you've identified which flows need new owners, you have two mechanisms for transferring ownership: the Admin API (for individual flows) and bulk operations through the CoE toolkit's Power Shell module.
function Set-FlowOwner {
param(
[string]$EnvironmentName,
[string]$FlowId,
[string]$NewOwnerObjectId,
[hashtable]$AdminHeaders
)
$uri = "https://api.flow.microsoft.com/providers/Microsoft.ProcessSimple/scopes/admin/environments/$EnvironmentName/flows/$FlowId/modifyowners?api-version=2016-11-01"
$body = @{
put = @(
@{
id = "/providers/Microsoft.ProcessSimple/scopes/admin/environments/$EnvironmentName/users/$NewOwnerObjectId"
}
)
} | ConvertTo-Json -Depth 5
try {
$result = Invoke-RestMethod -Uri $uri -Headers $AdminHeaders -Method POST -Body $body -ContentType "application/json"
return @{ Success = $true; FlowId = $FlowId }
}
catch {
$statusCode = $_.Exception.Response.StatusCode.value__
return @{ Success = $false; FlowId = $FlowId; Error = $_.ToString(); StatusCode = $statusCode }
}
}
Transferring ownership across hundreds of flows means you'll hit API rate limits. Build retry logic into the bulk operation:
function Invoke-BulkOwnershipTransfer {
param(
[array]$FlowsToRemediate,
[string]$NewOwnerObjectId,
[hashtable]$AdminHeaders,
[int]$DelayBetweenCallsMs = 200,
[int]$MaxRetries = 3
)
$results = @()
$successCount = 0
$failureCount = 0
foreach ($flow in $FlowsToRemediate) {
$attempt = 0
$transferred = $false
while ($attempt -lt $MaxRetries -and -not $transferred) {
$attempt++
$result = Set-FlowOwner `
-EnvironmentName $flow.EnvironmentName `
-FlowId $flow.FlowId `
-NewOwnerObjectId $NewOwnerObjectId `
-AdminHeaders $AdminHeaders
if ($result.Success) {
$transferred = $true
$successCount++
Write-Host "[$successCount transferred] $($flow.FlowDisplayName)" -ForegroundColor Green
}
elseif ($result.StatusCode -eq 429) {
# Rate limited — back off exponentially
$backoffSeconds = [Math]::Pow(2, $attempt) * 5
Write-Warning "Rate limited. Backing off for $backoffSeconds seconds..."
Start-Sleep -Seconds $backoffSeconds
}
else {
Write-Warning "Failed (attempt $attempt): $($flow.FlowDisplayName) — $($result.Error)"
if ($attempt -lt $MaxRetries) { Start-Sleep -Milliseconds $DelayBetweenCallsMs }
}
}
$results += [PSCustomObject]@{
FlowId = $flow.FlowId
FlowDisplayName = $flow.FlowDisplayName
Environment = $flow.EnvironmentName
Transferred = $transferred
Attempts = $attempt
}
Start-Sleep -Milliseconds $DelayBetweenCallsMs
}
Write-Host "`nTransfer complete: $successCount succeeded, $failureCount failed"
return $results
}
# Execute the transfer
$serviceAccountObjectId = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" # Your service account's AAD object ID
$transferResults = Invoke-BulkOwnershipTransfer `
-FlowsToRemediate $criticalFlows `
-NewOwnerObjectId $serviceAccountObjectId `
-AdminHeaders $headers
$transferResults | Export-Csv -Path ".\transfer-results-$(Get-Date -Format 'yyyyMMdd-HHmm').csv" -NoTypeInformation
Warning
Transferring ownership does not automatically fix broken connections. The new owner will own the flow, but any connections that are still tied to the departing user's credentials will remain broken. Ownership transfer and connection rehoming are separate operations that must both be performed.
Ownership transfer is the easy part. Connection rehoming is where most teams get stuck, because connections in Power Automate are first-class objects with their own identity, and you cannot simply "move" a connection from one user to another. You must create a new connection under the target identity and update all flows that reference the old one.
If your flows are solution-aware and use connection references (which they absolutely should be in enterprise environments), rehoming is significantly cleaner. The connection reference is the indirection layer: you update the connection reference to point to a new connection, and every flow using that reference automatically picks up the change.
If your flows use direct connections (common in flows created outside of solutions or in the default environment), you face a more manual process: each flow must have its connection swapped individually.
Key insight
This is why Implementing Connection References in Power Automate Solutions is not a nice-to-have governance practice — it's the architectural prerequisite that makes bulk connection rehoming tractable. If you haven't already migrated your production flows to use connection references, this crisis is your forcing function.
function Get-SolutionConnectionReferences {
param(
[string]$EnvironmentName,
[hashtable]$AdminHeaders
)
# Dataverse API to query connection references
$environmentDomain = "your-org.crm.dynamics.com" # Get from environment properties
$connectionRefsUri = "https://$environmentDomain/api/data/v9.2/connectionreferences?`$select=connectionreferenceid,connectionreferencelogicalname,connectorid,connectionid,statecode"
$connRefs = Invoke-RestMethod -Uri $connectionRefsUri -Headers @{
Authorization = $AdminHeaders.Authorization
"OData-MaxVersion" = "4.0"
"OData-Version" = "4.0"
Accept = "application/json"
} -Method GET
return $connRefs.value
}
The ideal rehoming target is a service principal connection — one that authenticates using an Azure AD application registration rather than a human user account. This is the most durable approach because application registrations don't get deprovisioned when people leave.
For connectors that support service principal auth (Dataverse, SharePoint with certain configurations, Teams), create the connection through the Power Apps API:
function New-ServicePrincipalConnection {
param(
[string]$EnvironmentName,
[string]$ConnectorId, # e.g., "/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps"
[string]$ServicePrincipalClientId,
[string]$ServicePrincipalTenantId,
[hashtable]$AdminHeaders
)
$createConnectionUri = "https://api.powerapps.com/providers/Microsoft.PowerApps/environments/$EnvironmentName/connections?api-version=2021-02-01"
$connectionBody = @{
properties = @{
connectionParameters = @{
token = @{
AuthType = "ServicePrincipal"
TenantId = $ServicePrincipalTenantId
ClientId = $ServicePrincipalClientId
}
}
apiId = $ConnectorId
environment = @{
id = "/providers/Microsoft.PowerApps/environments/$EnvironmentName"
name = $EnvironmentName
}
}
} | ConvertTo-Json -Depth 10
$newConnection = Invoke-RestMethod `
-Uri $createConnectionUri `
-Headers $AdminHeaders `
-Method POST `
-Body $connectionBody `
-ContentType "application/json"
return $newConnection.name # Returns the new connection ID
}
Note
Not every connector supports service principal authentication. SharePoint, for example, requires OAuth with delegated permissions for many operations. For connectors that cannot use service principals, the right fallback is a dedicated shared service account — a non-personal mailbox account with a Power Automate per-flow license attached. See Integrating Power Automate with Azure Key Vault and Managed Identities for the managed identity approach where it's available.
function Update-ConnectionReference {
param(
[string]$EnvironmentDomain, # e.g., "your-org.crm.dynamics.com"
[string]$ConnectionReferenceId,
[string]$NewConnectionId,
[hashtable]$DataverseHeaders
)
$updateUri = "https://$EnvironmentDomain/api/data/v9.2/connectionreferences($ConnectionReferenceId)"
$updateBody = @{
connectionid = $NewConnectionId
} | ConvertTo-Json
Invoke-RestMethod `
-Uri $updateUri `
-Headers $DataverseHeaders `
-Method PATCH `
-Body $updateBody `
-ContentType "application/json"
Write-Host "Updated connection reference $ConnectionReferenceId to use connection $NewConnectionId"
}
Once you update the connection reference, every solution-aware flow that uses it will immediately start using the new connection on its next run — no individual flow modification needed. This is the payoff for the connection reference investment.
Manual remediation scripts are useful for emergency response, but what you really need is a scheduled detection system that identifies at-risk flows before accounts are deprovisioned. Here's the architecture for an automated pipeline built as a Power Automate cloud flow.
The detection pipeline runs daily and follows this logic:
The flow uses HTTP actions against the Power Platform Admin API — the same endpoints you called from PowerShell above. For a deep dive on building production HTTP action flows with proper error handling and pagination, see Advanced Power Automate: Custom Connectors and HTTP Actions for Production Integration.
Create a Dataverse table called wsd_flowgovernancerecord with these columns:
| Column | Type | Purpose |
|---|---|---|
wsd_flowid |
Text | Flow GUID |
wsd_flowdisplayname |
Text | Human-readable name |
wsd_environmentname |
Text | Environment GUID |
wsd_ownerobjectid |
Text | AAD object ID of owner |
wsd_owneraccountenabled |
Yes/No | Current account status |
wsd_ownerleavingdate |
Date | If known from HR |
wsd_riskclassification |
Choice | Critical/High/Medium/OK |
wsd_remediationstatus |
Choice | Pending/InProgress/Completed/Failed |
wsd_lastscandate |
DateTime | When this record was last updated |
Because this flow spans multiple pages, here's the logical structure you'll implement, with the key action configurations called out:
Recurrence trigger: Daily at 2:00 AM
[Initialize Variables]
- varEnvironmentList: Array
- varFlowsAtRisk: Array
- varProcessed: Integer (0)
[HTTP - Get All Environments]
Method: GET
URI: https://api.bap.microsoft.com/providers/Microsoft.BusinessAppPlatform/environments
?api-version=2021-04-01&$top=250
Authentication: Active Directory OAuth
Tenant: @{parameters('TenantId')}
Audience: https://service.powerapps.com/
Client ID: @{parameters('AppClientId')}
Secret: @{parameters('AppClientSecret')} [stored in Key Vault env variable]
[Apply to Each - Environment]
[HTTP - Get Flows in Environment]
URI: https://api.flow.microsoft.com/providers/Microsoft.ProcessSimple/scopes/admin
/environments/@{items('Apply_to_each_Environment')?['name']}/flows
?api-version=2016-11-01&$top=100
[Apply to Each - Flow]
[HTTP - Check Owner in Graph]
URI: https://graph.microsoft.com/v1.0/users/@{items('Apply_to_each_Flow')?['properties']?['creator']?['objectId']}
?$select=accountEnabled,assignedLicenses,employeeLeaveDateTime
[Condition - Owner at Risk?]
If accountEnabled == false OR assignedLicenses is empty OR
employeeLeaveDateTime within 30 days:
[Dataverse - Upsert governance record]
Match on: wsd_flowid + wsd_environmentname
Set: risk classification, owner status, scan date
[Condition - Critical (account disabled/deleted)?]
Yes:
[Child Flow - Trigger Remediation]
Pass: FlowId, EnvironmentName, ReplacementOwnerObjectId
No:
[Send Email to Governance Team with risk details]
Tip
Use the child flow pattern for the remediation step so the detection flow doesn't blow up its own timeout budget executing ownership transfers. The detection flow records the intent in Dataverse and delegates execution to a remediation child flow. This pattern is covered in depth at Orchestrating Child Flows and Scoped Execution in Power Automate for Scalable, Reusable Automation Architecture.
The child flow receives three inputs: FlowId, EnvironmentName, and ReplacementOwnerObjectId. It:
In this exercise, you'll build the PowerShell-based detection report end-to-end and identify real at-risk flows in a dev/test environment. You'll need the Azure AD app registration with API permissions described in the Prerequisites section.
Step 1: Set up your environment
Create a directory flow-governance and a file config.ps1:
# config.ps1 - Do not commit to source control
$config = @{
TenantId = "your-tenant-id"
ClientId = "your-app-client-id"
ClientSecret = "your-app-client-secret" # Use Key Vault in production
ReplacementOwnerOid = "service-account-or-sp-object-id"
OutputDirectory = ".\reports"
AlertEmailTo = "platform-governance@yourcompany.com"
}
Step 2: Run the enumeration
Save the enumeration scripts from the sections above into enumerate-flows.ps1. Run it:
. .\config.ps1
.\enumerate-flows.ps1 -Config $config
This produces flow-inventory.csv and critical-flows-for-remediation.csv.
Step 3: Review the risk report
Open critical-flows-for-remediation.csv. For each flow, verify:
Step 4: Execute a test ownership transfer
Pick one non-critical flow in a dev environment and test the transfer:
. .\config.ps1
# Get tokens (from authenticate.ps1)
$testFlow = @{
EnvironmentName = "your-dev-environment-guid"
FlowId = "the-flow-guid-you-picked"
FlowDisplayName = "Test Flow for Ownership Transfer"
}
$result = Set-FlowOwner `
-EnvironmentName $testFlow.EnvironmentName `
-FlowId $testFlow.FlowId `
-NewOwnerObjectId $config.ReplacementOwnerOid `
-AdminHeaders $headers
if ($result.Success) {
Write-Host "Transfer successful. Verify in Power Automate portal that ownership changed."
} else {
Write-Host "Transfer failed: $($result.Error)"
}
Step 5: Verify in the portal
Go to make.powerautomate.com, switch to the dev environment, find the flow, open Details, and confirm the Owner field now shows the service account.
Step 6: Schedule the detection
Package the enumeration and risk-classification scripts as a scheduled Task in Azure Automation or as a Power Automate flow using the HTTP actions, targeting a weekly cadence initially. Once you've validated it doesn't generate false positives, move to daily.
Mistake 1: Transferring ownership without checking connection state first
You transfer ownership successfully, then discover the flow is still failing because its connections were tied to the old owner. Always inventory connection state before transfer so you know what remediation steps are needed after.
Diagnosis: After transfer, call the flow connections endpoint and check each connection's statuses array for "Unauthenticated" or "Error" status codes.
Mistake 2: Assuming all connectors support service principal auth
You build a beautiful service principal rehoming pipeline, then discover that the flows use the Office 365 Outlook connector, which requires delegated user permissions and cannot authenticate as an application. The remediation falls flat.
Prevention: Before planning your rehoming strategy, classify every connector used by at-risk flows into three buckets: (a) supports service principal, (b) supports shared service account, (c) requires individual user account. Bucket (c) connectors require a rethink at the architecture level, not just the credentials level.
Mistake 3: Rehoming connections without sharing them with the new owner
You create a new Dataverse connection under the service principal, update the connection reference to point to it, but forget that the connection itself must be shared with the flow's new owner (or with everyone who needs to use connection references backed by it). Flows start returning "Connection not shared" errors.
Fix: After creating the new connection, call the connection share API:
$shareUri = "https://api.powerapps.com/providers/Microsoft.PowerApps/environments/$envName/connections/$newConnectionId/modifyPermissions?api-version=2021-02-01"
$shareBody = @{
put = @(
@{
properties = @{
principal = @{
id = $newOwnerObjectId
type = "User"
}
roleName = "CanEdit"
}
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $shareUri -Headers $headers -Method POST -Body $shareBody -ContentType "application/json"
Mistake 4: Not accounting for flows in unmanaged solutions or the default environment
Your governance pipeline scans all maker environments but misses the default environment, which is where citizen developers and power users build flows outside of any solution. The default environment frequently contains the most orphaned flows.
Fix: Explicitly include the default environment in your enumeration list. It has a predictable naming pattern: it's the environment where isDefault: true in the environments API response.
Mistake 5: Ignoring the run history impact of ownership transfer
After transferring ownership, the flow's run history is not reassigned. If the new owner opens the flow, they see their name in Details but the run history shows entries from when the old owner ran it. This is cosmetic, but confuses teams who think something went wrong.
Warning
In some edge cases, particularly for flows that were suspended due to the owner's license expiry, the flow does not automatically re-enable after ownership transfer. You must explicitly call the enable endpoint: POST /flows/{flowId}/start via the Admin API after the transfer completes.
Mistake 6: Triggering remediation without a governance approval step
Automatically transferring ownership of all critical flows to a single service account sounds efficient until a flow owner comes back from vacation and can't find their flows. Or until a flow is transferred that was intentionally disabled.
Build a Dataverse-backed approval queue: the detection flow creates a remediation record with status "PendingApproval", sends an adaptive card to the governance team in Teams, and only executes the transfer after approval. For emergency cases (owner account completely deleted), auto-approve after a 4-hour window.
If your organization has deployed the CoE Starter Kit, you already have infrastructure for this work. The CoE Sync flows populate Dataverse tables with flow and connection inventory nightly. Rather than building enumeration from scratch, you can query these tables directly.
The relevant CoE tables are:
admin_flow — all flows across all environmentsadmin_connector — connector inventoryadmin_maker — maker (owner) profiles including last loginYour orphan detection query against CoE data becomes straightforward:
Dataverse: List rows from admin_flow
Filter: admin_enabled eq true
AND admin_owner_accountenabled eq false
OR admin_owner_lastlogin lt (today - 90 days)
The Auditing and Governing Power Automate at Scale lesson covers the full CoE toolkit setup and what each sync flow populates, which will save you significant time if you're building on top of it.
If your flows are deployed as part of solutions (as they should be in production), the connection rehoming story integrates naturally with your ALM pipeline. When you promote a solution from test to production, the deployment step should automatically map connection references to the appropriate production connections owned by the service principal.
This is handled through the DeploymentSettings.json file in your pipeline:
{
"ConnectionReferences": [
{
"LogicalName": "wsd_sharedcommondataserviceforapps_9a2f1",
"ConnectionId": "/providers/Microsoft.PowerApps/environments/prod-env-id/connections/prod-dataverse-sp-connection",
"ConnectorId": "/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps"
},
{
"LogicalName": "wsd_sharedsharepoint_b8c3e",
"ConnectionId": "/providers/Microsoft.PowerApps/environments/prod-env-id/connections/prod-sharepoint-svc-connection",
"ConnectorId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline"
}
]
}
This file is maintained per environment and checked into source control alongside the solution. Every deployment to production automatically uses the service principal connections, meaning no human-owned connection ever makes it to production through the formal pipeline. See Deploying and Managing Power Automate Solutions Across Environments for the full pipeline implementation.
Tip
Store connection IDs for each environment in your pipeline's variable groups rather than hardcoding them in DeploymentSettings.json. This lets you rotate connections (when service principal credentials change) without modifying solution files.
You now have a complete framework for managing flow ownership and connections at enterprise scale. Let's recap what you've built:
Detection capability: A PowerShell enumeration pipeline that cross-references flow owners against Azure AD account status, producing a risk-classified inventory of every flow in the tenant organized by urgency.
Ownership transfer: A reusable Set-FlowOwner function with retry logic and throttle handling, wrapped in a bulk transfer orchestrator for emergency remediation scenarios.
Connection rehoming: A systematic approach that distinguishes between connection references (the good path) and direct connections (the hard path), with concrete API calls for creating service principal connections and updating connection reference records.
Automated detection pipeline: A scheduled Power Automate flow that runs nightly, identifies at-risk flows before accounts are deprovisioned, routes critical cases to automated remediation, and maintains a Dataverse audit trail.
Next steps to build on this foundation:
Extend detection to include connection health — not just owner account status. Query each connection's status endpoint and flag any connection in "Error" or "Unauthenticated" state, regardless of whether the owner's account is active.
Add manager notification to your HR offboarding workflow — integrate with your identity lifecycle management system (Workday, ServiceNow, or Azure AD Lifecycle Workflows) to trigger the detection and remediation pipeline automatically when an employee's departure date is set, rather than catching it post-fact.
Review your licensing posture — when you transfer ownership to a service account, that service account's license determines whether flows can run. Ensure your service accounts have the right per-flow or per-user licensing to cover the flows transferred to them.
Implement proactive governance policies — use Managed Environments in Power Platform to enforce connection reference requirements at the maker level, so new flows can't be published without using the approved connection patterns that make rehoming tractable.
The goal is to reach a state where no production flow is one departing employee away from a silent failure. That's an achievable target, but it requires both the automated tooling you've built here and the architectural standards that make bulk remediation possible.