Having multiple calendars that are not synchronized often causes scheduling issues. To solve this problem, I built a completely new Next-Gen Microsoft Power Automate flow that synchronizes Outlook with Outlook, Outlook with Google, or Google with Google calendars. The flow is free and open-source. Here I explain how it works and how to set it up.
Introduction
If you have multiple calendars, you may know the following problem: when scheduling a calendar event, you need to check all calendars to determine your availability, and other people (who can access just one of your calendars) may send event invitations at time slots where you are not actually available (→ scheduling conflicts).
I certainly suffered from this, so that I built two Power Automate (PA) flows in the past, one to synchronize two Outlook 365 calendars (blog post, code), and one to sync Outlook 365 with Google (blog post, code), due to popular demand.
However, these flows had some severe issues, which is why I built a completely new calendar synchronization flow, referred to as the “next-gen” (NG) flow, which I’ll present in this post.
The new flow has the following features:
- Microsoft Power Automate flow that runs in the cloud in configurable intervals, e.g. every 30 minutes (→ your computer can be turned off)
- Synchronization directions:
- Outlook 365 <-> Outlook 365 (within the same tenant, or across different tenants, optionally via an HTTP mirror file)
- Outlook 365 <-> Google
- Google <-> Google
- Uni-directional or bi-directional synchronization
- Synchronizes the event fields: title, location, and description. If you synchronize Outlook 365 <-> Outlook 365, the fields sensitivity and show as are also synchronized
- Optional anonymization of event fields with potentially sensitive data (event title, location, and description)
- Configurable number of upcoming days to synchronize
- Various options to label the blocker events so that you can easily identify them in your calendar (e.g. by adding a prefix to the title, or using an Outlook category)
- Configurable which events to synchronize (e.g. excluding events from the sync that have an Outlook response type that you deem unsuitable, e.g. “tentatively accepted”, or excluding events that you created and that have no other attendees than yourself)
- Option to perform a “dry run”, where you can inspect what changes the flow would make, without actually making them
Those who knew and used the old flows might be interested in the history and differences between the new NG flow and the old ones:
How the NG flow works
Synchronization algorithm in a nutshell
In simplified terms, the PA flow iterates over all events in one calendar and creates blocker events in the other calendar. This happens in (configurable) regular intervals. In order to efficiently create, update, or delete only those blocker events that are really needed, the flow must correlate the respective events of both calendars. To do so, we use an unique identifier that each calendar provider (Google / Outlook) automatically assigns to the events. We copy the unique ID of a real event into a fake attendee email address of the corresponding blocker event (using an email such as “sync@<unique-id>.invalid
“). By checking for the presence of such an email address, the synchronization algorithm can also disambiguate your real events from the blocker events it created.
Let’s take a closer look at how the synchronization works, starting with uni-directional synchronization.
Uni-directional sync
The following image depicts a summary of the activities (blue boxes) of the PA flow, and where data is sent to or retrieved from (see the colored arrows). Let’s assume that we refer to the two calendars that we want to synchronize as X and Y, shown on the right (they could each be either a Google or Outlook calendar). The image only shows one flow instance (called #A). This one instance will only achieve uni-directional synchronization, writing blocker events (a.k.a. “SyncBlocker” events) only into calendar X.
Open the tabs to learn more details about the steps:
- 1.1: Retrieve calendar events between “now” and the configured number of future days, in the proprietary format of the calendar provider (Google vs. Outlook)
- 1.2: Store the events retrieved in step 1.1 in a PA variable
- 2.1: Submit the events retrieved in step 1 to the Sync helper service, which filters out blocker events, converts the “real” events into a “unified” format, and returns them. In that unified format, all fields not relevant for the synchronization are removed, and all relevant fields have the same name and date format (Google and Outlook do often use different field names for the same concept, e.g. the event title field is called subject by Outlook and summary by Google)
- 2.2: optional step! If you want to achieve bi-directional synchronization where the other flow instance is configured in a different Microsoft tenant (or PA environment), you can tell the Sync helper service to upload the events (via HTTP PUT/POST) to some web server, so that the other flow instance can download it from there.
- 2.3: Store the events retrieved in step 2.1 in a PA variable
This happens in one of two variants (you configure which one to use):
- Variant A (via HTTP mirror file)
- 3a.1 + 3a.2: retrieve the mirror file from some HTTP-based file server (such as Nextcloud) using a “HTTP GET” request. I’m assuming that the mirror file is access-protected, which typically happens via an “Authorization” HTTP header, that has e.g. “Basic 524dsf05..” as value (a.k.a. “Basic Auth”, e.g. used by Nextcloud), or maybe a different value (e.g. “Bearer …”). In my experiments, I found that it’s not possible to make the GET request with the PA action “Send an HTTP request to SharePoint”, because when setting a header named “Authorization”, the action hangs forever – presumably because of a bug in that action. Consequently, the “Send an HTTP request to SharePoint” action instead makes the GET request to the Sync helper service, provides it with custom headers (X-Auth-Header-Name, X-Auth-Header-Value), and the Sync helper service forwards the request to the mirror file server in step 3a.2.
- 3a.3: Store the events retrieved in step 3a.1/3a.2 in a Power Automate variable
- Variant B (direct calendar retrieval)
- 3b.1: Retrieve calendar 2 events between “now” and the configured number of future days, in the proprietary format of the calendar provider (Google vs. Outlook)
- 3b.2: Store the events retrieved in step 3b.1 in a PA variable
- 3b.3: see step 2.1 (filter out blocker events, convert the “real” events into a “unified” format)
- 3b.4: Store the events retrieved in step 3b.3 in a PA variable
- 4.1: submit the filtered event lists of both calendars to the Sync helper service, which returns three blocker event lists (some of which could be empty). One list indicates which blocker events to create in calendar 1 (in this case, calendar X), another list indicates which ones to change/update (because some fields of the corresponding calendar 2 event changed), another list indicates which ones to delete (because the corresponding real calendar 2 event has disappeared).
- 4.2: Store the three event lists retrieved in step 4.1 in PA variables
The PA flow iterates over the three lists and calls the right PA actions to make the changes to calendar 1 (X).
Bi-directional sync
To synchronize both calendars in both directions, you need to set up an additional second instance of the PA flow, called instance #B. In that second flow’s settings, “calendar 1” now refers to calendar Y, to which the flow writes blocker events. The following image shows the full view of the data flow of both flow instances. The steps (1.1, 1.2, etc.) are the same as for the uni-directional synchronization, please refer to the above section for a detailed explanation of the steps.
Data privacy considerations
Calendar events often contain sensitive information (e.g. unprotected links, names, email excerpts, etc.). You should always be careful where you send such data to. With the Next-Gen PA flow, there are two aspects that might worry you:
- By default, you are sending your calendar events (including all details) to the Sync helper service, which is a server hosted by “some dude on the Internet” (me, in this case). I provide and maintain that service for free, for your convenience. https://ng-outlook-google-calendar-sync.onrender.com/ is hosted on render.com and runs somewhere in the US. (spoiler: the PA platform also runs somewhere in the US)
- If you use the Mirror File feature (see Step 3, Variant A), you are also transmitting your credentials to the mirror file server to the Sync helper service.
- By default, the HTTP calls are made by the “Send an HTTP request to SharePoint” PA action. This may seem weird, given that we don’t actually want to use any SharePoint sites (instead, I configure that PA action with the URL of the Sync helper service). The only reason why the flow uses “Send an HTTP request to SharePoint” instead of the “HTTP” PA action is because the “HTTP” action is a Premium action, meaning that you need to pay extra for Power Automate. I’m assuming that you (or your employer’s IT department) don’t want to pay for the premium plan. The main problem with the “Send an HTTP request to SharePoint” action is that it also submits an “Authorization” header that contains your Microsoft 365 bearer token!
If you feel uneasy with these facts, consider self-hosting the Sync helper service. The code is open source (Git repo), so feel free to inspect the code for back doors. You will see that the service is completely stateless: no data is permanently stored, and the Authorization header (that contains your Microsoft 365 Bearer token) is not even accessible to the Python code (because I did not declare it as an argument in any of the endpoint handler functions).
This means that you can deploy the Sync helper service in your own trusted environment. The easiest approach would be render.com: fork my code, create a free render.com account, and configure it to build and deploy your forked repo. But you could also run the helper service in any other way you like, e.g. with Docker, or installing it onto a VM directly.
Step-by-step instructions to set up NG flow
Let’s see how we can synchronize two calendars, referred to as X and Y.
Preparation steps
Try this first
Before you continue reading the rest of the instructions, I recommend that you first verify whether the import feature is available to you. Open https://make.powerautomate.com, and click
My flows → Import → Import package (legacy)
If you managed to see these menu items and see an Upload form, you are good to go. Otherwise, you’ll need to pay for a PA subscription.
First, set up the necessary connections. On https://make.powerautomate.com, click on … More → Connections and add the following connections, by clicking on the “+ New connection” button at the top, then using the Search box at the top right, to find the correct connection type:
- One SharePoint connection (to any SharePoint site / account you like – we don’t connect to it, we just use the HTTP-related action of SharePoint to call the Sync helper service)
- If you want to synchronize between two Outlook 365 accounts: Two Office 365 Outlook connections (unless setting up the second connection fails because the tenant admin disabled cross-tenant replication – in this case, just set up one Outlook connection)
- If you want to synchronize between two Google accounts: Two Google Calendar connections
- If you want to synchronize between an Outlook 365 and a Google account: One Office 365 Outlook connection, one Google Calendar connection
Next, go to the GitHub repo and download the correct zip archive mentioned in the Readme, depending on which providers (Google / Outlook) your calendars have.
Setting up flow instance #A
We now set up the first flow instance (referred to as #A), which writes blocker events to calendar X.
Back on https://make.powerautomate.com, click My flows → Import → Import package (legacy), click the Upload button, and choose the zip archive you just downloaded. In the form that is shown afterwards, click the links in the “IMPORT SETUP” column to fix any errors. The first row lets you configure the name of the flow that is created, and in the other rows, you need to choose the connections you created earlier. Choosing the connections may cause you some confusion. You can choose the same Outlook / Google connection in both respective Outlook / Google rows, if you are importing the Outlook-Google flow (to synchronize Outlook with Google), or if you need to synchronize via the mirror file (using step 3 variant A, as explained in the “Uni-directional sync” section above).
Finally, click on “Import”.
Next, we need to activate the flow, because it has been imported in a deactivated state (in which we cannot even trigger a test run). To do so, open the imported flow and click the “Turn On” button in the top menu (the UI might take several seconds until your click shows any effect, just be patient).
Next, we need to tune the settings of the flow and test whether it works as expected. Click on “Edit” to open the editor, then click on all those actions at the top that start with “Setting:” and change the values as you want them. I added a note to each setting that explains what it does. Note that “calendar 1”-related settings refer to calendar X, and the “calendar 2”-related settings refer to calendar Y.
I highly recommend that you leave the “Days to sync” setting at 1 day initially and leave the “Dry run” setting at true. Click on “Test” to save and run the flow.
Once the run of the flow has finished, open the run. Take a look at the output of the action called “Compute calendar modification actions”, to verify that the correct blocker events are created: in the “events_to_create” entry, there should be one entry for each calendar 2 (calendar Y) event that occurs during the configured “Days to sync” period (except for those calendar 2 events you wanted to be filtered out).
If everything works, edit the flow again, and change the “Dry run” to false, then test the flow again. Open the calendar software that you normally use to manage calendar X and verify that the events were successfully created (with the Outlook Desktop app, the update can sometimes take some time). If everything works, you can edit the flow again and increase the “Days to sync” setting to a higher number.
How to choose “Days to sync”?
The Google- and Outlook-specific actions that retrieve your calendar events have a platform-side upper limit of events these actions return. These limits may change over time. As of November 2024, the Google limit seems to be 250 events. For Outlook you can specify a “Top count” value in the “Get calendar view of events (V3)” action, but it won’t return more than 1000 events. Although it’s theoretically possible to detect that the limit was reached and run these event-retrieval-actions in a loop (kind of doing “paginated calls”), I refrained from implementing this, to keep the flow simple.
Personally, I find that a value of 30 days is usually good enough. I never needed to synchronize my calendar any further into the future than that. But if your goal is to synchronize as long into the future as technically possible, you need to determine the “Days to sync” value experimentally! This depends on how full your calendar is. The more events you have on your calendar, the faster you will reach the limit of 250 (Google) or 1000 (Outlook) events. Play around with the “Days to sync” value and check how many events the corresponding Google- or Outlook-specific actions return. And leave some head room, because your calendar pressure may vary over time.
In this case, don’t forget to increase the value of the “Abort flow on too many events” setting. It is set to 250 by default, but if you want to synchronize Outlook only (which allows up to 1000), you need to increase the value.
Setting up flow instance #B
Once instance #A works, and if you want bi-directional synchronization, you can set up the second PA flow instance #B, either in the same Microsoft account/tenant or a different one.
Just repeat the steps explained in the “Setting up flow instance #A” section. But this time, in flow instance #B, the “calendar 1”-related settings refer to calendar Y, and the “calendar 2” settings refer to calendar X.
Remarks for synchronizing via an HTTP mirror file
Normally, you should not configure the HTTP mirror file and instead synchronize the two calendars directly with each other (by using the respective “Google calendar” or “Office 365 Outlook” connections). But if you are affected by cross-tenant replication restrictions, you need to use the mirror file, and you need to find a file server to host your two calendar mirror files. Here are a few helpful tips:
Uninstalling and cleaning your calendar
In case you have problems and want to stop using the calendar sync flow (or if you want to reset the synchronization), proceed as follows:
- Stop your calendar flow instance(s), e.g. by clicking the “Turn off” button, or deleting them completely
- Go to the GitHub repo to download the Delete flow zip file for Outlook or Google, and import it on https://make.powerautomate.com. Navigate to the imported flow, click “Turn on”, then edit the flow, read the instructions, and configure the settings.
Conclusion
I built this solution out of a personal need, where the lack of synchronization between my employer’s calendar and our customer’s calendar caused me considerable scheduling headaches.
I hope the flow helps you just as much as it helps me. If you run into any problems, feel free to post here in the comments, or to create an issue on GitHub.
The implemented Power Automate flow synchronizes two calendars, but it can be extended to synchronize three or more calendars: just duplicate the flow, always set the same Outlook/Google calendar as the “calendar 1”, and use a different “unique sync prefix” setting for each flow instance. The result is a star-shaped synchronization pattern, e.g. synchronizing calendars X with Y, X with Z, and X with W, which (by transitivity) achieves that all calendars (X, Y, Z, W) are synchronized.
Hi Marius,
Thanks a lot for the Automation! It is extremely helpful!
I got a question though. In one of my Google calendars there are quite a lot of events I declined, but I don’t want to delete them completely. Is it possible not to synchronize them from Google to Outlook? Or am I missing this feature?
Hi, thanks for your suggestion. I took a brief look. For those events that I decline (but are still part of your calendar), the events in the output of the action “Get Google calendar view of events cal1/2” seem to contain the field “privateCopy”: true. Can you confirm that this is the case for you too? And can you check whether “privateCopy”: true only exists for those kinds of declined events (or is that field set also for other kinds of events)?
I could imagine implementing a filter for this field, but I need to be sure that this filter won’t accidentally match other kinds of events too (or at least document it). Interestingly, that privateCopy field is not listed in the documentation (https://learn.microsoft.com/en-us/connectors/googlecalendar/#responseevent).
Hi Marius,
Thanks a lot for getting back to me!
1. I don’t have such a parameter at all. I queried calendar API from the documentation here: https://developers.google.com/calendar/api/v3/reference/events/list?apix_params=%7B%22calendarId%22%3A%22primary%22%2C%22eventTypes%22%3A%5B%22default%22%2C%22focusTime%22%2C%22fromGmail%22%2C%22outOfOffice%22%5D%2C%22showDeleted%22%3Afalse%2C%22timeMax%22%3A%222024-11-17T19%3A19%3A14.8765530Z%22%2C%22timeMin%22%3A%222024-11-10T19%3A19%3A14.8765461Z%22%7D and I see these parameters in the list of attendees next to my email:
{
“email”: “my@email.ai”,
“displayName”: “Alex”,
“self”: true,
“responseStatus”: “declined”
}
I’m querying V3 of the API though. Looks like you’re using an earlier version in the script.
And one more question:
2. In Google Calendar there are events with the type “workingLocation”, e.g. I can set that on Mondays I’m working from the office and on Tuesdays from home. These events are synced and block the destination calendar. Is is possible to exclude them?
P.S. I can share several examples of events from API in case it can help in any way, just in private.
Hi Alex. Thanks for your research. Unfortunately, since I’m using Power Automate to do the querying of calendars (be it Google or Outlook), I’m limited to the data that the Google connector of Power Automate offers. You’ll need to look at the runs of the Power Automate flows to observe which data is available (and ignore the Google V3 API). For instance, on https://github.com/MShekow/ng-outlook-google-calendar-sync/blob/main/implementation-notes.md you can see which fields are returned, under normal circumstances. And as I explained above, the “privateCopy” field seems to be returned only for declined events that were not deleted.
As for your 2nd question, I don’t think this is reflected by the data returned in Power Automate, but you should check for yourself.
Hello!
first of all, thank you very much for developing this flow, it worked great for me for the past weeks.
However, it seems like “https://ng-outlook-google-calendar-sync.onrender.com/” is now “{“detail”:”Not Found”}” and thus, the flow fails.
Any help? Thanks!
Hi Anton, there was a recent change in the Power Automate platform that breaks this calendar sync flow. Nothing you can do about it, for now, I’m afraid. I might be able to release another version, but it will require you to pay for Power Automate Premium. See also https://github.com/MShekow/ng-outlook-google-calendar-sync/issues/20
By the way, the response for the root URL, https://ng-outlook-google-calendar-sync.onrender.com/, has always been “{“detail”:”Not Found”}”, because that URL does not have a handler implemented.