Dynamics 365 Project Operations: Importing time entries, Part II – Recalls

Tested on:
Project Operations solution version 4.4.0.70 (UR 4), CE-only deployment

In my previous post I talked about importing and integrating time entries from a 3rd party system into Dynamics 365 Project Operations. Sometimes there might be a need to roll back an imported entry batch though. This doesn’t only apply to import scenarios but to everyday use of ProjOps as well. The ask of being able to recall a batch of time entries instead of manually clicking through the recall process is pretty frequent.

This blog post covers a Flow that can be used to manually recall a batch of time entries that have been approved. A prerequisite is that an imported batch has a unique batch number for identification so that time entries related to the batch can be recalled. It’s worth mentioning that if an approved time entry is already picked on an invoice, it can’t be recalled until the related Invoice Line Details are deleted. If an approved time entry has gone through the entire invoice revision process of confirm draft, correct invoice, confirm reversed invoice, the time entry can’t be recalled at all.

Recalling approved time entries

In part I, a service principal’s connection reference was used in all Flow actions. An application user for the service principal was set as a project approver on the project the time entries were submitted for. This naturally means that the time entry recall process will also be done in the context of the application user. As a quick recap, the idea is that by using a service principal connection reference in Flow, an application user approves time entries by being a project approver and also approves time entry recalls. This way both the Flow that is used to submit and the Flow that is used to recall can be fired off by anyone and a project approver doesn’t have to be tied to a specific named user.

An action based headache

Before we jump into the Flow and dissect it, I want to point out some challenges I faced building this Flow. Despite it being pretty simple and short, this was one of the most time-consuming Flows I’ve built to date. The reason is that testing the Flow with 10k+ records means time entries first need to go in and they need to be approved. So everything that was done in part I had to be done over and over again to make sure there is consistency is successful Flow runs. And was there consistency? Most certainly not.

Nearly every Flow run had an average of two failed time entry recalls (two out of 10k records failed). The failures resulted from either the msdyn_TimeEntriesRecall or the msdyn_TimeEntriesApprove action failing with an error “The status of one or more time entries couldn’t be changed. The action is not allowed”. What was extremely strange about this was that even though one of those actions failed, it still did what it was supposed to do: recall or approve a recall. It’s the first time I’ve seen something like this happen. As I’m writing this, I’m still trying to diagnose and understand the root cause. The reason I’m describing the inconsistent behavior of the actions is that this has also affected the way I’ve built the recall Flow. I’ve put in two auxiliary paths in the Flow to mitigate failures so that Flow runs are consistently successful.

1. Failed actions in Flow.
2. Failed ProjOps actions.

Time entry recall Flow

Let’s dive into the time entry recall Flow and dissect it. Like the Flow from part I, this Flow is also fired off manually. When an import batch is given, all Project Approval records related to the import batch are listed. As I tested the Flow with 10k+ records, I noticed that if the properties of the list records action are not limited, the action fill fail. The given error states“Cannot write more bytes to the buffer than the configured maximum buffer size: 104857600”.

To overcome the error, select query is used to limit the properties that are returned. Time entries need to be accessed later in the Flow so time entries related to approvals have to be returned. If you look at the image below you’ll notice that to return a related time entry, the correct syntax in the select query for a lookup is _msdyn_timeentry_value.

A filter query is also needed so that only approval records related to the import batch are returned. The Record Stage of those records has to be Approved.

3. Time entry recall Flow’s trigger.

Checking for returned time entries

A condition after the list records action checks whether the action returned any records or not. The expression used is:

empty(outputs('List_Project_Approvals')?['body/value'])

If it didn’t, then there are no approved time entries to recall and roll back. The reason I’ve included this condition and all the logic on its false side is that I wanted to make the Flow more versatile. There may be time entries that are part of the import batch but have already been rejected and thus returned. As the intention when running this Flow is to delete all time entries in the import batch, returned time entries should be accounted for as well. The false path of the condition accomplishes this.

If the List Project Approvals action doesn’t return any records, the Flow lists all time entries that have an Entry Status of Returned and are part of the import batch. Remember that import batch is specified when the Flow is fired off manually. If nothing is returned, the Flow terminates as succeeded. If time entries are returned, it means that their Entry Status is Returned, so they can be deleted. The delete action gets the job done for us and the Flow terminates as succeeded.

4. Checking for possible returned time entries.

Main logic

When approvals with a Record Stage of Approved are returned in the initial list records action, the main logic of the Flow comes into play. Before we dissect the unbound/global actions that actually return all time entries, it’s important to look at concurrency control in the apply to each loop for the listed project approvals. I tested the Flow tens of times and ended up with a “safe” concurrency of 15. Less than that makes the Flow too slow. Even with concurrency at 15 the Flow takes anywhere between 2,5 to 9,5 hours to run! If concurrency is higher than 15 or turned off, I kept getting failed runs. With all the problems I had with the ProjOsp actions, this was the least of my concerns so I played it safe with concurrency. Feel free to experiment with values greater than 15 and if you do, please leave feedback in this post’s comments about the results.

5. Experiment with concurrency control to find a good balance where the Flow succeeds consistently.

msdyn_TimeEntriesRecall unbound/global action

Let’s look at the main logic seen in in image 10 in more detail. A get records action will get all the details of a project approval. The second get records action gets the time entry related to the project approval record. Neither of these actions requires a select query. All straightforward up to now. Next, it’s time to look at a time entry recall process on a high level and then at ProjOps’s unboud actions. This is where the fun starts.

The process of recalling approved time entries in ProjOps is the following:

TableTime EntryProject ApprovalTime EntryProject Approval
ProcessSubmit TE ->Approve TE ->Recall TE ->Approve/Reject recall
Time entry recall process in ProjOps.

