Saturday, March 14, 2026

Building a Custom Plugin in Dataverse and Calling It from Power Automate

Building a Custom Plugin in Dataverse and Calling It from Power Automate

A step-by-step guide for developers

March 2026  |  Power Platform Series

What Are We Building?

In this post, we will walk through how to write a simple C# plugin, register it in Microsoft Dataverse, expose it as a custom API action, and finally call it from Power Automate as an unbound action.

To keep things simple, our plugin will do one thing: add two numbers together. The formula is:

 

  a = b + c

 

Simple math — but the plumbing around it covers everything you need to know to build real-world business logic in Dataverse.

 

What You Need Before Starting

Tool

Why You Need It

Visual Studio

Write and build the C# plugin

Power Platform / Dataverse environment

Deploy and test the plugin

Plugin Registration Tool (PRT)

Register the DLL in Dataverse

.NET Framework 4.6.2

Required target framework for Dataverse plugins

Microsoft.CrmSdk.CoreAssemblies (NuGet)

Gives you IPlugin and Dataverse base classes

 

Important: Dataverse plugins only support .NET Framework 4.6.2. Do NOT use .NET 5, 6, 7, or 8. This is the most common mistake developers make.

 

Step 1 — Create the Visual Studio Project

Open Visual Studio and create a new project:

1.    File > New > Project

2.    Choose Class Library (.NET Framework)

3.    Set the target framework to .NET Framework 4.6.2

4.    Name the project — for example, MyCalcPlugin

 

Then install the Dataverse SDK via NuGet Package Manager Console:

Install-Package Microsoft.CrmSdk.CoreAssemblies

 

After installing, confirm the DLL reference path contains 'net462' — for example: packages\Microsoft.CrmSdk.CoreAssemblies.9.x.x\lib\net462\Microsoft.Xrm.Sdk.dll

 

Step 2 — Write the Plugin Code

Replace the default class with this code. The plugin reads two input parameters (b and c), adds them together, and writes the result to an output parameter (a).

 

using Microsoft.Xrm.Sdk;

using System;

 

namespace MyCalcPlugin

{

    public class AddNumbers : IPlugin

    {

        public void Execute(IServiceProvider serviceProvider)

        {

            var context = (IPluginExecutionContext)

                serviceProvider.GetService(typeof(IPluginExecutionContext));

 

            var tracer = (ITracingService)

                serviceProvider.GetService(typeof(ITracingService));

 

            try

            {

                decimal b = (decimal)context.InputParameters["b"];

                decimal c = (decimal)context.InputParameters["c"];

                decimal a = b + c;

 

                context.OutputParameters["a"] = a;

                tracer.Trace($"Result: {b} + {c} = {a}");

            }

            catch (Exception ex)

            {

                throw new InvalidPluginExecutionException($"Error: {ex.Message}");

            }

        }

    }

}

 

Build the project in Release mode. Your DLL will be in the bin\Release\ folder.

 

Step 3 — Sign the Assembly (Required)

Dataverse requires all plugin DLLs to be strongly signed. Without signing, the Plugin Registration Tool will reject the file.

5.    Right-click the project in Solution Explorer > Properties

6.    Go to the Signing tab

7.    Check 'Sign the assembly'

8.    In the dropdown, choose New... and create a key file (e.g. MyCalcPlugin.snk)

9.    Leave password protection unchecked for simplicity

10. Click OK, then Clean Solution and Rebuild

 

To verify the signing worked, run this in a Developer Command Prompt from the bin\Release\ folder:

sn.exe -v MyCalcPlugin.dll

 

You should see: Assembly 'MyCalcPlugin.dll' is valid. If you see 'Strong name validation failed', delete bin\ and obj\ folders and do a full Rebuild.

 

Step 4 — Create the Custom Action in Dataverse

The Custom Action is what creates the message that your plugin listens to. Think of it as the API contract — it defines the input and output parameters.

