Find out how to include a link to the record (Record URL) when sending an email from Dynamics 365/Microsoft Dataverse using flow

When sending a notification email to users that involves a review of a Dynamics 365/CDS record , it is always handy to include a direct link to the record itself within the notification email, so that the user can open the record directly from the email instead of doing a search in the system, etc.

We can previously do this easily in Dynamics 365 classic workflow as there is a Record Url (Dynamic) property for each entity which can be used to include a direct link to a Dynamics 365/CDS record when sending an email to the users. And that will allow the users to open the record directly from within the email itself by clicking on the link. However now, with the introduction of the model-driven apps concept and the format changes in Unified Interface, the Record Url (Dynamic) is no longer working like before as it is lacking the "appid" query string parameter.

There are still a few ways to provide a link to a record in an email by building a custom code solution or using string manipulation third-party custom workflow library such as Ultimate Workflow Toolkit. But since Microsoft recommends using automated flow instead of background workflows, I will tackle this using automated flow in this post.

In this blog post, you will learn how to achieve the following using the automated flow from Power Automate.
  • retrieve the Environment Variable
  • create an email record in Dynamics 365/CDS and populate activity parties and email body
  • compose Record Url with specific model-driven app GUID
  • send email from Dynamics 365/CDS
The Use Case: Send an email notification to the Account owner when the Account is created or reassigned so that the Account owner can review the data.



To build an automated flow to achieve the above requirement, we need to do the steps listed down below (all CDS steps in the following sample are from the Common Data Service (current environment) connector).


🛈 Tip

Since the AppID of the app varies from one environment to another, "appname" query string parameter can be used with the unique name of the app instead of the "appid". The system will automatically redirect to the correct URL with "appid" based on the "appname" query string parameter. (Steps 4-8 are no longer required)

  1. When an Account record is created or Owner is updated
  2. Get a User record for Account Owner
  3. Compose Environment Hostname
  4. List Environment Variable Value records with App Id
  5. If there is no App Id configured in Environment Variable
  6. Terminate if no App Id is Configured
  7. Parse JSON to get Default Value
  8. Compose App Id Environment Variable
  9. Create a new Email record
  10.  Perform a bound action to Send Email

1. When an Account record is created or Owner is updated


The flow’s trigger is when the Account is created or reassigned, so we will use the CDS (current environment) trigger with Create or Update trigger condition. The Filtering attribute is set to "ownerid" here so that the flow will only run when the "Owner" field value is changed (it will ignore changes made to other fields).

2. Get a User record for Account Owner


This optional step is just to get the name of the owner so we can personalise the notification email. Getting the lookup field display name without additional query is one feature that I miss in the classic workflow. It is recommended to populate the "Select Query" with the schema name of the fields which are going to be used. It may not have a lot of impact on performance for "Get record" action but it is noticeably faster to retrieve the necessary columns only for "List records" especially if you are dealing with a lot of records. As an additional benefit, the list of dynamic content will only show those records instead of showing all fields from that entity.


3. Compose Environment Hostname


This step is to compose the variable for the hostname of the current Dynamics 365/CDS environment. (e.g. contoso.crm.dynamics.com). To build the record URL without hardcoding the environment hostname, uriHost function is used to get the hostname of the OData URL.

The trick here is to get the URL from "OData Id" of one of the CDS connector actions. If the "OData Id" property of CDS connector trigger step is used, it returns an empty value.

Another thing to take note is to update the "OData Id" property in the expression after choosing from the dynamic content. When the "OData Id" property is selected from the dynamic content after entering uriHost() in the expression bar, it populates as
 uriHost(outputs('Get_an_User_record_for_Approver')?['body/@odata']?['id'])  
which is not a valid property "['body/@odata']?['id']" and returns an empty data.
The expression needs to be changed as below "['body/@odata.id']" to make it work.
 uriHost(outputs('Get_a_User_record_for_Account_Owner')?['body/@odata.id'])  


4. List Environment Variable Value records with App Id


🛈 Note