I first investigated the option of simply canceling approvals. I found a bound action for that but wasn’t able to make it work so I decided to approach the whole rollback scenario from a transaction’s origin, which is a time entry. To initiate a time entry recall, the unbound action msdyn_tTmeEntriesRecall is called. The action requires four input parameters:

  • CorrelationId. The exact best practice use for this input parameter is unclear to me, however the action works when using a time entry’s GUID from the previous get records action as an input parameter. The description in the action states: The correlation Id identifies time entries that were recalled together.
  • TimeEntryIds. GUID of a time entry from the previous get records action. The description in the action states: Ids of the time entries.
  • Notes. The description in the action states: Notes for the recalled time entries.
  • IsRecallingApprovedAllowed. Needs to be set to Yes. The description in the action states: Flag for Call to validate and process entries, process only and only if everything is valid and no approved entry is present.
6. msdyn_TimeEntriesRecall action.

After this action has been called, a time entry will have an Entry Status of Recall Requested and the related project approval will have a Record Stage of Recall Requested. Before we look at the next action, it’s worth reminding that in nearly all import batches (i.e Flow runs) this specific action failed on average twice for 10k records processed. It doesn’t sound like much but an operation like this has to be 100 % reliable and any failures in a Flow like this are completely unacceptable. Otherwise there’s manual work to be done, and that’s exactly what this Flow is trying to mitigate in the overall recall process.

msdyn_TimeEntriesApprove unbound/global action

The next unbound action that is called is msdyn_TimeEntriesApprove. Before the action is called, a project approval will have a Record Stage of Recall Requested as I previously mentioned. If this was a manual process in ProjOps, the approval record would lurk under the Recall Requests for Approval view in the Project Approvals table. Approving a recall request in a manual process would be done by clicking on Approve on the ribbon. This is what threw me off at first because I thought I would have to approve a recall by using the msdyn_TimeEntriesReject unbound/global action. It turned out that approving a recall is done using the msdyn_TimeEntriesApprove action instead! One could say that approving a recall is approving a time entry, not rejecting it. It sounds confusing but that’s the action we have to use.

7. Approving a recall request in a manual process.

The required input parameters for msdyn_TimeEntriesApprove are similar to the previous action. CorrelationId and TimeEntryIds are the only two that are needed. The value for those is the time entry’s GUID, which comes from the Get Time Entry action.

8. msdyn_TimeEntriesApprove action.

The final action in the main logic deletes the time entry that has been recalled. This action is the final step in the whole recall process. To overcome the possible and likely failures of both of the previously mentioned msdyn_TimeEntriesRecall and msdyn_TimeEntriesApprove actions, I’ve used the Configure run after settings on the Perform an unbound action and approve time entry recall and Delete a Time Entry record actions to account for successes and failures in the ProjOps actions. This way the Flow will proceed to run even if either of the actions fails. As I previously mentioned, both actions always did what they were supposed to do even if they failed.

9. Error handling in Flow.
10. Main logic of the recall Flow. Note the configure run after settings.

Optional checks after main logic

The easiest way to handle errors with the ProjOps actions is to use error handling described in the previous chapter. The downside is that it’s not easy to spot which apply to each iteration actually has failures in the actions, without additional checks into the main logic. An alternative solution is to create auxiliary paths after the main logic, which run when one of the ProjOps actions fails in an apply to each loop’s iteration and thus causes the apply to each action to fail. These paths essentially have the main logic duplicated. Let’s see how they’re built and when they run.

Rerun of the msdyn_TimeEntriesRecall action

If there’s a failure with the msdyn_TimeEntriesRecall action, the Flow runs on both auxiliary paths seen in image 12. Both paths are configured to run after the apply to each action for main logic fails. When a failure is in the recall action, both paths run but only the path on the left has any logic that is executed.

The auxiliary path on the left relists project approvals. This time approvals with both a Record Stage of Approved (2) and Recall Requested (4) are needed. It’s better to play it safe since while the msdyn_TimeEntriesRecall action seems to set records to Recall Requested when it fails, this isn’t something I’d count on. Instead, it’s safer to assume that the action will genuinely fail and will leave records as Approved. In this case, records with both stages need to be listed.

11. Relisting project approvals.

The rest of the actions in this path are a rinse and repeat from the previous main logic. Configure run after is set in the final two actions Perform an unbound action and approve time entry recall 2 and Delete a Time Entry record 2 for is successful, has failed, and is skipped.

12. Auxiliary paths for when ProjOps actions fail.

Rerun of the msdyn_TimeEntriesApprove action

The auxiliary path on right has logic which runs when the msdyn_TimeEntriesApprove action fails and the related time entry is not deleted. Again, the action consistently seems to change the Record Stage of an approval from Recall Requested to Recall Request Approved, even though it fails. If the action would genuinely fail, the auxiliary path on the left would naturally handle recall requests and recall request approvals and then delete time entries.

A list records action looks at time entries with an Entry Status of Returned. I’ve also left the filter query to list Draft time entries as well. It’s a result of copying the action to clipboard in the Flow, while building it. It’s extra but does no harm – the goal is to delete time entries anyway. The apply to each loop that follows the list records action includes a get records action for time entries, followed by a delete action.

13. Relisting time entries.
14. Auxiliary path for listing returned time entries and deleting them.

As a final note, I want to thank a bunch of people for helping me diagnose the issues I ran into in both of the Flows covered in part I and this post. Thanks to MVPs Aiden Kaskela, Stefan Strube, George “The Enabler” Doubinski, Dave Yack, Jerry Weinstock, and Linn Zaw Win. Also thanks to the Power Automate and Logic Apps product teams.

This Flow and the Flow from part I will be available on GitHub as an unmanaged solution.

Disclaimer:
All my blog posts reflect my personal opinions and findings unless otherwise stated.