Collaborate, Innovate, Automate

Get-FlowsByPersonConnection

This PowerShell script is built for a specific offboarding question: "this person is leaving, what will break?" It searches every Power Automate flow, across one or all environments in a tenant, for two ways a departing person puts a flow at risk — they own the flow, or a connector action inside the flow authenticates using their credentials, regardless of who owns the flow itself. The connection-owner case is the more dangerous one, since the flow silently breaks the moment the account is disabled, even if someone else is listed as the flow's owner.

Purpose

Scope

Prerequisites

PowerShell Script

<#
.SYNOPSIS
    Finds every Power Automate flow, across all (or one) environment, where a
    specific person either owns the flow or owns a connection the flow uses.
    Built for offboarding checks — e.g. "this person is leaving, what will
    break?" — rather than a full tenant governance audit.

.DESCRIPTION
    Reuses the same flow-to-connection join logic as the full ownership
    audit script, but scoped to a single target person instead of flagging
    everyone who isn't a service account. Two ways a person shows up as a
    risk when they leave:

      1. FLOW OWNER - they created the flow. Ownership can usually be
         reassigned without breaking the flow, but it needs doing before
         their account is disabled or the flow becomes orphaned.
      2. CONNECTION OWNER - a connector action inside the flow authenticates
         using THEIR credentials, regardless of who owns the flow itself.
         This is the more dangerous case: the flow silently breaks the
         moment their account is disabled, even if someone else "owns" it.

.PARAMETER TargetUpn
    Entra Object ID (GUID) of the person leaving. Get-AdminFlow and
    Get-AdminPowerAppConnection record ownership by Object ID, not by
    email/UPN, so that's what this script matches against. Look the GUID up
    via entra.microsoft.com -> Users -> [person] -> Object ID field. The
    parameter is still named TargetUpn for backwards compatibility with
    earlier calls to this script - pass the GUID into it, not an email
    address.

.PARAMETER EnvironmentName
    Optional. GUID of a single environment to search. Omit to search every
    environment in the tenant (slower, but complete).

.PARAMETER OutputFolder
    Where to write the CSV. Defaults to current directory.

.EXAMPLE
    .\Get-FlowsByPersonConnection.ps1 -TargetUpn "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

.EXAMPLE
    .\Get-FlowsByPersonConnection.ps1 -TargetUpn "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -EnvironmentName "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

.NOTES
    Requires: Microsoft.PowerApps.Administration.PowerShell, Microsoft.PowerApps.PowerShell
    Requires: Power Platform Administrator or Global Administrator role
    Requires: Windows PowerShell 5.1 (Microsoft.PowerApps.Administration.PowerShell
    is not fully compatible with PowerShell 7 / pwsh - if you hit a
    "Could not load type 'System.Web.UI.WebResourceAttribute'" error, run
    this from powershell.exe rather than pwsh)
    To find someone's Object ID: entra.microsoft.com -> Users -> search their
    name -> Object ID field on their profile page (next to User principal name)
    Author:   Cameron Griffiths
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string]$TargetUpn =  "",

    [Parameter(Mandatory = $false)]
    [string]$EnvironmentName = "",

    [Parameter(Mandatory = $false)]
    [string]$OutputFolder = "."
)

# --- Modules ---
$requiredModules = @("Microsoft.PowerApps.Administration.PowerShell", "Microsoft.PowerApps.PowerShell")
foreach ($mod in $requiredModules) {
    if (-not (Get-Module -ListAvailable -Name $mod)) {
        Write-Host "Installing $mod ..." -ForegroundColor Cyan
        Install-Module -Name $mod -Scope CurrentUser -Force -AllowClobber
    }
}

# --- Auth ---
Write-Host "Signing in to Power Platform admin APIs ..." -ForegroundColor Cyan
Add-PowerAppsAccount

# --- Resolve environments in scope ---
if ($EnvironmentName) {
    $envs = Get-AdminPowerAppEnvironment -EnvironmentName $EnvironmentName
    if (-not $envs) {
        Write-Error "Environment '$EnvironmentName' not found or not accessible."
        return
    }
}
else {
    Write-Host "Enumerating all environments in the tenant ..." -ForegroundColor Cyan
    $envs = Get-AdminPowerAppEnvironment
}

Write-Host "Searching $($envs.Count) environment(s) for '$TargetUpn' ..." -ForegroundColor Cyan

$results = New-Object System.Collections.Generic.List[Object]
$envCounter = 0

