Friday, 29 January 2021

How to create and send a Teams Meeting from Microsoft Dataverse or Dynamics 365

Sending a Teams Meeting invite from Microsoft Dataverse or Dynamics 365 is not supported out-of-the-box (yet as of when this WTF episode was published). It also seems to be a requested feature,

Fear not my Power Platform citizens as you'll learn in this WTF episode how you can achieve it with my best friend Power Automate.

Note: since my last WTF episode, flow has been renamed to cloud flows so will stick with this from now on.

Brief background before we start

I originally came up with Version 1.0 of this solution at the end of 2019 and presented it at the Power Platform Saturday 2020 event in Sydney - yes a time when travel was still a thing.

Back then there was no action in the Microsoft Teams connector to create a Teams Meeting. Fast forward to today,

  1. There is an action available that will allow you to create a Teams Meeting
  2. My super powers with Power Automate have since level'd up, this is Version 2.0 - it's even more beautiful 😍

Can I use the Create a Teams meeting action?

Initially you think you can use the Create a Teams meeting action that's currently in Preview in the Teams connector for Power Automate.

However as mentioned in my previous WTF episode, any connector that is tied to Microsoft 365 will use the cloud flow maker's user account as the authentication. This means the Teams connector will use the cloud flow maker's user account for any of the actions. In the case of a Teams meeting action this is a no-go as when the Teams meeting is created and sent, it will only show in the cloud flow maker's Outlook calendar. It will not show in the Outlook calendar of the organiser who created it from Microsoft Dataverse or Dynamics 365.

For example Aaron creates a Teams Meeting Activity from Microsoft Dataverse/Dynamics 365.



The cloud flow is triggered and the "Create a Teams meeting" action is used. When the Teams Meeting is created and sent it will not show in Aaron's calendar. It will show in the the user's calendar who created the cloud flow - in other words the cloud flow maker.

This is not going to cut the cheese because for an enterprise ready solution... this ain't it.

The solution

Enter my other best friend, Graph API - hello 👋

A Teams Meeting can be created through Graph API by authenticating as an application instead of a user. In the previous WTF episode I showed you how to authenticate as an application with Graph API using the HTTP action.

The process will be whenever a Teams Meeting Activity is created from Microsoft Dataverse or Dynamics 365, a cloud flow in Power Automate will be triggered and perform a couple of requests to the Graph API so that the Teams Meeting can be created and sent to the attendees.

Prerequisites

New custom activity table

You will need to create a custom activity table in Microsoft Dataverse or Dynamics 365. I created a custom activity table called Teams Meeting Activity which is what you would have seen in my WTF vlog.



Why? We need to have a table that represents the Teams Meeting in Microsoft Dataverse or Dynamics 365 to allow users to associate the attendees and so forth.

But why can I not use the Appointment activity table? When server-side synchronisation is enabled for appointments in Microsoft Dataverse or Dynamics 365 and the user creates an appointment, it will automatically appear in a user's Outlook calendar. This means if my solution used the Appointment activity table there would be a double up in the Outlook calendar,

  1. The synchronised Appointment from Microsoft Dataverse and Dynamics 365
  2. And a Teams Meeting

In my vlog I had a custom activity table which I named Teams Meeting Activity and added the relevant fields to the form.

I will refer to my custom activity table as Teams Meeting Activity for the remainder of this blog post.

App registration in Azure portal

The cloud flow performs a couple of Graph API requests which requires the HTTP action authenticating as an application. Refer to my previous WTF episode on how to do this as this is applied in my cloud flow in Power Automate.

Understanding the anatomy of a Teams Meeting

From an end user perspective this is what they see as the organiser from the Teams application.

  1. Subject of the Teams Meeting
  2. Required Attendees
  3. Optional Attendees
  4. Scheduled Start Date and Scheduled End Date
  5. The timezone that the dates go by - this is usually in the context of the Organiser's timezone
  6. Whether the Teams Meeting is a recurring meeting (note I do not explain this in this WTF episode)
  7. The location of the Teams Meeting
  8. The content of the Teams Meeting
  9. The hyperlink which contains the URL for attendees to join the Teams Meeting
  10. The Organiser - I completely forgot to mention this one in my WTF vlog
All of these elements need to be defined for the Graph API request which is what we need to build in the cloud flow. A majority of these elements happen to be reflected in the out-of-the-box columns that come with a custom activity table in Microsoft Dataverse or Dynamics 365 so it really is a perfect pair 🍐

The Graph API requests used in this solution

The first Graph API request that will be used is the Get user mailbox settings which enables the ability to retrieve an Organiser's timezone defined in their Outlook profile.

