<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: yhzhu</title>
    <description>The latest articles on DEV Community by yhzhu (@yhzhu).</description>
    <link>https://dev.to/yhzhu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3704749%2F26bb03e4-ab6f-4e3f-80b1-d2868009b90d.jpeg</url>
      <title>DEV Community: yhzhu</title>
      <link>https://dev.to/yhzhu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yhzhu"/>
    <language>en</language>
    <item>
      <title>Synchronizing Recurring Outlook Meetings with EspoCRM: A Production-Grade Solution</title>
      <dc:creator>yhzhu</dc:creator>
      <pubDate>Sun, 11 Jan 2026 06:29:22 +0000</pubDate>
      <link>https://dev.to/yhzhu/synchronizing-recurring-outlook-meetings-with-espocrm-a-production-grade-solution-1e5d</link>
      <guid>https://dev.to/yhzhu/synchronizing-recurring-outlook-meetings-with-espocrm-a-production-grade-solution-1e5d</guid>
      <description>&lt;p&gt;This article was originally published on &lt;a href="https://www.yzhu.name/2026/01/10/espocrm/08-outlook-recurring-sync-en/" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;If you're building Outlook Calendar integration with EspoCRM, &lt;strong&gt;recurring meetings will break your standard delta sync logic&lt;/strong&gt;. This article presents a production-grade solution using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Windowed Expansion&lt;/strong&gt;: Only sync instances within &lt;code&gt;[Today - 7d, Today + 90d]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Series Rebuild&lt;/strong&gt;: When seriesMaster changes, fetch all instances via the Instances API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bootstrap State Machine&lt;/strong&gt;: Handle cold-start with explicit initialization scan&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Critical Warning&lt;/strong&gt;: Never use &lt;code&gt;iCalUId&lt;/code&gt; as a unique key for recurring instances&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Problem: Why Delta Sync Fails for Recurring Meetings
&lt;/h2&gt;

&lt;p&gt;Most developers assume Microsoft Graph's Delta Sync API will notify them of every calendar change. This works fine for single events. For recurring meetings, it breaks down.&lt;/p&gt;

&lt;h3&gt;
  
  
  Microsoft's Data Model
&lt;/h3&gt;

&lt;p&gt;In Microsoft Graph API, a recurring meeting isn't a single record. It's split into three types:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Stored in Outlook&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;seriesMaster&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The template defining recurrence rules (e.g., "every Thursday at 2 PM")&lt;/td&gt;
&lt;td&gt;Physical record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;occurrence&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A specific instance (e.g., "Jan 8, 2026 at 2 PM")&lt;/td&gt;
&lt;td&gt;Virtual, computed from Master&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;exception&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A modified instance (e.g., "Just this one, move to 3 PM")&lt;/td&gt;
&lt;td&gt;Physical record&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The Delta Sync Trap
&lt;/h3&gt;

&lt;p&gt;When you modify a seriesMaster (e.g., change "weekly Monday" to "weekly Tuesday"), the Delta API returns only the seriesMaster change. &lt;strong&gt;It does NOT return the 20+ occurrence changes&lt;/strong&gt; for future instances.&lt;/p&gt;