11. Go to make.powerapps.com and open your solution

12. Click New > Automation > Process

13. Set Category to Action and Entity to None (global) — this makes it unbound

14. Give it a name, for example AddNumbers

 

Then add parameters in the Process Arguments section:

 

Name

Direction / Type / Required

b

Input | Decimal Number | Required

c

Input | Decimal Number | Required

a

Output | Decimal Number

 

Save and Close, then click Activate. The unique name of your action (visible in the Name field) will look like new_AddNumbers — note this name, you will need it in the next steps.

 

Do not add any workflow steps inside the action designer. The plugin handles the logic — the action just defines the parameters.

 

Step 5 — Register the Plugin in PRT

The Plugin Registration Tool (PRT) connects your DLL to Dataverse. You can get it from NuGet or install via the Power Platform Tools extension in Visual Studio.

 

Register the Assembly

15. Open PRT and connect to your Dataverse environment

16. Click Register > Register New Assembly

17. Browse to your bin\Release\MyCalcPlugin.dll

18. Select Isolation Mode: Sandbox and Storage: Database

19. Click Register Selected Plugins

 

Register a Step on the Plugin

20. In the left tree, expand (Assembly) MyCalcPlugin

21. Right-click MyCalcPlugin.AddNumbers > Register New Step

22. Fill in the step details as shown below:

 

Field

Value

Message

