Two-way sync of bookings between Dynamics 365 and Outlook, Part I – Change Notifications

Syncing Bookable Resource Bookings between Dynamics 365 and Outlook has been a hot topic since Microsoft officially deprecated the feature. I wrote about syncing bookings to Outlook with Power Automate back in 2019. While a one-way sync has always been fairly easy to achieve, I received a fair amount of feedback that customers are looking for a two-way sync between D365 and Outlook. This has really been a topic of interest for me for a long time but it’s also a topic that has required a lot of research. All that research has finally resulted in a concept that I feel is worth sharing with the community.

Before we jump into the actual topic itself, I want to give huge kudos to fellow MVP Jan Vidar Elven. His blog posts about change notifications saved me a ton of time and essentially pushed me to finally create a concept for a two-way sync. I’m following many steps from his posts in this very article. MVP Joe Griffin also deserves kudos for helping me with some “dev stuff” for this blog post.

I also want to strongly emphasize that this article series covers a concept for a two-way sync of Bookable Resource Bookings between D365 and Outlook. As it is a concept, it’s not perfect and there are ways to refine it by using pro-code tools like the Event Framework and Azure Functions. My concept uses Power Automate’s cloud flows and is as low-code as possible. Why? Well, I’m not a developer so there’s that. Now, let’s dive into change notifications.

Change Notifications

Docs describe change notifications in a very easily understandable way. An app subscribes to change notifications on a resource and when Microsoft Graph accepts the subscription request, notifications are pushed to the URL specified in the subscription. As we’re interested in bookings, this series of articles focuses solely on the event resource type i.e. Outlook calendar events. When an event is created, updated, or deleted, a notification is sent and cloud flows then take action and sync bookings to Dynamics 365.

Microsoft Graph is a cool tool but it can feel very voodoo at times. It’s pretty full of surprises and a lot of times something that is being attempted is simply not supported with an API. This was also true for a lot of things I thought would make building the sync easier. Some limitations I encountered were:

  • Resource data isn’t supported for the event resource.
  • Creating and updating schema extensions is only supported using delegated permissions. My cloud flows use an AAD app for authentication so schema extensions didn’t work for me.
  • Open extensions don’t support OData filtering.
  • A POST to the event resource sometimes fails when using open extensions. A suggested fix was posted here but as I’m writing this post, I’ve not yet tried it.

Subscribing to change notifications

This blog post focuses on using cloud flows for subscribing to change notifications, processing notifications, and renewing subscriptions. Before we jump in the flows, let’s look at a way of creating a subscription to change notifications for a user. The most convenient place for subscribing and unsubscribing to change notifications is the Bookable Resource table. As the idea is to sync bookings between D365 and Outlook, all users will have a Bookable Resource row. For this example, I’ve simply added an Allowed/Not Allowed choice column (option set field) on the Bookable Resource table’s main form.

Choice columns are where the challenges of async processes calling APIs outside Dataverse become a reality. Users can change a value in the choice column and save the row, repeatedly firing off the cloud flow that subscribes/unsubscribes to change notifications. There’s no event execution pipeline, and it’s hard to say what’s going on in the background. In a real production scenario, subscribing and unsubscribing to change notifications should be done by leveraging the event execution pipeline – for example with plugins and Azure Functions – so that the subscribe/unsubscribe process can run, complete, and notify a user of a successful or a failed subscription/unsubscription.

Subscribe to change notifications to sync bookings.

Cloud flow – Subscribing and unsubscribing to change notifications

Let’s look at a cloud flow that is fired off when a value in the previously mentioned choice column changes. The initial actions give us the user id for the Bookable Resource row. This as well as the other flows related to change notifications require authentication with an Azure AD app. CDS CE triggers and actions used in the flows in this article series also use a service principal. More information about AAD apps and service principals can be found on docs.

Cloud flow for subscribing and unsubscribing to change notifications.
Creating subscriptions

The value for the choice row at the beginning defines whether the flow will run through the initial condition’s true or the false paths. Let’s look at the true path first for creating a subscription to change notifications. The first thing to do is to list subscriptions. A filter array action is then used to check whether or not the user in question has a subscription. Note that this example describes a scenario where a user only has a subscription to a single resource. If subscriptions to other resources existed, we’d need to check what they are and adapt the flow to take them into consideration.

