cancel
Showing results for 
Search instead for 
Did you mean: 
ThisIsAShame

How to Reassign Existing Approvals on the Behalf of Another User (Delegation for Admins)

There are many, many posts stating that delegating approvals on the behalf of another user is impossible in Power Automate. In the following article, I'll explain the method to reassign any existing approval without requiring the end user to intervene. I also want to clarify that this won't require you to edit your existing workflows, and you won't have to make weird workarounds to all your existing processes. I am simply showing a flow that behaves identically to the "Reassign" button end users have, but instead letting you do it on anyone's behalf.

 

ThisIsAShame_0-1705690401426.png

Users can already reassign their own requests if you let them. The above example is in the Approval GUI, so I'll show you how to get the equivalent result in the backend.

 

Now, people are fully correct in saying that there isn't some convenient admin menu to redirect an approval from one user to another. This will be a somewhat complicated process. The upside is that all the complexity is up front. Once the reassignment flow is made, it becomes reuseable for any other Approval in the environment.

 

Now, in case you didn't know, Approvals are stored in "Dataverse", Microsoft's Database tool that integrates with the Power Platform. There are 3 main Dataverse tables that are relevant to most approvals:

  • The Approvals Table: This stores all the basic info about an Approval, like name, details, and requestor.
  • The Approval Request Table: This table stores every pending approval request in an Environment. As an example, if an approval needs 3 separate approvers, 3 "Approval Requests" rows will be made.
  • The Approval Response Table: This stores every completed approval response. If 2 out of the 3 approval requests are replied with "Approved", then you will get 2 "Approved" rows in the Approval Response table.

From there, you can infer that "Approval Request" will be the topic of discussion. Whenever an approval is pending, it will have an associated row in "Approval Request". All reassignment does is forward an existing Approval Request to a new Approver. The "Reassign" button makes a new row, and marks the old one as delegated. This upcoming flow will do the exact same thing.

 

From that, Dataverse is fairly easy to control in Power Automate. That means that even though there isn't an easy button that reassigns other users for us, we can make the database changes required to get the same result. For any power users reading, if you know Dataverse, you'll have no problem skipping to the flow making parts. If you don't know Dataverse, I'd still recommend you be at a moderate Power Automate skill level, but hopefully my instructions will suffice.

 

                                                                                                                                                   

 

Before we continue, I want to highlight the 2 main requirements for this to work.

  1. You need to be an Environment Admin. Since we are directly editing the databases that control all approvals, you can assume that high-level access is needed. For advanced Dataverse users, Read/Write/Append access to the relevant tables is enough.
  2. You need a Power Automate Premium license. This is because we will be using the Dataverse actions in Power Automate, which require a license. Once again, if you are an advanced user, the Dataverse API can do everything we mention today, but in a less user-friendly way.

Before you get intimidated, do note that the 2 above requirements are easier and free if you use Dataverse for Teams. If you are the owner of the Team, you can edit the tables and run the premium connectors needed to make delegation.

 

So, the first thing you'll need is a trigger. We'll go over some ways to automatically trigger later, but to manually trigger a delegation, you'll need 2 things:

1. The Approval ID of the original Approval. Every Approval has a unique approval ID to identify it. If you used "Create an Approval", one of the Dynamic Content options is Approval ID:

ThisIsAShame_1-1705690401427.png

Your other option is to use Dataverse to get the ID. As you'll remember, all Approval info is stored in Dataverse, so if you don't know the approval ID, you can navigate to Approval Table, and simply search for it! In Power Automate, go to

"Tables" 

ThisIsAShame_2-1705690401427.png

"All Tables"

ThisIsAShame_3-1705690401427.png

"Approval" and then Filter by Title. You should be able to find it soon enough.

ThisIsAShame_4-1705690401428.png

Once in the Approval Table, you can find all the Approvals in your Org. You may need to unhide some columns to filter them.

 

2. The second input you'll need is the new User to be delegated to. If the original approver was "John Doe", then you'll need some way to specify that you want John's approval to be delegated to "Jane Doe". For all my examples, I will use UPN, or User Principal Name. As long as the input is fairly unique to an individual person, most identifiers should work instead (if you wish).

 

I chose "Manually Trigger a Flow", since it allows easy inputs and triggering. Additionally, all screenshots will be in the legacy flow editor, since the new one still has some growing pains, and is currently less efficient for screenshots.

ThisIsAShame_5-1705690401429.png