new_AddNumbers  (your action's unique name)

Primary Entity

(leave blank — unbound action)

Event Handler

MyCalcPlugin.AddNumbers

Eventing Pipeline Stage

Post-operation

Execution Mode

Synchronous

 

23. Click Register New Step

 

Always right-click the plugin class node to register a step. If you use the top toolbar instead, the Event Handler dropdown will be blank and registration will fail.

 

Step 6 — Call the Action from Power Automate

Now that everything is wired up, you can call the action from a flow.

24. Create a new flow in Power Automate

25. Add a new action and search for Perform an unbound action (Dataverse connector)

26. In the Action Name field, type your action's unique name: new_AddNumbers

27. The input fields b and c will appear — map them to values or dynamic content

28. In the next step, reference the output like this:

 

outputs('Perform_an_unbound_action')?['body/a']

 

If new_AddNumbers does not appear in the dropdown, the action is still in Draft state. Go back to make.powerapps.com and Activate it first.

 

Common Errors and How to Fix Them

Error

Fix

Could not load assembly System.Runtime Version=8.0.0.0

Wrong target framework. Change project to .NET Framework 4.6.2 and rebuild.

Assemblies must be strongly signed

Go to project Properties > Signing tab, enable signing, create a .snk key file, and rebuild.

Strong name validation failed (0x8013141A)

Delete bin\ and obj\ folders, clean solution, rebuild. Or run: sn.exe -Vr *

Invalid Message Name in PRT

The Custom Action has not been created in Dataverse yet. Create and activate the action first.

Plugin was not specified (required field)

You used the top toolbar to register the step. Right-click the plugin class node instead.

No values matching your search in Flow

Action is in Draft state or wrong unique name used. Activate the action and use the prefix e.g. new_AddNumbers.

 

How It All Fits Together

Here is a high-level view of the full architecture:

 

  C# Class Library (.NET Framework 4.6.2)

          |

          |  Build signed .dll

          v

  Plugin Registration Tool

          |

          |  Register assembly + step on message

          v

  Dataverse Custom Action  (new_AddNumbers)

          |

          |  Exposes as Web API endpoint

          v

  Power Automate  ->  Perform an unbound action

          |

          v

  Returns: a = b + c

 

Wrapping Up

To summarise what we covered:

      Write a C# plugin targeting .NET Framework 4.6.2

      Sign the assembly with a strong name key (.snk)

      Create a Custom Action in Dataverse to define input/output parameters

      Register the plugin DLL and a step in the Plugin Registration Tool

      Call the action from Power Automate using the Perform an unbound action connector

 

This pattern works for any business logic you want to centralise in Dataverse — validation, calculations, integrations, and more. Once the plugin is registered, it can be called from flows, canvas apps via Power Pages, or directly via the Web API.

The diagram shows the full journey:

  1. C# Plugin — code + signing
  2. Plugin Registration Tool — deploy the DLL
  3. Dataverse Custom Action — the API contract
  4. Web API endpoint — what gets exposed
  5. Power Automate — where you consume it

Happy coding!


Tuesday, March 10, 2026

Finding & Reassigning Abandoned Flows in Power Platform

Power Platform Administration

Finding & Reassigning Abandoned Flows

A complete, beginner-friendly guide using PowerShell

March 2025    Power Automate    PowerShell    Microsoft Graph

Introduction

If you manage a Microsoft Power Platform environment — even a small one — you have almost certainly run into this situation: flows that nobody owns, flows sitting in a broken state for months, flows owned by employees who have long since left the company. These are what we call abandoned flows.

 

This blog post walks you through exactly what abandoned flows are, why they are a problem, how to find them using PowerShell, and how to take ownership of them so you can fix or clean them up. No prior PowerShell experience is required — every step is explained in plain English.

 

What Are Abandoned Flows?

A Power Automate flow is a set of automated steps that runs on a trigger (like a new email, a form submission, or a schedule). Flows are owned by users in your organisation. An abandoned flow is any flow that has effectively become ownerless or unmanaged. This typically happens in one of four ways:

 

Category

Description

Orphaned

The original owner's account was deleted or disabled in Entra ID (Azure AD). The flow still exists but has no active owner.

Suspended

Power Platform automatically suspends a flow when its connections break (e.g. a password change, licence removal, or connector expiry).

Stopped

Someone turned the flow off and it has been sitting idle with no modification for a long time.

Stale

The flow is technically on, but has not been modified or checked in 90+ days and may be running on outdated logic.

 

💡  Why does this matter?  Abandoned flows can quietly fail, consume API quota, hold licences, or contain sensitive data with no one watching over them. In an audit or compliance review, they are a red flag.

 

Before You Start — Prerequisites

You will need the following before running any of the scripts in this guide:

 

       Power Platform Admin role or Global Admin in your Microsoft 365 tenant

       PowerShell 5.1 or PowerShell 7+ installed on your machine

       Internet access to reach the Power Platform admin APIs

 

Install the required PowerShell modules by running these commands once:

 

# Power Platform Admin module

Install-Module -Name Microsoft.PowerApps.Administration.PowerShell -Force -AllowClobber

 

# Microsoft Graph (for looking up Entra ID users)

Install-Module Microsoft.Graph -Scope CurrentUser -Force

 

⚠️  You only need to install these modules once. After that, you can just run the scripts directly.

 

Step 1 — Connect to Power Platform

Before the scripts can talk to your tenant, you must authenticate. Run the following:

 

# Sign in to Power Platform Admin

Add-PowerAppsAccount

 

# Sign in to Microsoft Graph (for user lookups)

Connect-MgGraph -Scopes 'User.Read.All'

 

A browser window will open asking you to sign in with your admin account. Once signed in, you are ready to proceed.

 

Step 2 — Find All Abandoned Flows

The script below loops through every environment in your tenant, collects all flows, and flags them as abandoned if they are suspended, stopped, or have not been modified in 90 days.

 

# How many days without modification counts as stale?

$staleDays = 90

$staleCutoff = (Get-Date).AddDays(-$staleDays)

 

# Collect all flows across all environments

$environments = Get-AdminPowerAppEnvironment

$abandonedFlows = @()

 

foreach ($env in $environments) {

    Write-Host "Scanning: $($env.DisplayName)" -ForegroundColor Cyan

    $flows = Get-AdminFlow -EnvironmentName $env.EnvironmentName

 

    foreach ($flow in $flows) {

        $state   = $flow.Internal.properties.state

        $lastMod = $flow.LastModifiedTime

 

        if ($state -in @("Suspended","Stopped") -or $lastMod -lt $staleCutoff) {

            $abandonedFlows += [PSCustomObject]@{

                DisplayName = $flow.DisplayName

                FlowName    = $flow.FlowName

                Environment = $env.EnvironmentName

                EnvDisplay  = $env.DisplayName

                State       = $state

                LastMod     = $lastMod

                Owner       = $flow.CreatedBy.userPrincipalName

            }

        }

    }

}

 

Write-Host "Found $($abandonedFlows.Count) abandoned flows" -ForegroundColor Yellow

$abandonedFlows | Format-Table DisplayName, EnvDisplay, State, Owner -AutoSize

 

At this point, you will see a table printed in your PowerShell window listing every abandoned flow. Take a moment to review it before moving on.

 

Bonus: Find Flows Owned by Deleted Users

If you also want to catch flows whose owner account no longer exists in Entra ID (for example, ex-employees), add this extra check:

 

# Get all active user UPNs from Entra ID

$activeUsers = Get-MgUser -All | Select-Object -ExpandProperty UserPrincipalName

 

# Filter flows where owner is not in active user list

$orphanedFlows = $abandonedFlows | Where-Object {

    $_.Owner -notin $activeUsers -and $_.Owner -ne $null

}

 

Write-Host "Orphaned (owner deleted): $($orphanedFlows.Count)"

$orphanedFlows | Format-Table DisplayName, Owner, EnvDisplay -AutoSize

 

Step 3 — Assign Abandoned Flows to Yourself

Now that you have a list, you can assign yourself as a co-owner (CanEdit) of each abandoned flow. This gives you full control to fix, update, or delete them.

 

📝  Note: Power Platform does not support changing the original creator of a flow. What you can do is add yourself as a co-owner with CanEdit permission, which gives you the same level of control as the original owner.

 

First, get your own Object ID. This is a unique identifier for your account in Entra ID. The simplest way is:

 

# Option A: Get your Object ID from the Power Platform session (no extra module needed)

$myObjectId = (Get-PowerAppsAccount).UserId

Write-Host "Your Object ID: $myObjectId"

 

# Option B: Look it up via Microsoft Graph

$myUPN      = "your.email@company.com"

$myObjectId = (Get-MgUser -UserId $myUPN).Id

 

Now run the reassignment loop. Each flow is processed in its own background job with a 30-second timeout to prevent the script from hanging on a single slow API call:

 

$success = @()

$failed  = @()

$i       = 0

 

foreach ($flow in $abandonedFlows) {

    $i++

    Write-Host "[$i/$($abandonedFlows.Count)] $($flow.DisplayName)..." -NoNewline

 

    try {

        $job = Start-Job -ScriptBlock {

            param($envName, $flowName, $oid)

            Add-PowerAppsAccount

            Set-AdminFlowOwnerRole `

                -EnvironmentName $envName `

                -FlowName        $flowName `

                -RoleName        CanEdit `

                -PrincipalType   User `

                -PrincipalObjectId $oid

        } -ArgumentList $flow.Environment, $flow.FlowName, $myObjectId

 

        $done = Wait-Job $job -Timeout 30

        if ($done) {

            Receive-Job $job

            Write-Host " OK" -ForegroundColor Green

            $success += $flow

        } else {

            Stop-Job $job

            Write-Host " TIMEOUT" -ForegroundColor Yellow

            $failed += $flow

        }

        Remove-Job $job -Force

    }

    catch {

        Write-Host " ERROR: $_" -ForegroundColor Red

        $failed += $flow

    }

 

    # Pause every 10 flows to avoid API throttling

    if ($i % 10 -eq 0) { Start-Sleep -Seconds 5 }

}

 

Write-Host ""

Write-Host "=== Done ===" -ForegroundColor Cyan

Write-Host "Assigned : $($success.Count)" -ForegroundColor Green

Write-Host "Failed   : $($failed.Count)"  -ForegroundColor Red

 

Step 4 — Export a Report

It is good practice to save a record of what was changed. Export the results to CSV files so you have an audit trail:

 

$date = Get-Date -Format "yyyyMMdd"

 

# All abandoned flows found

$abandonedFlows | Export-Csv "AbandonedFlows_$date.csv"     -NoTypeInformation

 

# Flows successfully reassigned

$success        | Export-Csv "Reassigned_$date.csv"          -NoTypeInformation

 

# Flows that failed or timed out

$failed         | Export-Csv "Failed_$date.csv"              -NoTypeInformation

 

Write-Host "Reports saved to current directory."

 

Troubleshooting Common Issues

 

Category

Description

Script hangs for 10+ min

The cmdlet is stuck on a single API call. Use the Start-Job / Wait-Job pattern (Step 3) with a 30-second timeout to prevent this.

Get-AzureADUser not found

The AzureAD module is deprecated. Use Get-MgUser from the Microsoft.Graph module instead.

Access denied errors

Your account needs Power Platform Admin or Global Admin. Check your role in the Microsoft 365 Admin Center.

Set-AdminFlowOwnerRole no output

This is normal — the cmdlet returns nothing on success. Check $success count after the loop.

Throttling / 429 errors

Add Start-Sleep -Seconds 5 every 10 flows (already included in Step 3 script).

 

Best Practices — Keep Your Environment Clean

Now that you know how to find and claim abandoned flows, here are some habits to prevent the problem from building up again:

 

       Run this script monthly as part of your regular admin routine

       Set up a DLP (Data Loss Prevention) policy to limit which connectors flows can use

       Before offboarding a user, reassign their flows to a service account or a team owner

       Use solutions to package flows, which makes ownership and lifecycle management easier

       Enable environment-level auditing so you get alerts when flows enter a suspended state

 

Quick Reference Summary

 

Category

Description

Step 1

Install modules: Microsoft.PowerApps.Administration.PowerShell and Microsoft.Graph

Step 2

Connect: Add-PowerAppsAccount and Connect-MgGraph

Step 3

Find abandoned flows: loop environments, filter by state and last modified date

Step 4

Get your Object ID: (Get-PowerAppsAccount).UserId

Step 5

Reassign with timeout: Set-AdminFlowOwnerRole inside Start-Job / Wait-Job

Step 6

Export results to CSV for audit trail

 

 

Conclusion

Abandoned flows are a silent problem in most Power Platform tenants — they go unnoticed until something breaks or an auditor asks questions. With the scripts and steps in this guide, you can find every abandoned flow in your environment, understand why it was abandoned, and take ownership of it in minutes.

 

The entire process uses built-in Microsoft modules, requires no third-party tools, and can be scheduled to run automatically. Start with a single test flow, confirm it works, then scale up to your full environment.

 

Happy automating!

 -----------------------------------------------------------------------------------------------------------

# Install module if needed

Install-Module -Name Microsoft.PowerApps.Administration.PowerShell

 

# Connect

Add-PowerAppsAccount

 

# Get the flow (you need Environment Name + Flow ID)

Get-AdminFlow -EnvironmentName "Default-<tenantid>" | Where-Object { $_.DisplayName -like "*FlowName*" }

 

# Assign

Set-AdminFlowOwnerRole `

    -EnvironmentName $flow.Environment `

    -FlowName $flow.FlowName `

    -RoleName CanEdit `

    -PrincipalType User `

    -PrincipalObjectId $newOwnerOID   

 -----------------------------------------------------------------------------------------------------------


Featured Post

Microsoft Entra ID — A Practical Introduction for M365 Admins

Microsoft Entra ID — A Practical Introduction for M365 Admins Meta Description: New to Microsoft Entra ID? This practical guide covers the...

Popular posts