If the users only have an access to only one model-driven app, App Id is not required in the record URL. Starting from the 2020 release wave 1, model-driven apps have been updated to remember the last app and opening the URL without specifying an app will open the last app used by the user.


🛈 Tip

Instead of using Environment Variables, you can also use the "List record" action on "Model-driven apps" entity to lookup the AppID of the app by filtering with the uniquename of the app. That is the easier and preferred way of getting an AppID. You can find more details in this post by Thomas Peschat and this post by Sara Lagerquist.

One of the main challenges to build a record URL is the "appid" which is the GUID of the model-driven app, and that is because the GUID is different for different environments. To overcome this, the environment variable is used to store the GUID of the main model-driven app.


The environment has two values
1. Default Value (stored in the Environment Variable Definition)
2. Current Value (stored in the Environment Variable Value)


Both values from related entities can be retrieved in one List Records CDS action by using the following FetchXML.

 <fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false' >  
  <entity name='environmentvariabledefinition' >  
   <attribute name='environmentvariabledefinitionid' />  
   <attribute name='defaultvalue' />  
   <filter type='and' >  
    <condition attribute='schemaname' operator='eq' value='lzw_PowerPlatformPlaygroundAppId' />  
   </filter>  
   <link-entity name='environmentvariablevalue' from='environmentvariabledefinitionid' to='environmentvariabledefinitionid' link-type='outer' alias='environmentvariablevalue' >  
    <attribute name='value' />  
   </link-entity>  
  </entity>  
 </fetch>  

Read Sara Lagerquist's blog post to learn more on how to Retrieve Related Data in Power Automate using the FetchXML.

5. If there is no App Id configured in Environment Variable


This control step is to check the number of records returned from the List Records step. This is the complete expression.
 length(outputs('List_Environment_Variable_Value_records_with_App_Id')?['body/value'])  

6. Terminate if no App Id is Configured

If there is no record found (i.e. no Environment Variable configured with for the model-driven app GUID with the specified schema name), the flow will be terminated with a status Failed. To increase the readability of the flow, the guard condition is used to terminate the flow for "no" condition without nested actions in the "yes" condition. Read Jonas Rapp's blog post to learn more about how to Avoid nested conditionals in Microsoft Flow by using Guard Conditions.

7. Parse JSON to get Default Value


The first record of the output from List Records step is used to parse the JSON output data and get the value of the related entity.

 first(outputs('List_Environment_Variable_Value_records_with_App_Id')?['body/value'])  

8. Compose App Id Environment Variable


This step will retrieve the App Id from the parse JSON output. The logic is to get the current value and if the current value is empty, get the default value.

 if(empty(body('Parse_JSON_to_get_Default_Value')?['environmentvariablevalue.value']), body('Parse_JSON_to_get_Default_Value')?['defaultvalue'], body('Parse_JSON_to_get_Default_Value')?['environmentvariablevalue.value'])  

9. Create a new Email record

Instead of sending an email from flow using "Send an email" action from Office 365 Outlook Connector, an email record will be created in Dynamics 365/CDS and sent from there. One of the benefits of sending an email within Dynamics 365/CDS  is that you will be able to see the email record against the related record (regarding) and see the history from the Timeline. It is also quite handy for the organisations with on-premise exchange to send an email without workaround HTTP request action.


The first step of sending an email from Dynamics 365/CDS is to create a new email record. Set the email recipient as one of the Activity Party lookup value. (read my other blog post to learn more about how to Set Lookup Field Value). Set the "Regarding" lookup value in the corresponding entity field.
The description (email body) field need to be populated with HTML text. Make sure to add <br /> tags for the line breaks.
A record URL is built in the following format.

 https://<<EnvironmentHostname>>/main.aspx?appname=<<AppUniqueName>>&pagetype=entityrecord&etn=account&id=<<RecordGuid>>  
e.g. https://contoso.crm.dynamics.com/main.aspx?appname=customerservicehub&pagetype=entityrecord&etn=account&id=6e060750-ab16-eb11-a812-000d3a6aa8dc

