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!