View merged Lead, Contact or Account details in Dynamics 365

Merge functionality for Leads, Contacts and Accounts in Dynamics 365 and Dataverse has been around for a while, if you open the record which was deactivated in the merge process you can see where it was merged into. But from the ‘Master’ row there is no clear indicator that data has been merged from another row into this one, nor what data was merged. You can check the audit history but not all users have permissions to view this area and it can be notoriously slow to load.

Customer : Can you add a note in the timeline to show when a row has been merged into, and also what data has been updated from the deactivated row?

Me : Apparently - yes!

This post focuses on the merging of two Contacts but could easily be modified to work for Accounts and/or Leads too. Roll on down to the bottom for an ended to end image of the flow steps if you need it along the way.

Create an ‘Automated’ flow

The merge action is initiated with a Dataverse action, which we will use as the trigger to the flow. Add a trigger Dataverse > When an action is performed

Catalog: (ALL)
Category: (ALL)
Table Name: contact
Action name: Merge

Get the deactivated Contact row

Suring merging the ‘subordinate’ row is deactivated, we need to get the row so we can retrieve data for the details of what was updated. Add a new action Dataverse > Get a row by ID

Action Name: GetDeactivatedContactValues
Table Name: Contacts
Row ID: (below)

triggerOutputs()?['body/InputParameters/SubordinateId']

Initialize a variable

This will be used to capture the human readable version of the updated columns. Add a new action Variable > Initialize variable

Action Name: FormattedUpdateContentResponse
Name: Response
Type: String 

Add a condition to check if any values were merged from the deactivated row

When merging, you can choose what data to take from the subordinate row (i.e. the one you are deactivating) so it’s possible that there are no values to show here. The details on what was merged from the subordinate are stored in a property called ‘UpdateContact’. So first we check if there is anything updated. Add a new action Control > Condition

Action Name: NoValuesTakenFromPreviousRecord
Condition: (below)

null
is equal to
triggerOutputs()?['body/InputParameters/UpdateContent']

The Merge function allows you to choose which values you take from the subordinate record before it is deactivated.

The ‘UpdateContent’ message in merge displays all the values taken from the deactivated row in a not human readable format

Connect to HTTP with Microsoft Entra ID (preauthorized)

In the No branch, add a new action HTTP with Microsoft Entra ID (preauthorized) > Invoke an HTTP request.

You will first need to create the connection - this is a connection reference which is defined per environment and will be set up during the solution import to another environment without making unmanaged changes or completing post deployment steps. You cannot use an environment variable here. Identify ‘Your Environment Base URL’ by opening up one of your model-driven apps and copying everything before /main.aspx… from the URL e.g. https://farmers.crm6.dynamics.com.

Base Resource URL: [Your Environment Base URL] 
Microsoft Entra ID URI (Application ID URI): [Your Environment Base URL] 

Then ‘Sign in.

Reminder to use HTTP with Microsoft Entra ID (preauthorized)

Don’t say that I didn’t remind you!

Invoke an HTTP Request for Column Metadata

This action identifies the metadata of the Contact columns we need to transform the values into a human readable text summary.

Action Name: GetDisplayNames
Method: GET
Url of the request: (below)

https://@{uriHost(outputs('GetDeactivatedContactValues')?['body/@odata.id'])}/api/data/v9.2/EntityDefinitions(LogicalName='contact')/Attributes?($select=AttributeType,DisplayName,LogicalName)

Apply to each merged column

For each value column that was merged from the subordinate up to the parent row, we need to convert the value into a human readable format. Inside the No branch, below the ‘GetDisplayNames‘ action. Add a new action Control > Apply to each

Action Name: ForEachLine
Select an output from previous steps: (below)

split(replace(replace(replace(string(triggerOutputs()?['body/InputParameters/UpdateContent']),'"',''), '{',''), '}',''),',')

Compose the human readable version of the values

As you saw above, the format of the response shows choice columns as numbers, lookups as GUIDs and currencies unformatted. This action will convert each value into it’s lovely human readable version. I’m sorry for the terrifyingly long expression, its a bit scary.