&lt;p&gt;Your CRM continues showing meetings on Mondays. Users complain. Debugging reveals no sync errors—the delta data simply never contained those changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common Failure Patterns
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Root Cause&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Missing instances&lt;/td&gt;
&lt;td&gt;Only processed seriesMaster, never expanded occurrences&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate instances&lt;/td&gt;
&lt;td&gt;Used &lt;code&gt;iCalUId&lt;/code&gt; as dedupe key (same for entire series)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orphaned instances&lt;/td&gt;
&lt;td&gt;Deleted series in Outlook, only Master removed from CRM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time drift&lt;/td&gt;
&lt;td&gt;Expanded in UTC without proper timezone handling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Solution: Windowed Expansion + Series Rebuild
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Core Principle: The Window
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Never sync infinite future.&lt;/strong&gt; Define a "window of interest" such as &lt;code&gt;[Today - 7d, Today + 90d]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This makes any recurring series—even "no end date" ones—produce a constant, bounded number of instances.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ2FudHQKICAgIHRpdGxlIFdpbmRvdyBTbGlkaW5nIE92ZXIgVGltZQogICAgZGF0ZUZvcm1hdCAgWVlZWS1NTS1ERAogICAgYXhpc0Zvcm1hdCAgJW0vJWQKICAgIHNlY3Rpb24gV2luZG93ICg5MGQpCiAgICBJbml0aWFsIHdpbmRvdyAgOmExLCAyMDI2LTAxLTAxLCA5MGQKICAgIFNsaWQgd2luZG93ICAgICA6YTIsIDIwMjYtMDItMDEsIDkwZAogICAgc2VjdGlvbiBTZXJpZXMKICAgIFdlZWtseSBpbnN0YW5jZXMgOmNyaXQsIDIwMjYtMDEtMDEsIDE4MGQ" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ2FudHQKICAgIHRpdGxlIFdpbmRvdyBTbGlkaW5nIE92ZXIgVGltZQogICAgZGF0ZUZvcm1hdCAgWVlZWS1NTS1ERAogICAgYXhpc0Zvcm1hdCAgJW0vJWQKICAgIHNlY3Rpb24gV2luZG93ICg5MGQpCiAgICBJbml0aWFsIHdpbmRvdyAgOmExLCAyMDI2LTAxLTAxLCA5MGQKICAgIFNsaWQgd2luZG93ICAgICA6YTIsIDIwMjYtMDItMDEsIDkwZAogICAgc2VjdGlvbiBTZXJpZXMKICAgIFdlZWtseSBpbnN0YW5jZXMgOmNyaXQsIDIwMjYtMDEtMDEsIDE4MGQ" alt="diagram" width="1904" height="172"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture Overview
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBTdGFydFsiU3RhcnQgU3luYyJdIC0tPiBHZXREZWx0YVsiR3JhcGggQVBJOiBEZWx0YSBTeW5jIl0KICAgIEdldERlbHRhIC0tPiBMb29weyJQcm9jZXNzIENoYW5nZXMifQoKICAgIExvb3AgLS0gIlR5cGU9c2VyaWVzTWFzdGVyIiAtLT4gTWFya1Jlc3luY1siTWFyayBmb3IgUmVidWlsZCJdCiAgICBMb29wIC0tICJUeXBlPW9jY3VycmVuY2UiIC0tPiBTeW5jSXRlbVsiU3luYyBJbnN0YW5jZSAoTm8gaUNhbFVJZCBGYWxsYmFjaykiXQogICAgTG9vcCAtLSAiVHlwZT1zaW5nbGVJbnN0YW5jZSIgLS0-IFN5bmNOb3JtYWxbIlN0YW5kYXJkIFN5bmMiXQoKICAgIE1hcmtSZXN5bmMgLS0-IFRyaWdnZXJSZXN5bmNbIlRyaWdnZXIgU2VyaWVzIFJlYnVpbGQiXQoKICAgIHN1YmdyYXBoIFNlcmllc1Jlc3luYyBbIlNlcmllcyBSZWJ1aWxkIl0KICAgICAgICBGZXRjaFsiR3JhcGggQVBJOiBMaXN0IEluc3RhbmNlcyAoV2luZG93ZWQpIl0KICAgICAgICBVcHNlcnRbIkJ1bGsgVXBzZXJ0IEluc3RhbmNlcyJdCiAgICAgICAgQ2xlYW51cFsiQ2xlYW4gUmVtb3ZlZCBJbnN0YW5jZXMiXQogICAgZW5k" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBTdGFydFsiU3RhcnQgU3luYyJdIC0tPiBHZXREZWx0YVsiR3JhcGggQVBJOiBEZWx0YSBTeW5jIl0KICAgIEdldERlbHRhIC0tPiBMb29weyJQcm9jZXNzIENoYW5nZXMifQoKICAgIExvb3AgLS0gIlR5cGU9c2VyaWVzTWFzdGVyIiAtLT4gTWFya1Jlc3luY1siTWFyayBmb3IgUmVidWlsZCJdCiAgICBMb29wIC0tICJUeXBlPW9jY3VycmVuY2UiIC0tPiBTeW5jSXRlbVsiU3luYyBJbnN0YW5jZSAoTm8gaUNhbFVJZCBGYWxsYmFjaykiXQogICAgTG9vcCAtLSAiVHlwZT1zaW5nbGVJbnN0YW5jZSIgLS0-IFN5bmNOb3JtYWxbIlN0YW5kYXJkIFN5bmMiXQoKICAgIE1hcmtSZXN5bmMgLS0-IFRyaWdnZXJSZXN5bmNbIlRyaWdnZXIgU2VyaWVzIFJlYnVpbGQiXQoKICAgIHN1YmdyYXBoIFNlcmllc1Jlc3luYyBbIlNlcmllcyBSZWJ1aWxkIl0KICAgICAgICBGZXRjaFsiR3JhcGggQVBJOiBMaXN0IEluc3RhbmNlcyAoV2luZG93ZWQpIl0KICAgICAgICBVcHNlcnRbIkJ1bGsgVXBzZXJ0IEluc3RhbmNlcyJdCiAgICAgICAgQ2xlYW51cFsiQ2xlYW4gUmVtb3ZlZCBJbnN0YW5jZXMiXQogICAgZW5k" alt="diagram" width="805" height="962"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Implementation Details
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The Bootstrap State Machine
&lt;/h3&gt;