The second Graph API request that we're using is Create Event. In the sample request you can see the elements I mentioned earlier which is what we're going to build in our cloud flow. The key part here is the Attendees array where we need to ensure that there is a row that represents each attendee (required and optional) in the Teams Meeting Activity.

Time to automate. I'll next break down the cloud flow. 

By the way this cloud flow is in a solution so the CDS Current Environment connector is used.

Warning: I will be explaining a few underlying principles of Microsoft Dataverse/Dynamics 365 so grab a cup of coffee or tea to help with your learning vibes ☕😊

Let's Automate

This is what my cloud flow looks like in Power Automate.

The trigger

The trigger will be "When a record is created" where the entity value will be the custom activity table that was created.

Get the user record of the Organiser

I used a parallel branch to perform some actions in parallel. The CDS Get Record action is used to retrieve the user record based on the Organiser column of the Teams Meeting Activity. There is a column in the User table that represents the Object ID of their Active Directory User profile which is needed in the Graph API requests downstream in the cloud flow.

List Activity Party records

On the left hand side of the parallel branch, we have actions that build the Attendees array that is required in the Graph API request that creates the Teams Meeting.

The first action is the CDS List records action that retrieves all of the Activity Party records associated to the Teams Meeting Activity created which is defined in the filter query field.

What's an Activity Party record?

In Microsoft Dataverse or Dynamics 365 Activity Parties represents a person or a collection of people who are associated to the activity record. For a Teams Meeting Activity this would be
  • The Organiser
  • The Required Attendees 
  • The Optional Attendees
  • The Owner of the record
  • The Regarding value of the record - for example a Lead
The Required Attendees and Optional Attendees is what will be referenced to build the attendee rows in the Attendees array of the Graph API Create Event request.


As you can see for the emailAddress property the values of email address and name is required. These values need to be retrieved from the selected records in the Required Attendees and Optional Attendees columns of the Teams Meeting Activity.


If you review the response of the CDS List records action of the Activity Party it will only show values from the Activity Party entity. You will not be able to see the email address and name details which is needed for the attendee row in the attendees array are within the table that represents the selected attendee.

There's only four tables that are applicable to the Required Attendees and Optional Attendees columns in the Teams Meeting Activity. These are
  1. Contact
  2. Account
  3. Lead
  4. User

Each table has their own column with its own schema name that represents the email address or the name which is what needs to be retrieved to form the emailAddress property in the Attendees array of the Graph API Create Event request. How do we get this information since these are related tables to the Activity Party?

Use the $expand query and $select query

Not a problem! The CDS List records action handles Odata queries and one of query options available is $expand which in the context of Microsoft Dataverse and Dynamics 365 allows you to retrieve related entities by expanding navigation properties.

Let's revisit the response of the Teams Meeting Activity CDS List records action. There is a property "_partyid_value@Microsoft.Dynamics.CRM.associatednavigationproperty" which can be used in the expand query field in the CDS List records action to retrieve the column values of the related table specified.


For example "partyid_lead" will return the the column values of the selected lead in the Required Attendees column. To narrow the column values returned from the Lead table, another query can be applied. Use $select to only retrieve the email address and name information from the related table.


For the four tables it will be the following columns by schema name
  • Contact
    • emailaddress1
    • fullname
  • Account
    • emailaddress1
      • This same value can be used for the Name property in the emailAddress property of the Graph API Create Event request
  • Lead
    • emailaddress1
    • firstname
    • lastname
  • User
    • internalemailaddress
    • fullname

Filter attendees array

As mentioned earlier only the Required Attendees and Optional Attendees activity parties is required to form the Attendeess array in the Graph API Create Event request. The CDS List records action will return the following activity parties of the Teams Meeting Activity,
  1. Required Attendee
  2. Optional Attendee
  3. Owner
  4. Regarding
Since a CDS List records action retrieves multiple records it's an array behind the scenes therefore the Filter an array action can be used. The condition expression needs to be done through Advanced mode since an OR statement needs to be applied. The filter an array action needs to only retrieve the Required Attendee and Optional Attendee.

In the Activity Party table there is a property that you won't see through the customization settings of the table but is visible in the CDS List records action response. This property is "participationtypemask" which represents the different activity party types through an integer value.

  • 5 represents a Required Attendee
  • 6 represents an Optional Attendee
This can be used in our condition expression in the filter an array action,

or(equals(item()?['participationtypemask'], 5), equals(item()?['participationtypemask'], 6))


I have covered in a previous WTF episode how to do OR and AND statements in an expression which is also applicable to the advanced mode editor of a filter an array action if you need some more guidance.

Apply to each