With "Manually Trigger a Flow", you can simply paste your inputs in to delegate.

 

Next, I use the trim() expression to remove any spaces from the input. This is to be more safe than sorry when it comes to pasting in values.

ThisIsAShame_6-1705690401429.png

If you've never seen a scope action before, it lets you group multiple actions into one, like a folder. In this case, I scoped the input trims.

 

After getting our inputs ready, we need to get the current "Approval Request" row connected to the inputted "Approval ID". This is so that we can deactivate the no longer needed request.

First, we'll make a "List Rows" action from Dataverse. You will choose the "Approval Requests" table. Under advanced, we are going to do an Odata Filter. (If you've done Odata in SharePoint before, it's the same syntax)

ThisIsAShame_7-1705690401434.png

The full filter I used is msdyn_flow_approvalrequestidx_approvalid eq '@{outputs('Trim_Inputted_Approval_ID')}'

 

As you'll notice, just like in SharePoint Odata filters, the column name in the filter is the backend name, not the display name. To get the correct name for a Dataverse column, go to the table in Dataverse, and go to "Edit Column" on the field you want. There, you'll see its Logical name, which works for Odata. For this workflow, I will provide you the column names, but any customizations you make will need the Logical Name trick.

ThisIsAShame_8-1705690401434.png

 

Next up, you likely know that Power Automate will automatically force an "Apply to Each" whenever you use "List Rows" (even if there is only 1 row). To prevent that, we use the expression @{outputs('List_Approval_Requests')?['body/value'][0]?['msdyn_flow_approvalrequestId']} to only get the value of "Request ID" for the returned row. Using [0]?['msdyn_flow_approvalrequestId'] gives me the first row from the List Approval Request values, since the array of values starts at [0]. This lets me skip needing an Apply to Each. You can still make it work with the Apply to Each, but I am prettying it up for maintainability and easier sharing.

ThisIsAShame_1-1705695268659.png

@{outputs('List_Approval_Requests')?['body/value'][0]?['msdyn_flow_approvalrequestId']}

 

After that, we will do the same thing all over again with the "Users" table in Dataverse. This is because our new delegated approval will need ownership over the new request. Ownership in Dataverse is assigned to each user's Dataverse GUID in the "Users" Table. If you input Owner GUID directly, then you won't need the upcoming steps, but I'm assuming that you only know the user's AD UPN, since that's much easier to get for most people.

 

ThisIsAShame_11-1705690401436.png

domainname eq '@{outputs('Trim_Inputted_UPN')}'

ThisIsAShame_12-1705690401437.png

@{outputs('List_User_rows')?['body/value'][0]?['systemuserid']}

 

As you can see, this is basically the same as the last process. The main difference is that I used "Select Columns" this time. That is not necessary, it just makes the output of "List rows" much easier to view by removing unneeded columns. I only need the "systemuserid" since that's what is used to assign approval ownership.

 

                                                                                                                                                   

 

Finally, we have enough setup to actually do the Delegation part! As mentioned earlier, Delegation really is just doing 2 things:

  1. Marking that the old pending request is no longer needed
  2. Making a new request to take over the old one

ThisIsAShame_2-1705692023588.png

As shown in this screenshot, once reassigned, a connection is made where the old row is connected to the new one via "Reassigned From"

 

My guess for the reason why this isn't well known is that Power Automate makes it fairly difficult to format the data to do those 2 things. All the previous work was just to get us enough data to make a valid row in "Approval Request". Dataverse gives no wiggle room for bad data, so all the upcoming steps are the most important for success.

ThisIsAShame_13-1705690401437.png

The first step (marking that the old pending request is no longer needed) is an "Update a row" action. This is because we are updating the old request to be inactive. For both of the 2 steps, I will go over each parameter individually and provide my exact inputs.

 

  • Table Name: "Approval Requests"

I simply used the dropdown and chose Approval Requests

 

  • Row ID: @{outputs('Compose_Old_Request_ID')}

This is what the "List Approval Requests" action was for, as its value would have been the older approver.

 

  • Status Reason: "Reassigned"

This feature is officially called Reassignment, not delegation, so that's why the status is called that. You can use the dropdown to choose "Reassigned". The Status Reason lets Dataverse know that a delegation occurred, rather than something like a failure.

 

  • Lastly, Status: "Inactive"

This lets Dataverse know to no longer wait on this request. You also use the dropdown for this value.

 

                                                                                                                                                   

 