&lt;p&gt;Delta Sync is "change-driven," not "presence-driven." Meetings created years ago with no recent changes won't appear in delta results. This causes silent missing data on first sync.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Track bootstrap state per user calendar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OutlookCalendarService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;STATUS_PENDING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Pending'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;STATUS_COMPLETED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Completed'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;syncUserCalendar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$calendar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getCalendar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$calendar&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getBootstrapStatus&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;STATUS_PENDING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;runBootstrapScan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$calendar&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$calendar&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setBootstrapStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;STATUS_COMPLETED&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;em&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$calendar&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;runDeltaSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$calendar&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;runBootstrapScan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;OutlookCalendar&lt;/span&gt; &lt;span class="nv"&gt;$calendar&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$windowStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'-7 days'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$windowEnd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'+90 days'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Get ALL seriesMasters in window, not just changed ones&lt;/span&gt;
        &lt;span class="nv"&gt;$masters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;graphClient&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getCalendarView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;$calendar&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getExternalAccountId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="nv"&gt;$windowStart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$windowEnd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'$filter'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"type eq 'seriesMaster'"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$masters&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$master&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;rebuildSeries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$master&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$windowStart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$windowEnd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Series Rebuild Strategy
&lt;/h3&gt;

&lt;p&gt;When seriesMaster changes, trigger instance rebuild:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;rebuildSeries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$masterId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;\DateTime&lt;/span&gt; &lt;span class="nv"&gt;$start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;\DateTime&lt;/span&gt; &lt;span class="nv"&gt;$end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Fetch all instances in window&lt;/span&gt;
    &lt;span class="nv"&gt;$instances&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;graphClient&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInstances&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$masterId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$end&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$apiInstanceIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$instances&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Get existing CRM instances for this series&lt;/span&gt;
    &lt;span class="nv"&gt;$existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;em&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OutlookEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;findBy&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'seriesMasterId'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$masterId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nv"&gt;$existingIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getEventId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nv"&gt;$existing&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Diff: Create new, Update existing, Delete missing&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$instances&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;syncInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$instance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Delete instances that disappeared from API (but within window)&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$existing&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getEventId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nv"&gt;$apiInstanceIds&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getStart&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;between&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$end&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;em&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. The iCalUId Trap (CRITICAL!)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;RFC 5545 specifies that ALL occurrences in a recurring series share the same UID.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This means &lt;code&gt;iCalUId&lt;/code&gt; is &lt;strong&gt;NOT unique&lt;/strong&gt; for recurring instances. Using it for deduplication will merge all your weekly meetings into one record.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// WRONG: This will break recurring meetings&lt;/span&gt;