Add a new action Data Operation > Compose

Action Name: logicalname
Inputs: (below)

Add a new action Data Operation > Compose

Action Name: Value
Inputs: (below)

first(split(items('ForEachLine'), ':'))
coalesce(outputs('GetDeactivatedContactValues')?[concat('body/', outputs('logicalname'), '@OData.Community.Display.V1.FormattedValue')], 
outputs('GetDeactivatedContactValues')?[concat('body/_', outputs('logicalname'), '_value@OData.Community.Display.V1.FormattedValue')], 
outputs('GetDeactivatedContactValues')?[concat('body/', outputs('logicalname'))])

I know this looks wild generating formula dynamically but seriously it works - even on lookups and choices!

Query metadata for display names

Inside the for each loop - now we need to transform the name of each column from the logical name into the display name e.g. ‘firstname’ -> First Name.

Ada new action Data Operation > Filter array

Action Name: FilterDisplayNames
From: outputs('GetDisplayNames')?['body/value']
Condition: (below)

item()?['LogicalName']
is equal to
outputs('logicalname')

Compose the display name

Inside the for each loop - not essential and could be skipped but handy for tracing any issues. Compose the display name found in your filter action. Add a new action Data Operation > Compose

Action Name: DisplayName
Inputs: (below)

first(body('FilterDisplayNames'))?['DisplayName/UserLocalizedLabel/Label']

Append the display name and value to the string variable

Inside the for each loop - formulate your ‘FormattedUpdateContentResponse’ variable with a summary of each value and its display name taken from the subordinate row. As we are creating a Note with the outputs, using <br> adds a line break between each line. Add a new action Variable > Append to string variable

Action Name: AppendFormattedUpdateContentResponse
Inputs: (below)

@{outputs('DisplayName')}: @{outputs('Value')}<br>

Format the note text

Outside of the for each loop, but inside the ‘No’ branch we can just tidy up the text to add to the note into a compose step. The substring formula removes the final <br> from the text, and the <br> above the message helps to format the note nicer when its combined with the other details. Add a new action Data Operation > Compose

Action Name: FormatPrevRecordValues
Inputs: (below)

<br>
<br>
The below values have been taken from the previous record:<br>
@{substring(variables('FormattedUpdateContentResponse'), 0, sub(length(variables('FormattedUpdateContentResponse')), 4))}

Add a note to the timeline of the Active Contact

Finally you can but together all the details into a note on the Contact timeline. It’s a bit busy below but you can see how we are using inputs from the trigger and previous flow steps to formulate this one. The owner of the note will be the user that merged the rows. The note will be added to the active contact which was merged into. Add a new action Microsoft Dataverse > Add a new row

Action Name: AddNoteToActiveRow
Table name: Note
Inputs: (below)

Title: Merged with Contact '@{outputs('GetDeactivatedContactValues')?['body/fullname']}'
Description: Link to merged contact: <a href="https://@{uriHost(outputs('GetDeactivatedContactValues')?['body/@odata.id'])}/main.aspx?forceUCI=1&pagetype=entityrecord&etn=contact&id=@{triggerOutputs()?['body/InputParameters/SubordinateId']}">@{outputs('GetDeactivatedContactValues')?['body/fullname']}</a>
@{outputs('FormatPrevRecordValues')}
Owner (Owners): systemusers/@{triggerOutputs()?['body/RunAsSystemUserId']}
Regarding (Contacts): contacts/@{triggerOutputs()?['body/InputParameters/Target/Id']}

And they all lived happily merged for ever after

Add a note in the timeline to show when a row has been merged into, and also what data has been updated from the deactivated row

In case you got lost along the way there is an end to end screenshot of the flow created from the steps above.

And if you scrolled all the way to here I hope you are on your way to leave a lovely comment to say hello also!

Previous
Previous

Trigger a Power Automate Flow from a Custom Button in Dynamics 365 or Dataverse

Next
Next

Dataverse Choice & Choices Conversion in Power Automate