Now that the attendees have been filtered we can loop through each of the attendees to form each row in the Attendees array for the Graph API Create Event request.

Switch

There are four tables that an Organiser can choose from when selecting the Required Attendees and Optional Attendees, and each table has their own schema name for the columns. The properties in the attendee row needs to accommodate these four tables. This can be achieved using a Switch action to perform a logical check based on a property value defined. The property that is referenced for the Switch is "_partyid_value@Microsoft.Dynamics.CRM.lookuplogicalname" as this will indicate which of the four tables the activity party derives from as defined by the selected record of the attendee.

The expression used is item()?['_party_id_value@Microsoft.Dynamics.CRM.lookuplogicalname']

Initialise variable and Append to an array variable actions

Each row needs to be built to represent an attendee for the Attendees array in the Graph API Create Event request and this is where the Initialise variable action and Append to an array variable actions can be used.

Back to the Switch action

The values used for each of the Switch cases will be the four tables and in each switch case the column values from the expand and select query are referenced in the expression associated to the properties of the attendee row that is required by the Attendees array in the Graph API Create Event request.

  • Contact
    • emailaddress1
      • item()?['partyid_contact']?['emailaddress1']
    • fullname
      • item()?['partyid_contact']?['fullname']
  • Account
    • emailaddress1
      • item()?['partyid_account']?['emailaddress1']
      • This same value can be used for the Name property in the emailAddress property of the Graph API Create Event request
  • Lead
    • emailaddress1
      • item()?['partyid_lead']?['emailaddress1']
    • firstname
      • item()?['partyid_lead']?['firstname']
    • lastname
      • item()?['partyid_lead']?['lastname']
  • User
    • internalemailaddress
      • item()?['partyid_systemuser']?['internalemailaddress']
    • fullname
      • item()?['partyid_systemuser']?['fullname']

The final property in the attendee row array is "type" which represents whether the attendee is required or optional as seen in the Graph API documenation.

This expression is quite straight forward by using the if function since the filter an array action only retrieves the required or optional attendees of the Teams Meeting activity.

if(equals(item()?['participationtypemask'],5), 'required', 'optional')

Compose - demo purposes only

The next action I had in my WTF vlog was a compose action that shows the output of the Initialise variable action. This is for the purpose of showing you that the Attendees array was correctly built.

Graph API Mailbox Settings request

The Get user mailbox settings API request will be used in this HTTP action. The azureactivedirectoryobjectid property is referenced from the Get Organiser record action in the URI and we're also narrowing the response to only return the timeZone value as defined in the Graph API documentation. The timeZone property will be used in the final action of the cloud flow.


Reminder that
  • you need an app registration in Azure portal to authenticate as an application via the HTTP request action in Power Automate
  • you need to add an application permission for the app registration in Azure portal.
Refer to my previous WTF episode on how to do this.

Graph API Create Event request

The grand finale! But wait, there's more learning vibes coming so please make sure you have refilled your coffee or tea before reading this section 😉

The Create Event API request will be used in this HTTP action. Don't forget to add the application permission for this request for the app registration in Azure portal.

The azureactivedirectoryobjectid property is once again referenced from the Get Organiser record action in the URI so that the Teams Meeting is created as the Organiser but authenticating as an application with the Graph API.


You'll notice that there is an additional request header this time, 
Prefer: outlook.timezone="timezone"

This request header as explained in the Graph API documentation ensures that the Teams Meeting is created in a specified timezone which in this solution will be the timezone of the Organiser as defined in their Outlook profile.

The next part of the HTTP action is the Body which represents the elements of the Teams Meeting which I explained earlier (The anatomy of a Teams Meeting). 


I'll next explain the properties of the body.

subject

The subject is retrieved from the trigger, the Teams Meeting Activity record created. The expression is triggerOutputs()?['body/subject']

content

The content is retrieved from from the trigger, the Teams Meeting Activity record created. There can be scenarios where the Organiser does not provide a value in the Description column of the Teams Meeting Activity therefore this needs to be accounted for in the expression which is where the coalesce function can be used. The expression is coalesce(triggerOutputs()?['body/description'], triggerOutputs()?['body/description'], '')

isOnlineMeeting and onlineMeetingProvider

The Graph API documentation provides a list of the properties available for the Create Event API request.

The isOnlineMeeting property defines that the Team Meeting is an online meeting and the value needs to be set to true as per the Graph API documentation.


The onlineMeetingProvider property defines that Microsoft Teams is the platform used as per the Graph API documentation. Apparently you also have the ability to create Skype For Business meetings still!

start dateTime and end dateTime

I encountered issues with this so that you don't have to 😶