&lt;span class="nv"&gt;$meeting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;findByEventId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$eventId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;findByICalUId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$iCalUId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// ❌ DO NOT DO THIS&lt;/span&gt;

&lt;span class="c1"&gt;// CORRECT: Never use iCalUId fallback for recurring instances&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$eventType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'occurrence'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$eventType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'exception'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$meeting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;findByEventId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$eventId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// ✅ Use eventId only&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$meeting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;findByEventId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$eventId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;findByICalUId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$iCalUId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// ✅ OK for single events&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Complete Sync Sequence
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fc2VxdWVuY2VEaWFncmFtCiAgcGFydGljaXBhbnQgUyBhcyBTY2hlZHVsZXIvSm9iCiAgcGFydGljaXBhbnQgU1MgYXMgU3luY1NlcnZpY2UKICBwYXJ0aWNpcGFudCBEQiBhcyBEYXRhYmFzZQogIHBhcnRpY2lwYW50IFAgYXMgUHJvdmlkZXIoT3V0bG9vay9Hb29nbGUpCiAgcGFydGljaXBhbnQgVyBhcyBXaW5kb3dFeHBhbmRlcgoKICBTLT4-U1M6IFRyaWdnZXIgdXNlciBzeW5jCiAgU1MtPj5EQjogQ2hlY2sgYm9vdHN0cmFwU3RhdHVzCgogIGFsdCBTdGF0dXMgaXMgUGVuZGluZyAoQ29sZCBTdGFydCkKICAgIFNTLT4-VzogRm9yY2UgV2luZG93ZWQgU2NhbiAoLTdkLi4rOTBkKQogICAgVy0-PlA6IExpc3QgYWxsIGluc3RhbmNlcyBpbiB3aW5kb3cKICAgIFctPj5EQjogSW5pdGlhbGl6ZSBTZXJpZXMgJiBJbnN0YW5jZXMKICAgIFNTLT4-REI6IFVwZGF0ZSBib290c3RyYXBTdGF0dXMgPSBDb21wbGV0ZWQKICBlbHNlIFN0YXR1cyBpcyBDb21wbGV0ZWQgKEluY3JlbWVudGFsKQogICAgU1MtPj5QOiBGZXRjaCBkZWx0YSAodG9rZW4vcGFnZSkKICAgIGFsdCBTZXJpZXMgbWFzdGVyIGNoYW5nZWQKICAgICAgU1MtPj5XOiBNYXJrIHNlcmllcyBmb3IgcmVidWlsZAogICAgICBXLT4-UDogTGlzdCBpbnN0YW5jZXMgKHdpbmRvdzogLTdkLi4rOTBkKQogICAgICBXLT4-REI6IFVwc2VydCBjcmVhdGVkL3VwZGF0ZWQKICAgICAgVy0-PkRCOiBEZWxldGUgd2luZG93LW1pc3NpbmcKICAgIGVsc2UgT2NjdXJyZW5jZS9FeGNlcHRpb24gY2hhbmdlZAogICAgICBTUy0-PkRCOiBVcHNlcnQgb2NjdXJyZW5jZSAobm8gaUNhbFVJZCBmYWxsYmFjaykKICAgIGVuZAogIGVuZAogIFNTLS0-PlM6IFJlcG9ydCBtZXRyaWNz" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fc2VxdWVuY2VEaWFncmFtCiAgcGFydGljaXBhbnQgUyBhcyBTY2hlZHVsZXIvSm9iCiAgcGFydGljaXBhbnQgU1MgYXMgU3luY1NlcnZpY2UKICBwYXJ0aWNpcGFudCBEQiBhcyBEYXRhYmFzZQogIHBhcnRpY2lwYW50IFAgYXMgUHJvdmlkZXIoT3V0bG9vay9Hb29nbGUpCiAgcGFydGljaXBhbnQgVyBhcyBXaW5kb3dFeHBhbmRlcgoKICBTLT4-U1M6IFRyaWdnZXIgdXNlciBzeW5jCiAgU1MtPj5EQjogQ2hlY2sgYm9vdHN0cmFwU3RhdHVzCgogIGFsdCBTdGF0dXMgaXMgUGVuZGluZyAoQ29sZCBTdGFydCkKICAgIFNTLT4-VzogRm9yY2UgV2luZG93ZWQgU2NhbiAoLTdkLi4rOTBkKQogICAgVy0-PlA6IExpc3QgYWxsIGluc3RhbmNlcyBpbiB3aW5kb3cKICAgIFctPj5EQjogSW5pdGlhbGl6ZSBTZXJpZXMgJiBJbnN0YW5jZXMKICAgIFNTLT4-REI6IFVwZGF0ZSBib290c3RyYXBTdGF0dXMgPSBDb21wbGV0ZWQKICBlbHNlIFN0YXR1cyBpcyBDb21wbGV0ZWQgKEluY3JlbWVudGFsKQogICAgU1MtPj5QOiBGZXRjaCBkZWx0YSAodG9rZW4vcGFnZSkKICAgIGFsdCBTZXJpZXMgbWFzdGVyIGNoYW5nZWQKICAgICAgU1MtPj5XOiBNYXJrIHNlcmllcyBmb3IgcmVidWlsZAogICAgICBXLT4-UDogTGlzdCBpbnN0YW5jZXMgKHdpbmRvdzogLTdkLi4rOTBkKQogICAgICBXLT4-REI6IFVwc2VydCBjcmVhdGVkL3VwZGF0ZWQKICAgICAgVy0-PkRCOiBEZWxldGUgd2luZG93LW1pc3NpbmcKICAgIGVsc2UgT2NjdXJyZW5jZS9FeGNlcHRpb24gY2hhbmdlZAogICAgICBTUy0-PkRCOiBVcHNlcnQgb2NjdXJyZW5jZSAobm8gaUNhbFVJZCBmYWxsYmFjaykKICAgIGVuZAogIGVuZAogIFNTLS0-PlM6IFJlcG9ydCBtZXRyaWNz" alt="diagram" width="1294" height="963"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Quota Management
&lt;/h3&gt;

