<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Blog posts by Ritu Madan</title><link href="https://world.optimizely.com/blogs/ritu-madan/" /><updated>2026-10-14T01:06:16.0000000Z</updated><id>https://world.optimizely.com/blogs/ritu-madan/</id><generator uri="https://world.optimizely.com/blogs/ritu-madan/" version="2.0">Optimizely World</generator><entry><title>Opal Workflow Agents And The Graph Webhook Trap</title><link href="https://world.optimizely.com/blogs/ritu-madan/dates/2026/10/opal-workflow-agents-and-the-graph-webhook-trap/" /><id>https://world.optimizely.com/blogs/ritu-madan/dates/2026/10/opal-workflow-agents-and-the-graph-webhook-trap/</id><published>2026-10-14T01:06:16.0000000Z</published><updated>2026-10-14T01:06:16.0000000Z</updated><author><name>ritu.madan@valtech.com</name></author><category term="Blog post" /><summary type="html">&lt;h2&gt;What we did?&lt;/h2&gt;
&lt;p class="FirstParagraph"&gt;&lt;span style="mso-bookmark: what-we-were-building;"&gt;We built a compliance review workflow agent in Opal. The job was simple: when an editor submits a page for review in Optimizely CMS 13, the agent reads the content, checks it against defined compliance rules, and writes its findings back onto the item so a human reviewer sees them before approving.&lt;/span&gt;&lt;/p&gt;
&lt;p class="MsoBodyText"&gt;&lt;span style="mso-bookmark: what-we-were-building;"&gt;One editorial action in, one review out. That was the whole contract. A complete run costs 100&amp;ndash;130 credits and takes 2-4 minutes. This makes a single run fairly expensive on credits. Now picture that same run firing over and over, several at once, and the consumption climbs fast.&lt;/span&gt;&lt;/p&gt;
&lt;p class="MsoBodyText"&gt;The trigger was the only open question. &lt;span style="text-decoration: underline;"&gt;&lt;span style="color: rgb(35, 111, 161); text-decoration: underline;"&gt;&lt;a style="color: rgb(35, 111, 161); text-decoration: underline;" title="Opal workflow agents can fire a few ways" href="https://support.optimizely.com/hc/en-us/articles/39865892823693-Workflow-agent-triggers"&gt;Opal workflow agents can fire a few ways&lt;/a&gt;&lt;/span&gt;&lt;/span&gt; &amp;mdash; chat input, a schedule, email, or a webhook &amp;mdash; but for a run that kicks off automatically when something happens in the CMS, the webhook is the one that fits. So something in Optimizely CMS had to notice &amp;ldquo;this page entered review&amp;rdquo; and POST to the trigger URL. The obvious source was &lt;span style="text-decoration: underline;"&gt;&lt;span style="color: rgb(35, 111, 161); text-decoration: underline;"&gt;&lt;a style="color: rgb(35, 111, 161); text-decoration: underline;" title="Optimizely Graph" href="https://docs.developers.optimizely.com/platform-optimizely/docs/manage-webhooks"&gt;Optimizely Graph&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;. It&amp;rsquo;s pure configuration &amp;mdash; register a webhook through the API, no CMS code to deploy &amp;mdash; and it already models content changes, which is more or less what we were reacting to. So we started there: a Graph subscription pointed at the Opal trigger, filtered to the InReview status.&lt;/p&gt;
&lt;h2&gt;What we didn&amp;rsquo;t see coming?&lt;/h2&gt;
&lt;p class="FirstParagraph"&gt;&lt;span style="mso-bookmark: what-we-didnt-see-coming;"&gt;The first run worked exactly as intended. An editor submitted a page, the status went to InReview, Graph synced, the webhook fired, and the agent ran its compliance check.&lt;/span&gt;&lt;/p&gt;
&lt;p class="MsoBodyText"&gt;&lt;span style="mso-bookmark: what-we-didnt-see-coming;"&gt;Then the agent did the one thing it was built to do: it wrote its findings back onto the page.&lt;/span&gt;&lt;/p&gt;
&lt;p class="MsoBodyText"&gt;&lt;span style="mso-bookmark: what-we-didnt-see-coming;"&gt;That write-back was a save. The save synced to Graph. The content was still InReview &amp;mdash; writing findings doesn&amp;rsquo;t change an approval status &amp;mdash; so Graph delivered a webhook for an InReview document, which is exactly the event we&amp;rsquo;d subscribed to. The agent fired again. Checked again. Wrote back again. Synced again.&lt;/span&gt;&lt;/p&gt;
&lt;p class="MsoBodyText"&gt;&lt;span style="mso-bookmark: what-we-didnt-see-coming;"&gt;By the time we were looking at it, we had four pages of execution logs: the same agent, on the same page, running over and over, several instances going at once. We started cancelling runs manually to stop them burning through credits. The agent was triggering itself, and nothing in the loop was going to stop it.&lt;/span&gt;&lt;/p&gt;
&lt;p class="MsoBodyText"&gt;&lt;span style="mso-bookmark: what-we-didnt-see-coming;"&gt;Nothing was broken, which is what made it hard. Graph was doing exactly what Graph does.&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;Why it looped?&lt;/h2&gt;
&lt;p class="FirstParagraph"&gt;&lt;span style="mso-bookmark: why-it-looped;"&gt;Seeing the behaviour was easy. Finding the cause took a while. Two facts about Graph, neither of them a bug, combined into the loop:&lt;/span&gt;&lt;/p&gt;
&lt;p class="MsoBodyText"&gt;&lt;span style="mso-bookmark: why-it-looped;"&gt;&lt;a title="Graph syncs on save, not on intent." href="https://docs.developers.optimizely.com/platform-optimizely/docs/synchronize-content-events-to-optimizely-graph"&gt;&lt;strong&gt;&lt;span style="text-decoration: underline; color: rgb(35, 111, 161);"&gt;Graph syncs on save, not on intent.&lt;/span&gt;&lt;/strong&gt;&lt;/a&gt; It re-indexes whenever content is saved, published, moved, deleted, or expired. Our agent&amp;rsquo;s write-back is a save like any other, so every write re-syncs.&lt;/span&gt;&lt;/p&gt;
&lt;p class="MsoBodyText"&gt;&lt;span style="mso-bookmark: why-it-looped;"&gt;&lt;strong&gt;Status is part of a document&amp;rsquo;s identity.&lt;/strong&gt; Graph identifies an indexed document as &lt;/span&gt;&lt;span style="mso-bookmark: why-it-looped;"&gt;&lt;em&gt;&lt;span class="VerbatimChar" style="font-size: 10pt;"&gt;{contentGuid}_{version}_{locale}_{status}&lt;/span&gt;&lt;/em&gt;. Our write-back changed the content but left the status at InReview &amp;mdash; so every re-sync produced the &lt;em&gt;same&lt;/em&gt; InReview document, indistinguishable from the submit event we&amp;rsquo;d subscribed to. There was no status change to mark the work as handled, and no state anywhere saying &amp;ldquo;this one&amp;rsquo;s done.&amp;rdquo; So each sync looked like a fresh reason to run.&lt;/span&gt;&lt;/p&gt;
&lt;p class="MsoBodyText"&gt;&lt;span style="mso-bookmark: why-it-looped;"&gt;Underneath it, the mistake was conceptual: we were inferring an editorial decision from index activity. But the index was churning every time our own agent touched the page &amp;mdash; and the agent never stopped touching the page.&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;The fix: subscribe to the decision, not the side effect&lt;/h2&gt;
&lt;p class="FirstParagraph"&gt;&lt;span style="mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;"&gt;Optimizely CMS already raises an event for the exact moment we cared about. The &lt;span style="text-decoration: underline; color: rgb(35, 111, 161);"&gt;&lt;a style="color: rgb(35, 111, 161); text-decoration: underline;" title="approval engine" href="https://docs.developers.optimizely.com/content-management-system/v13.0.0-CMS/docs/content-approvals"&gt;approval engine&lt;/a&gt;&lt;/span&gt; exposes &lt;/span&gt;&lt;span style="mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;"&gt;&lt;em&gt;&lt;span class="VerbatimChar" style="font-size: 10pt;"&gt;IApprovalEngineEvents&lt;/span&gt;&lt;/em&gt;, and its &lt;/span&gt;&lt;span style="mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;"&gt;&lt;em&gt;&lt;span class="VerbatimChar" style="font-size: 10pt;"&gt;Started&lt;/span&gt;&lt;/em&gt; event fires once, when an approval instance begins &amp;mdash; i.e.&amp;nbsp;when a human submits for review. It&amp;rsquo;s raised by the approval lifecycle, not the persistence layer. Background saves, forced writes, and our own agent&amp;rsquo;s write-back don&amp;rsquo;t start approval instances, so they don&amp;rsquo;t raise it.&lt;/span&gt;&lt;/p&gt;
&lt;p class="MsoBodyText"&gt;&lt;span style="mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;"&gt;So instead of inferring the decision from Graph, we subscribed to it directly in the CMS solution and POSTed to the Opal trigger ourselves:&lt;/span&gt;&lt;/p&gt;
&lt;p class="SourceCode"&gt;&lt;span style="color: rgb(35, 111, 161);"&gt;&lt;code&gt;&lt;span style="mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;"&gt;&lt;span class="OperatorTok"&gt;[&lt;/span&gt;&lt;span class="NormalTok"&gt;InitializableModule&lt;/span&gt;&lt;span class="OperatorTok"&gt;]&lt;/span&gt;&lt;br /&gt;&lt;span class="OperatorTok"&gt;[&lt;/span&gt;&lt;span class="FunctionTok"&gt;ModuleDependency&lt;/span&gt;&lt;span class="OperatorTok"&gt;(&lt;/span&gt;&lt;span class="KeywordTok"&gt;typeof&lt;/span&gt;&lt;span class="OperatorTok"&gt;(&lt;/span&gt;&lt;span class="NormalTok"&gt;EPiServer&lt;/span&gt;&lt;span class="OperatorTok"&gt;.&lt;/span&gt;&lt;span class="FunctionTok"&gt;Web&lt;/span&gt;&lt;span class="OperatorTok"&gt;.&lt;/span&gt;&lt;span class="FunctionTok"&gt;InitializationModule&lt;/span&gt;&lt;span class="OperatorTok"&gt;))]&lt;/span&gt;&lt;br /&gt;&lt;span class="KeywordTok"&gt;public&lt;/span&gt; &lt;span class="KeywordTok"&gt;class&lt;/span&gt;&lt;span class="NormalTok"&gt; ComplianceReviewTriggerModule &lt;/span&gt;&lt;span class="OperatorTok"&gt;:&lt;/span&gt;&lt;span class="NormalTok"&gt; IInitializableModule&lt;/span&gt;&lt;br /&gt;&lt;span class="OperatorTok"&gt;{&lt;/span&gt;&lt;br /&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class="KeywordTok"&gt;private&lt;/span&gt;&lt;span class="NormalTok"&gt; IApprovalEngineEvents _approvalEvents&lt;/span&gt;&lt;span class="OperatorTok"&gt;;&lt;/span&gt;&lt;br /&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class="CommentTok"&gt;// IContentLoader, IHttpClientFactory, config resolved in Initialize()&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class="KeywordTok"&gt;public&lt;/span&gt; &lt;span class="DataTypeTok"&gt;void&lt;/span&gt; &lt;span class="FunctionTok"&gt;Initialize&lt;/span&gt;&lt;span class="OperatorTok"&gt;(&lt;/span&gt;&lt;span class="NormalTok"&gt;InitializationEngine context&lt;/span&gt;&lt;span class="OperatorTok"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class="OperatorTok"&gt;{&lt;/span&gt;&lt;br /&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;_approvalEvents &lt;/span&gt;&lt;span class="OperatorTok"&gt;=&lt;/span&gt;&lt;span class="NormalTok"&gt; context&lt;/span&gt;&lt;span class="OperatorTok"&gt;.&lt;/span&gt;&lt;span class="FunctionTok"&gt;Services&lt;/span&gt;&lt;span class="OperatorTok"&gt;.&lt;/span&gt;&lt;span class="FunctionTok"&gt;GetRequiredService&lt;/span&gt;&lt;span class="OperatorTok"&gt;&amp;lt;&lt;/span&gt;&lt;span class="NormalTok"&gt;IApprovalEngineEvents&lt;/span&gt;&lt;span class="OperatorTok"&gt;&amp;gt;();&lt;/span&gt;&lt;br /&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;_approvalEvents&lt;/span&gt;&lt;span class="OperatorTok"&gt;.&lt;/span&gt;&lt;span class="FunctionTok"&gt;Started&lt;/span&gt; &lt;span class="OperatorTok"&gt;+=&lt;/span&gt;&lt;span class="NormalTok"&gt; OnApprovalStarted&lt;/span&gt;&lt;span class="OperatorTok"&gt;;&lt;/span&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class="CommentTok"&gt;// raised once, on submit&lt;/span&gt;&lt;br /&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class="OperatorTok"&gt;}&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class="KeywordTok"&gt;private&lt;/span&gt; &lt;span class="DataTypeTok"&gt;void&lt;/span&gt; &lt;span class="FunctionTok"&gt;OnApprovalStarted&lt;/span&gt;&lt;span class="OperatorTok"&gt;(&lt;/span&gt;&lt;span class="DataTypeTok"&gt;object&lt;/span&gt;&lt;span class="NormalTok"&gt; sender&lt;/span&gt;&lt;span class="OperatorTok"&gt;,&lt;/span&gt;&lt;span class="NormalTok"&gt; ApprovalEventArgs e&lt;/span&gt;&lt;span class="OperatorTok"&gt;)&lt;/span&gt;&lt;br /&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class="OperatorTok"&gt;{&lt;/span&gt;&lt;br /&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class="KeywordTok"&gt;if&lt;/span&gt; &lt;span class="OperatorTok"&gt;(!&lt;/span&gt;&lt;span class="NormalTok"&gt;e&lt;/span&gt;&lt;span class="OperatorTok"&gt;.&lt;/span&gt;&lt;span class="FunctionTok"&gt;IsContentApproval&lt;/span&gt;&lt;span class="OperatorTok"&gt;())&lt;/span&gt; &lt;span class="KeywordTok"&gt;return&lt;/span&gt;&lt;span class="OperatorTok"&gt;;&lt;/span&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class="CommentTok"&gt;// approvals are generic&lt;/span&gt;&lt;br /&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class="DataTypeTok"&gt;var&lt;/span&gt;&lt;span class="NormalTok"&gt; contentLink &lt;/span&gt;&lt;span class="OperatorTok"&gt;=&lt;/span&gt;&lt;span class="NormalTok"&gt; e&lt;/span&gt;&lt;span class="OperatorTok"&gt;.&lt;/span&gt;&lt;span class="FunctionTok"&gt;GetContentLink&lt;/span&gt;&lt;span class="OperatorTok"&gt;();&lt;/span&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class="CommentTok"&gt;// extension method&lt;/span&gt;&lt;br /&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class="KeywordTok"&gt;if&lt;/span&gt; &lt;span class="OperatorTok"&gt;(&lt;/span&gt;&lt;span class="NormalTok"&gt;ContentReference&lt;/span&gt;&lt;span class="OperatorTok"&gt;.&lt;/span&gt;&lt;span class="FunctionTok"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="OperatorTok"&gt;(&lt;/span&gt;&lt;span class="NormalTok"&gt;contentLink&lt;/span&gt;&lt;span class="OperatorTok"&gt;))&lt;/span&gt; &lt;span class="KeywordTok"&gt;return&lt;/span&gt;&lt;span class="OperatorTok"&gt;;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class="KeywordTok"&gt;if&lt;/span&gt; &lt;span class="OperatorTok"&gt;(!&lt;/span&gt;&lt;span class="NormalTok"&gt;_contentLoader&lt;/span&gt;&lt;span class="OperatorTok"&gt;.&lt;/span&gt;&lt;span class="FunctionTok"&gt;TryGet&lt;/span&gt;&lt;span class="OperatorTok"&gt;&amp;lt;&lt;/span&gt;&lt;span class="NormalTok"&gt;MyCustomPage&lt;/span&gt;&lt;span class="OperatorTok"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="NormalTok"&gt;contentLink&lt;/span&gt;&lt;span class="OperatorTok"&gt;,&lt;/span&gt; &lt;span class="KeywordTok"&gt;out&lt;/span&gt; &lt;span class="DataTypeTok"&gt;var&lt;/span&gt;&lt;span class="NormalTok"&gt; page&lt;/span&gt;&lt;span class="OperatorTok"&gt;))&lt;/span&gt;&lt;br /&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class="KeywordTok"&gt;return&lt;/span&gt;&lt;span class="OperatorTok"&gt;;&lt;/span&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class="CommentTok"&gt;// scope to our content type&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class="CommentTok"&gt;// Mirror Graph's docId shape so the workflow is source-agnostic.&lt;/span&gt;&lt;br /&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class="DataTypeTok"&gt;var&lt;/span&gt;&lt;span class="NormalTok"&gt; docId &lt;/span&gt;&lt;span class="OperatorTok"&gt;=&lt;/span&gt;&lt;span class="NormalTok"&gt; $&lt;/span&gt;&lt;span class="StringTok"&gt;"{page.ContentGuid}_{contentLink.WorkID}_{page.Language.Name}_InReview"&lt;/span&gt;&lt;span class="OperatorTok"&gt;;&lt;/span&gt;&lt;br /&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;_ &lt;/span&gt;&lt;span class="OperatorTok"&gt;=&lt;/span&gt;&lt;span class="NormalTok"&gt; Task&lt;/span&gt;&lt;span class="OperatorTok"&gt;.&lt;/span&gt;&lt;span class="FunctionTok"&gt;Run&lt;/span&gt;&lt;span class="OperatorTok"&gt;(()&lt;/span&gt; &lt;span class="OperatorTok"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="FunctionTok"&gt;TriggerOpalAsync&lt;/span&gt;&lt;span class="OperatorTok"&gt;(&lt;/span&gt;&lt;span class="NormalTok"&gt;docId&lt;/span&gt;&lt;span class="OperatorTok"&gt;));&lt;/span&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class="CommentTok"&gt;// fire and forget, with a timeout&lt;/span&gt;&lt;br /&gt;&lt;span class="NormalTok"&gt;&lt;span style="mso-spacerun: yes;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class="OperatorTok"&gt;}&lt;/span&gt;&lt;br /&gt;&lt;span class="OperatorTok"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class="FirstParagraph"&gt;&lt;span style="mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;"&gt;A few things that mattered in practice:&lt;/span&gt;&lt;/p&gt;
&lt;p class="Compact" style="margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;"&gt;&lt;span style="mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;"&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style="mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;"&gt;&lt;span style="mso-list: Ignore;"&gt;&amp;bull;&lt;span style="font: 7.0pt 'Times New Roman';"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;em&gt;&lt;span class="VerbatimChar" style="font-size: 10pt;"&gt;&lt;strong&gt;ApprovalEventArgs&lt;/strong&gt;&lt;/span&gt;&lt;/em&gt;&lt;strong&gt; has no &lt;/strong&gt;&lt;/span&gt;&lt;span style="mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;"&gt;&lt;em&gt;&lt;span class="VerbatimChar" style="font-size: 10pt;"&gt;&lt;strong&gt;ContentLink&lt;/strong&gt;&lt;/span&gt;&lt;/em&gt;&lt;strong&gt;.&lt;/strong&gt; Approvals are a general mechanism; content approvals are one implementation. You reach the item through the &lt;/span&gt;&lt;span style="mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;"&gt;&lt;em&gt;&lt;span class="VerbatimChar" style="font-size: 10pt;"&gt;IsContentApproval()&lt;/span&gt;&lt;/em&gt; and &lt;/span&gt;&lt;span style="mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;"&gt;&lt;em&gt;&lt;span class="VerbatimChar" style="font-size: 10pt;"&gt;GetContentLink()&lt;/span&gt;&lt;/em&gt; extension methods in &lt;/span&gt;&lt;span style="mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;"&gt;&lt;em&gt;&lt;span class="VerbatimChar" style="font-size: 10pt;"&gt;EPiServer.Approvals.ContentApprovals&lt;/span&gt;&lt;/em&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p class="Compact" style="margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;"&gt;&lt;span style="mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;"&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style="mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;"&gt;&lt;span style="mso-list: Ignore;"&gt;&amp;bull;&lt;span style="font: 7.0pt 'Times New Roman';"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;strong&gt;The Opal trigger wants auth.&lt;/strong&gt; The webhook trigger takes an Auth Key and sends it as a header (e.g.&amp;nbsp;&lt;/span&gt;&lt;span style="mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;"&gt;&lt;em&gt;&lt;span class="VerbatimChar" style="font-size: 10pt;"&gt;Authorization: Bearer {token}&lt;/span&gt;&lt;/em&gt;). Our POST sets that header from config; a request that doesn&amp;rsquo;t match is rejected.&lt;/span&gt;&lt;/p&gt;
&lt;p class="Compact" style="margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;"&gt;&lt;span style="mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;"&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style="mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;"&gt;&lt;span style="mso-list: Ignore;"&gt;&amp;bull;&lt;span style="font: 7.0pt 'Times New Roman';"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;strong&gt;We mirrored Graph&amp;rsquo;s &lt;/strong&gt;&lt;/span&gt;&lt;span style="mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;"&gt;&lt;em&gt;&lt;span class="VerbatimChar" style="font-size: 10pt;"&gt;&lt;strong&gt;docId&lt;/strong&gt;&lt;/span&gt;&lt;/em&gt;&lt;strong&gt;&lt;em&gt;&lt;span style="font-size: 10pt;"&gt; &lt;/span&gt;&lt;/em&gt;shape&lt;/strong&gt; in the payload, so the downstream workflow didn&amp;rsquo;t need to change. If we ever switch sources again, the agent doesn&amp;rsquo;t care.&lt;/span&gt;&lt;/p&gt;
&lt;p class="Compact" style="margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;"&gt;&lt;span style="mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;"&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style="mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;"&gt;&lt;span style="mso-list: Ignore;"&gt;&amp;bull;&lt;span style="font: 7.0pt 'Times New Roman';"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;strong&gt;Fire-and-forget has a cost.&lt;/strong&gt; Blocking the editor&amp;rsquo;s submit on an outbound call is a bad idea, so we don&amp;rsquo;t. The trade is that if Opal is unreachable the moment &lt;/span&gt;&lt;span style="mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;"&gt;&lt;em&gt;&lt;span style="font-size: 10pt;"&gt;&lt;span class="VerbatimChar"&gt;Started&lt;/span&gt; &lt;/span&gt;&lt;/em&gt;fires, that trigger is lost &amp;mdash; at-most-once. For a review flow that&amp;rsquo;s fine, because an editor can resubmit. If it weren&amp;rsquo;t, we&amp;rsquo;d add retries or a durable queue.&lt;/span&gt;&lt;/p&gt;
&lt;p class="FirstParagraph"&gt;&lt;span style="mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;"&gt;Result: one submit, one execution. The write-back still syncs to Graph, but we&amp;rsquo;re no longer listening to Graph for this trigger, and writing content doesn&amp;rsquo;t start an approval &amp;mdash; so there&amp;rsquo;s nothing left to re-fire. The loop was gone, not because we out-guarded it, but because we stopped listening to the wrong layer.&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;So when is Graph the right source?&lt;/h2&gt;
&lt;p class="FirstParagraph"&gt;&lt;span style="mso-bookmark: so-when-is-graph-the-right-source;"&gt;The lesson isn&amp;rsquo;t &amp;ldquo;Graph is wrong.&amp;rdquo; Graph is excellent &amp;mdash; for the thing it actually reports, which is &lt;em&gt;content as the published world sees it&lt;/em&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p class="MsoBodyText"&gt;&lt;span style="mso-bookmark: so-when-is-graph-the-right-source;"&gt;&lt;strong&gt;Reach for Graph when:&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class="Compact" style="margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;"&gt;&lt;span style="mso-bookmark: so-when-is-graph-the-right-source;"&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style="mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;"&gt;&lt;span style="mso-list: Ignore;"&gt;&amp;bull;&lt;span style="font: 7.0pt 'Times New Roman';"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;You&amp;rsquo;re reacting to &lt;strong&gt;content state&lt;/strong&gt;, not editorial intent &amp;mdash; a page got published, purge the CDN; rebuild the static site. This is Graph&amp;rsquo;s canonical use case, and duplicate deliveries are harmless because the second purge is a no-op.&lt;/span&gt;&lt;/p&gt;
&lt;p class="Compact" style="margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;"&gt;&lt;span style="mso-bookmark: so-when-is-graph-the-right-source;"&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style="mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;"&gt;&lt;span style="mso-list: Ignore;"&gt;&amp;bull;&lt;span style="font: 7.0pt 'Times New Roman';"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;The consumer is &lt;strong&gt;idempotent&lt;/strong&gt; &amp;mdash; ISR cache invalidation, deploy hooks, downstream sync. Firing twice costs nothing.&lt;/span&gt;&lt;/p&gt;
&lt;p class="Compact" style="margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;"&gt;&lt;span style="mso-bookmark: so-when-is-graph-the-right-source;"&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style="mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;"&gt;&lt;span style="mso-list: Ignore;"&gt;&amp;bull;&lt;span style="font: 7.0pt 'Times New Roman';"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;You &lt;strong&gt;don&amp;rsquo;t own the CMS codebase&lt;/strong&gt;, or you&amp;rsquo;re on &lt;strong&gt;SaaS CMS&lt;/strong&gt;. Graph is configuration, no deploy required.&lt;/span&gt;&lt;/p&gt;
&lt;p class="Compact" style="margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;"&gt;&lt;span style="mso-bookmark: so-when-is-graph-the-right-source;"&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style="mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;"&gt;&lt;span style="mso-list: Ignore;"&gt;&amp;bull;&lt;span style="font: 7.0pt 'Times New Roman';"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;The consuming system is &lt;strong&gt;decoupled from CMS&lt;/strong&gt; and you don&amp;rsquo;t want to couple to its internals.&lt;/span&gt;&lt;/p&gt;
&lt;p class="FirstParagraph"&gt;&lt;span style="mso-bookmark: so-when-is-graph-the-right-source;"&gt;&lt;strong&gt;Reach for a CMS server-side event (the code path) when:&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class="Compact" style="margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;"&gt;&lt;span style="mso-bookmark: so-when-is-graph-the-right-source;"&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style="mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;"&gt;&lt;span style="mso-list: Ignore;"&gt;&amp;bull;&lt;span style="font: 7.0pt 'Times New Roman';"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;The trigger is an &lt;strong&gt;editorial decision&lt;/strong&gt; &amp;mdash; submitted, approved, rejected. That lives in the approval engine, and Graph can&amp;rsquo;t model it.&lt;/span&gt;&lt;/p&gt;
&lt;p class="Compact" style="margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;"&gt;&lt;span style="mso-bookmark: so-when-is-graph-the-right-source;"&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style="mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;"&gt;&lt;span style="mso-list: Ignore;"&gt;&amp;bull;&lt;span style="font: 7.0pt 'Times New Roman';"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;Your workflow &lt;strong&gt;writes back to the item that triggered it&lt;/strong&gt;. This is the one that bit us; any index-based trigger risks the same loop.&lt;/span&gt;&lt;/p&gt;
&lt;p class="Compact" style="margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;"&gt;&lt;span style="mso-bookmark: so-when-is-graph-the-right-source;"&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style="mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;"&gt;&lt;span style="mso-list: Ignore;"&gt;&amp;bull;&lt;span style="font: 7.0pt 'Times New Roman';"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;strong&gt;Each execution is expensive.&lt;/strong&gt; When a run costs real credits and minutes, precise firing matters far more than easy setup.&lt;/span&gt;&lt;/p&gt;
&lt;p class="Compact" style="margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;"&gt;&lt;span style="mso-bookmark: so-when-is-graph-the-right-source;"&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style="mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;"&gt;&lt;span style="mso-list: Ignore;"&gt;&amp;bull;&lt;span style="font: 7.0pt 'Times New Roman';"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;You need &lt;strong&gt;business rules before firing&lt;/strong&gt; &amp;mdash; content type, language, site. Strongly typed C# beats filtering on payload shape.&lt;/span&gt;&lt;/p&gt;
&lt;p class="FirstParagraph"&gt;&lt;span style="mso-bookmark: so-when-is-graph-the-right-source;"&gt;For us it was a compliance review agent that wrote back to the page it reviewed, at 100+ credits a run. Three of those four boxes. Graph was the wrong tool for this job. We kept Graph for delivery-side concerns and moved the editorial trigger to the approval event. They answer different questions.&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;The takeaway&lt;/h2&gt;
&lt;p class="FirstParagraph"&gt;Both sources are honest. Graph faithfully reports that an indexed document changed. The approval engine faithfully reports that a person made a decision. Neither is broken.&lt;/p&gt;
&lt;p class="MsoBodyText"&gt;The real question is which of those you care about &amp;mdash; &lt;em&gt;a document changing&lt;/em&gt; or &lt;em&gt;a person deciding&lt;/em&gt; &amp;mdash; and subscribing at that layer instead of inferring it from the one below. When a workflow fires more often than the business event behind it, the instinct is to bolt on a guard. Sometimes you need one. But first ask what your event source is actually reporting.&lt;/p&gt;
&lt;p class="MsoBodyText"&gt;Ours was reporting every save. Our agent just happened to be the thing doing the saving.&lt;/p&gt;</summary></entry><entry><title>Thomas in the AI Discovery Era</title><link href="https://world.optimizely.com/blogs/ritu-madan/dates/2026/10/thomas-in-the-ai-discovery-era/" /><id>https://world.optimizely.com/blogs/ritu-madan/dates/2026/10/thomas-in-the-ai-discovery-era/</id><published>2026-10-06T02:48:33.0000000Z</published><updated>2026-10-06T02:48:33.0000000Z</updated><author><name>ritu.madan@valtech.com</name></author><category term="Blog post" /><summary type="html">&lt;p&gt;&lt;span class="ckymss ckya1h ckyjz7 ckymt4"&gt;Its been a while, but hope you haven't forgotten Thomas, our most loyal visitor, the one who suddenly stops showing up.&lt;br /&gt;Not because he lost interest. Because he asked an AI tool the question he used to Google, got his answer in thirty seconds, and never came to the site at all.&lt;br /&gt;That's the shift most brands haven't clocked yet. The front door moved. Search isn't the only way in anymore, and a lot of the traffic you're losing to AI never shows up as a loss in your analytics. It just quietly isn't there.&lt;br /&gt;But here's the part worth sitting with: AI isn't only taking visits. It's also sending them, to the brands it decides are worth citing. Which means the game isn't over. It just changed.&lt;br /&gt;I pulled apart what actually earns those citations, and the one tactic everyone's doubling down on that the 2026 data says barely moves the needle. Plus what it means for the visitor who lands on your site already halfway through a thought.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class="ckymss ckya1h ckyjz7 ckymt4"&gt;Read the full article &lt;a href="https://www.linkedin.com/pulse/thomass-new-entry-point-ritu-madan-t1pze/"&gt;here&lt;/a&gt;.&lt;/span&gt;&lt;/p&gt;</summary></entry><entry><title>Finding Thomas Part 5 - The Closed Loop</title><link href="https://world.optimizely.com/blogs/ritu-madan/dates/2026/7/finding-thomas-part-5---the-closed-loop/" /><id>https://world.optimizely.com/blogs/ritu-madan/dates/2026/7/finding-thomas-part-5---the-closed-loop/</id><published>2026-07-20T14:34:06.0000000Z</published><updated>2026-07-20T14:34:06.0000000Z</updated><author><name>ritu.madan@valtech.com</name></author><category term="Blog post" /><summary type="html">&lt;p class="font-claude-response-body break-words whitespace-normal"&gt;Five weeks. Five layers. One Thomas.&lt;/p&gt;
&lt;p class="font-claude-response-body break-words whitespace-normal"&gt;If you've followed this series from the start &amp;mdash; thank you. If you're just landing here, the short version: Thomas is the returning visitor who reads everything, opens every email, converts on nothing, and one day quietly stops coming back. No warning. No signal. Just gone.&lt;/p&gt;
&lt;p class="font-claude-response-body break-words whitespace-normal"&gt;Over the last four posts we traced exactly how that happens &amp;mdash; and how to stop it. The &lt;span style="color: rgb(35, 111, 161);"&gt;&lt;strong&gt;&lt;a style="color: rgb(35, 111, 161);" href="https://www.linkedin.com/pulse/finding-thomas-part-1-observation-post-ritu-madan-8kkwe"&gt;CMS&lt;/a&gt;&lt;/strong&gt;&lt;/span&gt; watching Thomas's behavior as it unfolds. &lt;strong&gt;&lt;span style="color: rgb(35, 111, 161);"&gt;&lt;a style="color: rgb(35, 111, 161);" href="https://www.linkedin.com/pulse/finding-thomas-part-2-recognition-engine-ritu-madan-wq2ef"&gt;ODP&lt;/a&gt;&lt;/span&gt;&lt;/strong&gt; turning that behavior into a persistent, scorable profile. &lt;strong&gt;&lt;span style="color: rgb(35, 111, 161);"&gt;&lt;a style="color: rgb(35, 111, 161);" href="https://www.linkedin.com/pulse/finding-thomas-part-3-moment-recognition-ritu-madan-5hvwe"&gt;Personalization&lt;/a&gt;&lt;/span&gt;&lt;/strong&gt; making him feel seen for the first time. &lt;strong&gt;&lt;span style="color: rgb(35, 111, 161);"&gt;&lt;a style="color: rgb(35, 111, 161);" href="https://www.linkedin.com/pulse/finding-thomas-part-4-intelligence-layer-ritu-madan-tgvgc"&gt;Opal&lt;/a&gt;&lt;/span&gt;&lt;/strong&gt; scaling that recognition to every Thomas in your audience without a human bottleneck.&lt;/p&gt;
&lt;p class="font-claude-response-body break-words whitespace-normal"&gt;Part 5 is the final part where all four connect.&lt;/p&gt;
&lt;p class="font-claude-response-body break-words whitespace-normal"&gt;Not as adjacent tools. Not as a martech stack. As a single, continuous loop &amp;mdash; one that finds Thomas while he's still Drifting, responds before he reaches At Risk, and gets smarter with every intervention it makes.&lt;/p&gt;
&lt;p class="font-claude-response-body break-words whitespace-normal"&gt;It also ends where the whole series began. With Thomas. And with the question worth asking honestly of any Optimizely implementation that has been live for more than a year:&lt;/p&gt;
&lt;p class="font-claude-response-body break-words whitespace-normal"&gt;Is your platform pointed at finding Thomas &amp;mdash; or is it still primarily pointed at bringing new visitors through the door?&lt;/p&gt;
&lt;p class="font-claude-response-body break-words whitespace-normal"&gt;Read the full story &lt;strong&gt;&lt;span style="color: rgb(35, 111, 161);"&gt;&lt;a style="color: rgb(35, 111, 161);" href="https://www.linkedin.com/pulse/finding-thomas-part-5-closed-loop-ritu-madan-pmkoc/"&gt;here&lt;/a&gt;&lt;/span&gt;&lt;/strong&gt;.&lt;/p&gt;</summary></entry><entry><title>Finding Thomas Part 4 - The Intelligence Layer</title><link href="https://world.optimizely.com/blogs/ritu-madan/dates/2026/7/finding-thomas-part-4---the-intelligence-layer/" /><id>https://world.optimizely.com/blogs/ritu-madan/dates/2026/7/finding-thomas-part-4---the-intelligence-layer/</id><published>2026-07-14T19:01:56.0000000Z</published><updated>2026-07-14T19:01:56.0000000Z</updated><author><name>ritu.madan@valtech.com</name></author><category term="Blog post" /><summary type="html">&lt;p class="font-claude-response-body break-words whitespace-normal"&gt;I've been finding Thomas for a couple weeks now. Bear with me &amp;mdash; we're almost at the full picture.&lt;/p&gt;
&lt;p class="font-claude-response-body break-words whitespace-normal"&gt;Quick catch-up : Thomas is the returning visitor who reads everything, opens every email, converts on nothing &amp;mdash; and one day quietly stops coming back. No warning. No signal. Just gone.&lt;/p&gt;
&lt;p class="font-claude-response-body break-words whitespace-normal"&gt;In Parts 1 through 3 we built the foundation &amp;mdash; the &lt;a href="https://www.linkedin.com/pulse/finding-thomas-part-1-observation-post-ritu-madan-8kkwe/"&gt;&lt;strong&gt;CMS&lt;/strong&gt;&lt;/a&gt; watching him, &lt;a href="https://www.linkedin.com/pulse/finding-thomas-part-2-recognition-engine-ritu-madan-wq2ef/"&gt;&lt;strong&gt;ODP&lt;/strong&gt;&lt;/a&gt; profiling him, &lt;a href="https://www.linkedin.com/pulse/finding-thomas-part-3-moment-recognition-ritu-madan-5hvwe/"&gt;&lt;strong&gt;Personalization&lt;/strong&gt;&lt;/a&gt; making him feel seen for the first time.&lt;/p&gt;
&lt;p class="font-claude-response-body break-words whitespace-normal"&gt;Part 4 is where it gets interesting.&lt;/p&gt;
&lt;p class="font-claude-response-body break-words whitespace-normal"&gt;Because everything we've built so far has a ceiling. Someone still has to hypothesize the Thomas profile, build the segment manually, monitor the scores, and make judgment calls. At the scale of a real content ecosystem, that human dependency is the bottleneck.&lt;/p&gt;
&lt;p class="font-claude-response-body break-words whitespace-normal"&gt;Opal removes the ceiling.&lt;/p&gt;
&lt;p class="font-claude-response-body break-words whitespace-normal"&gt;It doesn't guess what Thomas looks like. It learns what Thomas looks like &amp;mdash; from the behavior of every visitor who eventually stopped coming back. Then it scales that recognition to thousands of Thomas-profile users without manual intervention.&lt;/p&gt;
&lt;p class="font-claude-response-body break-words whitespace-normal"&gt;There's also a productive paradox at the heart of this one: the answer to AI displacing your site traffic is AI deployed inside your own ecosystem. Part 4 makes that case. Read it &lt;span style="color: rgb(35, 111, 161);"&gt;&lt;strong&gt;&lt;a style="color: rgb(35, 111, 161);" href="https://www.linkedin.com/pulse/finding-thomas-part-4-intelligence-layer-ritu-madan-tgvgc"&gt;here&lt;/a&gt;&lt;/strong&gt;&lt;/span&gt;.&lt;/p&gt;</summary></entry><entry><title>Finding Thomas Part 3 - The Moment of Recognition</title><link href="https://world.optimizely.com/blogs/ritu-madan/dates/2026/6/finding-thomas-part-3---the-moment-of-recognition/" /><id>https://world.optimizely.com/blogs/ritu-madan/dates/2026/6/finding-thomas-part-3---the-moment-of-recognition/</id><published>2026-06-26T17:08:48.0000000Z</published><updated>2026-06-26T17:08:48.0000000Z</updated><author><name>ritu.madan@valtech.com</name></author><category term="Blog post" /><summary type="html">&lt;h3 class="ember-view reader-text-block__heading-3"&gt;Remember Thomas?&lt;/h3&gt;
&lt;p class="ember-view reader-text-block__paragraph"&gt;&lt;em&gt;In digital landscape, Thomas is the returning visitor who reads everything, opens every email, converts on nothing. In standard reporting, &lt;strong&gt;he is invisible&lt;/strong&gt; &amp;mdash; absorbed into aggregate metrics that look healthy, precisely because he keeps showing up.&lt;/em&gt;&lt;/p&gt;
&lt;p class="ember-view reader-text-block__paragraph"&gt;In &lt;span style="color: rgb(35, 111, 161);"&gt;&lt;em&gt;&lt;strong&gt;&lt;a class="uPrscYCJahCprYhlPAURdHcDRCZBqqmbEI " style="color: rgb(35, 111, 161);" href="https://www.linkedin.com/pulse/finding-thomas-part-1-observation-post-ritu-madan-8kkwe"&gt;Part 1&lt;/a&gt;&lt;/strong&gt;&lt;/em&gt;&lt;/span&gt;, we talked about the &lt;strong&gt;CMS&lt;/strong&gt; - how it watches Thomas's behavior, page by page, scroll by scroll, without anyone assembling that data into a story.&lt;/p&gt;
&lt;p class="ember-view reader-text-block__paragraph"&gt;In &lt;span style="color: rgb(35, 111, 161);"&gt;&lt;em&gt;&lt;strong&gt;&lt;a class="uPrscYCJahCprYhlPAURdHcDRCZBqqmbEI " style="color: rgb(35, 111, 161);" href="https://www.linkedin.com/pulse/finding-thomas-part-2-recognition-engine-ritu-madan-wq2ef/"&gt;Part 2&lt;/a&gt;&lt;/strong&gt;&lt;/em&gt;&lt;/span&gt;, we talked about &lt;strong&gt;ODP&lt;/strong&gt; - how it takes that behavioral data and turns it into a real, scorable profile, complete with an Engagement Trajectory that tells you Thomas is Drifting before he's gone.&lt;/p&gt;
&lt;p class="ember-view reader-text-block__paragraph"&gt;Here's where we left off - the data has recognized Thomas but the experience hasn't!&lt;/p&gt;
&lt;h3 class="ember-view reader-text-block__heading-3"&gt;In Part 3,&amp;nbsp; we recognize Thomas and curate his experience of your site.&lt;/h3&gt;
&lt;p class="ember-view reader-text-block__paragraph"&gt;This is where &lt;strong&gt;Optimizely Personalization&lt;/strong&gt; comes into play. It translates Thomas's behavioural data profile from ODP into an experience that finally makes him feel seen without naing him or using surveillance language.&lt;/p&gt;
&lt;p class="ember-view reader-text-block__paragraph"&gt;Read the full story&amp;nbsp;&lt;em&gt;&lt;strong&gt;&lt;a href="https://www.linkedin.com/pulse/finding-thomas-part-3-moment-recognition-ritu-madan-5hvwe/"&gt;&lt;span style="color: rgb(35, 111, 161);"&gt;here&lt;/span&gt;&lt;/a&gt;&lt;/strong&gt;&lt;/em&gt; on how we remove the friction points and recalibrate the experience to appreciate Thomas's loyalty.&lt;/p&gt;</summary></entry><entry><title>Finding Thomas Part 2 - The Recognition Engine</title><link href="https://world.optimizely.com/blogs/ritu-madan/dates/2026/6/finding-thomas-part-2---the-recognition-engine/" /><id>https://world.optimizely.com/blogs/ritu-madan/dates/2026/6/finding-thomas-part-2---the-recognition-engine/</id><published>2026-06-01T16:59:04.0000000Z</published><updated>2026-06-01T16:59:04.0000000Z</updated><author><name>ritu.madan@valtech.com</name></author><category term="Blog post" /><summary type="html">&lt;h3 class="ember-view reader-text-block__heading-3"&gt;Remember Thomas?&lt;/h3&gt;
&lt;p class="ember-view reader-text-block__paragraph"&gt;&lt;em&gt;In digital landscape, Thomas is the returning visitor who reads everything, opens every email, converts on nothing. In standard reporting, &lt;strong&gt;he is invisible&lt;/strong&gt; &amp;mdash; absorbed into aggregate metrics that look healthy, precisely because he keeps showing up.&lt;/em&gt;&lt;/p&gt;
&lt;p class="ember-view reader-text-block__paragraph"&gt;In &lt;em&gt;&lt;strong&gt;&lt;a class="txPgeVsVxoqXupMimruaYRfcYiAbVbmTSCT " href="https://www.linkedin.com/pulse/finding-thomas-part-1-observation-post-ritu-madan-8kkwe"&gt;&lt;span style="color: rgb(35, 111, 161);"&gt;Part 1&lt;/span&gt;&lt;/a&gt;&lt;/strong&gt;&lt;/em&gt;, we talked about the &lt;strong&gt;CMS&lt;/strong&gt; - how Optimizely CMS is not just a publishing platform but an observation post. Every visit Thomas makes generates behavioral data - which content he reads, how deeply he scrolls, which paths he returns through, how his visit frequency is trending over time. The data is there. The problem is that most implementations are not built to read it as a story. The narrative exists. Nobody is assembling it.&lt;/p&gt;
&lt;h3 class="ember-view reader-text-block__heading-3"&gt;In Part 2, Thomas Becomes Visible&lt;/h3&gt;
&lt;p class="ember-view reader-text-block__paragraph"&gt;This is where &lt;strong&gt;ODP&lt;/strong&gt; takes the raw behavioral events from the CMS, stitches them together across sessions and channels, and builds a persistent, unified profile that can be segmented, scored, and acted upon. This is not a data warehouse exercise. &lt;strong&gt;It is a recognition exercise.&lt;/strong&gt;&lt;/p&gt;
&lt;p class="ember-view reader-text-block__paragraph"&gt;Read the full story &lt;em&gt;&lt;strong&gt;&lt;a href="https://www.linkedin.com/pulse/finding-thomas-part-2-recognition-engine-ritu-madan-wq2ef"&gt;&lt;span style="color: rgb(35, 111, 161);"&gt;here&lt;/span&gt;&lt;/a&gt;&lt;/strong&gt;&lt;/em&gt; on how we put together an engagement trajectory for Thomas in ODP, that then helps your digital estate recognize him.&lt;/p&gt;</summary></entry><entry><title>Finding Thomas Part 1 - The Observation Post</title><link href="https://world.optimizely.com/blogs/ritu-madan/dates/2026/5/finding-thomas-part-1---the-observation-post/" /><id>https://world.optimizely.com/blogs/ritu-madan/dates/2026/5/finding-thomas-part-1---the-observation-post/</id><published>2026-05-28T14:40:53.0000000Z</published><updated>2026-05-28T14:40:53.0000000Z</updated><author><name>ritu.madan@valtech.com</name></author><category term="Blog post" /><summary type="html">&lt;div style="mso-element: para-border-div; border: none; border-bottom: solid #1A56DB 1.0pt; mso-border-bottom-alt: solid #1A56DB .5pt; padding: 0in 0in 6.0pt 0in;"&gt;
&lt;h2 style="border: none; mso-border-bottom-alt: solid #1A56DB .5pt; padding: 0in; mso-padding-alt: 0in 0in 6.0pt 0in;"&gt;Meet Thomas&lt;/h2&gt;
&lt;/div&gt;
&lt;p class="MsoNormal" style="margin-bottom: 9.0pt;"&gt;Thomas is the returning visitor who has been to your site forty times but has never filled out a form. He opens every newsletter but rarely clicks through. He reads your long-form content to the end while your analytics records him as just another session. He has a deep, genuine interest in the topics you cover &amp;mdash; and an equally deep invisibility in the metrics your team reviews every week.&lt;/p&gt;
&lt;p class="font-claude-response-body break-words whitespace-normal leading-[1.7]"&gt;&lt;span style="font-size: 11.0pt; font-family: 'Arial',sans-serif; mso-fareast-font-family: Arial; mso-ansi-language: EN-US; mso-fareast-language: EN-US; mso-bidi-language: AR-SA;"&gt;He does not bounce. He does not unsubscribe with a parting comment. He does not leave a one-star review. He just quietly, incrementally, recalibrates whether your experience is worth his time. And when he decides it is not &amp;mdash; when the accumulated weight of feeling unseen finally tips the balance &amp;mdash; he stops coming. Without a word. Without a signal. &lt;/span&gt;&lt;/p&gt;
&lt;p class="font-claude-response-body break-words whitespace-normal leading-[1.7]"&gt;We call this a recognition failure. And it's the most expensive problem in digital experiences that almost nobody is actively measuring.&lt;/p&gt;
&lt;div style="mso-element: para-border-div; border: none; border-bottom: solid #1A56DB 1.0pt; mso-border-bottom-alt: solid #1A56DB .5pt; padding: 0in 0in 6.0pt 0in;"&gt;
&lt;h2 style="border: none; mso-border-bottom-alt: solid #1A56DB .5pt; padding: 0in; mso-padding-alt: 0in 0in 6.0pt 0in;"&gt;About This Series&lt;/h2&gt;
&lt;/div&gt;
&lt;p class="MsoNormal" style="margin-bottom: 9.0pt;"&gt;Finding Thomas is a five-part series about understanding and solving this problem using the Optimizely platform. Across these posts, we follow Thomas through the full digital experience layer &amp;mdash; what your CMS can reveal about him, what ODP can connect across his behavioral history, how Personalization can respond in ways that make him feel known, and how Opal helps teams move from insight to action at scale.&lt;/p&gt;
&lt;p class="MsoNormal" style="margin-bottom: 9.0pt;"&gt;Each post covers one layer of the stack. Together they trace a complete, closed-loop recognition system &amp;mdash; one that finds Thomas before he finishes leaving, and makes him feel, for the first time, like someone noticed he was there.&lt;/p&gt;
&lt;p class="font-claude-response-body break-words whitespace-normal leading-[1.7]"&gt;In Part 1 of our Finding Thomas series, we trace where this starts &amp;mdash; and why the CMS most teams treat purely as a publishing tool is actually the first place Thomas's story can be caught before it ends.&lt;/p&gt;
&lt;p class="font-claude-response-body break-words whitespace-normal leading-[1.7]"&gt;Read the full story &lt;span style="color: rgb(35, 111, 161);"&gt;&lt;em&gt;&lt;strong&gt;&lt;a style="color: rgb(35, 111, 161);" href="https://www.linkedin.com/pulse/finding-thomas-part-1-observation-post-ritu-madan-8kkwe"&gt;here&lt;/a&gt;&lt;/strong&gt;&lt;/em&gt;&lt;/span&gt;.&lt;/p&gt;
&lt;p class="font-claude-response-body break-words whitespace-normal leading-[1.7]"&gt;&amp;nbsp;&lt;/p&gt;</summary></entry><entry><title>Optimizely Configured Commerce and Spire CMS - Figuring out Handlers</title><link href="https://world.optimizely.com/blogs/ritu-madan/dates/2025/3/optimizely-configured-commerce-and-spire-cms---figuring-out-handlers/" /><id>https://world.optimizely.com/blogs/ritu-madan/dates/2025/3/optimizely-configured-commerce-and-spire-cms---figuring-out-handlers/</id><published>2025-03-12T19:33:49.0000000Z</published><updated>2025-03-12T19:33:49.0000000Z</updated><author><name>ritu.madan@valtech.com</name></author><category term="Blog post" /><summary type="html">&lt;p&gt;I recently entered the world of Optimizely Configured Commerce and Spire CMS. Intriguing, interesting and challenging at the same time, especially when my past experience had been with traditional Optimizely CMS and Customized Commerce. The idea is the same, but the implementation is quite different. So yes it becomes a bit daunting, to forget old ways and get used to these new ones.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;But I believe in learning on the job, so figuring things out sort of comes as part of the deal. This time, my challenge lies around figuring out how frontend handlers work in Spire CMS, where code lives, where should I intercept and tweak. Read below for the full story on how I went about this learning.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blogs.perficient.com/2025/03/10/optimizely-b2b-commerce-and-spire-cms-figuring-out-handlers/"&gt;https://blogs.perficient.com/2025/03/10/optimizely-b2b-commerce-and-spire-cms-figuring-out-handlers/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Hoping to share more such learning as I gain them. :)&lt;/p&gt;</summary></entry><entry><title>Decoding DI in Optimizely CMS 12</title><link href="https://world.optimizely.com/blogs/ritu-madan/dates/2024/1/decoding-di-in-optimizely-cms-12/" /><id>https://world.optimizely.com/blogs/ritu-madan/dates/2024/1/decoding-di-in-optimizely-cms-12/</id><published>2024-01-23T21:31:43.0000000Z</published><updated>2024-01-23T21:31:43.0000000Z</updated><author><name>ritu.madan@valtech.com</name></author><category term="Blog post" /><summary type="html">&lt;p&gt;DI or Dependency Injection is a complex topic, not just in Optimizely CMS, but in general in .net core implementations. Some aspects we get, like the different ways dependencies can be injected and which is better over others, while some leave us scratching our heads. Often times we end up doing a lot of troubleshooting, digging into internal code and finding the right alternatives to DI, to get things to work.&lt;/p&gt;
&lt;p&gt;The biggest blocker here is understanding the order in which steps get executed, modules get initialized and services get registered. Once this is clear, we know where to add our custom code and avoid any surprise runtime errors around missing service registrations. One such error led me to this discovery so sharing my findings and learnings.&lt;/p&gt;
&lt;p&gt;Read the complete post &lt;a href="https://blogs.perficient.com/2024/01/23/decoding-di-in-optimizely-cms-12/"&gt;here &lt;/a&gt;for details.&lt;/p&gt;</summary></entry><entry><title>Custom form element with dependencies</title><link href="https://world.optimizely.com/blogs/ritu-madan/dates/2024/1/custom-form-element-with-dependencies/" /><id>https://world.optimizely.com/blogs/ritu-madan/dates/2024/1/custom-form-element-with-dependencies/</id><published>2024-01-17T18:26:42.0000000Z</published><updated>2024-01-17T18:26:42.0000000Z</updated><author><name>ritu.madan@valtech.com</name></author><category term="Blog post" /><summary type="html">&lt;p&gt;Episerver Forms are cool as is. They provide solid ground to build basic forms out of the box. But often times, we have custom needs that require us to customize or add to the out of the box functionality.&lt;/p&gt;
&lt;p&gt;In my case, I needed a custom form element that can support some unique requirements and at the same time I needed this to support the built in dependency logic.&lt;/p&gt;
&lt;p&gt;For those of you unfamiliar with the forms dependency logic, see the Optimizely document &lt;a href="https://support.optimizely.com/hc/en-us/articles/4413192345101-Form-element-types#DependenciesTab"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Read the complete blog &lt;a href="https://blogs.perficient.com/2024/01/17/optimizely-cms-custom-form-element-with-dependencies/"&gt;here &lt;/a&gt;for all the fun details on how this was achieved with minor code tweaks.&lt;/p&gt;</summary></entry><entry><title>Special symbols in TinyMCE</title><link href="https://world.optimizely.com/blogs/ritu-madan/dates/2024/1/special-symbols-in-tinymce/" /><id>https://world.optimizely.com/blogs/ritu-madan/dates/2024/1/special-symbols-in-tinymce/</id><published>2024-01-10T23:27:54.0000000Z</published><updated>2024-01-10T23:27:54.0000000Z</updated><author><name>ritu.madan@valtech.com</name></author><category term="Blog post" /><summary type="html">&lt;p&gt;TinyMCE has evolved over the years, allowing more and more flexibility with the features it offers and capabilities it allows to customize. And this is pretty handy as every client has their unique needs and requirements for what they want their Rich Text Editors to do.&lt;/p&gt;
&lt;p&gt;My use case - pretty straightforward - among the many customizations already on, we needed to accommodate very special symbols in the RTE. Now I'm not the first person to present this need ofcourse, which is why TinyMCE already had a plugin called Charmap that contains a whole ensemble of special symbols to use from. And talk about flexibility, it also gives you, the developer, the capability to customize this plugin and add more symbols to it, in case yours are not in the preset list.&lt;/p&gt;
&lt;p&gt;And, what do you know.. not all of mine made the preset list cut, ofcourse! :)&lt;/p&gt;
&lt;p&gt;So, I added them in there. Read the complete post &lt;a href="https://blogs.perficient.com/2024/01/10/working-with-special-symbols-in-tinymce/"&gt;here&lt;/a&gt; to find out exactly how! There are some good to know details in there for sure.&lt;/p&gt;
</summary></entry><entry><title>Optimizely CMS 12 Upgrade takeaways</title><link href="https://world.optimizely.com/blogs/ritu-madan/dates/2023/8/optimizely-cms-12-upgrade-takeaways/" /><id>https://world.optimizely.com/blogs/ritu-madan/dates/2023/8/optimizely-cms-12-upgrade-takeaways/</id><published>2023-08-01T20:01:40.0000000Z</published><updated>2023-08-01T20:01:40.0000000Z</updated><author><name>ritu.madan@valtech.com</name></author><category term="Blog post" /><summary type="html">&lt;p&gt;I've now gone through multiple CMS 12 upgrade projects. Each project had its own unique customizations and hence its own unique set of challenges when upgrading. I learnt something new, overcame a new hurdle, found/solved a new problem with every project. Some took quite a bit of time and synergy with Opti community, some back and forth on Opti support tickets, and solutions from other MVPs. So I thought it would be nice to put them all together in one place, for others on their upgrade journey, looking for same or similar solutions.&lt;/p&gt;
&lt;p&gt;So here goes :&lt;/p&gt;
&lt;p&gt;This &lt;a href="https://blogs.perficient.com/2023/02/17/optimizely-cms-12-unique-upgrade-challenges/"&gt;first blog post&lt;/a&gt; talks about things like Marketing connector and Visitor group related issues.&lt;/p&gt;
&lt;p&gt;This &lt;a href="https://blogs.perficient.com/2023/07/27/optimizely-cms-12-takeaways"&gt;second one&lt;/a&gt; talks about small nitty gritty, more generic things, but if you don't know about them, they can end up giving you massive headaches.&lt;/p&gt;
&lt;p&gt;Hope you find this one stop post helpful!&lt;/p&gt;
&lt;p&gt;I am looking forward to hear other's upgrade stories and challenges faced/resolved, so please do share in comments if you encountered something unique.&lt;/p&gt;</summary></entry><entry><title>Icon Library in Optimizely CMS</title><link href="https://world.optimizely.com/blogs/ritu-madan/dates/2023/5/icon-library-in-optimizely-cms/" /><id>https://world.optimizely.com/blogs/ritu-madan/dates/2023/5/icon-library-in-optimizely-cms/</id><published>2023-05-15T21:20:56.0000000Z</published><updated>2023-05-15T21:20:56.0000000Z</updated><author><name>ritu.madan@valtech.com</name></author><category term="Blog post" /><summary type="html">&lt;p&gt;Icons are graphical representations of different elements on a website. They make the webpages visually appealing and like any other image, have a stronger connection and retention to the end user&amp;rsquo;s memory than plain text. When used in moderation and the right way, they can be self-explanatory and reduce the need for text content in a lot of use cases.&lt;/p&gt;
&lt;p&gt;Icon Library is basically a curated list/dictionary of icons that ties an icon to a meaningful name. This library can then be used to pick icons for the different elements of the website. A good use case would be websites that use icons heavily and need to give CMS Editors control to pick icons with content within CMS, in a way that's well in line with the site's theme and design as well as maintains consistency in terms of meaning and usage. It also allows to have dedicated user roles that control the creation and modification of the actual Icon Library, so they can ensure the icons added are per design and theme and that other user roles can't mess the list up. And by providing a select list of the icon library on content types, instead of a ContentReference property where any media can be added, it can be ensured that no icons outside of this curated list are added to content.&lt;/p&gt;
&lt;p&gt;Please see my detailed post &lt;a href="https://blogs.perficient.com/2023/05/15/icon-library-in-optimizely-cms/"&gt;here &lt;/a&gt;on how to setup the Icon Library and then use it in CMS.&lt;/p&gt;</summary></entry><entry><title>Bitbucket CI/CD pipelines for Optimizely CMS 12</title><link href="https://world.optimizely.com/blogs/ritu-madan/dates/2023/5/bitbucket-cicd-pipelines-for-optimizely-cms-12/" /><id>https://world.optimizely.com/blogs/ritu-madan/dates/2023/5/bitbucket-cicd-pipelines-for-optimizely-cms-12/</id><published>2023-05-05T20:10:20.0000000Z</published><updated>2023-05-05T20:10:20.0000000Z</updated><author><name>ritu.madan@valtech.com</name></author><category term="Blog post" /><summary type="html">&lt;p&gt;Recently, we had a client with code repository hosted on Bitbucket and they showed interest in setting up CI/CD pipelines for automating Optimizely CMS deployments from within Bitbucket. When I started researching, I found several blog posts and documentation around building Azure Devops pipelines for Optimizely CMS deployments. But none that would guide us through building Optimizely CI/CD pipelines in Bitbucket. So after spending quite some time understanding Bitbucket pipeline basics, a lot of trial and error and help from other fellow colleagues, I was able to setup a Bitbucket CI/CD build pipeline for my project.&lt;/p&gt;
&lt;p&gt;This exercise was quite the learning as it wasn't just Bitbucket pipeline specific. I learnt Bash and Powershell scripting. I learnt how Azure Devops and Bitbucket pipelines are so drastically and syntactically different from each other.&lt;/p&gt;
&lt;p&gt;I also learnt that this effort needed sharing, because this topic is relevant and very much in need of more readily available, one stop shop content. Hopefully, after this, others on the same path won't have to spend as much time figuring smaller pieces and syntax for Bitbucket pipelines like I did.&lt;/p&gt;
&lt;p&gt;Please follow along to the detailed blog &lt;a href="https://blogs.perficient.com/2023/05/05/bitbucket-ci-cd-pipelines-for-optimizely-cms-12/"&gt;here&lt;/a&gt;, providing step by step breakdown of the Bitbucket pipeline code.&lt;/p&gt;
&lt;p&gt;I will look forward to feedback and recommendation from others here, to make this better.&lt;/p&gt;</summary></entry><entry><title>Optimizely CMS 12 – Unique upgrade challenges</title><link href="https://blogs.perficient.com/?p=327190" /><id>https://blogs.perficient.com/?p=327190</id><published>2023-02-17T20:56:45.0000000Z</published><updated>2023-02-17T21:01:01.0000000Z</updated><author><name>ritu.madan@valtech.com</name></author><category term="Blog post" /><summary type="html">By now, a lot of us have already participated in one or more CMS 12 upgrade projects. The others are still evaluating whether they should do it now or wait some more. Regardless of when you do it, there will definitely be some unique upgrade challenges you will come across, as I and many others [&amp;#8230;]</summary></entry><entry><title>Optimizely CMS – the importance of GUIDs and Assembly Names</title><link href="https://blogs.perficient.com/?p=316390" /><id>https://blogs.perficient.com/?p=316390</id><published>2022-08-16T16:34:16.0000000Z</published><updated>2022-08-16T17:00:58.0000000Z</updated><author><name>ritu.madan@valtech.com</name></author><category term="Blog post" /><summary type="html">In Optimizely CMS world, we see GUIDs everywhere. They are the unique identifiers for content types and more. As part of developing with Optimizely CMS, developers are advised to always specify GUIDs in there Content Type declarations. If one isn&amp;#8217;t specified, the DB assigns one dynamically when saving the Content type. A big reason for [&amp;#8230;]</summary></entry><entry><title>Optimizely CMS 12 – Dependency Injection(DI) – StructureMap vs ASP.Net Core</title><link href="https://blogs.perficient.com/?p=311946" /><id>https://blogs.perficient.com/?p=311946</id><published>2022-06-30T19:43:59.0000000Z</published><updated>2022-06-30T20:01:16.0000000Z</updated><author><name>ritu.madan@valtech.com</name></author><category term="Blog post" /><summary type="html">If you are upgrading to Optimizely CMS 12, one of the breaking changes to consider is the change to the Dependency Injection(DI) framework. Earlier versions of Optimizely CMS had their own DI hosting framework that supported other concrete DI implementations, like StructureMap. With CMS 12 and ASP.Net Core, DI framework is built into the system. [&amp;#8230;]</summary></entry><entry><title>Optimizely Page Publish Options – Do’s and Dont’s</title><link href="https://blogs.perficient.com/?p=309602" /><id>https://blogs.perficient.com/?p=309602</id><published>2022-05-13T13:58:55.0000000Z</published><updated>2022-05-13T14:01:17.0000000Z</updated><author><name>ritu.madan@valtech.com</name></author><category term="Blog post" /><summary type="html">Page Publish is the most basic feature of a CMS. This is what makes our content visible to the world. Episerver CMS (now known as Optimizely Content Cloud) too has the feature and some more flexibility built around it to aid our content authors. There&amp;#8217;s the usual page publish, where you hit a button and [&amp;#8230;]</summary></entry><entry><title>Optimizely Search : How to search for all expired assets</title><link href="https://blogs.perficient.com/?p=308937" /><id>https://blogs.perficient.com/?p=308937</id><published>2022-05-03T14:41:05.0000000Z</published><updated>2022-05-03T21:01:02.0000000Z</updated><author><name>ritu.madan@valtech.com</name></author><category term="Blog post" /><summary type="html">First of all, I want to mention that EPiServer Find (now known as Optimizely Search and Navigation) is a great tool to search for content on an Optimizely site, whether its CMS or Commerce. It comes with a range of options to customize and with filtering and boosting ability to get the best results. Often [&amp;#8230;]</summary></entry><entry><title>Kathryn Bogen Recognized on 2022 CRN Women of the Channel List</title><link href="https://blogs.perficient.com/?p=309872" /><id>https://blogs.perficient.com/?p=309872</id><published>2012-05-19T18:06:00.0000000Z</published><updated>2022-05-19T19:32:35.0000000Z</updated><author><name>ritu.madan@valtech.com</name></author><category term="Blog post" /><summary type="html">CRN’s annual Women of the Channel program honors some of the most influential women leaders within the channel. We’re proud to have five Perficient colleagues recognized in the 2022 CRN Women of the Channel list, and we’re highlighting their individual accomplishments in a special blog series. We previously celebrated Liza Sisler, Liz Stuart, and Lynn [&amp;#8230;]</summary></entry></feed>