foreach ($env in $envs) {
    $envCounter++
    Write-Host "`n[$envCounter/$($envs.Count)] Environment: $($env.DisplayName)" -ForegroundColor Yellow

    # --- Connections in this environment, keyed by connection ID ---
    $connLookup = @{}
    try {
        $conns = Get-AdminPowerAppConnection -EnvironmentName $env.EnvironmentName -ErrorAction Stop
        foreach ($c in $conns) {
            $connLookup[$c.ConnectionName] = $c
        }
        Write-Host "  $($conns.Count) connection(s) in this environment." -ForegroundColor Gray

        $personConns = $conns | Where-Object { $_.CreatedBy.userId -eq $TargetUpn }
        if ($personConns.Count -gt 0) {
            Write-Host "  -> $($personConns.Count) connection(s) owned by $TargetUpn" -ForegroundColor Magenta
        }
    }
    catch {
        Write-Host "  Could not enumerate connections: $($_.Exception.Message)" -ForegroundColor Red
        continue
    }

    # --- Flows in this environment ---
    try {
        $flows = Get-AdminFlow -EnvironmentName $env.EnvironmentName -ErrorAction Stop
        Write-Host "  $($flows.Count) flow(s) in this environment." -ForegroundColor Gray
    }
    catch {
        Write-Host "  Could not enumerate flows: $($_.Exception.Message)" -ForegroundColor Red
        continue
    }

    $flowCounter = 0
    foreach ($flow in $flows) {
        $flowCounter++
        Write-Progress -Activity "Scanning flows in $($env.DisplayName)" `
                        -Status $flow.DisplayName `
                        -PercentComplete (($flowCounter / [Math]::Max($flows.Count,1)) * 100)

        $isFlowOwner = $flow.CreatedBy.userId -eq $TargetUpn

        # Pull connection references for this flow
        $connectionRefs = $null
        try {
            $connectionRefs = Get-AdminFlow -EnvironmentName $env.EnvironmentName `
                                             -FlowName $flow.FlowName `
                                             -ReturnConnectionReferences -ErrorAction Stop
        }
        catch {
            # Some flows fail to resolve connection references - skip silently, ownership check still applies
        }

        $refs = $connectionRefs.Internal.properties.connectionReferences
        $matchedOnConnection = $false

        if ($refs -and $refs.PSObject.Properties.Count -gt 0) {
            foreach ($refProp in $refs.PSObject.Properties) {
                $ref = $refProp.Value
                $connId = $ref.connectionName
                $connectorName = $ref.id -replace ".*/providers/Microsoft.PowerApps/apis/", ""
                $matchedConn = $connLookup[$connId]

                if ($matchedConn -and $matchedConn.CreatedBy.userId -eq $TargetUpn) {
                    $matchedOnConnection = $true
                    $results.Add([PSCustomObject]@{
                        EnvironmentName = $env.DisplayName
                        FlowName        = $flow.DisplayName
                        FlowId          = $flow.FlowName
                        FlowOwner       = $flow.CreatedBy.userId
                        MatchReason     = "Connection owner"
                        ConnectorName   = $connectorName
                        ConnectionId    = $connId
                        FlowEnabled     = $flow.Enabled
                    })
                }
            }
        }

        # If they own the flow but weren't already captured via a connection match, add a row for that too
        if ($isFlowOwner -and -not $matchedOnConnection) {
            $results.Add([PSCustomObject]@{
                EnvironmentName = $env.DisplayName
                FlowName        = $flow.DisplayName
                FlowId          = $flow.FlowName
                FlowOwner       = $flow.CreatedBy.userId
                MatchReason     = "Flow owner"
                ConnectorName   = ""
                ConnectionId    = ""
                FlowEnabled     = $flow.Enabled
            })
        }
    }
    Write-Progress -Activity "Scanning flows in $($env.DisplayName)" -Completed
}

# --- Output ---
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$safeUpn = ($TargetUpn -replace "[^a-zA-Z0-9]", "_")
$outPath = Join-Path $OutputFolder "FlowsFor_${safeUpn}_$timestamp.csv"

$results | Sort-Object EnvironmentName, FlowName, MatchReason | Export-Csv -Path $outPath -NoTypeInformation -Encoding UTF8

Write-Host "`n============================================" -ForegroundColor Green
Write-Host "Search complete for: $TargetUpn" -ForegroundColor Green
Write-Host "Report: $outPath" -ForegroundColor Green
Write-Host "Total matching rows: $($results.Count)" -ForegroundColor Green
Write-Host "============================================`n" -ForegroundColor Green

if ($results.Count -gt 0) {
    $byReason = $results | Group-Object MatchReason | Select-Object Name, Count
    Write-Host "Breakdown:" -ForegroundColor Yellow
    $byReason | Format-Table -AutoSize

    Write-Host "`nFlows where this person owns the CONNECTION (highest risk - breaks on offboarding regardless of flow owner):" -ForegroundColor Red
    $results | Where-Object { $_.MatchReason -eq "Connection owner" } |
        Select-Object EnvironmentName, FlowName, ConnectorName, FlowEnabled |
        Format-Table -AutoSize
}
else {
    Write-Host "No flows found where '$TargetUpn' is the flow owner or a connection owner." -ForegroundColor Green
}

Usage Notes

Related