10. Perform a bound action to Send Email


This final step is to send out the created email from step #9 above using "Perform a bound action". The IssueSend attribute needs to be set to "Yes" to send out the email from the system. If the value is "No", the email will only be marked as Sent and won’t be sent out from the system.

Summary

By using the "Perform a bound action" from Common Data Service (current environment) connector and Environment Variable, we can build an automatic flow which sends an email from Dynamics 365/CDS with a Record URL and fill the gap of not having the Record URL (Dynamic) like in the classic workflow.


You can download the above sample flow from my GitHub repository via this link.

Comments

  1. Hi Linn,

    Some really good info here. Do you know if its possible to create the Email record without the CRM:0001021 reference suffix in the Subject field?

    Thanks

    ReplyDelete
    Replies
    1. Yes. You can simply uncheck the "Use tracking token" under System Settings > Email tab
      https://carldesouza.com/dynamics-crm-email-tracking-token/

      Delete
  2. Where do we find "Create a new e-mail record"? I don't see it in Premium actions or elsewhere. I like the idea of sending from within Dynamics, so the message is trackable in the timeline.

    An important part is including the url in the message... your method, above, looks rather involved. Is that really the easiest way to get the url??

    ReplyDelete
    Replies
    1. There is no separate action for "Create a new e-mail record" in flow.
      You just need to choose "Common Data Service (current environment)" and "Create a new record" action from it.
      https://docs.microsoft.com/en-us/connectors/commondataserviceforapps/#create-a-new-record

      Yes, getting the record URL is relatively simple. Just follow my blog post and if you have any issue, please let a comment or DM me in Twitter.

      Delete
  3. Thank You very much for this...

    ReplyDelete
  4. Thanks for this I try it out with Opportunity, but I get the Error message "Unable to process template language expressions in action 'Compose_Environment_Hostname' inputs at line '1' and column '29693': 'The provided parameters for template language function 'uriHost' are not valid.

    I'm Using the Commondata Service connection and only one Adminuser to verify the Flow.
    Any Idea?

    Thanks

    ReplyDelete
    Replies
    1. I think I've seen similar error message but I don't quite remember where. I guess I had that error when I tried to test my flow with previous run data after adding a new action step with a new connector which hasn't been authorised.

      Which step of your flow are you getting that error?

      Delete
  5. Hello Linn,

    The Main context is There is Parent and Child Entities. Based on the Frequency field in Parent child records will be created. There will be a date field in child based on that pulling parent and child (steps using List records fetch xml and Get records). One more point is there is a CC field in parent it is a text email field with multiple emails semi colon separated like Gmail etc. not stored in CRM.

    As I Have two Questions.

    1. we have to create email message in CRM to track and send email to the Primary owner and Email Recipients CC(Text field with multiple emails and not stored in CRM) How can retrieve those emails and assign in activity party attribute and create a email message in CRM and send.

    2. Get Dynamic Url link of that Child record in the same flow.

    Thanks in Advance

    ReplyDelete
    Replies
    1. Apologies for the late reply. I just saw this comment.

      For question 1,
      - split() the semi colon separated emails value of CC field and set into an array variable
      - Populate the Activity Party array with JSON array as in the link below using the array variable above
      https://linnzawwin.blogspot.com/2021/01/populate-activity-party-value-with-json.html
      - While populating the Activity Party array, use addressused property as mentioned in the link below
      Make sure to enable the unresolved email setting in system setting.
      https://linnzawwin.blogspot.com/2020/06/send-email-to-unresolved-recipients-in.html

      Delete
    2. For question 2,
      - Compose the URL of the child table as in this blog post with Compose action but instead of the row GUID, use a place holder
      e.g. https://contoso.crm.dynamics.com/main.aspx?appid=83305946-e9b7-4315-9de4-91e4cbdb1349&pagetype=entityrecord&etn=childtable&id=ZZZZZ
      - In the Apply to each loop for the child rows, replace() the place holder (e.g. ZZZZZ) with the GUID of the child row

      Delete
  6. For me it always redirects to the app without doing any of it.

    ReplyDelete
    Replies
    1. What does the URL in the email look like? Can you post it here without the organisation name?

      Delete
  7. Hello Linn,

    I used your site as inspiration, but still don't know how to get the app id from the flow without passing in the name or id manually. Sure wish MS would just add recordURL to the returned data for triggers and actions!

    Anyway, I have to send quite a few email notifications with links to the record so I created a child flow to build the recordURL by passing it the odata.id and appid. the response is the recordURL.

    Manual flow with 2 text fields.
    Add 5 (string) initialize variable steps

    1. odata.id
    2. appid
    3. hostname
    4. table
    5. recordid

    odata.id = triggerBody()['text']
    appid = triggerBody()['text_1']

    The remaining variables need to be built with expressions.
    hostname = uriHost(variables('odata.id'))
    table = substring(first(split(substring(last(split(variables('odata.id'),'/')),0,sub(length(last(split(variables('odata.id'),'/'))),1)),'(')),0,sub(length (first(split(substring(last(split(variables('odata.id'),'/')),0,sub(length(last(split(variables('odata.id'),'/'))),1)),'('))),1))
    recordid = last(split(substring(last(split(variables('odata.id'),'/')),0,sub(length(last(split(variables('odata.id'),'/'))),1)),'('))

    Then add the powerapps - response step with value = https://@{variables('hostname')}/main.aspx?appid=@{variables('appid')}&cmdbar=true&navbar=on&pagetype=entityrecord&etn=@{variables('table')}&id=@{variables('recordid')}

    ReplyDelete
    Replies
    1. Note: This hasn't been fully tested yet. I just finished building it.

      Delete
    2. If the users only have an access to one app, you don't have to pass the App Id in the record URL. Just pass the other parameters and the system will use the last app if an app has not been provided.

      https://docs.microsoft.com/en-us/power-platform-release-plan/2020wave1/microsoft-powerapps/improvements-model-driven-app

      Delete
    3. Hi Linn,

      We have 3 model-driven apps so we have to use the appid to make sure the link opens the correct app. Unfortunately, my earlier comment won't work with all tables/entities due to the way Dynamics uses the singular and plural versions of table names at different times. My expression above doesn't take that into account so say a table called Category/Categories will cause an issue. Back to the drawing board! Why oh why don't they just include the hostname, appid, entitytype, and recordid in the triggers and actions. They already have part of it! It feels like this is such a low hanging issue that it would be easily added!

      Thanks!

      Delete
    4. Just found out that we can use the app unique name to open the specific app. I have updated my blog post.
      e.g. main.aspx?appname=customerservicehub

      If you really want to get the table logical name from the EntitySetName dynamically for all scenarios, I'm afraid you'll have to query the metadata first.

      Delete
  8. Thanks Linn,
    This is really helpful, this is what I've been doing recently. It's so easy

    ReplyDelete
  9. the compose step (step 3) is throwing an error for me
    I am using dataverse instead of cds
    the error:-
    "Unable to process template lanuage expressions in action 'Compose' inputs at line '0' and column '0': 'The provided parameters for template language function 'uriHost' are not valid.'."

    ReplyDelete
    Replies
    1. Does the step name in the uriHost match with your Dataverse step? Can you try to Compose the body/@odata.id output without uriHost and see the output is correct?

      If possible, leave a comment with your expression and I'll check it out.

      Delete
    2. Hello, Linn. Is everything the same with the exception of the comment above for Dataverse? I have my flow working great using Outlook, but love the idea of having the response be part of the record timeline. Step one is getting the current flow with the record URL included in the email, and second is creating the email from D365. I'm just curious before I start if everything is the same with Dataverse flow compared to CDS?

      Delete
    3. Hi Tracy,

      Yes. It works exactly the same in the Dataverse connector as the Common Data Service (current environment) connector as demonstrated in this blog post.

      Delete

Post a Comment

Popular Posts