Any web service, application and system out there deals with dateTime as UTC - that's how it has always been. This tripped me up which is surprising because I dealt with this same issue before in an earlier WTF episode.

Attempt No. 1
If you reference the schedule start time and schedule end time from the Teams Meeting Activity (the trigger) as-is, it will deceivingly look OK when you review it in the Microsoft Teams application.


However when you review the actual Teams Meeting by editing it, the timezone shows as UTC and the scheduled start date and scheduled end date show as UTC too.


The reason why this occurs is because the scheduled start date and scheduled end date from Microsoft Dataverse and Dynamics 365 has a "Z" character at the end.


In Power Automate and LogicApps if the "Z" is still present when a local timezone has been provided, it will ignore the local timezone and continue to operate as UTC. Refer to the screenshot below and this documentation for more details.



Attempt No.2
If you then use a couple of functions to remove the "Z" character from the scheduled start date and scheduled end date, it half works 😓 The timezone will display correctly when editing the Teams Meeting in the Microsoft Teams application however the scheduled start date and scheduled end date will appear as UTC still instead of the expected times.



I should have known better because I actually knew about this from previous WTF episodes when I showed how to send a birthday email in a Contact's local timezone. So luckily, I know how to resolve this slight hiccup.

Attempt No. 3
The scheduled start date and scheduled end date values from Microsoft Dataverse and Dynamics 365 first need to be converted from UTC to the local timezone, and formatted where the "Z" is not included. This way the Graph API Create Event request will create it in the specified timezone from the request header (prefer outlook.timezone='timezone') and from the timezone properties within the body of the request. 

The expression for the start and end time will be like the following
convertFromUtc(triggerOutputs()?['body/scheduledstart'], body('Graph_API_Mailbox_Settings_request')?['value'], 'yyyy-MM-ddTHH:mm:ss')

The timezone value from the previous Graph API Get user mailbox settings request is used in the expression.

location

This property is straight forward as it will be set to Teams Meeting since it's an online meeting for the attendees

attendees

This will be the array built upstream in the cloud flow where the attendee rows were formed from the Apply to each action that loops through only the required and optional attendees. All the hard work was done earlier.

Cloud flow in action

Create a new Teams Meeting Activity in Microsoft Dataverse or Dynamics 365 and let the magic flow ✨

In my WTF vlog I created the Teams Meeting Activity as myself but below I do it as Aaron Rodgers to show you that it will work for any user who creates a Teams Meeting Activity record as the HTTP request action is authenticating as an application with Microsoft Graph API.


What I forgot to show you in the WTF vlog is how the Teams Meeting Activity will also appear in the timeline of the required or optional attendees. Below is a screenshot of what it will look like in the timeline of the lead record.


Since this is an activity type table it will continue to display as active until you mark the activity as complete. This is no different to how appointments or phone calls are handled in Dynamics 365 or Microsoft Dataverse.

Constraints

  • This solution only accommodates when a Teams Meeting Activity is created but it's not difficult to manage update, delete and cancelations from Microsoft Dataverse or Dynamics 365 as the Graph API have all three of these requests available.
  • As a follow on from the first point, the Teams Meeting Activity from Microsoft Dataverse or Dynamics 365 is the trigger therefore create, update, delete and cancelation operations must be done from Microsoft Dataverse or Dynamics 365. They cannot be performed in the Microsoft Teams application.
As of today you cannot track a Teams Meeting from Outlook through the Dynamics 365 App for Outlook. This I cannot help with as I do not have the knowledge or the access to extend the Dynamics 365 App for Outlook. However as mentioned previously, since the Teams Meeting Activity is created from within Microsoft Dataverse or Dynamics 365 it will show in the timeline of the attendee or regarding record so there is visibility of Teams Meetings.

Summary

Even though the ability to create and send a Teams Meeting is not a feature available across the platform in Microsoft Dataverse or Dynamics 365 yet, it can be achieved with cloud flow in Power Automate using the Graph API. As mentioned in my previous WTF episode the Graph API is essentially our gateway to extending the Microsoft 365 ecosystem and when you combine it with Power Automate it allows you to be more creative. So get creating! #LetsAutomate

Shout outs

I'd like to thank Jim Daly in the Power Automate product team for continuing to expose more properties in the CDS Current Environment connector. When I initially came up with this solution back in 2019, I had to use the CDS standard connector at the time because there were some properties that were not returned in the JSON response of the CDS List records action when using the CDS Current Environment connector. The product team have worked on providing these additional properties and it's been a lot easier to create more beautiful cloud flows so thank you team 🙂

I'd also like to thank John Liu as it was John who introduced me to the filter an array action back in 2018 in my early days of learning clouds flows.