&lt;p&gt;Recurring series generate massive record counts. One user with 10 weekly meetings = ~120 instances per quarter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Protect against API throttling:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SyncLimits&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;MAX_INSTANCES_PER_RUN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;MAX_SERIES_REBUILD_PER_RUN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nv"&gt;$instancesProcessed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nv"&gt;$seriesRebuilt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;shouldProcessMoreInstances&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;instancesProcessed&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MAX_INSTANCES_PER_RUN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;shouldRebuildMoreSeries&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;seriesRebuilt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MAX_SERIES_REBUILD_PER_RUN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;recordInstance&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;instancesProcessed&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;recordSeriesRebuild&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;seriesRebuilt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Production Checklist
&lt;/h2&gt;

&lt;p&gt;Before deploying to production, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;DST Handling&lt;/strong&gt;: Create a weekly meeting spanning DST transition (March/November). Verify times don't drift&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Exception Preservation&lt;/strong&gt;: Modify one instance's time, then change series subject. Verify the modified instance keeps its time&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Deletion Sync&lt;/strong&gt;: Delete one future instance in Outlook. Confirm CRM removes it. Delete entire series. Confirm all instances are removed&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Long-running Series&lt;/strong&gt;: Create a monthly meeting for 2 years. Verify only ~90 days are synced&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;All-day Events&lt;/strong&gt;: Create all-day recurring event. Verify it doesn't span two days due to timezone conversion&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Token Recovery&lt;/strong&gt;: Simulate delta token expiration. Verify system falls back to full sync&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Logging&lt;/strong&gt;: All sync operations log (seriesMasterId, instanceId, operation) for debugging&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Comparison: Outlook vs Google Calendar
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Microsoft Graph (Outlook)&lt;/th&gt;
&lt;th&gt;Google Calendar API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Recurring Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Master + Exception (occurrences are virtual)&lt;/td&gt;
&lt;td&gt;Event + Recurrence (can expand with singleEvents=true)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Delta Behavior&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Master change returns only Master&lt;/td&gt;
&lt;td&gt;Master change can return all affected instances&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ID Reference&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;seriesMasterId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;recurringEventId&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deletion&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Physical delete or status change&lt;/td&gt;
&lt;td&gt;Often &lt;code&gt;status: cancelled&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Google's &lt;code&gt;singleEvents=true&lt;/code&gt; parameter makes expansion easier, but the windowed approach remains necessary for performance.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never trust Delta Sync alone&lt;/strong&gt; for recurring meetings. Use delta as a signal, not as truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windowed expansion is essential&lt;/strong&gt;. Infinite series must be bounded to a constant number of instances.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iCalUId is half-truth&lt;/strong&gt;. Same for all occurrences—dangerous as a unique key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bootstrap state matters&lt;/strong&gt;. Explicit initialization scan prevents "cold start" data gaps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log everything&lt;/strong&gt;. When debugging sync issues, you'll need the history.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Data Model Reference
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZXJEaWFncmFtCiAgICBPVVRMT09LX0NBTEVOREFSX1VTRVIgfHwtLW97IE9VVExPT0tfQ0FMRU5EQVJfU0VSSUVTIDogdHJhY2tzCiAgICBPVVRMT09LX0NBTEVOREFSX1VTRVIgfHwtLW97IE9VVExPT0tfQ0FMRU5EQVJfRVZFTlQgOiBvd25zCiAgICBJTlRFUk5BTF9FVkVOVCB8fC0tb3sgT1VUTE9PS19DQUxFTkRBUl9FVkVOVCA6IGxpbmtlZC1ieQoKICAgIE9VVExPT0tfQ0FMRU5EQVJfVVNFUiB7CiAgICAgICAgc3RyaW5nIGlkIFBLCiAgICAgICAgc3RyaW5nIGRlbHRhVG9rZW4KICAgICAgICBzdHJpbmcgYm9vdHN0cmFwU3RhdHVzCiAgICB9CgogICAgT1VUTE9PS19DQUxFTkRBUl9TRVJJRVMgewogICAgICAgIHN0cmluZyBpZCBQSwogICAgICAgIHN0cmluZyBzZXJpZXNNYXN0ZXJFdmVudElkCiAgICAgICAgYm9vbGVhbiByZXN5bmNSZXF1ZXN0ZWQKICAgIH0KCiAgICBPVVRMT09LX0NBTEVOREFSX0VWRU5UIHsKICAgICAgICBzdHJpbmcgaWQgUEsKICAgICAgICBzdHJpbmcgZXZlbnRJZCAiSW1tdXRhYmxlIE91dGxvb2sgSUQiCiAgICAgICAgc3RyaW5nIGlDYWxVSWQgIlNoYXJlZCBieSBTZXJpZXMgLSBOT1QgdW5pcXVlISIKICAgICAgICBzdHJpbmcgZXZlbnRUeXBlICJvY2N1cnJlbmNlL21hc3Rlci9leGNlcHRpb24iCiAgICAgICAgc3RyaW5nIHNlcmllc01hc3RlckV2ZW50SWQKICAgICAgICBkYXRldGltZSB3aW5kb3dTdGFydAogICAgICAgIGRhdGV0aW1lIHdpbmRvd0VuZAogICAgfQoKICAgIElOVEVSTkFMX0VWRU5UIHsKICAgICAgICBzdHJpbmcgaWQgUEsKICAgICAgICBzdHJpbmcgc3ViamVjdAogICAgICAgIGRhdGV0aW1lIHN0YXJ0CiAgICAgICAgZGF0ZXRpbWUgZW5kCiAgICB9" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZXJEaWFncmFtCiAgICBPVVRMT09LX0NBTEVOREFSX1VTRVIgfHwtLW97IE9VVExPT0tfQ0FMRU5EQVJfU0VSSUVTIDogdHJhY2tzCiAgICBPVVRMT09LX0NBTEVOREFSX1VTRVIgfHwtLW97IE9VVExPT0tfQ0FMRU5EQVJfRVZFTlQgOiBvd25zCiAgICBJTlRFUk5BTF9FVkVOVCB8fC0tb3sgT1VUTE9PS19DQUxFTkRBUl9FVkVOVCA6IGxpbmtlZC1ieQoKICAgIE9VVExPT0tfQ0FMRU5EQVJfVVNFUiB7CiAgICAgICAgc3RyaW5nIGlkIFBLCiAgICAgICAgc3RyaW5nIGRlbHRhVG9rZW4KICAgICAgICBzdHJpbmcgYm9vdHN0cmFwU3RhdHVzCiAgICB9CgogICAgT1VUTE9PS19DQUxFTkRBUl9TRVJJRVMgewogICAgICAgIHN0cmluZyBpZCBQSwogICAgICAgIHN0cmluZyBzZXJpZXNNYXN0ZXJFdmVudElkCiAgICAgICAgYm9vbGVhbiByZXN5bmNSZXF1ZXN0ZWQKICAgIH0KCiAgICBPVVRMT09LX0NBTEVOREFSX0VWRU5UIHsKICAgICAgICBzdHJpbmcgaWQgUEsKICAgICAgICBzdHJpbmcgZXZlbnRJZCAiSW1tdXRhYmxlIE91dGxvb2sgSUQiCiAgICAgICAgc3RyaW5nIGlDYWxVSWQgIlNoYXJlZCBieSBTZXJpZXMgLSBOT1QgdW5pcXVlISIKICAgICAgICBzdHJpbmcgZXZlbnRUeXBlICJvY2N1cnJlbmNlL21hc3Rlci9leGNlcHRpb24iCiAgICAgICAgc3RyaW5nIHNlcmllc01hc3RlckV2ZW50SWQKICAgICAgICBkYXRldGltZSB3aW5kb3dTdGFydAogICAgICAgIGRhdGV0aW1lIHdpbmRvd0VuZAogICAgfQoKICAgIElOVEVSTkFMX0VWRU5UIHsKICAgICAgICBzdHJpbmcgaWQgUEsKICAgICAgICBzdHJpbmcgc3ViamVjdAogICAgICAgIGRhdGV0aW1lIHN0YXJ0CiAgICAgICAgZGF0ZXRpbWUgZW5kCiAgICB9" alt="diagram" width="1011" height="673"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/graph/delta-query-events" rel="noopener noreferrer"&gt;Microsoft Graph Delta Query Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/graph/delta-query-events" rel="noopener noreferrer"&gt;Get incremental changes to events in a calendar view&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/graph/best-practices-concept" rel="noopener noreferrer"&gt;Best practices for working with Microsoft Graph&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc5545" rel="noopener noreferrer"&gt;RFC 5545: iCalendar Specification&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Thanks for reading! You can find the original post on &lt;a href="https://www.yzhu.name/2026/01/10/espocrm/08-outlook-recurring-sync-en/" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you found this helpful, feel free to connect with me&lt;/p&gt;

</description>
      <category>espocrm</category>
      <category>outlook</category>
      <category>microsoftgraph</category>
      <category>calendar</category>
    </item>
  </channel>
</rss>