After all that hype, that probably didn't sound so bad. And I agree, cancelling the old request is the easy part. The hard part is coming up next: Adding a new request row.

 

  • Table Name: "Approval Requests"

Just like before, you can use the dropdown to select this.

 

  • Approval (Approvals): msdyn_flow_approvals(@{outputs('Trim_Inputted_Approval_ID')})

This is one the most important parameters, and the biggest block in the way of creating this row. "Approval (Approvals)" is a lookup column that connects the "Approvals" table to "Approval Request". However, the weird part of lookups in Dataverse is that when using Power Automate, you need to use the format of SET_NAME(LOOKUP_GUID). Those 2 phrases should look esoteric to you, but that's not your fault.

"SET_NAME" is the backend name for a Dataverse table in its plural, or "set" form. As an example, the display name of one of the tables is "Approval", but its plural is "Approvals". Dataverse uses singular and plural grammar to distinguish from 1 row/table and multiple rows/tables. In the backend, the logical name is "msdyn_flow_approval" and the set name is "msdyn_flow_approvals". However, you can't just always add an "s". Some tables don't add anything to their set name, and some add "es". Just like in English, the plural of "fish" is "fish" or "fishes", but not "fishs". To avoid any issues, simply go to a Dataverse Table > Tools > Copy Set Name. I have to thank this thread and this article for helping me learn this. In this article however, you can trust that all Approval tables are the same, and just use my Set name.

ThisIsAShame_14-1705690401439.png

Copy set name lets you not worry about guessing the wrong name. That's important, because every other Dataverse lookup also needs an exact set name to work, Power Automate isn't nice enough to look it up for you.

 

The next part you need is the unique ID of the row you want to lookup. This "LOOKUP_GUID" often has the same name as the table's display name. As an example, the GUID of the Approval table is a column called "approval". Additionally, that "approval" column is the "Approval ID" we submitted way at the beginning. With that info, you can now see that SET_NAME(LOOKUP_GUID) turns into msdyn_flow_approvals(@{outputs('Trim_Inputted_Approval_ID')})

 

  • Approval_Stage_Key: @{outputs('Trim_Inputted_Approval_ID')}_BASIC

This is a combination of Approval ID and the type of approval. I'll get into this more later, but the flow I'm showing now is assuming a basic "Approve or Reject" workflow. As such, we just need to add "_BASIC" to the Approval ID. I learned this formatting by simply viewing the Approval Request table before and after a reassignment. I recommend you do the same if you want to fully understand this process.

 

  • Name: null (the expression)

The name field is normally blank, so I used null. Incase you don't know how to use it, null is an expression for when you want to put nothing into a field, not even "" or a space.

ThisIsAShame_15-1705690401439.png

ThisIsAShame_16-1705690401439.png

  • Response Options: ["Approve","Reject"]

This is the exact same as the default value of this field. Once again, this assumes a normal "Approve"/"Reject" workflow.

ThisIsAShame_17-1705690401439.png

  • Response Options: null (the expression)

This field is normally blank, so I used null. This is not the same column as above, but it does have the same display name. You can use "Peek Code View" to verify that they have different logical names in the backend.

ThisIsAShame_18-1705690401439.png

  • Response Options Type: "BasicApproveReject"

This is a Dropdown that once again we are pre-assuming is a Basic "Approve"/"Reject".

 

  • Stage: "Basic"

This is a Dropdown that once again we are pre-assuming is a Basic "Approve"/"Reject".

 

  • Status Reason: "Active"

This one is pretty self-explanatory, if the old request is inactive, then it makes sense that the new one is active.

 

  • Allow Reassignment: "Yes"

Also self-explanatory, we want to allow reassignment because this is a reassignment.

 

  • Approval ID Index: @{toUpper(outputs('Trim_Inputted_Approval_ID'))}

In Dataverse, the Approval ID Index is an exact copy of the Approval (Approvals) field to make Dataverse queries easier. This is because the Index is a simple text field rather than a lookup, which is easier to transform. Additionally, the rows made by Power Automate have the Approval ID Index in all caps. This is likely not strictly necessary (since you can make Fetch XML Queries case insensitive), but using toUpper() adds negligible complexity to better match the native reassignment process.

 

  • Approval Request: No input necessary

Sorry to break up the previous formatting here, but I know that since basically every column gets used here, I couldn't just ignore this column without scaring people. Approval Request is the auto-generated GUID, so it needs no input. For columns that are not handled by Power Automate or not required, I put nothing. A screenshot is attached below. 