Listing existing subscriptions.

A condition after the filter array checks whether the filter array’s body is empty or not. If it’s not empty, a subscription exists and the flow is terminated. If the body is empty the user doesn’t have a subscription and one needs to be created. As subscriptions to Outlook events are only valid for a max of 4230 minutes before they need to be renewed, a compose action is used to add 4230 minutes to utcNow.

A clientState is used to verify that received change notifications originate from the Microsoft Graph service. The value for the clientState property can be freely set but it should remain secret. You can read Jan Vidar Elven’s post for another example on using clientState.

The final action in this branch is a HTTP POST to create a subscription. The permissions an Azure AD app needs are listed on docs. Let’s go through the properties in the JSON in detail.

changeType: Possible values are created, updated, deleted. The reason why I’ve only used updated is that I observed that change notifications are delivered when an event is created or deleted even when the changeType in the subscription is only for updates. An Outlook event seems to internally patch (update) itself especially when an event is created. This causes multiple change notifications to be delivered so the less delivered, the better. More on the impact of sent change notifications in part II.

notificationUrl: Microsoft Graph validates the notification endpoint provided in the notificationUrl property of the subscription request before creating the subscription. More on this in the next chapter, which covers the validation flow. The URL of the notification endpoint is sensitive information.

resource: This property is used to subscribe to the event resource for the user whose Bookable Resource row was interacted with in the beginning of this blog post.

expirationDateTime: Output of the compose for the max duration of 4230 minutes from utcNow.

clientState: Output of the compose action for clientState.

Creating a subscription.
Deleting subscriptions

If the value in the choice column on Bookable Resource is Not Allowed, an existing subscription will be deleted. Like the true branch, the false one is also based on a user only having a single subscription. If your organization uses change notifications for other resources and there can be several subscriptions, this flow needs to be adapted to your specific scenario.

First, existing subscriptions are listed, the JSON is parsed, and a filter array is then used to check whether the array contains the id of the user in question. If the parse JSON returns a value, a subscription for the user exists. A compose with an integer index is used to get the first returned property, which is the id of the subscription. The expression used is body('Filter_array_from_Parse_JSON_on_no_side')?[0]?['id']. If the filter array is empty, the flow is terminated. If the filter array contains the subscription’s id, it is used in an HTTP DELETE to delete the subscription.

Deleting a subscription.

Cloud flow – Validating created subscriptions

When a subscription for change notifications is created with an HTTP POST, the notification endpoint provided the notificationUrl property is validated. In this example, the notification endpoint is a cloud flow with a When a HTTP request is received trigger. Check out docs for more information on the validation process.

A compose action is used to store the content-type from the trigger’s header. When the content-type is text/plain; charset=utf-8, a new subscription is being created and validation is needed. If the content-type is application/json; charset=utf-8, a change notification is sent. Part II covers the processing of change notifications. The expression used in the compose action for content-type is triggerOutputs()['headers']['Content-Type'].

A switch determines whether a subscription is being validated or if a change notification is being processed. When a subscription is being validated, a validation token query parameter needs to be decoded. As docs state, “Microsoft Graph encodes a validation token and includes it in a POST request to the notification URL.” The expression used for decoding a validation token is as simple as triggerOutputs()['queries']['validationToken'].

The final action is to respond with the decoded validation token. It’s important to respond with a status code of 200 and to set the content-type in the header to text/plain. The body of the response needs to contain the output of the compose for the decoded validation token.

Validating a subscription.

Could flow – Updating subscriptions

The third and final flow that is covered in this blog post updates subscriptions. As the max length of a subscription to Outlook events is 4230 minutes, all subscriptions to that resource need to be renewed before they expire. The solution is simple: A recurring cloud flow lists all subscriptions to the event resource and updates them. I’ve set a flow to run every 2 days so that there is a small buffer to react to possible errors before subscriptions expire.

To get a hold of subscriptions to a specific resource, a filter array is used. If the resource property contains events, the related subscription is for the Outlook event resource. An id can then be extracted and used to PATCH i.e. update the related subscription.

Updating subscriptions.

In part II we’ll look at how bookings are synced from D365 to Outlook. If you have played with change notifications and would like to share your experiences and feedback, please be sure to leave a comment below!

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

Leave a Reply

Your email address will not be published. Required fields are marked *