ThisIsAShame_19-1705690401440.png

You could likely input null and not hurt anything, but that adds more steps for no real benefit. For future unneeded columns, I'll just put "No input necessary".

 

  • Due On: @{outputs('Update_the_old_row')?['body/msdyn_flow_approvalrequest_dueon']}

For many upcoming inputs, you will simply use the dynamic content from the previous "Update row". In this example, if the old request was due on 12/12/25, then it makes sense that the new one is due at the same time. Using the dynamic content also helps prevent formatting errors, as Dataverse can't give you typos in its dynamic content.

 

  • Expires On: @{outputs('Update_the_old_row')?['body/msdyn_flow_approvalrequest_expireson']}

The logic is the exact same as the above.

 

  • Input Sequence Number: No input necessary
  • Last Notified On: @{outputs('Update_the_old_row')?['body/msdyn_flow_approvalrequest_lastnotifiedon']}

Same logic as "Expires On". If you are like me and hoped that this was some hidden reminders feature, I can't figure out how to trigger it. Maybe you can!

 

  • Notification Frequency: -1

This basically means no notifications sent by Teams. Once again, I don't know how to use this weird undocumented feature, but that's a different story than delegation.

 

  • Owner (Owners): systemusers(@{outputs('Compose_User_GUID')})

This is another Lookup table. This is also the use case of the User GUID. You may notice that we never inputted the name of who this new request is for. The "Owner" column is what decides who needs to approve the request. A quick list below shows what ownership means for Approval tables.

Approval - Owner means who requested an approval

Approval Requests - Owner means who was asked to approve something

Approval Response - Owner means who responded to a request

 

  • Owning User Index: @{toUpper(outputs('Compose_User_GUID'))}

Like the last Index, this is just a copy of the Owner column. And once again, the uppercase is likely not critical.

 

  • Partner Metadata: No input necessary

Just to mention it real quick, you are allowed to use this Partner Metadata field however you want in all 3 approval tables. If you have a Dynamics 365 Vendor, the column may already be used, but you can use this column to do things like link approvals to SharePoint lists and all sorts of cool stuff. For this article however, nothing is used.

 

  • Reassigned From (Approval Requests): msdyn_flow_approvalrequests(@{outputs('Update_the_old_row')?['body/msdyn_flow_approvalrequestid']})

Once again, another lookup. This one lets Dataverse know which inactive request to associate with the new one.

 

  • Reassigned From Index: @{outputs('Update_the_old_row')?['body/msdyn_flow_approvalrequestid']}

Another Index field, same as the others.

 

  • Record Created On: No input necessary
  • Stage Index: "Basic"

This is a dropdown that once again is assuming a basic request.

 

  • Status: "Active"

Self-explanatory, the new request will be the active one.

 

  • Step Number: 0

Incase you didn't know, Microsoft has pushed a new feature called "steps" in Approvals for Teams. It lets you make 1 approval with tiered approvals, meaning that instead of having to make 4 separate approvals that are made one after another, you can make it so each step needs to be approved one after another in a single approval. It sounds like a great feature! The catch is that it currently isn't automated. Seeing as you now know so much about Dataverse, you can understand why I think steps can also likely automated in Dataverse, but until an official connector is made, you can likely ignore steps and just make them all 0.

 

  • Time Zone Rule Version Number: 4

This is the last column with data, and it refers to the offset for dates needed for approvals. According to this thread, it's deprecated and is just always 4. However, I would double check if I were you, and see if all your data is also 4.

 

  • UTC Conversion Time Zone Code: No input necessary

 

ThisIsAShame_3-1705693082987.png

The entire flow, a more detailed one will be shown at the bottom.

 

                                                                                                                                                   

 

Finally, we are at the end! Shown below is a gif on how the process looks:

Showcase-ezgif.com-video-to-gif-converter (1).gif

For a high-res look at the video and flow, I have uploaded them to Imgur.

 

So, with the flow made, it's time to talk some caveats and potential uses. There are 2 main caveats that I've seen when using the above process:

  1. I haven't gotten it to work with group approvals yet. Instead of replacing an approver, I had the issue of it adding the new approver and never removing the original. We don't use many group approvals in my Org, so I haven't felt extremely compelled to figure that out yet. The workaround to this is to just use this process on "First one to respond" approvals. I see no reason why Group Approval reassignment wouldn't work, so it's probably just formatting somewhere.
  2. I haven't tried custom approvals yet. Any approval where the options aren't "Approve"/"Reject" wasn't part of my testing, and I wanted to go ahead and get this out and published to the community. I see no reason why it wouldn't work, but I haven't tried that for you.

Now, for potential uses. The most obvious one is manual delegation. Employee A accidentally sent something to Employee B, so now an admin can delegate it to Employee C.

Before, Employee B would have had to know how to delegate themselves, so manual delegation will improve user experience and simplify your job.

The next potential use is Automatic Delegation. A simple example is an existing workflow now running a child flow that does delegation. With my above design, any premium flow can delegate with just an Approval ID and a UPN by running the above as a child flow. This means that you can do things like if a certain condition is met, delegate to someone new. Another potential use of Automatic Delegation is Approval Forwarding.

 

Now, I want to preface this by saying that this is just a mockup, and I plan to work on a better article on Approval Forwarding later. But, just to whet your appetite, shown below is an example flow that runs every time an Approval is made in a Dataverse environment. Unless you are getting thousands of Approvals a day, this flow could likely handle being run for every approval, and it would automatically check if someone needs to be delegated. If delegation is needed, it runs my delegation flow. That way, if Employee C is out of office, this flow can automatically run the delegation flow. I recommend trying it out. It's worked well in my testing, and I see no reason why it wouldn't work well in prod, as manual delegation has worked perfectly for me. The two dynamic content you'll see below is "Owner (Value)" to let us delegate specific Owners, and "Approval ID Index", to automatically get the relevant Approval ID.

ThisIsAShame_22-1705690401514.png

 

For a performance boost, I think using "Trigger Conditions" could make this work extremely efficiently. Additionally, do note that to run a manual flow as a child flow, you need to add a "Response" action. That is very easy however.

 

                                                                                                                                                   

 

Thanks for reading! I have written articles on LinkedIn and Reddit in the past, so I plan to share some of them and newer ones here to help the community. - Nigel Smith

Comments
About the Author
  • Experienced Consultant with a demonstrated history of working in the information technology and services industry. Skilled in Office 365, Azure, SharePoint Online, PowerShell, Nintex, K2, SharePoint Designer workflow automation, PowerApps, Microsoft Flow, PowerShell, Active Directory, Operating Systems, Networking, and JavaScript. Strong consulting professional with a Bachelor of Engineering (B.E.) focused in Information Technology from Mumbai University.
  • I am a Microsoft Business Applications MVP and a Senior Manager at EY. I am a technology enthusiast and problem solver. I work/speak/blog/Vlog on Microsoft technology, including Office 365, Power Apps, Power Automate, SharePoint, and Teams Etc. I am helping global clients on Power Platform adoption and empowering them with Power Platform possibilities, capabilities, and easiness. I am a leader of the Houston Power Platform User Group and Power Automate community superuser. I love traveling , exploring new places, and meeting people from different cultures.
  • Read more about me and my achievements at: https://ganeshsanapblogs.wordpress.com/about MCT | SharePoint, Microsoft 365 and Power Platform Consultant | Contributor on SharePoint StackExchange, MSFT Techcommunity
  • Encodian Owner / Founder - Ex Microsoft Consulting Services - Architect / Developer - 20 years in SharePoint - PowerPlatform Fan
  • Founder of SKILLFUL SARDINE, a company focused on productivity and the Power Platform. You can find me on LinkedIn: https://linkedin.com/in/manueltgomes and twitter http://twitter.com/manueltgomes. I also write at https://www.manueltgomes.com, so if you want some Power Automate, SharePoint or Power Apps content I'm your guy 🙂
  • I am the Owner/Principal Architect at Don't Pa..Panic Consulting. I've been working in the information technology industry for over 30 years, and have played key roles in several enterprise SharePoint architectural design review, Intranet deployment, application development, and migration projects. I've been a Microsoft Most Valuable Professional (MVP) 15 consecutive years and am also a Microsoft Certified SharePoint Masters (MCSM) since 2013.
  • Big fan of Power Platform technologies and implemented many solutions.
  • Passionate #Programmer #SharePoint #SPFx #M365 #Power Platform| Microsoft MVP | SharePoint StackOverflow, Github, PnP contributor
  • Web site – https://kamdaryash.wordpress.com Youtube channel - https://www.youtube.com/channel/UCM149rFkLNgerSvgDVeYTZQ/