<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Blogs</title><link href="http://world.optimizely.com" /><updated>2026-06-24T11:26:37.0000000Z</updated><id>https://world.optimizely.com/blogs/</id> <generator uri="http://world.optimizely.com" version="2.0">Optimizely World</generator> <entry><title>Migrating from Find to Graph: Lessons Learned from a Real CMS 13 Project</title><link href="https://world.optimizely.com/blogs/binh-nguyen/dates/2026/6/some-found-issues-when-migrating-find-to-optimizely-graph/" /><id>&lt;p class=&quot;FirstParagraph&quot;&gt;While migrating a search solution from Optimizely Search &amp;amp; Navigation (Find) to Optimizely Graph in CMS 13, I encountered several issues that were not immediately obvious from the documentation.&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;Most of these issues were discovered through experimentation, debugging startup errors, investigating generated Graph schemas, and comparing behavior with Search &amp;amp; Navigation.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;Here are details about them:&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&lt;strong&gt;1. Graph Conventions Only Existing Starting from CMS 13.1.0&lt;/strong&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;It took me quite some time to figure out how to apply Graph Conventions based on the following documentation while working on a CMS 13.0.0 project:&lt;/p&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/v13.0.0-CMS/docs/indexing-conventions&quot;&gt;https://docs.developers.optimizely.com/content-management-system/v13.0.0-CMS/docs/indexing-conventions&lt;/a&gt;&lt;/p&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;The documentation describes Graph Conventions using APIs such as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services.ConfigureGraphConventions(...)&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;OR&lt;br /&gt;&lt;br /&gt;&lt;code&gt;services.AddContentGraph(configureConventions: c =&amp;gt;
{
    //add conventions here
});&lt;/code&gt;&lt;/pre&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;span class=&quot;OperatorTok&quot;&gt;However, I was unable to find this API in a CMS 13.0.0 implementation.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;After further investigation, I discovered that the Graph Convention API was introduced in CMS 13.1.0 together with Optimizely.Graph.Cms 13.1.0 and is not available in earlier CMS 13 releases.&lt;/p&gt;
&lt;p&gt;This initially caused some confusion because the documentation is published under the CMS 13 documentation section without clearly indicating the minimum version required for the feature.&lt;/p&gt;
&lt;p&gt;My recommendation is to use CMS 13.1.0 or later if you plan to leverage Graph Conventions during your migration. And It would be helpful if the documentation included version-specific notes or minimum version requirements for Graph Convention APIs, as this could save developers a considerable amount of troubleshooting time during migration projects.&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;&lt;strong&gt;2. Extension Methods Work for Queries but Not for Facets&lt;span style=&quot;font-size: 12.0pt; font-family: &#39;Aptos&#39;,sans-serif; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: Aptos; mso-fareast-theme-font: minor-latin; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: &#39;Times New Roman&#39;; mso-bidi-theme-font: minor-bidi; mso-ansi-language: EN-US; mso-fareast-language: EN-US; mso-bidi-language: AR-SA;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;One of the first surprises I encountered was that extension methods included through Graph Conventions can be queried successfully but cannot be used for faceting.&lt;br /&gt;Here is sample code:&lt;br /&gt;&lt;br /&gt;- Configuration&lt;br /&gt;&lt;code&gt;&lt;span class=&quot;NormalTok&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;ConfigureGraphConventions&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;conventions &lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;OperatorTok&quot;&gt;{&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;NormalTok&quot;&gt;&lt;span style=&quot;mso-spacerun: yes;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;conventions&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;NormalTok&quot;&gt;&lt;span style=&quot;mso-spacerun: yes;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;ForInstancesOf&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;StandardPage&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&amp;gt;()&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;NormalTok&quot;&gt;&lt;span style=&quot;mso-spacerun: yes;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;IncludeField&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;x &lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; x&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;ContentTypeName&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;(),&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; IndexingType&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;Searchable&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;);&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;OperatorTok&quot;&gt;});&lt;/span&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;- Extension method:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static string ContentTypeName(this ISitePageData sitePageData)
{
    return sitePageData.GetOriginalType().Name;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p class=&quot;SourceCode&quot;&gt;However, when attempting to create a facet using the extension method:&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;code&gt;&lt;span class=&quot;NormalTok&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;Facet&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;x &lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; x&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;ContentTypeName&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;());&lt;br /&gt;&lt;/span&gt;&lt;/code&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&lt;br /&gt;I saw this error&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&lt;img src=&quot;/link/52e5237e18cf42718fe3a17a10072ee9.aspx&quot; width=&quot;1029&quot; height=&quot;664&quot; /&gt;&lt;br /&gt;&lt;br /&gt;I tried running the same facet query directly in Graph Explorer, and it worked as expected. This leads me to believe that the issue may originate from the Optimizely Graph Client API when it builds the query from a lambda expression. However, I&#39;m not sure whether this behavior is intentional or if it should be considered a bug.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Impact:&lt;/strong&gt; Existing Find implementations that rely on calculated fields for faceting may require redesign when migrating to Graph.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;3. Search By Base Type Works Differently Than Search &amp;amp; Navigation&lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;In Search &amp;amp; Navigation, it is common to search across multiple content types by using a shared base class or marker interface.&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;Examples:&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;code&gt;&lt;span class=&quot;NormalTok&quot;&gt;Search&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;SitePageData&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&amp;gt;()&lt;/span&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;or&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;code&gt;&lt;span class=&quot;NormalTok&quot;&gt;Search&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;ISearchableContent&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&amp;gt;()&lt;/span&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;Many developers expect the same approach to work in Optimizely Graph. Unfortunately, Graph uses a different model and does not automatically support these patterns.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;- What does not work&lt;br /&gt;&lt;/strong&gt;&lt;br /&gt;The following approaches do not create a shared Graph schema that can be queried across all implementing content types.&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;&lt;span style=&quot;mso-bookmark: what-does-not-work;&quot;&gt;Simple interfaces:&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;code&gt;&lt;span style=&quot;mso-bookmark: what-does-not-work;&quot;&gt;&lt;span class=&quot;KeywordTok&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;KeywordTok&quot;&gt;interface&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; ISearchableContent&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;OperatorTok&quot;&gt;{&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;OperatorTok&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;&lt;span style=&quot;mso-bookmark: what-does-not-work;&quot;&gt;Abstract base classes:&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;code&gt;&lt;span style=&quot;mso-bookmark: what-does-not-work;&quot;&gt;&lt;span class=&quot;KeywordTok&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;KeywordTok&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;KeywordTok&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; SearchablePage &lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; PageData&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;OperatorTok&quot;&gt;{&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;OperatorTok&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;Although these patterns work well in Search &amp;amp; Navigation, Optimizely Graph does not automatically generate a queryable Graph contract from them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;- What works&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;To query multiple content types through a shared schema, Optimizely Graph requires a Graph Contract.&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;For example:&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;code&gt;&lt;span class=&quot;OperatorTok&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;ContentType&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&lt;span class=&quot;KeywordTok&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;KeywordTok&quot;&gt;interface&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; ISitePageData&lt;/span&gt;&lt;/code&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;The contract must then be implemented by each content type that should participate in the shared schema:&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;&lt;code&gt;&lt;span class=&quot;KeywordTok&quot;&gt;public class&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; ArticlePage &lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; ISitePageData&lt;/span&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;&lt;br /&gt;&lt;code&gt;&lt;span class=&quot;KeywordTok&quot;&gt;public class&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; NewsPage &lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; ISitePageData&lt;/span&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;When content is synchronized to Graph, a shared Graph type is generated for the contract, allowing queries across all implementing content types.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Content Type Management UI Behavior&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;One side effect of using a Graph Contract is that the interface becomes visible in the CMS Content Type Management UI.&lt;/p&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;For example, after adding the &lt;code&gt;&lt;span class=&quot;text-token-text-primary cursor-text rounded-sm&quot;&gt;[ContentType]&lt;/span&gt;&lt;/code&gt; attribute to the interface:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;text-token-text-primary cursor-text rounded-sm&quot;&gt;[ContentType]&lt;/span&gt;
public interface ISitePageData
{
}&lt;/code&gt;&lt;/pre&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;the contract appears as a content type in the CMS administration interface as following:&lt;br /&gt;&lt;img src=&quot;/link/abee47fc9c3149f891781f28a055797b.aspx&quot; width=&quot;1437&quot; height=&quot;414&quot; /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;This behavior can be surprising at first, especially since the interface is not intended to be created or edited by content editors. However, this is currently the mechanism Optimizely Graph uses to discover and generate shared schema contracts.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; In Optimizely Graph, interfaces annotated with &lt;code&gt;[ContentType]&lt;/code&gt; enable cross-type queries. Base class inheritance does not.&lt;br /&gt;&lt;strong&gt;Impact:&lt;/strong&gt; Search architectures based on inheritance hierarchies or marker interfaces must be redesigned using Graph Contracts. This can affect query models, schema design, and migration effort.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;4. Using the Content Type Management UI to Change a Property&#39;s Indexing Type&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;/link/0c261ffa36a241f09ab67bea6b4d32e6.aspx&quot; width=&quot;1147&quot; height=&quot;535&quot; /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The indexing convention API cannot be used to exclude a field from Graph indexing. It only supports extension methods for calculated properties, not actual content type properties.&lt;/p&gt;
&lt;p&gt;I tried to add this below to disable an actual field:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services.AddContentGraph(configureConventions: c =&amp;gt;
{
    c.ForInstancesOf&amp;lt;StandardPage&amp;gt;().IncludeField(x =&amp;gt; x.ContentAreaCssClass, IndexingType.Disabled);
});&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;Here is the error when I try to disable a field from indexing via convention:&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;/link/c483e294bb81491b89e2937c0d7b958e.aspx&quot; width=&quot;792&quot; height=&quot;268&quot; /&gt;&lt;/pre&gt;
&lt;p&gt;To disable indexing for a field, use the Content Type Management UI or apply the following attribute to the property in your content type:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[IndexingType(IndexingType.Disabled)]
public virtual bool HideSiteFooter { get; set; }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;5. &lt;span style=&quot;font-size: 12.0pt; font-family: &#39;Aptos&#39;,sans-serif; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: Aptos; mso-fareast-theme-font: minor-latin; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: &#39;Times New Roman&#39;; mso-bidi-theme-font: minor-bidi; mso-ansi-language: EN-US; mso-fareast-language: EN-US; mso-bidi-language: AR-SA;&quot;&gt;Graph Contracts and Content Types Must Use the Same IndexingType&lt;br /&gt;&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;When working with Graph Contracts, I discovered an unexpected validation rule that can prevent the application from starting.&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;Optimizely Graph validates property metadata across the entire contract hierarchy during startup. If the same property exists on both a Graph Contract and a content type, the IndexingType must be identical everywhere.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;Graph Contract:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ContentType]
public interface ISitePageData
{    
    string ContentTypeName { get; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;Base content type:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public abstract class SitePageData : PageData, ISitePageData
{
    [GraphProperty(IndexingType.Queryable)]
    public virtual string ContentTypeName =&amp;gt;
    GetOriginalType().Name;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&lt;br /&gt;Here is the error you will see if this happens:&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;/link/2162fb9599484f8cb164cddc1b8897a3.aspx&quot; width=&quot;849&quot; height=&quot;535&quot; /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&lt;strong&gt;Impact:&lt;/strong&gt; Inconsistent indexing metadata can prevent the application from starting. This validation rule becomes especially important when multiple teams maintain shared contracts and content models.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&lt;strong&gt;6.&amp;nbsp;&lt;/strong&gt;&lt;/span&gt;&lt;strong&gt;Getter-only/fallback properties are included in the Graph schema, but their indexed values are null&lt;br /&gt;&lt;/strong&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&lt;strong&gt;&lt;br /&gt;&lt;/strong&gt;For example:&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public string ContentAreaCssClass =&amp;gt; &quot;teaserblock&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public virtual string TeaserText
{
    get
    {
        var teaserText = this.GetPropertyValue(p =&amp;gt; p.TeaserText);

        // Use explicitly set teaser text, otherwise fall back to description
        return !string.IsNullOrWhiteSpace(teaserText)
            ? teaserText
            : MetaDescription;
    }
    set =&amp;gt; this.SetPropertyValue(p =&amp;gt; p.TeaserText, value);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Here are the their value when indexing:&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;/link/f52a342aacf54f4c9cad1852c93e61dd.aspx&quot; width=&quot;935&quot; height=&quot;461&quot; /&gt;&lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Impact: &lt;/strong&gt;Properties that appear to work correctly in CMS may produce unexpected Graph results. Search queries, filters, and facets relying on computed values may silently return incorrect data.&lt;strong&gt;&lt;br /&gt;&lt;br /&gt;7. Extension Methods Cannot Be Added to a Graph Contract as Calculated Fields&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;span class=&quot;OperatorTok&quot;&gt;When using a Graph Contract, extension methods cannot be added as calculated fields on the contract itself.&lt;br /&gt;Here is the error that I got when trying to do this:&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;/link/12b8f476d6424d569475e365043348c1.aspx&quot; width=&quot;779&quot; height=&quot;264&quot; /&gt;&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Impact:&lt;/strong&gt; Shared Graph Contracts cannot be enriched with convention-based calculated fields. Developers may need to duplicate calculated properties across content types or redesign their schema strategy.&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Optimizely Graph provides a powerful and flexible search platform, but it differs significantly from Search &amp;amp; Navigation in several areas.&lt;/p&gt;
&lt;p&gt;When migrating existing Find implementations, pay particular attention to:&lt;/p&gt;
&lt;p&gt;- Graph Contracts&lt;br /&gt;- Graph Convention limitations&lt;br /&gt;- Faceting behavior&lt;br /&gt;- Property indexing rules&lt;br /&gt;- Calculated fields&lt;/p&gt;
&lt;p&gt;Understanding these differences early can save significant debugging and migration effort.&lt;/p&gt;</id><updated>2026-06-24T11:26:37.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Optimizely: Upgrade Opti-ID and .NET 10 in CMS 12</title><link href="https://madhuanbalagan.com/?p=5000" /><id>&lt;p&gt;Many Optimizely customers are planning their roadmap around a future migration to Optimizely CMS 13. As a result, upgrades such as Opti ID adoption and&amp;#46;&amp;#46;&amp;#46;&lt;/p&gt;
&lt;p&gt;The post &lt;a href=&quot;https://madhuanbalagan.com/optimizely-upgrade-opti-id-and-net-10-in-cms-12&quot;&gt;Optimizely: Upgrade Opti-ID and .NET 10 in CMS 12&lt;/a&gt; appeared first on &lt;a href=&quot;https://madhuanbalagan.com&quot;&gt;Madhu Anbalagan&amp;#039;s Blog&lt;/a&gt;.&lt;/p&gt;
</id><updated>2026-06-23T16:22:29.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Understanding Optimizely Graph: Caching, Webhooks &amp; Avoiding Stale Content (Optimizely SaaS CMS)</title><link href="https://kpbasics.com/?p=8321" /><id>&amp;#x1f4cc; Scope: This post covers Optimizely CMS (SaaS) only — using the official @optimizely/cms-sdk and @optimizely/cms-cli packages with Next.js 15. If you&amp;#8217;re on Optimizely CMS 12 (PaaS/DXP), the caching architecture and tooling are different. See the DXP ISR docs for that path. Your editor hit Publish. Five minutes later the page still shows the old [&amp;#8230;]</id><updated>2026-06-23T04:28:27.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Optimizely Content APIs: the Setup the Docs Don&#39;t Walk You Through</title><link href="https://world.optimizely.com/blogs/andre-gabriel-coetzee/dates/2026/6/optimizely-content-apis-the-setup-the-docs-dont-walk-you-through/" /><id>&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;CMS 13 is pushing things firmly in the direction of Optimizely Graph, but plenty of teams are still running on older CMS versions, or have good reasons to stick with the Content Delivery, Content Definitions, or Content Management APIs regardless of what version they&#39;re on. This guide is for those teams.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;What follows is a step-by-step walkthrough to get any of the content APIs up and running, tested locally, and fully understood, in one place. No jumping between API reference pages, no piecing together fragments from several different resources. Just the things the documentation doesn&#39;t make obvious, learned the hard way.&lt;/p&gt;
&lt;hr class=&quot;border-border-200 border-t-0.5 my-3 mx-1.5&quot; /&gt;
&lt;h3 class=&quot;text-text-100 mt-3 -mb-1 text-[1.125rem] font-bold&quot;&gt;&lt;strong&gt;The mental model first&lt;/strong&gt;&lt;/h3&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Optimizely uses OpenID Connect (via a library called OpenIddict under the hood) to issue JWT bearer tokens. When you want a system to be able to call one of the content APIs, you define it as an OpenID Connect application, either in code, in the CMS Admin UI, or both. We&#39;ll cover the specifics of each in the steps below.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&quot;Application&quot; in this context is not referring to your CMS instance or front-end site, it refers to discrete client identities registered specifically for machine-to-machine communication. Your CMS can have as many of these as you need.&lt;/p&gt;
&lt;h4 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&lt;strong&gt;Why grant_type=client_credentials and not something else&lt;/strong&gt;&lt;/h4&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;OAuth 2.0 has several grant types: different flows for getting a token, each suited to a different kind of requester or use case.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;strong&gt;authorization_code&lt;/strong&gt;&amp;nbsp;is for interactive users: a person clicks login, gets redirected, enters their credentials, and the app receives a token on their behalf.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;strong&gt;client_credentials&lt;/strong&gt;&amp;nbsp;is for machine-to-machine communication. Your service acts as itself, not on behalf of a user. There are no redirects, it simply exchanges its credentials directly for a token.&lt;/p&gt;
&lt;h4 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&lt;strong&gt;Why the token endpoint is at /connect/token&lt;/strong&gt;&lt;/h4&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;This is OpenIddict&#39;s default endpoint path, and OpenIddict is the OpenID Connect server library Optimizely uses under the hood. Optimizely didn&#39;t invent this URL, it&#39;s baked into the library. The &lt;strong&gt;/api/episerver&lt;/strong&gt;&amp;nbsp;prefix in front of it is the actual Optimizely part of the route, sitting in the same legacy episerver namespace you&#39;ll see across the older API paths.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;The flow from here is straightforward: you request an access token for one of these applications, then pass that token in subsequent requests to the content APIs. Importantly, the access rights you configure in the CMS are tied to the application identity itself, so when a request comes in using that token, content access is evaluated against whatever permissions that application has been granted.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;/link/97584b3856524d738de8085f065eccdb.aspx&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-6axZYk-gPv/0d7f2a7de1e85c0b2044c8e8f0f8d8592c1549a00a4746a00110d39d57f2a00ea426e35a907df27fefe33102d25f33b8158227cf15f4e51bef79f26d202ef1f636a7da0e82114cb5dc624aaddad115be213b1da5054a9af57d4bfbf223a6c0f0f6565710?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;em&gt;The Add User/Group dropdown showing the Applications section with two OpenID applications listed&lt;/em&gt;&lt;/p&gt;
&lt;hr class=&quot;border-border-200 border-t-0.5 my-3 mx-1.5&quot; /&gt;
&lt;h3 class=&quot;text-text-100 mt-3 -mb-1 text-[1.125rem] font-bold&quot;&gt;&lt;strong&gt;Let&#39;s set it all up&lt;/strong&gt;&lt;/h3&gt;
&lt;h4 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&lt;strong&gt;Step 1: Install the required NuGet packages&lt;/strong&gt;&lt;/h4&gt;
&lt;div class=&quot;relative group/copy bg-bg-000/50 border-0.5 border-border-400 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-100&quot;&gt;
&lt;div class=&quot;overflow-x-auto&quot;&gt;
&lt;pre class=&quot;code-block__code !my-0 !rounded-lg !text-sm !leading-relaxed p-3.5&quot;&gt;&lt;code&gt;dotnet add package EPiServer.OpenIDConnect
dotnet add package EPiServer.OpenIDConnect.UI # Optional&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;You&#39;ll also need the API package for whichever API you&#39;re securing:&lt;/p&gt;
&lt;ul class=&quot;[li_&amp;amp;]:mb-0 [li_&amp;amp;]:mt-1 [li_&amp;amp;]:gap-1 [&amp;amp;:not(:last-child)_ul]:pb-1 [&amp;amp;:not(:last-child)_ol]:pb-1 list-disc flex flex-col gap-1 pl-8 mb-3&quot;&gt;
&lt;li class=&quot;font-claude-response-body whitespace-normal break-words pl-2&quot;&gt;Content Delivery API: &lt;strong&gt;EPiServer.ContentDeliveryApi.Cms&lt;/strong&gt;&lt;/li&gt;
&lt;li class=&quot;font-claude-response-body whitespace-normal break-words pl-2&quot;&gt;Content Management API: &lt;strong&gt;EPiServer.ContentManagementApi&lt;/strong&gt;&lt;/li&gt;
&lt;li class=&quot;font-claude-response-body whitespace-normal break-words pl-2&quot;&gt;Content Definitions API: &lt;strong&gt;EPiServer.ContentDefinitionsApi&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;strong&gt;Version gotcha:&lt;/strong&gt; Make sure your &lt;strong&gt;EPiServer.OpenIDConnect &lt;/strong&gt;version is compatible with your CMS version. Mismatched versions are a common source of silent failures at startup.&lt;/p&gt;
&lt;h4 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&lt;strong&gt;Step 2: Wire up the configuration in code&lt;/strong&gt;&lt;/h4&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;In your &lt;strong&gt;Startup.cs&lt;/strong&gt;, you need to configure OpenID Connect and register whichever content APIs you need:&lt;/p&gt;
&lt;div class=&quot;relative group/copy bg-bg-000/50 border-0.5 border-border-400 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-100&quot;&gt;
&lt;div class=&quot;overflow-x-auto&quot;&gt;
&lt;pre class=&quot;code-block__code !my-0 !rounded-lg !text-sm !leading-relaxed p-3.5&quot;&gt;&lt;code&gt;public void ConfigureServices(IServiceCollection services)
{
    // ASP.NET Identity must be configured before OpenID Connect
    services.AddCmsAspNetIdentity&amp;lt;ApplicationUser&amp;gt;();

    services.AddOpenIDConnect&amp;lt;ApplicationUser&amp;gt;(
        useDevelopmentCertificate: _webHostingEnvironment.IsDevelopment(),
        createSchema: true,
        options =&amp;gt;
        {
            // Seeding an application here is optional: see note below
            options.Applications.Add(new OpenIDConnectApplication
            {
                ClientId = &quot;your-client-id&quot;,
                ClientSecret = &quot;your-client-secret&quot;,
                Scopes =
                {
                    &quot;epi_content_delivery&quot;,
                    &quot;epi_content_management&quot;,
                    &quot;epi_content_definitions&quot;
                }
            });
        });

    services.AddOpenIDConnectUI(); // Optional: adds the OpenID Connect panel in CMS Admin

    services.AddContentDeliveryApi(OpenIDConnectOptionsDefaults.AuthenticationScheme);
    services.AddContentDefinitionsApi(OpenIDConnectOptionsDefaults.AuthenticationScheme);
    services.AddContentManagementApi(OpenIDConnectOptionsDefaults.AuthenticationScheme);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;A few things worth noting here:&lt;/p&gt;
&lt;ul class=&quot;[li_&amp;amp;]:mb-0 [li_&amp;amp;]:mt-1 [li_&amp;amp;]:gap-1 [&amp;amp;:not(:last-child)_ul]:pb-1 [&amp;amp;:not(:last-child)_ol]:pb-1 list-disc flex flex-col gap-1 pl-8 mb-3&quot;&gt;
&lt;li class=&quot;font-claude-response-body whitespace-normal break-words pl-2&quot;&gt;Each API registration method accepts an authentication scheme parameter, don&#39;t leave it out. Without it, the API has no way to validate incoming Bearer tokens and will return a &lt;strong&gt;401&lt;/strong&gt; regardless of whether your token is valid. Pass &lt;strong&gt;OpenIDConnectOptionsDefaults.AuthenticationScheme&lt;/strong&gt; to wire them up correctly.&lt;/li&gt;
&lt;li class=&quot;font-claude-response-body whitespace-normal break-words pl-2&quot;&gt;&lt;strong&gt;useDevelopmentCertificate &lt;/strong&gt;: tying this to &lt;strong&gt;IsDevelopment() &lt;/strong&gt;means it uses a local dev certificate in your local environment and expects a real one in production. On DXP, certificates are provided automatically via &lt;strong&gt;EPiServer.CloudPlatform.Cms&lt;/strong&gt; 1.6.1 or later.&lt;/li&gt;
&lt;li class=&quot;font-claude-response-body whitespace-normal break-words pl-2&quot;&gt;Scopes: each scope string controls which API that client is permitted to call, and must match the scope you pass when requesting a token. There are constants available if you&#39;d prefer not to use raw strings: &lt;strong&gt;ContentDeliveryOptionsDefaults.Scope&lt;/strong&gt;, &lt;strong&gt;ContentDefinitionsApiOptionsDefaults.Scope&lt;/strong&gt;, and &lt;strong&gt;ContentManagementApiOptionsDefaults.Scope&lt;/strong&gt;.&lt;/li&gt;
&lt;li class=&quot;font-claude-response-body whitespace-normal break-words pl-2&quot;&gt;Seeding applications in code is optional. The &lt;strong&gt;options.Applications.Add(...)&lt;/strong&gt; block pre-creates the client on startup, which is convenient for local development. If you&#39;d rather manage clients entirely through the UI, you can omit it, that&#39;s what the &lt;strong&gt;AddOpenIDConnectUI()&lt;/strong&gt; line enables. It adds an OpenID Connect section under Settings in CMS Admin where you can create and manage applications directly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-oki8fxiyA7/5cd57dd6794f296df6654bc4c7aa473b1171a4d8575e67bd58ded795e9c955eead6210f9123ca6f85e7f93ea9de301c69d838208b36c8325f6744862c30110e55fd837c8291ec1448d6c817c7bd3e8ff859e13f6e235447f2ac3d73256f2466a0dfb76c4?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;em&gt;The &quot;OpenID Connect&quot; settings panel can be seen in the screenshot above.&lt;/em&gt;&lt;/p&gt;
&lt;h4 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&lt;strong&gt;Step 3: Request a token&lt;/strong&gt;&lt;/h4&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Once you have an application configured, either in code or the UI, you first need to request an access token for the application before calls to any of the content APIs can be made.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Using curl:&lt;/p&gt;
&lt;div class=&quot;relative group/copy bg-bg-000/50 border-0.5 border-border-400 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-100&quot;&gt;
&lt;div class=&quot;overflow-x-auto&quot;&gt;
&lt;pre class=&quot;code-block__code !my-0 !rounded-lg !text-sm !leading-relaxed p-3.5&quot;&gt;&lt;code&gt;curl -X POST https://your-site.com/api/episerver/connect/token \
  -H &quot;Content-Type: application/x-www-form-urlencoded&quot; \
  -d &quot;grant_type=client_credentials&amp;amp;client_id=your-client-id&amp;amp;client_secret=your-client-secret&amp;amp;scope=epi_content_management&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Using Postman:&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-gPVIKzvOJE/f7bf91e744372274b5dccfdc85dd0dbc9baa8eb91fdee7b93a4b8c26793ee8898b698a77b9c5aa805a5fc3934d04b8642cbc3a6059ffaa92136afd1eaa2eb5f17688bd706702a0fc3613ac6f7c6d74fb7898db17dbd5b35368fd65fae32753032b11c7e6?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Set the request to POST, the URL to your token endpoint, and under the Body tab select &lt;strong&gt;x-www-form-urlencoded&lt;/strong&gt;. Add the four key-value pairs: &lt;strong&gt;grant_type, client_id, client_secret, scope&lt;/strong&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;A successful response looks like this:&lt;/p&gt;
&lt;div class=&quot;relative group/copy bg-bg-000/50 border-0.5 border-border-400 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-100&quot;&gt;
&lt;div class=&quot;overflow-x-auto&quot;&gt;
&lt;pre class=&quot;code-block__code !my-0 !rounded-lg !text-sm !leading-relaxed p-3.5&quot;&gt;&lt;code&gt;{
  &quot;access_token&quot;: &quot;eyJhbGci...&quot;,
  &quot;expires_in&quot;: 3599,
  &quot;token_type&quot;: &quot;Bearer&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Copy the &lt;strong&gt;access_token&lt;/strong&gt; value, you&#39;ll use it in Step 5.&lt;/p&gt;
&lt;h4 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&lt;strong&gt;Step 4: Assign CMS access rights to the client&lt;/strong&gt;&lt;/h4&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;This is the most commonly missed step, and it causes some of the most confusing symptoms, you have a valid token, your requests aren&#39;t getting &lt;strong&gt;401s&lt;/strong&gt;, but you&#39;re getting empty results or unexpected &lt;strong&gt;403s&lt;/strong&gt;.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;The API client you created is effectively a user in the CMS. Like any user, it needs to be granted access rights to the content it&#39;s trying to read or write. There are different levels you could configure access rights, let&#39;s get into them below.&lt;/p&gt;
&lt;h5 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&amp;nbsp;&lt;/h5&gt;
&lt;h5 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&lt;strong&gt;Global access rights&lt;/strong&gt;&lt;/h5&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Global access rights apply at the page or section level in your content tree and cascade down to every item beneath. For instance, if you want to deny a specific OpenID application Read access to a sensitive area, say, an &quot;Admin&quot; page or a &quot;Members Only&quot; section, you set the restriction at that page, and the cascade ensures every sub-page inherits the same rule automatically. You don&#39;t have to configure each one individually.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-wHVz_SRQT6/9a2b8822c3e7ca4442dfc503adbbff00f14ed9b74eedbd81d1487dec35f33061b21d46266513d0c1e8ecbc519ea9c4fe60e0db4cdf5c8676fc955aad580be95add7cc628e35c54434f4363ddf43a528a0c579d79c3413007238721d09e47b0710261574c?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;em&gt;On this level, either all content is returned, or none of it. Either 200-OK, or 403-Forbidden&lt;/em&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Navigate to Settings &amp;rarr; Set Access Rights, select the page or section you want the client to access, and add it using Add User/Group. Your registered applications (from either the code or created in the CMS on the OpenID Connect settings panel) will appear under the Applications group in the dropdown.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Grant it the appropriate permissions: Read is sufficient for Content Delivery, while Content Management will need Read, Change, and Publish depending on what operations you need to perform. Check Apply settings for all subitems to cascade the rights down the content tree.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-G7Oqc_d9dv/65f22110078ca502ef75b1a83a1ec3652580830c21791120f685a2cfde53b42d62737429b8ba88f183d645b66eb5160468ef90752518b2e807fd94ba6fff3b580442c4c65a742453a0e8a73515650aa577db9adad1b41ccb824c0997d8618475f3621c33?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;em&gt;alloy-client application granted Read access on the Start page, cascading to all subitems.&lt;/em&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Skipping this step produces one of two outcomes depending on your setup: either the API returns a &lt;strong&gt;403&lt;/strong&gt; Forbidden response, or it returns content that was never actually restricted to begin with, because the Everyone group already has read access. In the latter case everything appears to be working, but your access rights configuration isn&#39;t doing anything. If you ever tighten down content permissions, your client will silently lose access.&lt;/p&gt;
&lt;h5 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&amp;nbsp;&lt;/h5&gt;
&lt;h5 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&lt;strong&gt;Content-level access rights (per page/block instance in the tree)&lt;/strong&gt;&lt;/h5&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Content-level access rights give you more granular control than the global settings screen, you can restrict access to specific pages or blocks directly in the content tree rather than applying a blanket rule across everything.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;A practical example: imagine you have a pricing page that should only be visible to authenticated partners. Rather than locking down your entire content tree, you can grant your API client access to everything by default and then explicitly restrict access on those specific items, or invert it entirely, grant no global access, and only open up the specific content the client needs to see.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-KagMkB0nHI/84fd374988f4ffa674e61c110c8dd272a70b53a3aaac9b9e69a3c7a2f7904af1773ebb6a4731948214519ccbc5ec37577bafd38a338a450b09e5e0eba3bb6fa71cdd6284c0442e856b1467f4de13af0e9fd093cf7735eb69e73f71e0c32465a8cf3f00c1?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;em&gt;Same request, different response. The restricted client&#39;s &quot;Pricing&quot; entry isn&#39;t returned as a 403 error, it&#39;s simply omitted from the response. This is what makes content-level access rights powerful: the client doesn&#39;t need to know what they can&#39;t see, and the API does the filtering server-side.&lt;/em&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;The result isn&#39;t binary. It&#39;s not just 200 or 403, you can build quite precise access models by combining global and content-level rights.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Content-level access rights are set directly on a specific page or block in the edit interface. To access them, right-click the item in the content area or page tree and select Edit, then look for the Visible to panel at the top of the properties view. If access is unrestricted it will show as Everyone. Click Manage to open the Access Rights dialog and configure it the same way as the global screen.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-qR4ivN8SRr/b1a50ba1bf085a982d94ee754c078da101225a16e25244fc5d9c8cbc736292fe097f308e83577c3f01d0fae8184840d72d542e812eb4618722716dc813d7e5abb3de10c714b93db366a20b4bd0b3c5b580d83a9ddbd9f031491b687fa4d36d3d1d50f4ca?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;em&gt;Right-clicking a content item in the page tree to access its properties&lt;/em&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-WSEoq3HxlV/50e6bbbae79aa96c3d964669a0a78de6d46fef1b38a276d48351652090e0cf572a26a24277995b9aac202280dd6e1a98f69eb35f29ab8c264b53d408910f12febb1db17a875ab48629dde4cf5385c47fd04443d133025250ed1f71e4beddb28d649acdcf?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;em&gt;The &quot;Visible to&quot; panel showing the current access restriction status for a content item, with the Manage link to open the Access Rights dialog&lt;/em&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-gRug85CPa2/a655f207b12969e892ee42f18c06fe4559edc2458edd8eac4fdba97ca66f1b57940a77f4656e4e94deb5d320983ab7ce7b56f93992910a7c1f502243c1294c8c8349eb52f165b0cbd6caddda4cbe605aaf827322729400de2e1d68bc28f44516d684351c?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;em&gt;The Access Rights dialog for a specific content item, with alloy-client granted Read access and &quot;Inherit settings from parent item&quot; unchecked to override the global defaults&lt;/em&gt;&lt;/p&gt;
&lt;h4 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&lt;strong&gt;Step 5: Make your first authenticated request&lt;/strong&gt;&lt;/h4&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;With your token in hand (from Step 3) and access rights set, you can now call the API. Pass the token as a Bearer token in the &lt;strong&gt;Authorization&lt;/strong&gt; header.&lt;/p&gt;
&lt;div class=&quot;relative group/copy bg-bg-000/50 border-0.5 border-border-400 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-100&quot;&gt;
&lt;div class=&quot;overflow-x-auto&quot;&gt;
&lt;pre class=&quot;code-block__code !my-0 !rounded-lg !text-sm !leading-relaxed p-3.5&quot;&gt;&lt;code&gt;curl -X GET https://your-site.com/api/episerver/v3.0/content/{contentGuid} \
  -H &quot;Authorization: Bearer eyJhbGci...&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-FU_Ev1Aesh/e10d121a20a2164e0dae719606c8455da4b0249791f5133404aa422e9e1305da7a25b2c0c706bad935e45143206bb1573e2b9cc0d2ba6bd276a2b647b477549c2853b76b35191888168aaa288e0c1580f4596e7bf7d5c0283f5f076a95e09b06e0cebea2?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-NXSO-5LtU5/e6aa4fcd051b0c43b89b8c85ad1dce2c781e14eb93a6f63afb206b3780777bf8e57bab1dadce5aa8e54b7a205305f2ba10bfb1a792846708e43936e9ae6a37e80ab382f0de3587e031b5aaa2dc4a88bf2655eece10f86e21527fd89bd1e5f87d155ff6b0?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;em&gt;Success response from the Content Delivery API (200 - OK)&lt;/em&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;However, if I revoke read access from the OpenId Application, I get the following:&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-qxuy8jsqAm/3bb819b8f43ca3a5307fb37c86c495da49bb3b98fede2645dcb989df111a4d3581549553ca3323489dbc9c169cdfbba8cfcf395c34c048d66d598408810c86281c422c293e4bed5605e2a0e7099c49380d8c3bbc2d3725707840509617be42aa044340b0?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-4bM8czjC1c/1f9f13813cdbbf1744799c9aa89168fde0b36192909142daf589b5707d800ec9b9c91de53a2dfe3bf26c28d318cf550e9cedf3f32a18daea6b22f5dd0f884ca723c2ec77b1a9270724da6dc853706a241ee58975287d316102a1a095bef86ee8849e61bb?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;em&gt;Forbidden response from Content Delivery API (403 - Forbidden)&lt;/em&gt;&lt;/p&gt;
&lt;hr class=&quot;border-border-200 border-t-0.5 my-3 mx-1.5&quot; /&gt;
&lt;h3 class=&quot;text-text-100 mt-3 -mb-1 text-[1.125rem] font-bold&quot;&gt;&lt;strong&gt;Common errors and what they actually mean&lt;/strong&gt;&lt;/h3&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;For the last example, the &lt;strong&gt;403 &lt;/strong&gt;is exactly what we&#39;d expect, but it&#39;s worth understanding the difference between a &lt;strong&gt;401&lt;/strong&gt; and a &lt;strong&gt;403&lt;/strong&gt;, since they&#39;re easy to confuse and point to very different problems.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;strong&gt;401 Unauthorized&lt;/strong&gt; means the API doesn&#39;t recognise or can&#39;t validate the token itself. Either it&#39;s malformed, it&#39;s expired, or the API isn&#39;t configured to accept it. Check that you&#39;re passing &lt;strong&gt;OpenIDConnectOptionsDefaults.AuthenticationScheme &lt;/strong&gt;into your API registrations in &lt;strong&gt;Startup.cs&lt;/strong&gt;, that your token hasn&#39;t expired (3599 seconds), and that your &lt;strong&gt;client_id&lt;/strong&gt; and &lt;strong&gt;client_secret&lt;/strong&gt; match exactly what&#39;s registered in CMS Admin.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;strong&gt;403 Forbidden&lt;/strong&gt; means the token is valid and recognised, but the application it belongs to doesn&#39;t have permission to access that content. This is the access rights problem covered in the previous step, the API knows who you are, it just won&#39;t let you in.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;strong&gt;Token endpoint returns 404.&lt;/strong&gt; The OpenID Connect middleware isn&#39;t registered, or the package isn&#39;t installed. Confirm the NuGet package is installed and correctly configured in &lt;strong&gt;Startup.cs&lt;/strong&gt;.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;strong&gt;invalid_client error from the token endpoint.&lt;/strong&gt; The &lt;strong&gt;client_id&lt;/strong&gt; or &lt;strong&gt;client_secret&lt;/strong&gt; doesn&#39;t match what&#39;s registered in CMS Admin (or code). These must be identical, including case.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;strong&gt;invalid_scope error from the token endpoint.&lt;/strong&gt; The scope you&#39;re requesting (&lt;strong&gt;epi_content_management&lt;/strong&gt;, etc.) doesn&#39;t match what was registered for the client in CMS Admin.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;strong&gt;unsupported_grant_type from the token endpoint.&lt;/strong&gt; The client was not registered with the &lt;strong&gt;client_credentials&lt;/strong&gt; grant type. Check the application configuration in CMS Admin and ensure Client Credentials is selected as a permitted grant type.&lt;/p&gt;
&lt;hr class=&quot;border-border-200 border-t-0.5 my-3 mx-1.5&quot; /&gt;
&lt;h3 class=&quot;text-text-100 mt-3 -mb-1 text-[1.125rem] font-bold&quot;&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/h3&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Getting the Optimizely content APIs working for the first time involves piecing together information from several different sources, the official docs, community posts, and a fair amount of trial and error. Hopefully this post brings it all into one place and saves someone the same legwork.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;If you&#39;re already on CMS 13 or considering the move, Optimizely Graph changes the delivery layer significantly and much of the authentication setup described here won&#39;t apply in the same way. But for teams on CMS 12 or those still needing to use any of the content APIs, this remains the path.&lt;/p&gt;</id><updated>2026-06-22T13:31:38.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Translating content in Optimizely CMS with Anthropic Claude</title><link href="https://www.gulla.net/en/blog/translating-content-in-optimizely-cms-with-anthropic-claude/" /><id>An add-on with an Anthropic translator provider that lets you translate content in Optimizely CMS using Anthropic Claude.</id><updated>2026-06-20T05:48:44.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Controlling Optimizely Forms Cookie Expiration in .NET Core</title><link href="https://blog.novacare.no/controlling-optimizely-forms-cookie-expiration/" /><id>Learn how to make Optimizely Forms cookies behave as session cookies in CMS 12+ (.NET Core) using a simple middleware - and why the official documentation won&#39;t help you get there.</id><updated>2026-06-19T07:43:39.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>ReloadOnChange in Optimizely CMS: The Attribute Nobody Talks About</title><link href="https://techhub.iodigital.com/articles/optimizely-cms-reloadonchange-attribute" /><id>Optimizely CMS has a little-known attribute that reloads the editor when a property changes — perfect for dependent dropdowns and checkboxes. Here is how I found it, proved it worked, and shipped it on a Product Detail Page.</id><updated>2026-06-19T00:00:00.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Opal: Beyond the Obvious</title><link href="https://blog.danisaacs.net/posts/opal-beyond-the-obvious/" /><id>A couple of less-obvious ways I&#39;m using Optimizely&#39;s Opal to automate real work across Salesforce, GitHub, and Teams.</id><updated>2026-06-18T21:52:49.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>EOL Announcement for Preview3 CMS REST API</title><link href="https://world.optimizely.com/blogs/kathy-copeland/dates/2026/6/eol-announcement-for-preview3-cms-rest-api-experimental/" /><id>&lt;p&gt;&lt;span class=&quot;___qsff000 f1a3p1vp f14t3ns0 f4l2907 f1j863za&quot;&gt;&lt;img src=&quot;https://statics.teams.cdn.office.net/evergreen-assets/personal-expressions/v2/assets/emoticons/stopsign/default/30_f.png?v=v6&quot; alt=&quot;&#128721;&quot; width=&quot;20&quot; /&gt;&lt;/span&gt;&lt;strong&gt;Action Required: Migrate Preview3 API Integrations to CMS REST API v1 Before August 1, 2026&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If your SaaS CMS applications are currently using the &lt;strong&gt;Preview3 CMS REST API endpoints&lt;/strong&gt;, please plan to migrate to &lt;strong&gt;CMS REST API v1&lt;/strong&gt; before &lt;strong&gt;August 1, 2026&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;On August 1, 2026, we will retire the Preview3 endpoints and begin disabling access. Applications that continue to rely on Preview3 after this date may experience failed requests and service interruptions.&lt;/p&gt;
&lt;h3&gt;Who Is Affected?&lt;/h3&gt;
&lt;p&gt;You are affected by this change if your SaaS CMS integrations are currently making requests to &lt;strong&gt;Preview3 endpoints&lt;/strong&gt; (typically URLs containing &lt;code&gt;/preview3/&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;If your applications are already using &lt;strong&gt;CMS REST API v1&lt;/strong&gt;, no action is required.&lt;/p&gt;
&lt;p&gt;We recognize that API migrations require planning and effort, and we appreciate the work our customers invest in keeping their integrations current. To help ensure a smooth transition, we&#39;ve provided a migration guide (see below) and have maintained support for both versions since the release of v1 in May. This announcement is the first to underscore the importance of staying current and making the change to CMS REST API v1 to avoid disruption.&lt;/p&gt;
&lt;h2&gt;What Is Changing?&lt;/h2&gt;
&lt;p&gt;With the general availability of &lt;strong&gt;CMS REST API v1&lt;/strong&gt; on &lt;strong&gt;May 5, 2026&lt;/strong&gt;, the Preview3 APIs entered a deprecation phase and are now approaching end-of-life.&lt;/p&gt;
&lt;p&gt;Key points to know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Preview3 support ends August 1, 2026.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Preview3 was released as an &lt;strong&gt;experimental API&lt;/strong&gt; and was not recommended for production workloads.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CMS REST API v1&lt;/strong&gt; provides the same core capabilities through stabilized, production-ready endpoints.&lt;/li&gt;
&lt;li&gt;Mixing Preview3 and v1 endpoints is supported until the August 1, 2026 retirement date.&lt;/li&gt;
&lt;li&gt;No new fixes, enhancements, or support updates are being made to Preview3. All ongoing investment is focused on v1 and future releases.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Will There Be Breaking Changes?&lt;/h2&gt;
&lt;p&gt;Yes. Some breaking changes were introduced as part of the transition from Preview3 to v1.&lt;/p&gt;
&lt;p&gt;Before migrating, please review the migration documentation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class=&quot;fui-Link ___1q1shib f2hkw1w f3rmtva f1ewtqcl fyind8e f1k6fduh f1w7gpdv fk6fouc fjoy568 figsok6 f1s184ao f1mk8lai fnbmjn9 f1o700av f13mvf36 f1cmlufx f9n3di6 f1ids18y f1tx3yz7 f1deo86v f1eh06m1 f1iescvh fhgqx19 f1olyrje f1p93eir f1nev41a f1h8hb77 f1lqvz6u f10aw75t fsle3fq f17ae5zn&quot; title=&quot;https://docs.developers.optimizely.com/content-management-system/v1.0.0-cms-saas/reference/migrate-content-api-preview3-to-v1&quot; href=&quot;https://docs.developers.optimizely.com/content-management-system/v1.0.0-CMS-SaaS/reference/migrate-content-api-preview3-to-v1&quot;&gt;Migrate from Content API Preview3 to CMS REST API v1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;fui-Link ___1q1shib f2hkw1w f3rmtva f1ewtqcl fyind8e f1k6fduh f1w7gpdv fk6fouc fjoy568 figsok6 f1s184ao f1mk8lai fnbmjn9 f1o700av f13mvf36 f1cmlufx f9n3di6 f1ids18y f1tx3yz7 f1deo86v f1eh06m1 f1iescvh fhgqx19 f1olyrje f1p93eir f1nev41a f1h8hb77 f1lqvz6u f10aw75t fsle3fq f17ae5zn&quot; title=&quot;https://docs.developers.optimizely.com/content-management-system/v1.0.0-cms-saas/reference/experimental-apis&quot; href=&quot;https://docs.developers.optimizely.com/content-management-system/v1.0.0-CMS-SaaS/reference/experimental-apis&quot;&gt;Experimental APIs Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These resources provide detailed guidance on the changes and recommended migration approach.&lt;/p&gt;
&lt;h2&gt;&lt;span class=&quot;___qsff000 f1a3p1vp f14t3ns0 f4l2907 f1j863za&quot;&gt;&lt;img src=&quot;https://statics.teams.cdn.office.net/evergreen-assets/personal-expressions/v2/assets/emoticons/1f552_threeoclock/default/30_f.png?v=v13&quot; alt=&quot;&#128338;&quot; width=&quot;20&quot; /&gt;&lt;/span&gt;Why Are We Retiring Preview3?&lt;/h2&gt;
&lt;p&gt;Maintaining multiple API versions increases complexity for both customers and Optimizely. As v1 has been generally available for several months and provides a stable, supported alternative, retiring Preview3 allows us to focus on delivering higher reliability, better performance, and faster innovation across the platform.&lt;/p&gt;
&lt;h2&gt;&lt;span class=&quot;___qsff000 f1a3p1vp f14t3ns0 f4l2907 f1j863za&quot;&gt;&lt;img src=&quot;https://statics.teams.cdn.office.net/evergreen-assets/personal-expressions/v2/assets/emoticons/1f4e2_publicaddressloudspeaker/default/30_f.png?v=v8&quot; alt=&quot;&#128226;&quot; width=&quot;20&quot; /&gt;&lt;/span&gt;How Can I Stay Informed?&lt;/h2&gt;
&lt;p&gt;We recommend subscribing to the following channels for future API lifecycle and platform updates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CMS Release Notes (support.optimizely.com)&lt;/li&gt;
&lt;li&gt;Optimizely Slack Community&lt;/li&gt;
&lt;li&gt;Optimizely Developer Blog Community (World.optimizely.com)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These channels are typically the first places where deprecations, support timelines, and migration guidance are communicated.&lt;/p&gt;
&lt;p&gt;For additional information about API lifecycle and support policies, please refer to the &lt;a class=&quot;fui-Link ___1q1shib f2hkw1w f3rmtva f1ewtqcl fyind8e f1k6fduh f1w7gpdv fk6fouc fjoy568 figsok6 f1s184ao f1mk8lai fnbmjn9 f1o700av f13mvf36 f1cmlufx f9n3di6 f1ids18y f1tx3yz7 f1deo86v f1eh06m1 f1iescvh fhgqx19 f1olyrje f1p93eir f1nev41a f1h8hb77 f1lqvz6u f10aw75t fsle3fq f17ae5zn&quot; title=&quot;https://docs.developers.optimizely.com/content-management-system/v1.0.0-cms-saas/reference/experimental-apis&quot; href=&quot;https://docs.developers.optimizely.com/content-management-system/v1.0.0-CMS-SaaS/reference/experimental-apis&quot;&gt;CMS API documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Thank You&lt;/h2&gt;
&lt;p&gt;Thank you for your partnership and for helping us continue to improve the CMS developer experience. If you have questions about your migration strategy or encounter challenges during the transition, please reach out to our team&amp;mdash;we&#39;re here to help.&lt;/p&gt;</id><updated>2026-06-18T15:05:21.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Environmental DXP badges</title><link href="https://world.optimizely.com/blogs/matt-pallatt-is-not-a-developer/dates/2026/6/an-indicator-for-your-particular-dxp-environment/" /><id>&lt;p class=&quot;MsoNormal&quot;&gt;I recently released my first Optimizely add-on in a while, that was based on some client feedback on the Optimizely DXP &amp;ndash; they wanted to be able to &lt;a href=&quot;https://nuget.optimizely.com/packages/mp.dxpcontenttransfer/&quot;&gt;push content from preproduction to production&lt;/a&gt; due to their workflow involving content creation taking place in the non-live environment.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Whilst working to make the UI for that tool as clean and helpful as I could - chiseling all the corners and gilding all the lilies, etc. - I ended up putting in a little UI mechanism to allow editors and administrators to see what environment they are in.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Just a little badge, that makes it clear where you are so that you know where your content is coming from...&lt;span style=&quot;mso-no-proof: yes;&quot;&gt;&lt;!--[endif]--&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;img src=&quot;/link/811af14eb171458a837399012fd247c0.aspx&quot; width=&quot;424&quot; height=&quot;155&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Having put it into the content transfer tool, I then came across this&amp;hellip;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;a href=&quot;https://feedback.optimizely.com/forums/966081-cms-paas-content-management-system/suggestions/50995978-ui-change-to-distinguish-prod-from-preprod-and-int&quot;&gt;https://feedback.optimizely.com/forums/966081-cms-paas-content-management-system/suggestions/50995978-ui-change-to-distinguish-prod-from-preprod-and-int&lt;/a&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&amp;hellip;and thought it might actually be nice to also give people who don&amp;rsquo;t want to copy content between DXP environments the same UI niceties to help them know which environment they&amp;rsquo;re working in - so it&#39;s now standalone, removed from content transfer tool, and working in CMS 12 and 13.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;It&#39;s mega simple and also completely configurable on a per environment basis, so you&#39;re not stuck with integration, preproduction and production if your pipeline uses different terminology - TEST, UAT, LIVE, etc.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;img src=&quot;/link/29491f1ced1c4d00a3f24131fa3599c5.aspx&quot; width=&quot;602&quot; height=&quot;201&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;You can nuget it over at&amp;nbsp;&lt;a href=&quot;https://nuget.optimizely.com/packages/mp.dxpenvironmentindicator/&quot;&gt;https://nuget.optimizely.com/packages/mp.dxpenvironmentindicator/&lt;/a&gt; if you&#39;re interested.&lt;/p&gt;</id><updated>2026-06-18T09:02:56.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>CI/CD deployment failure in Optimizely DXP Passportal. .NET 10 and GitHub Runner Compatibility</title><link href="https://world.optimizely.com/blogs/owaiskhan/dates/2026/6/cicd-deployment-failure-in-optimizely-dxp-passportal/" /><id>&lt;h3&gt;.NET 10 and GitHub Runner Compatibility&lt;/h3&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;With the release of .NET 10, the &lt;code&gt;windows-latest&lt;/code&gt; and &lt;code&gt;ubuntu-latest&lt;/code&gt; GitHub Actions runners may use the latest installed .NET SDK by default.&lt;/p&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;As a result, if your application is built using a different SDK version and no SDK version is explicitly specified, the build and deployment process may use the latest SDK available on the runner. This can lead to build failures, deployment issues, or the website failing to start after deployment.&lt;/p&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;To avoid this issue, define the required SDK version in a &lt;code&gt;global.json&lt;/code&gt; file at the root of your repository. This ensures that the build pipeline consistently uses the intended .NET SDK version regardless of updates to the GitHub-hosted runners.&lt;/p&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;sdk&quot;: {
    &quot;version&quot;: &quot;8.0.100&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By pinning the SDK version in &lt;code&gt;global.json&lt;/code&gt;, you can ensure predictable builds and deployments across different environments and runner updates.&lt;/p&gt;</id><updated>2026-06-16T13:43:41.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Integrating Optimizely CMP Assets into an Existing Document Listing Page</title><link href="https://world.optimizely.com/blogs/owaiskhan/dates/2026/6/optimizely-cmp-assets-with-optimizely-episerver-search-index/" /><id>&lt;p class=&quot;isSelectedEnd&quot;&gt;Integrating Optimizely CMP Assets into an Existing Document Listing Page&lt;/p&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;In some projects, there may be a requirement to retrieve assets from Optimizely CMP and display them alongside existing content on the website.&lt;/p&gt;
&lt;h3&gt;Scenario&lt;/h3&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;Consider an existing &lt;strong&gt;Document Listing&lt;/strong&gt; page where all documents are managed in &lt;strong&gt;Optimizely CMS&lt;/strong&gt; and indexed through &lt;strong&gt;Episerver Search &amp;amp; Navigation (Find)&lt;/strong&gt;. A new requirement is introduced to display assets stored in &lt;strong&gt;Optimizely CMP&lt;/strong&gt; within the same listing page, together with CMS documents.&lt;/p&gt;
&lt;h3&gt;Available Options for Retrieving CMP Assets&lt;/h3&gt;
&lt;h4&gt;1. Optimizely CMP Graph API&lt;/h4&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;&lt;a href=&quot;https://cg.optimizely.com/app/graphiql?auth={singlekey&quot;&gt;https://cg.optimizely.com/app/graphiql?auth={singlekey&lt;/a&gt;}&lt;/p&gt;
&lt;h4&gt;2. Optimizely CMP Open REST API&lt;/h4&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;&lt;a href=&quot;https://www.postman.com/optimizely/optimizely-s-postman-collection-s/collection/9vnewz3/optimizely-cmp-open-rest-api?sideView=agentMode&quot;&gt;https://www.postman.com/optimizely/optimizely-s-postman-collection-s/collection/9vnewz3/optimizely-cmp-open-rest-api?sideView=agentMode&lt;/a&gt;&lt;/p&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;&lt;a href=&quot;https://developers.welcomesoftware.com/openapi.html&quot;&gt;https://developers.welcomesoftware.com/openapi.html&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Proposed Solution&lt;/h3&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;Implement a dedicated &lt;strong&gt;CMP API Service&lt;/strong&gt; responsible for retrieving assets from Optimizely CMP. The service should:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fetch asset data using either the CMP Graph API or Open REST API.&lt;/li&gt;
&lt;li&gt;Map CMP asset data to a common document model already used by the existing Document Listing page.&lt;/li&gt;
&lt;li&gt;Merge CMP assets and Optimizely CMS documents into a unified collection.&lt;/li&gt;
&lt;li&gt;Apply existing filtering, sorting, and pagination logic consistently across both content sources.&lt;/li&gt;
&lt;li&gt;Render the combined results on the same listing page without requiring significant changes to the front-end implementation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This approach allows the existing listing page to support content from multiple sources while maintaining a consistent user experience and minimizing changes to the current architecture.&lt;/p&gt;</id><updated>2026-06-16T12:56:07.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Setting up Opti Id SSO with and without SCIM</title><link href="https://world.optimizely.com/blogs/muhammad-talha/dates/2026/6/setting-up-opti-id-sso-with-and-without-scim/" /><id>&lt;p&gt;&lt;span style=&quot;font-size: 11.0pt; line-height: 107%; font-family: &#39;Verdana&#39;,sans-serif; mso-fareast-font-family: Calibri; mso-fareast-theme-font: minor-latin; mso-bidi-font-family: Helvetica; mso-ansi-language: #0C00; mso-fareast-language: EN-US; mso-bidi-language: AR-SA;&quot;&gt;This blog compiled learnings, when setting up Opti Id for large enterprises using SCIM and its pros and cons when setting up Opti Id without SCIM.&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;What is Opti ID and why does provisioning matter?&lt;/h2&gt;
&lt;p class=&quot;MsoNormal&quot; style=&quot;margin-bottom: 6.0pt;&quot;&gt;&lt;span style=&quot;font-family: Verdana, sans-serif; color: rgb(0, 0, 0);&quot;&gt;&lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif;&quot;&gt;Opti ID is Optimizely&#39;s unified identity layer &amp;mdash; a single login that spans all Optimizely products including CMS, CMP, and beyond. Once authenticated, users access every product and instance they are entitled to from a single dashboard, without logging in again&lt;/span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot; style=&quot;margin-bottom: 6.0pt;&quot;&gt;&lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif; color: rgb(0, 0, 0);&quot;&gt;But authentication is only part of the story. For large, fluid user populations, the harder challenge is &lt;strong&gt;provisioning&lt;/strong&gt; &amp;mdash; how user accounts are created, kept current, and deactivated as people join, change roles, or leave. Opti ID supports two distinct models for this.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot; style=&quot;margin-bottom: 6.0pt;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table class=&quot;MsoNormalTable&quot; style=&quot;width: 901px; border-collapse: collapse; border-image: initial; height: 229px; border: medium none currentcolor;&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 229px;&quot;&gt;
&lt;td style=&quot;width: 422.344px; border: 1pt solid rgb(216, 221, 232); background: rgb(255, 248, 245); padding: 9pt 10pt; height: 229px;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot; style=&quot;margin-bottom: 4.0pt;&quot;&gt;&lt;strong&gt;&lt;span style=&quot;font-size: 8.5pt; line-height: 107%; color: #d94f1e; letter-spacing: 5.0pt;&quot;&gt;APPROACH A&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot; style=&quot;margin-bottom: 5.0pt;&quot;&gt;&lt;strong&gt;&lt;span style=&quot;font-size: 13.0pt; line-height: 107%; color: #0a1628;&quot;&gt;SSO without SCIM&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;span style=&quot;color: #1a202c;&quot;&gt;Users authenticate through your identity provider via SAML or OIDC. Access rights, roles, and group assignments are configured and maintained manually within the Opti ID Admin Centre. Works with any standards-compliant IdP.&lt;/span&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 422.344px; border-width: 1pt 1pt 1pt medium; border-style: solid solid solid none; border-color: rgb(216, 221, 232) rgb(216, 221, 232) rgb(216, 221, 232) currentcolor; border-image: initial; background: rgb(232, 241, 255); padding: 9pt 10pt; height: 229px;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot; style=&quot;margin-bottom: 4.0pt;&quot;&gt;&lt;strong&gt;&lt;span style=&quot;font-size: 8.5pt; line-height: 107%; color: #005bd4; letter-spacing: 5.0pt;&quot;&gt;APPROACH B&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot; style=&quot;margin-bottom: 5.0pt;&quot;&gt;&lt;strong&gt;&lt;span style=&quot;font-size: 13.0pt; line-height: 107%; color: #0a1628;&quot;&gt;SSO with SCIM&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;span style=&quot;color: #1a202c;&quot;&gt;Authentication is federated via your IdP, and user provisioning is fully automated via the SCIM protocol. Users and groups defined in your IdP are pushed to Opti ID automatically &amp;mdash; including creation, updates, and deactivation. Requires Entra ID, Okta, or PingOne.&lt;/span&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;&lt;span style=&quot;font-size: 9.0pt; line-height: 107%; color: #005bd4; letter-spacing: 5.0pt;&quot;&gt;▸&lt;span style=&quot;mso-spacerun: yes;&quot;&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-size: 9.0pt; line-height: 107%; color: #005bd4; letter-spacing: 5.0pt;&quot;&gt;APPROACH A&lt;/span&gt;&lt;/h2&gt;
&lt;h2&gt;SSO without SCIM &amp;mdash; Setup &amp;amp; Considerations&lt;/h2&gt;
&lt;p class=&quot;MsoNormal&quot; style=&quot;margin-bottom: 9.0pt;&quot;&gt;&lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif; color: rgb(0, 0, 0);&quot;&gt;This approach uses your identity provider purely for authentication. All user management &amp;mdash; invitations, group assignments, role changes, and deactivations &amp;mdash; is carried out manually inside the Opti ID Admin Centre. Any IdP supporting SAML or OIDC is compatible.&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;&lt;span style=&quot;font-family: verdana, geneva, sans-serif;&quot;&gt;Setup Steps&lt;/span&gt;&lt;/h3&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;margin-bottom: 6.0pt; text-indent: -18.0pt; line-height: normal; mso-list: l0 level1 lfo1;&quot;&gt;&lt;span style=&quot;font-family: verdana, geneva, sans-serif; color: rgb(0, 0, 0);&quot;&gt;&lt;strong&gt;&lt;strong&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;1. &lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif;&quot;&gt;Designate a technical owner &amp;mdash; &lt;/span&gt;&lt;/span&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif;&quot;&gt;One person logs into Opti ID first to establish the organisation and handle initial configuration. This person acts as the first Opti ID administrator and delegates access from there.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;margin-bottom: 6.0pt; text-indent: -18.0pt; line-height: normal; mso-list: l0 level1 lfo1;&quot;&gt;&lt;span style=&quot;font-family: verdana, geneva, sans-serif; color: rgb(0, 0, 0);&quot;&gt;&lt;strong&gt;&lt;strong&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;2. &lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif;&quot;&gt;Configure SSO connections in your IdP &amp;mdash; &lt;/span&gt;&lt;/span&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif;&quot;&gt;Add Opti ID as a SAML or OIDC application in your identity provider. Up to five SSO connections can be configured &amp;mdash; useful for separating geographies, business units, or external users under distinct connections&lt;/span&gt;&lt;/span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;margin-bottom: 6.0pt; text-indent: -18.0pt; line-height: normal; mso-list: l0 level1 lfo1;&quot;&gt;&lt;span style=&quot;font-family: verdana, geneva, sans-serif; color: rgb(0, 0, 0);&quot;&gt;&lt;strong&gt;&lt;strong&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;3. &lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif;&quot;&gt;Register your organisational domains &amp;mdash; &lt;/span&gt;&lt;/span&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif;&quot;&gt;In Opti ID Admin Centre under Settings &amp;gt; SSO/ Domains, register the SSO / DNS domains your internal users authenticate with. This auto-routes users to the correct SSO connection at login.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;margin-bottom: 6.0pt; text-indent: -18.0pt; line-height: normal; mso-list: l0 level1 lfo1;&quot;&gt;&lt;span style=&quot;font-family: verdana, geneva, sans-serif; color: rgb(0, 0, 0);&quot;&gt;&lt;strong&gt;&lt;strong&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;4. &lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif;&quot;&gt;Handle external and partner users via selective local login &amp;mdash; &lt;/span&gt;&lt;/span&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif;&quot;&gt;Enable selective local login to give partner individuals a standalone Opti ID account (email + password, MFA optional). Internal users continue authenticating via SSO; external users get a local account managed per-user in the Admin Centre.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: verdana, geneva, sans-serif; color: rgb(0, 0, 0);&quot;&gt;&lt;strong&gt;&lt;span style=&quot;font-size: 11pt; line-height: 107%;&quot;&gt;5. &lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif;&quot;&gt;Create groups and assign roles manually &amp;mdash; &lt;/span&gt;&lt;/span&gt;&lt;/strong&gt;&lt;span style=&quot;font-size: 11pt; line-height: 107%;&quot;&gt;&lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif;&quot;&gt;Build a group structure in the Admin Centre reflecting your organisational needs &amp;mdash; by region, product, or access tier. Invite users individually, assign them to groups, and those groups determine which product instances and roles each user can access&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-size: 11pt; line-height: 107%;&quot;&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table class=&quot;MsoNormalTable&quot; style=&quot;width: 1121px; border-collapse: collapse; height: 214px; border-image: initial; border: medium none currentcolor;&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 214px;&quot;&gt;
&lt;td style=&quot;width: 531.688px; border: 1pt solid rgb(216, 221, 232); background: rgb(240, 255, 248); padding: 9pt 10pt; height: 214px;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot; style=&quot;margin-bottom: 6.0pt;&quot;&gt;&lt;strong&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; color: #00a86b;&quot;&gt;+ ADVANTAGES&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;text-indent: -16.0pt; line-height: normal; mso-list: l0 level1 lfo1; margin: 0cm 0cm 4.0pt 32.0pt;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; mso-fareast-font-family: Verdana; mso-bidi-font-family: Verdana;&quot;&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;bull;&lt;span style=&quot;font: 7.0pt &#39;Times New Roman&#39;;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; color: #1a202c;&quot;&gt;Works with any SAML or OIDC identity provider &amp;mdash; no vendor restriction&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;text-indent: -16.0pt; line-height: normal; mso-list: l0 level1 lfo1; margin: 0cm 0cm 4.0pt 32.0pt;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; mso-fareast-font-family: Verdana; mso-bidi-font-family: Verdana;&quot;&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;bull;&lt;span style=&quot;font: 7.0pt &#39;Times New Roman&#39;;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; color: #1a202c;&quot;&gt;Simpler to configure and operate; fewer integrated components&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;text-indent: -16.0pt; line-height: normal; mso-list: l0 level1 lfo1; margin: 0cm 0cm 4.0pt 32.0pt;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; mso-fareast-font-family: Verdana; mso-bidi-font-family: Verdana;&quot;&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;bull;&lt;span style=&quot;font: 7.0pt &#39;Times New Roman&#39;;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; color: #1a202c;&quot;&gt;Partners and external users onboarded via local login without changes to your corporate IdP&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;text-indent: -16.0pt; line-height: normal; mso-list: l0 level1 lfo1; margin: 0cm 0cm 4.0pt 32.0pt;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; mso-fareast-font-family: Verdana; mso-bidi-font-family: Verdana;&quot;&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;bull;&lt;span style=&quot;font: 7.0pt &#39;Times New Roman&#39;;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; color: #1a202c;&quot;&gt;Lower initial setup risk; easier to troubleshoot&lt;/span&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 533px; border-width: 1pt 1pt 1pt medium; border-style: solid solid solid none; border-color: rgb(216, 221, 232) rgb(216, 221, 232) rgb(216, 221, 232) currentcolor; border-image: initial; background: rgb(255, 245, 245); padding: 9pt 10pt; height: 214px;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot; style=&quot;margin-bottom: 6.0pt;&quot;&gt;&lt;strong&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; color: #e53e3e;&quot;&gt;&amp;minus; DISADVANTAGES&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;text-indent: -16.0pt; line-height: normal; mso-list: l0 level1 lfo1; margin: 0cm 0cm 4.0pt 32.0pt;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; mso-fareast-font-family: Verdana; mso-bidi-font-family: Verdana;&quot;&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;bull;&lt;span style=&quot;font: 7.0pt &#39;Times New Roman&#39;;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; color: #1a202c;&quot;&gt;No automated offboarding &amp;mdash; access must be manually revoked when users leave&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;text-indent: -16.0pt; line-height: normal; mso-list: l0 level1 lfo1; margin: 0cm 0cm 4.0pt 32.0pt;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; mso-fareast-font-family: Verdana; mso-bidi-font-family: Verdana;&quot;&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;bull;&lt;span style=&quot;font: 7.0pt &#39;Times New Roman&#39;;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; color: #1a202c;&quot;&gt;At large scale, individual invitations and role assignments are operationally unsustainable&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;text-indent: -16.0pt; line-height: normal; mso-list: l0 level1 lfo1; margin: 0cm 0cm 4.0pt 32.0pt;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; mso-fareast-font-family: Verdana; mso-bidi-font-family: Verdana;&quot;&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;bull;&lt;span style=&quot;font: 7.0pt &#39;Times New Roman&#39;;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; color: #1a202c;&quot;&gt;Compliance and audit readiness require continuous manual effort&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;text-indent: -16.0pt; line-height: normal; mso-list: l0 level1 lfo1; margin: 0cm 0cm 4.0pt 32.0pt;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; mso-fareast-font-family: Verdana; mso-bidi-font-family: Verdana;&quot;&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;bull;&lt;span style=&quot;font: 7.0pt &#39;Times New Roman&#39;;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; color: #1a202c;&quot;&gt;High risk of orphaned accounts if offboarding processes are not rigorously followed&lt;/span&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p class=&quot;MsoNormal&quot; style=&quot;margin: 2.0pt 0cm 3.0pt 0cm;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot; style=&quot;margin: 2.0pt 0cm 3.0pt 0cm;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot; style=&quot;margin: 2.0pt 0cm 3.0pt 0cm;&quot;&gt;&lt;strong&gt;&lt;span style=&quot;font-size: 9.0pt; line-height: 107%; color: #005bd4; letter-spacing: 5.0pt;&quot;&gt;▸&lt;span style=&quot;mso-spacerun: yes;&quot;&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/strong&gt;&lt;strong&gt;&lt;span style=&quot;font-size: 9.0pt; line-height: 107%; color: #005bd4; letter-spacing: 5.0pt;&quot;&gt;APPROACH B&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;SSO with SCIM &amp;mdash; Setup &amp;amp; Considerations&lt;/h2&gt;
&lt;p class=&quot;MsoNormal&quot; style=&quot;margin-bottom: 9.0pt;&quot;&gt;&lt;span style=&quot;font-size: 11.0pt; line-height: 107%; font-family: &#39;Verdana&#39;,sans-serif; mso-fareast-font-family: Calibri; mso-fareast-theme-font: minor-latin; mso-bidi-font-family: Helvetica; color: #1a202c; mso-ansi-language: #0C00; mso-fareast-language: EN-US; mso-bidi-language: AR-SA;&quot;&gt;SCIM (System for Cross-domain Identity Management) extends your SSO setup by automating the full user lifecycle. Your identity provider becomes the single source of truth &amp;mdash; users and groups defined are pushed to Opti ID automatically, including deprovisioning when someone leaves. Officially supported with &lt;strong&gt;Microsoft Entra ID, Okta, and PingOne&lt;/strong&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;Setup Steps&lt;/h3&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;margin-bottom: 6.0pt; text-indent: -18.0pt; line-height: normal; mso-list: l0 level1 lfo1;&quot;&gt;&lt;span style=&quot;font-family: verdana, geneva, sans-serif; color: rgb(0, 0, 0);&quot;&gt;&lt;strong&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 1. &lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif;&quot;&gt;Designate a technical owner and configure SSO &amp;mdash; &lt;/span&gt;&lt;/strong&gt;&lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif;&quot;&gt;Same starting point as Approach A. Configure your primary SSO connection first &amp;mdash; the SCIM configuration option only becomes available after at least one SSO connection is active.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;margin-bottom: 6.0pt; text-indent: -18.0pt; line-height: normal; mso-list: l0 level1 lfo1;&quot;&gt;&lt;span style=&quot;font-family: verdana, geneva, sans-serif; color: rgb(0, 0, 0);&quot;&gt;&lt;strong&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 2. &lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif;&quot;&gt;Generate a SCIM token in Opti ID &amp;mdash; &lt;/span&gt;&lt;/strong&gt;&lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif;&quot;&gt;Navigate to Admin Centre &amp;gt; Settings &amp;gt; Authentication &amp;gt; SCIM. Select the SSO connection you want to associate SCIM with and generate the bearer token. This token authenticates your IdP when it pushes user data to Opti ID.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;margin-bottom: 6.0pt; text-indent: -18.0pt; line-height: normal; mso-list: l0 level1 lfo1;&quot;&gt;&lt;span style=&quot;font-family: verdana, geneva, sans-serif; color: rgb(0, 0, 0);&quot;&gt;&lt;strong&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 3. &lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif;&quot;&gt;Create a SCIM provisioning app in your IdP &amp;mdash; &lt;/span&gt;&lt;/strong&gt;&lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif;&quot;&gt;Most enterprise IdPs require a separate application for SCIM provisioning, distinct from the SSO app. Configure it using the Opti ID SCIM endpoint URL and token generated above. Some IdPs (such as Okta) allow combining SSO and SCIM in a single app &amp;mdash; check your provider&#39;s documentation.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;margin-bottom: 6.0pt; text-indent: -18.0pt; line-height: normal; mso-list: l0 level1 lfo1;&quot;&gt;&lt;span style=&quot;font-family: verdana, geneva, sans-serif; color: rgb(0, 0, 0);&quot;&gt;&lt;strong&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&lt;span style=&quot;font-style: normal; font-variant: normal; font-size-adjust: none; font-language-override: normal; font-kerning: auto; font-optical-sizing: auto; font-feature-settings: normal; font-variation-settings: normal; font-weight: normal; font-stretch: normal; font-size: 7pt; line-height: normal;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/strong&gt;&lt;strong&gt;4. &lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif;&quot;&gt;Design your group structure and push groups from your IdP &amp;mdash; &lt;/span&gt;&lt;/strong&gt;&lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif;&quot;&gt;Create groups in your IdP corresponding to the access tiers you need &amp;mdash; by region, role, or product. Assign users to these groups. When SCIM syncs, those groups and members are automatically created in Opti ID. Future membership changes propagate within minutes.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;margin-bottom: 6.0pt; text-indent: -18.0pt; line-height: normal; mso-list: l0 level1 lfo1;&quot;&gt;&lt;span style=&quot;font-family: verdana, geneva, sans-serif; color: rgb(0, 0, 0);&quot;&gt;&lt;strong&gt;&lt;span style=&quot;font-size: 11pt; line-height: 107%;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;5. &lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif;&quot;&gt;Map groups to product instances and roles in the Admin Centre &amp;mdash; &lt;/span&gt;&lt;/span&gt;&lt;/strong&gt;&lt;span style=&quot;font-size: 11pt; line-height: 107%;&quot;&gt;&lt;span style=&quot;font-size: 11pt; line-height: 107%; font-family: Verdana, sans-serif;&quot;&gt;SCIM delivers users and group memberships &amp;mdash; it does not configure product instance access or role assignments. In the Admin Centre, map each SCIM-synced group to the appropriate instances and permission levels.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;margin-bottom: 6.0pt; text-indent: -18.0pt; line-height: normal; mso-list: l0 level1 lfo1;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table class=&quot;MsoNormalTable&quot; style=&quot;width: 1217px; border-collapse: collapse; border-image: initial; border: medium none currentcolor;&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;mso-yfti-irow: 0; mso-yfti-firstrow: yes; mso-yfti-lastrow: yes;&quot;&gt;
&lt;td style=&quot;width: 580.344px; border: 1pt solid rgb(216, 221, 232); background: rgb(240, 255, 248); padding: 9pt 10pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot; style=&quot;margin-bottom: 6.0pt;&quot;&gt;&lt;strong&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; color: #00a86b;&quot;&gt;+ ADVANTAGES&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;text-indent: -16.0pt; line-height: normal; mso-list: l0 level1 lfo1; margin: 0cm 0cm 4.0pt 32.0pt;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; mso-fareast-font-family: Verdana; mso-bidi-font-family: Verdana;&quot;&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;bull;&lt;span style=&quot;font: 7.0pt &#39;Times New Roman&#39;;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; color: #1a202c;&quot;&gt;Full user lifecycle automated &amp;mdash; joiners, movers, and leavers handled at IdP level without manual Opti ID steps&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;text-indent: -16.0pt; line-height: normal; mso-list: l0 level1 lfo1; margin: 0cm 0cm 4.0pt 32.0pt;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; mso-fareast-font-family: Verdana; mso-bidi-font-family: Verdana;&quot;&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;bull;&lt;span style=&quot;font: 7.0pt &#39;Times New Roman&#39;;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; color: #1a202c;&quot;&gt;Scales to large user populations with no proportional increase in admin overhead&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;text-indent: -16.0pt; line-height: normal; mso-list: l0 level1 lfo1; margin: 0cm 0cm 4.0pt 32.0pt;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; mso-fareast-font-family: Verdana; mso-bidi-font-family: Verdana;&quot;&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;bull;&lt;span style=&quot;font: 7.0pt &#39;Times New Roman&#39;;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; color: #1a202c;&quot;&gt;Access revocation is immediate when a user is removed from an IdP group&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;text-indent: -16.0pt; line-height: normal; mso-list: l0 level1 lfo1; margin: 0cm 0cm 4.0pt 32.0pt;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; mso-fareast-font-family: Verdana; mso-bidi-font-family: Verdana;&quot;&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;bull;&lt;span style=&quot;font: 7.0pt &#39;Times New Roman&#39;;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; color: #1a202c;&quot;&gt;Single source of truth &amp;mdash; your IdP directory drives everything; Opti ID stays in sync&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;text-indent: -16.0pt; line-height: normal; mso-list: l0 level1 lfo1; margin: 0cm 0cm 4.0pt 32.0pt;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; mso-fareast-font-family: Verdana; mso-bidi-font-family: Verdana;&quot;&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;bull;&lt;span style=&quot;font: 7.0pt &#39;Times New Roman&#39;;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; color: #1a202c;&quot;&gt;Significantly reduces compliance audit effort &amp;mdash; IdP logs are the system of record&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;text-indent: -16.0pt; line-height: normal; mso-list: l0 level1 lfo1; margin: 0cm 0cm 4.0pt 32.0pt;&quot;&gt;&lt;span style=&quot;font-size: 11.0pt; line-height: 107%; font-family: &#39;Verdana&#39;,sans-serif; mso-fareast-font-family: Calibri; mso-fareast-theme-font: minor-latin; mso-bidi-font-family: Arial; mso-bidi-theme-font: minor-bidi; color: #1a202c; mso-ansi-language: #0C00; mso-fareast-language: EN-US; mso-bidi-language: AR-SA;&quot;&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; mso-fareast-font-family: Verdana; mso-bidi-font-family: Verdana;&quot;&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;bull;&lt;span style=&quot;font: 7.0pt &#39;Times New Roman&#39;;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;Eliminates orphaned account risk through automated deprovisioning&lt;/span&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 580.344px; border-width: 1pt 1pt 1pt medium; border-style: solid solid solid none; border-color: rgb(216, 221, 232) rgb(216, 221, 232) rgb(216, 221, 232) currentcolor; border-image: initial; background: rgb(255, 245, 245); padding: 9pt 10pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot; style=&quot;margin-bottom: 6.0pt;&quot;&gt;&lt;strong&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; color: #e53e3e;&quot;&gt;&amp;minus; DISADVANTAGES&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;text-indent: -16.0pt; line-height: normal; mso-list: l0 level1 lfo1; margin: 0cm 0cm 4.0pt 32.0pt;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; mso-fareast-font-family: Verdana; mso-bidi-font-family: Verdana;&quot;&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;bull;&lt;span style=&quot;font: 7.0pt &#39;Times New Roman&#39;;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; color: #1a202c;&quot;&gt;Only officially supported with Entra ID, Okta, and PingOne&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;text-indent: -16.0pt; line-height: normal; mso-list: l0 level1 lfo1; margin: 0cm 0cm 4.0pt 32.0pt;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; mso-fareast-font-family: Verdana; mso-bidi-font-family: Verdana;&quot;&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;bull;&lt;span style=&quot;font: 7.0pt &#39;Times New Roman&#39;;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; color: #1a202c;&quot;&gt;SCIM is restricted to one SSO connection &amp;mdash; separate IdP tenants per region means only one gets full automation&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;text-indent: -16.0pt; line-height: normal; mso-list: l0 level1 lfo1; margin: 0cm 0cm 4.0pt 32.0pt;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; mso-fareast-font-family: Verdana; mso-bidi-font-family: Verdana;&quot;&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;bull;&lt;span style=&quot;font: 7.0pt &#39;Times New Roman&#39;;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; color: #1a202c;&quot;&gt;Product instance assignments and role mappings still require one-time manual setup in the Admin Centre&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;text-indent: -16.0pt; line-height: normal; mso-list: l0 level1 lfo1; margin: 0cm 0cm 4.0pt 32.0pt;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; mso-fareast-font-family: Verdana; mso-bidi-font-family: Verdana;&quot;&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;bull;&lt;span style=&quot;font: 7.0pt &#39;Times New Roman&#39;;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; color: #1a202c;&quot;&gt;Initial setup is more complex &amp;mdash; requires two separate IdP apps (SSO + SCIM)&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoListParagraph&quot; style=&quot;text-indent: -16.0pt; line-height: normal; mso-list: l0 level1 lfo1; margin: 0cm 0cm 4.0pt 32.0pt;&quot;&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; color: #1a202c;&quot;&gt;&lt;span style=&quot;font-family: &#39;Verdana&#39;,sans-serif; mso-fareast-font-family: Verdana; mso-bidi-font-family: Verdana;&quot;&gt;&lt;span style=&quot;mso-list: Ignore;&quot;&gt;&amp;bull;&lt;span style=&quot;font: 7.0pt &#39;Times New Roman&#39;;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;Partner users on external domains cannot share the SCIM-enabled connection&lt;/span&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</id><updated>2026-06-16T12:37:47.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Understanding Optimizely Opal Cost vs Value</title><link href="https://world.optimizely.com/blogs/K-Khan-/Dates/2026/6/understanding-optimizely-opal-cost-vs-value/" /><id>&lt;p&gt;Every Opal conversation seems to start with the same question: &quot;What does it cost?&quot; Fair, but it&#39;s only half the question. Cost tells you what you&#39;ll spend. Value tells you whether it was worth it, It can be completely different for a retailer - chasing campaign speed, a bank - protecting compliance, or an experimentation team - measuring throughput.&lt;/p&gt;
&lt;p&gt;In my follow-up to &quot;Understanding Optimizely Opal Cost,&quot; I try to determineif we can actually measure Opal&#39;s value.&lt;br /&gt;&lt;br /&gt;&lt;a href=&quot;https://www.linkedin.com/pulse/optimizely-opal-cost-vs-value-khurram-khan-vsume/&quot;&gt;https://www.linkedin.com/pulse/optimizely-opal-cost-vs-value-khurram-khan-vsume/&lt;/a&gt;&amp;nbsp;&lt;/p&gt;</id><updated>2026-06-15T14:02:57.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Leverage — The CMS Edits One Item at a Time. The Work Doesn&#39;t.</title><link href="https://umage.ai/insights/editor-power-tools-leverage/" /><id>Editorial work arrives in batches — a product rename across two hundred support articles, five hundred FAQs that should become blocks, an SEO refresh across a whole section. The productivity group in Editor Power Tools exists for exactly that: bulk editing, bulk import, and the views that keep a site&#39;s operational rhythm visible.</id><updated>2026-06-15T00:00:00.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>“Learning by Doing – Optimizely OPAL Series” | Episode 02 is Live!</title><link href="https://optimizleyunboxed.blogspot.com/2026/06/learning-by-doing-optimizely-opal.html" /><id>&lt;div style=&quot;text-align: left;&quot;&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3TSnigp1qGyhNZM3pexS2hxFe0BQFA2Ichdjro5EOBY7MVlyyeEF6RIA0sWbJoBnnGPvBa1mh4SNwpLXmIdNGmN2hfsKJcKqKZQoCxULwhKLFzTI1SPKrQ_8WmMG3iC5zrlCbX0PeNfeoWmsDbWE_e_vuMyzvpsow7q1p3z3sECrGPtCEE_KOKHv2PhA/s1024/OptimizleyBlogBanner.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1024&quot; data-original-width=&quot;1024&quot; height=&quot;636&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3TSnigp1qGyhNZM3pexS2hxFe0BQFA2Ichdjro5EOBY7MVlyyeEF6RIA0sWbJoBnnGPvBa1mh4SNwpLXmIdNGmN2hfsKJcKqKZQoCxULwhKLFzTI1SPKrQ_8WmMG3iC5zrlCbX0PeNfeoWmsDbWE_e_vuMyzvpsow7q1p3z3sECrGPtCEE_KOKHv2PhA/w636-h636/OptimizleyBlogBanner.png&quot; width=&quot;636&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;&lt;br /&gt;Introduction&lt;/span&gt;&lt;/h2&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;With Optimizely OPAL, we’re not just generating content—we’re designing intelligent workflows.&lt;br /&gt;But after working with teams and experimenting hands-on, I noticed a recurring pattern:&lt;br /&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Many are using OPAL&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;But not getting consistent results&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;And often struggling to move beyond basic prompts&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;The reason is simple:&lt;br /&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;OPAL is only as powerful as the prompts you give it.&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;&lt;br /&gt;That’s exactly what Episode 02 focuses on.&lt;br /&gt;&lt;/span&gt;&lt;h3 style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;&lt;a href=&quot;https://youtu.be/95Ox0sXLs9Y&quot; target=&quot;_blank&quot;&gt;Episode 02: Prompt to Better Prompt&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;The second episode is now live—and this is where things start getting practical.&lt;br /&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;&amp;nbsp;Because this is not about using AI…&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;&amp;nbsp;This is about using AI effectively&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&amp;nbsp;In this episode, we cover&lt;br /&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;&amp;nbsp;What is Prompt Engineering and why it matters&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;&amp;nbsp;Why most AI outputs fail (and how to fix them)&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/span&gt;&lt;/div&gt;&lt;h3 style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Popular frameworks:&lt;/span&gt;&lt;/h3&gt;&lt;div style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;RACE (Speed &amp;amp; simplicity)&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;CRISPE (Creativity &amp;amp; refinement)&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;COSTAR (Marketing &amp;amp; audience alignment)&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;CLEAR (Most practical for OPAL)&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;KERNEL (Automation &amp;amp; consistency)&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;br /&gt;&lt;/span&gt;&lt;h3 style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;The Shift: Prompt → Better Prompt&lt;/span&gt;&lt;/h3&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;One of the most important ideas in this episode is a simple but powerful shift:&lt;/span&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;&lt;br /&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;&amp;nbsp;Don’t struggle writing prompts manually&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Instead:&lt;/span&gt;&lt;/li&gt;&lt;ul&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Use tools like Copilot / ChatGPT&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Ask them to generate structured prompts (using frameworks)&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Refine with your business context&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Use them inside OPAL&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/ul&gt;&amp;nbsp;Key Insight&lt;br /&gt;Use AI to improve your prompts → Then use OPAL to execute them&lt;br /&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;text-align: left;&quot;&gt;&lt;h3 style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Real Use Case Covered&lt;/span&gt;&lt;/h3&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;We also walk through a real-world enterprise scenario:&lt;br /&gt;Building an SEO Blog Generator Agent&lt;br /&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Where the agent can generate:&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;SEO-optimized titles&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Meta descriptions&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Structured headings (H2, H3)&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Engaging introductions&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Full content body&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Internal &amp;amp; external link suggestions&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Call-to-action&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Related keywords&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/span&gt;&lt;h3 style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;&amp;nbsp;Why this matters&lt;/span&gt;&lt;/h3&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;This is not a simple prompt.&lt;br /&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;It requires:&lt;/span&gt;&lt;/li&gt;&lt;ul&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Structure&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Context&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Constraints&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Output definition&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/ul&gt;&amp;nbsp;And that’s where prompt frameworks become critical.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;text-align: left;&quot;&gt;&lt;h3 style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;&amp;nbsp;What Makes This Episode Different?&lt;/span&gt;&lt;/h3&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Instead of just explaining frameworks…&lt;br /&gt;This episode focuses on:&lt;br /&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;&amp;nbsp;When to use each framework&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;&amp;nbsp;How to think structurally&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;&amp;nbsp;How to apply it in OPAL&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;&amp;nbsp;How to scale prompts into reusable agents&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&amp;nbsp;Who Should Watch This Episode?&lt;br /&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;If you fall into any of these categories, this episode is for you:&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Optimizely developers &amp;amp; architects&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;Digital marketers working with AI&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;DXP practitioners exploring OPAL&lt;/span&gt;&lt;/li&gt;&lt;li&gt;Anyone struggling with inconsistent AI outputs&lt;/li&gt;&lt;/ul&gt;Watch the video here:&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;text-align: left;&quot;&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;iframe allowfullscreen=&quot;&quot; class=&quot;BLOG_video_class&quot; height=&quot;354&quot; src=&quot;https://www.youtube.com/embed/95Ox0sXLs9Y&quot; width=&quot;426&quot; youtube-src-id=&quot;95Ox0sXLs9Y&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;br /&gt;&lt;span style=&quot;font-family: verdana;&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/div&gt;</id><updated>2026-06-14T17:53:38.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Content Variations in CMS 13, Part 3: Audiences vs Audiences</title><link href="https://world.optimizely.com/blogs/piotr-nowak---optimizely--azure/dates/2026/6/content-variations-in-cms-13-part-3-audiences-vs-audiences/" /><id>&lt;blockquote style=&quot;margin: 24px 0; padding: 18px 20px; background: #f5f5f4; border: 1px solid #e7e5e4; border-radius: 6px;&quot;&gt;
&lt;p&gt;&lt;strong&gt;Executive summary.&lt;/strong&gt; Part 2 left the experiment running against &lt;em&gt;Everyone&lt;/em&gt;. Real projects don&#39;t look like that. So this part wires those same CMS Content Variations to two rival audience engines and measures what each actually does. Every number below comes from a live CMS 13.1.0 + FX SDK instance. A targeted delivery served &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;variant_a&lt;/span&gt; to 60 of 60 mobile visitors; at 100% allocation that is deterministic, so a single miss would be a bug rather than noise. The desktop experiment held its 33/33/34 split (&amp;chi;&amp;sup2; = 1.465, n = 300). The MVC head and the headless Next.js head agreed on the arm &lt;em&gt;and&lt;/em&gt; the rule for 20 of 20 visitor/device pairs, with zero coordination code. The same runs found the boundary. CMS Audiences evaluate only inside the CMS runtime: a personalized block vanished from Optimizely Graph entirely, invisible to every headless consumer, and nothing threw an error. If your requirement says &amp;ldquo;both heads&amp;rdquo;, the audience decision is made before anyone asks about features.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;Parts 1 and 2 built the machine: Content Variations as the quiet hero of CMS 13, then Feature Experimentation as its stats engine, with one string, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;VariationKey == variation name&lt;/span&gt;, as the entire integration contract. This part answers the question that contract postpones: who decides who the visitor is? CMS 13 has an answer. FX has a different answer. Both are called Audiences now, and they are not the same thing. Everything below ran on CMS 13.1.0 with the C# SDK 4.3.0 and the JavaScript SDK 6.4.0. Every number is from those runs.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;In this part:&lt;/strong&gt; the Audiences naming collision &amp;middot; the three-layer opt-in (and the startup self-check it forced) &amp;middot; the five-attribute pipeline with native geolocation &amp;middot; targeted delivery semantics the docs bury &amp;middot; the coexistence proof and the Graph void &amp;middot; the day the headless head went dark &amp;middot; three QA override levels &amp;middot; a troubleshooting runbook &amp;middot; eight sharp edges ranked by blood &amp;middot; FAQ &amp;middot; glossary.&lt;/p&gt;
&lt;h2&gt;The machine at a glance&lt;/h2&gt;
&lt;p&gt;One picture before the prose. Both heads serve the same experiment from the same definitions. The only shared state is configuration-as-data:&lt;/p&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center; max-width: 800px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; border: 1px solid #e1e4e8; border-radius: 6px;&quot; src=&quot;https://pino-labs.com/blog/content-variations-cms-13-part-3/machine-at-a-glance.svg&quot; alt=&quot;Both heads read the same FX datafile and CMS Graph index. The MVC head runs Decide, loads the CMS variation and renders, with the CMS Audience filtering content-area items server-side. The Next.js head runs the same decision and renders via a Graph query, but the personalized items are absent from Graph.&quot; /&gt;
&lt;figcaption style=&quot;font-size: 13px; color: #666; margin-top: 6px;&quot;&gt;&lt;em&gt;Both heads serve the same experiment from the same definitions; only the Graph path drops the personalized item. The CMS Audience layer has no representation on the headless route.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The CMS Audience layer simply has no representation on the Graph path. The rest of this article is the evidence for every arrow.&lt;/p&gt;
&lt;h2&gt;Two engines, one word: both of them are called Audiences now&lt;/h2&gt;
&lt;p&gt;Start with the vocabulary trap, because it will find you anyway. Visitor Groups have been rebranding to &lt;strong&gt;Audiences&lt;/strong&gt; since the CMS 12 admin redesign. CMS 13 finishes the job: the admin package describes itself as the &amp;ldquo;audiences management UI&amp;rdquo;, the docs say &amp;ldquo;an audience (formerly called a visitor group)&amp;rdquo;, and the old name survives mostly in role names and API types. Feature Experimentation has shipped &lt;strong&gt;Audiences&lt;/strong&gt; for years. Same word, two engines, and they answer the same question with opposite architectures:&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin: 20px 0; font-size: 14px;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;&amp;nbsp;&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;CMS Audiences (formerly Visitor Groups)&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;FX Audiences&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Evaluated by&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;The CMS, server-side, during rendering&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Every SDK, in-process, at &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;Decide()&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Evaluation input&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;IPrincipal&lt;/span&gt; + &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;HttpContext&lt;/span&gt;, full request, roles, visit history&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Only the attributes your code passed at context creation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Definition lives in&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;CMS admin (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;VisitorGroupAdmins&lt;/span&gt; role)&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;FX dashboard, ships in the datafile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Personalizes&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;A &lt;strong&gt;fragment&lt;/strong&gt;, one content-area item&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;A &lt;strong&gt;page version&lt;/strong&gt;, via the variation key contract from part 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Works headless&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;No, and the docs say so out loud&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Yes, the evaluation is a pure function, portable by construction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Measurement&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&amp;ldquo;Enable statistics&amp;rdquo; view counts, with no exposures, no conversions, no significance&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;The stats engine: exposures, conversions, significance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The last two rows are the ones that matter. A CMS Audience can reach into everything the CMS knows about the request, and pays for it by existing only where the CMS renders. An FX Audience knows nothing you didn&#39;t tell it, and pays for &lt;em&gt;that&lt;/em&gt; with a pipeline you must build. But the function (datafile + attributes) &amp;rarr; bool runs identically in C#, in Node, in anything with an SDK. Keep that trade in view. Every decision below falls out of it.&lt;/p&gt;
&lt;p&gt;One governance row to add to part 2&#39;s who-does-what table: audience definitions get owners too. CMS Audiences belong to whoever holds &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;VisitorGroupAdmins&lt;/span&gt;. FX Audiences belong to the dashboard. Attribute &lt;em&gt;keys&lt;/em&gt;, though, are an API contract between the codebase and the dashboard: a developer renames &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;device&lt;/span&gt; and the marketer&#39;s audience quietly stops matching. Write them down somewhere both sides actually read.&lt;/p&gt;
&lt;h2&gt;Switching the engine on: in CMS 13, personalization is opt-in three layers deep&lt;/h2&gt;
&lt;p&gt;Part 2&#39;s sharp-edges list opened with a package that crashed CMS 13 at startup. This part&#39;s equivalent is gentler and stranger: in CMS 13, Visitor Groups are not in the box you already have. The &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;EPiServer.CMS&lt;/span&gt; metapackage ships &lt;strong&gt;neither the evaluation core nor the UI&lt;/strong&gt;. The install docs state the philosophy plainly: &amp;ldquo;every NuGet package your project references must have its services explicitly registered, or the application fails at startup.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The documented pair of calls is &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;services.AddVisitorGroupsMvc().AddVisitorGroupsUI()&lt;/span&gt;, which also adds the &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;EPiServer.CMS.UI.VisitorGroups&lt;/span&gt; 13.1.0 package.&lt;/p&gt;
&lt;p&gt;I learned what each one carries the honest way, by registering less than that. &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;AddVisitorGroupsUI()&lt;/span&gt; alone boots clean. Then the Audiences screen throws &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;Unable to resolve service for type &#39;IVisitorGroupCriterionRepository&#39;&lt;/span&gt; the moment an editor clicks it: the screen&#39;s own API services, the ones that load criterion lists and persist rules, resolve against registrations the UI package does not carry. A UI that renders is not a UI that saves. Registering the two internal layers that error message points at fixes the admin screen. It also quietly skips the &lt;em&gt;rendering&lt;/em&gt; layer, so personalized blocks would render for everyone. That&#39;s the worst possible failure mode: a working configuration surface for a filter that doesn&#39;t run. &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;AddVisitorGroupsMvc()&lt;/span&gt; wraps all of it: repositories, the built-in criteria, the content-area rendering filter, and the &amp;ldquo;View as Audience&amp;rdquo; impersonation service.&lt;/p&gt;
&lt;p&gt;Notice what the docs promised and what actually happened. The promise: misregistration &lt;em&gt;fails at startup&lt;/em&gt;. The observation: it failed at click time, with a green boot log. So the demo project now resolves the services the Audiences API needs at startup, in Development, and logs every registered criterion: &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;DeviceCriterion, DisplayChannelCriterion, &amp;hellip; TimeOfDayCriterion, &amp;hellip;&lt;/span&gt; (22 of them), making the documentation&#39;s promise true at exactly the layer where it broke. Twenty-two criteria, including &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;RoleCriterion&lt;/span&gt; and &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;UserProfileCriterion&lt;/span&gt; that the docs list doesn&#39;t mention, plus &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;DeviceCriterion&lt;/span&gt;, which is ours. The whole class fits on a slide, and it earns the line this demo is built around: one definition of &amp;ldquo;who you are&amp;rdquo;, two engines reading it.&lt;/p&gt;
&lt;pre style=&quot;background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 6px; padding: 16px; overflow: auto; font-family: Consolas,Monaco,monospace; font-size: 13px; line-height: 1.5;&quot;&gt;[VisitorGroupCriterion(
    Category = &quot;Technical&quot;,
    DisplayName = &quot;Device class&quot;,
    Description = &quot;Matches the visitor&#39;s device class (mobile / tablet / desktop) &quot;
                + &quot;using the same User-Agent heuristic that feeds the FX &#39;device&#39; attribute.&quot;)]
public class DeviceCriterion(IVisitorAttributesProvider attributesProvider)
    : CriterionBase&amp;lt;DeviceCriterionModel&amp;gt;
{
    public override bool IsMatch(IPrincipal principal, HttpContext httpContext)
    {
        // Editors type the value by hand - trim + lowercase forgives &quot;Mobile &quot;
        // (the FX exact matcher would not; see sharp edge #1).
        var expected = Model.Device?.Trim().ToLowerInvariant();
        return !string.IsNullOrEmpty(expected)
               &amp;amp;&amp;amp; attributesProvider.GetAttributes().TryGetValue(&quot;device&quot;, out var device)
               &amp;amp;&amp;amp; Equals(device, expected);
    }
}&lt;/pre&gt;
&lt;p&gt;Constructor injection works (the built-in geographic criteria take &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;IClientGeolocationResolver&lt;/span&gt; the same way), &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;[VisitorGroupCriterion]&lt;/span&gt; registers the class through plugin scanning, and the model is &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;CriterionModelBase&lt;/span&gt; with one string property and a &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;Copy() =&amp;gt; ShallowCopy()&lt;/span&gt;. The built-in &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;OSBrowserCriterion&lt;/span&gt; could classify devices without custom code. Ours exists for consistency with the FX attribute, not for extra capability.&lt;/p&gt;
&lt;h3&gt;Reference card: the CMS 13 personalization bill of materials&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin: 20px 0; font-size: 14px;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;Call&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;Package&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;What it actually registers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;AddVisitorGroupsMvc()&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;EPiServer.Cms.AspNetCore.Mvc&lt;/span&gt; (already referenced)&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;The whole working engine: repositories and statistics, role infrastructure, the built-in criteria, the content-area &lt;strong&gt;rendering filter&lt;/strong&gt;, fragment handlers, View-as-Audience impersonation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;AddVisitorGroupsUI()&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;EPiServer.CMS.UI.VisitorGroups&lt;/span&gt; (add it; not in the metapackage)&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;The Audiences management screen (protected module &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;VisitorGroups.zip&lt;/span&gt;) and its API controllers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;AddCmsClientGeolocation(o =&amp;gt; o.LocationHeader = &amp;hellip;)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;EPiServer.Geolocation&lt;/span&gt; (already referenced)&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Header-based geolocation: feeds the geographic criteria &lt;em&gt;and&lt;/em&gt; anything else consuming &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;IClientGeolocationResolver&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;[VisitorGroupCriterion]&lt;/span&gt; on your class&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;your project&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Custom criteria via plugin scanning; explicit &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;AddVisitorGroupsCriterion&amp;lt;T&amp;gt;()&lt;/span&gt; only if you disable scanning&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;And the criteria catalog as measured on 13.1.0, the self-check log, deduplicated and grouped, because no docs page currently lists all of them:&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin: 20px 0; font-size: 14px;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;Group&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;Criteria&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Behavior&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;NumberOfVisits, ViewedPages, ViewedCategories, Download, TimeOnSite, Event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Arrival&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Referrer, SearchWordReferrer, StartUrl, QueryString&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Time&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;TimeOfDay, TimePeriod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Place &amp;amp; client&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;GeographicLocation, GeographicCoordinate, IPRange, OSBrowser, DisplayChannel, SelectedLanguage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Identity&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Role, UserProfile, VisitorGroupMembership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Custom (this demo)&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;DeviceCriterion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Two of those built-ins, configured: a time-of-day window and a country match, both reading request and CMS state the FX engine never sees.&lt;/p&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center; max-width: 800px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; border: 1px solid #e1e4e8; border-radius: 6px;&quot; src=&quot;https://pino-labs.com/blog/content-variations-cms-13-part-3/vg-criterion-timeofday.png&quot; alt=&quot;Built-in Time of Day criterion on an Office hours CMS audience: 08:00-16:00, Monday to Friday.&quot; /&gt;
&lt;figcaption style=&quot;font-size: 13px; color: #666; margin-top: 6px;&quot;&gt;&lt;em&gt;Built-in Time of Day criterion on an &amp;ldquo;Office hours&amp;rdquo; CMS audience: 08:00&amp;ndash;16:00, Monday to Friday.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center; max-width: 800px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; border: 1px solid #e1e4e8; border-radius: 6px;&quot; src=&quot;https://pino-labs.com/blog/content-variations-cms-13-part-3/vg-criterion-geo.png&quot; alt=&quot;Built-in Geographic Location criterion on a Visitors from PL CMS audience: Europe / Poland.&quot; /&gt;
&lt;figcaption style=&quot;font-size: 13px; color: #666; margin-top: 6px;&quot;&gt;&lt;em&gt;Built-in Geographic Location criterion on a &amp;ldquo;Visitors from PL&amp;rdquo; CMS audience: Europe / Poland.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2&gt;The attribute pipeline: FX only knows what you tell it&lt;/h2&gt;
&lt;p&gt;The part-2 provider sent two attributes derived from the request: &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;device&lt;/span&gt; from the User-Agent, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;location&lt;/span&gt; from geo headers. Part 3 grows it to five, and the growth is where the lessons are:&lt;/p&gt;
&lt;pre style=&quot;background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 6px; padding: 16px; overflow: auto; font-family: Consolas,Monaco,monospace; font-size: 13px; line-height: 1.5;&quot;&gt;return new Dictionary&amp;lt;string, object?&amp;gt;
{
    [&quot;device&quot;]    = ResolveDevice(request),          // &quot;mobile&quot; | &quot;tablet&quot; | &quot;desktop&quot;
    [&quot;location&quot;]  = ResolveLocation(context),        // &quot;PL&quot;, &quot;SE&quot;, ... | &quot;unknown&quot;
    [&quot;logged_in&quot;] = context?.User.Identity?.IsAuthenticated == true,   // a real bool
    [&quot;cms_role&quot;]  = ResolveCmsRole(context?.User),   // &quot;admin&quot; | &quot;editor&quot; | &quot;none&quot;
    [&quot;consent&quot;]   = HasConsent(request)              // a real bool, from a cookie
};&lt;/pre&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center; max-width: 800px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; border: 1px solid #e1e4e8; border-radius: 6px;&quot; src=&quot;https://pino-labs.com/blog/content-variations-cms-13-part-3/fx-attributes.png&quot; alt=&quot;FX dashboard, Settings to Audiences to Attributes: the five registered attribute keys.&quot; /&gt;
&lt;figcaption style=&quot;font-size: 13px; color: #666; margin-top: 6px;&quot;&gt;&lt;em&gt;The five registered attribute keys (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;cms_role&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;consent&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;device&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;location&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;logged_in&lt;/span&gt;), the code-to-dashboard contract.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Three rules. All three carried more weight than I expected when I wrote them down. (Throughout this article, &amp;ldquo;the panel&amp;rdquo; means part 2&#39;s instrument set. The demo page renders a &lt;em&gt;variation pill&lt;/em&gt; carrying the served arm plus a &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;data-rule&lt;/span&gt; attribute naming the rule that served it, and a Development-only &lt;em&gt;diagnostics panel&lt;/em&gt; showing the visitor ID, the attribute dictionary and the SDK&#39;s &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;Decide&lt;/span&gt; reasons. Every measurement below reads off those two surfaces.)&lt;/p&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center; max-width: 800px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; border: 1px solid #e1e4e8; border-radius: 6px;&quot; src=&quot;https://pino-labs.com/blog/content-variations-cms-13-part-3/demo-diagnostics.png&quot; alt=&quot;The variation pill and the FX decision diagnostics.&quot; /&gt;
&lt;figcaption style=&quot;font-size: 13px; color: #666; margin-top: 6px;&quot;&gt;&lt;em&gt;&amp;ldquo;The panel&amp;rdquo;: the variation pill and the FX decision diagnostics: visitor ID, served variation, the attribute dictionary, and the SDK INCLUDE_REASONS trace.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Types are the contract.&lt;/strong&gt; Attributes are untyped in the dashboard. The value you &lt;em&gt;send&lt;/em&gt; decides which comparisons can match. Send the string &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;&quot;true&quot;&lt;/span&gt; against a boolean condition and the condition is skipped: evaluated to UNKNOWN, audience false, visitor falls through, nobody logs an error at default verbosity. The booleans above are real booleans for exactly that reason. (Sharp edge #3 has the measurement.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Absent and false are different inputs.&lt;/strong&gt; An anonymous visitor on the MVC head sends &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;logged_in = false&lt;/span&gt;. If the headless head simply omitted the key, an audience targeting &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;logged_in is false&lt;/span&gt; would match on one head and skip on the other. Same visitor, same flag, different arms, and you would hunt the bug in the hashing where it isn&#39;t. The Next.js head therefore sends the constants its reality justifies: its visitors are, truthfully, never logged in.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One request, one context, attributes enter once.&lt;/strong&gt; The decision service is request-scoped and builds the SDK user context a single time. The banner flag, the experiment and the conversion all agree on who the visitor is. Change an attribute mid-request and nothing happens. That&#39;s by design, and worth a comment in the code so nobody &amp;ldquo;fixes&amp;rdquo; it.&lt;/p&gt;
&lt;h3&gt;Geolocation: one header now feeds both engines&lt;/h3&gt;
&lt;p&gt;The hand-rolled four-header geo parser from part 2 is gone. CMS 13 ships native client geolocation: country from a single configured CDN header, no local IP database. One line wires it: &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;services.AddCmsClientGeolocation(o =&amp;gt; o.LocationHeader = &quot;CF-IPCountry&quot;)&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;The same &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;IClientGeolocationResolver&lt;/span&gt; now answers two callers: our FX &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;location&lt;/span&gt; attribute and the built-in geographic criterion of CMS Audiences. One configuration line, two engines. That also means one spoofed header fools both at once. &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;curl -H &quot;CF-IPCountry: SE&quot;&lt;/span&gt; and the panel reads &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;location=SE&lt;/span&gt;. The part-2 caveat about attacker-supplied input didn&#39;t go away. It doubled its blast radius. And a detail measured the hard way: the resolver wants an &lt;strong&gt;uppercase&lt;/strong&gt; ISO code. &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;PL&lt;/span&gt; resolves; &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;pl&lt;/span&gt; resolves to nothing. Cloudflare sends uppercase, so production works. But any homegrown proxy that normalizes headers to lowercase turns your geographic targeting off, &lt;em&gt;both engines&#39; worth of it&lt;/em&gt;, with no warning, no log line, no sound. The diagnostics panel reading &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;location=unknown&lt;/span&gt; while the header is plainly there is the only tell.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The consent attribute is not an attribute.&lt;/strong&gt; Or rather: it is targetable like any other, but its real job is a switch. Impression and conversion events carry the visitor ID and the attribute dictionary to Optimizely&#39;s backend. That payload is precisely the thing a non-consenting visitor declined. So when &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;consent&lt;/span&gt; is false, the service calls &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;Decide&lt;/span&gt; with &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;DISABLE_DECISION_EVENT&lt;/span&gt; and turns &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;Track&lt;/span&gt; into a logged no-op. Flags keep working, because the datafile evaluation is local, but nothing leaves the building. Consent isn&#39;t an audience attribute. It&#39;s an event-egress switch. (The demo toggles it with a cookie; a real implementation wires it to your CMP. And the conversion button now reports &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;tracked: false&lt;/span&gt; instead of celebrating an event that never left. A demo that lies to you is worse than no demo.) One boundary disclosed rather than implied: the visitor-ID cookie itself is set, and bucketing happens, before any consent. This build treats the identifier as functional state and gates only what leaves the building. Your CMP, or your DPO, may read that line differently. That conversation belongs in your project rather than an SDK option.&lt;/p&gt;
&lt;p&gt;One honest note before moving on: FX has no server-side bot filtering. A crawler&#39;s User-Agent contains no &amp;ldquo;Mobi&amp;rdquo;, so every bot enters your experiment as a desktop visitor and dilutes whatever it touches. Either gate &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;Decide&lt;/span&gt; on known-bot UAs, or accept the noise with your eyes open.&lt;/p&gt;
&lt;h2&gt;Targeted Delivery: personalization without the dice&lt;/h2&gt;
&lt;p&gt;The plan was seductively simple: put a targeted rule &lt;em&gt;above&lt;/em&gt; the everyone-else A/B test. Mobile gets the personalized arm, everyone else keeps experimenting. The dashboard said no, and the way it says no is the finding. The ruleset isn&#39;t one sortable list. It&#39;s three fixed sections whose headings literally narrate the evaluation order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;First, match experiment rule&lt;/strong&gt; &amp;rarr; &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;a_b_test&lt;/span&gt; (A/B, audience: Non-mobile visitors)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Then, the following rules will be evaluated for all visitors&lt;/strong&gt; &amp;rarr; &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;mobile_delivery&lt;/span&gt; (deliver &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;variant_a&lt;/span&gt;, 100%)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Then, for everyone else&lt;/strong&gt; &amp;rarr; Off&lt;/li&gt;
&lt;/ol&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center; max-width: 800px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; border: 1px solid #e1e4e8; border-radius: 6px;&quot; src=&quot;https://pino-labs.com/blog/content-variations-cms-13-part-3/ruleset-layered.png&quot; alt=&quot;The ruleset three fixed sections: experiment first, then deliveries for all visitors, then everyone-else.&quot; /&gt;
&lt;figcaption style=&quot;font-size: 13px; color: #666; margin-top: 6px;&quot;&gt;&lt;em&gt;The ruleset&#39;s three fixed sections: experiment first, then deliveries for all visitors, then everyone-else. No shared list to drag a delivery above an experiment.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center; max-width: 800px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; border: 1px solid #e1e4e8; border-radius: 6px;&quot; src=&quot;https://pino-labs.com/blog/content-variations-cms-13-part-3/rule-targeted.png&quot; alt=&quot;The mobile_delivery rule: Targeted Delivery, audience Mobile visitors, 100%, deliver variant_a.&quot; /&gt;
&lt;figcaption style=&quot;font-size: 13px; color: #666; margin-top: 6px;&quot;&gt;&lt;em&gt;The &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;mobile_delivery&lt;/span&gt; rule: Targeted Delivery, audience Mobile visitors, 100%, deliver &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;variant_a&lt;/span&gt;.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center; max-width: 800px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; border: 1px solid #e1e4e8; border-radius: 6px;&quot; src=&quot;https://pino-labs.com/blog/content-variations-cms-13-part-3/fx-audience-mobile.png&quot; alt=&quot;The Mobile visitors FX audience: device equals mobile.&quot; /&gt;
&lt;figcaption style=&quot;font-size: 13px; color: #666; margin-top: 6px;&quot;&gt;&lt;em&gt;The Mobile visitors FX audience: &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;device&lt;/span&gt; equals &amp;ldquo;mobile&amp;rdquo;, with all five attribute keys in the browser on the right.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center; max-width: 800px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; border: 1px solid #e1e4e8; border-radius: 6px;&quot; src=&quot;https://pino-labs.com/blog/content-variations-cms-13-part-3/fx-audience-codemode.png&quot; alt=&quot;The same audience in Code Mode: match_type exact.&quot; /&gt;
&lt;figcaption style=&quot;font-size: 13px; color: #666; margin-top: 6px;&quot;&gt;&lt;em&gt;The same audience in Code Mode: &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;match_type&lt;/span&gt; &amp;ldquo;exact&amp;rdquo;. The datafile the SDKs evaluate spells the field &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;match&lt;/span&gt; (sharp edge #1).&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center; max-width: 800px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; border: 1px solid #e1e4e8; border-radius: 6px;&quot; src=&quot;https://pino-labs.com/blog/content-variations-cms-13-part-3/fx-audience-nonmobile.png&quot; alt=&quot;The Non-mobile visitors audience: device equals desktop OR tablet.&quot; /&gt;
&lt;figcaption style=&quot;font-size: 13px; color: #666; margin-top: 6px;&quot;&gt;&lt;em&gt;The Non-mobile visitors audience used to carve the experiment: a positive list, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;device&lt;/span&gt; equals desktop OR tablet, so an unknown device matches nothing and lands on master.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;FX evaluates experiments before deliveries. Always. The UI encodes the law in its page structure rather than enforcing it with an error. So carving mobile out of the experiment happens in the &lt;em&gt;audience&lt;/em&gt;, not in rule order. The A/B rule&#39;s audience changed from &lt;em&gt;Everyone&lt;/em&gt; to &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;Non-mobile visitors&lt;/span&gt;: a deliberately positive list (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;device = desktop OR tablet&lt;/span&gt;) rather than a negation. That way a visitor with a missing or mangled &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;device&lt;/span&gt; attribute matches &lt;em&gt;nothing&lt;/em&gt; and lands on master, which is the honest outcome for &amp;ldquo;we don&#39;t know who this is&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Two semantics from the docs that almost nobody quotes, both load-bearing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;experiment&lt;/strong&gt; whose audience matches but whose traffic allocation misses &lt;em&gt;rolls down&lt;/em&gt; to the next rule. A &lt;strong&gt;delivery&lt;/strong&gt; in the same situation &lt;em&gt;jumps straight to everyone-else&lt;/em&gt;, skipping any deliveries below it.&lt;/li&gt;
&lt;li&gt;Deliveries produce &lt;strong&gt;no Results page&lt;/strong&gt;: &amp;ldquo;No decision events show up on the results page.&amp;rdquo; A delivery is deployment, not measurement. Our mobile visitors get their personalization and leave the experiment&#39;s bookkeeping entirely. The A/B sample quietly becomes a desktop-and-tablet sample. Write that sentence in your analysis doc before someone asks why the experiment&#39;s traffic dropped.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And one behavioral nuance that will eventually puzzle a stakeholder: bucketing is sticky, attributes are not. The same visitor who taps &amp;ldquo;Request desktop site&amp;rdquo; in mobile Chrome flips &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;device&lt;/span&gt; mid-cookie, exits the delivery, and enters the experiment. Same visitor ID, different arm, both serves correct. Attribute-driven targeting re-evaluates every request. Only the hash is forever.&lt;/p&gt;
&lt;p&gt;Configuration footnote from the audience builder: the dashboard&#39;s Code Mode validates conditions with the key &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;match_type&lt;/span&gt;, while the datafile the SDKs download spells the very same field &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;match&lt;/span&gt;. Two serializations of one concept. Harmless until you copy a condition from the datafile into Code Mode and the validator rejects what the SDK just evaluated. The SDK&#39;s own log line later in this article shows the &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;match&lt;/span&gt; spelling in the wild.&lt;/p&gt;
&lt;h2&gt;Can both engines coexist on one page? Yes, at different layers, measurably&lt;/h2&gt;
&lt;p&gt;This is the part people actually google, so here is the layered setup, built and measured. FX picks the &lt;em&gt;page version&lt;/em&gt; (mobile &amp;rarr; &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;variant_a&lt;/span&gt; via the delivery), and inside that version one block carries a CMS Audience (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;Mobile visitors (CMS)&lt;/span&gt;, our &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;DeviceCriterion&lt;/span&gt;). Two engines, two layers, same page.&lt;/p&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center; max-width: 800px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; border: 1px solid #e1e4e8; border-radius: 6px;&quot; src=&quot;https://pino-labs.com/blog/content-variations-cms-13-part-3/vg-audiences-list.png&quot; alt=&quot;CMS Audiences admin: Mobile visitors (CMS), Office hours, Visitors from PL.&quot; /&gt;
&lt;figcaption style=&quot;font-size: 13px; color: #666; margin-top: 6px;&quot;&gt;&lt;em&gt;CMS Audiences admin: Mobile visitors (CMS), Office hours, Visitors from PL.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center; max-width: 800px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; border: 1px solid #e1e4e8; border-radius: 6px;&quot; src=&quot;https://pino-labs.com/blog/content-variations-cms-13-part-3/vg-admin.png&quot; alt=&quot;The Mobile visitors (CMS) audience on the custom Device class criterion.&quot; /&gt;
&lt;figcaption style=&quot;font-size: 13px; color: #666; margin-top: 6px;&quot;&gt;&lt;em&gt;The Mobile visitors (CMS) audience on the custom Device class criterion (value &amp;ldquo;mobile&amp;rdquo;), statistics enabled.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center; max-width: 800px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; border: 1px solid #e1e4e8; border-radius: 6px;&quot; src=&quot;https://pino-labs.com/blog/content-variations-cms-13-part-3/vg-personalize.png&quot; alt=&quot;Assigning the audience to a content-area block inside variant_a in the CMS editor.&quot; /&gt;
&lt;figcaption style=&quot;font-size: 13px; color: #666; margin-top: 6px;&quot;&gt;&lt;em&gt;Assigning the audience to a content-area block inside &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;variant_a&lt;/span&gt; in the CMS editor, with the rendered variant on the right.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The measurement needed care, because FX variation noise would drown the block signal. Take one visitor ID that desktop-buckets into &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;variant_a&lt;/span&gt; &lt;em&gt;through the experiment&lt;/em&gt;, then request the page twice: same visitor, same page version, only the User-Agent differs.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin: 20px 0; font-size: 14px;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;Probe (same visitor, same &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;variant_a&lt;/span&gt;)&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;Rule that served it&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;Content area&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Desktop&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;a_b_test&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;empty, block filtered out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Mobile&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;mobile_delivery&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&amp;ldquo;Mobile Quiet Hero&amp;rdquo; renders&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Pure visitor-group effect, isolated from FX. The coexistence answer: &lt;strong&gt;no conflict, different layers&lt;/strong&gt;. The FX decision happens in the controller before rendering. The CMS Audience filters content-area items during rendering. The pitfall isn&#39;t a conflict, it&#39;s unreachability: personalize a block for mobile in the &lt;em&gt;master&lt;/em&gt; version and no one will ever see it, because under this ruleset mobile never receives master. The FX rule one layer up decides which content area exists at all.&lt;/p&gt;
&lt;p&gt;There&#39;s a sour bonus measurement. An earlier build of this demo, the one registered from decompiled internals instead of the documented &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;AddVisitorGroupsMvc()&lt;/span&gt;, served that personalized block to the desktop probe too. Same content, same audiences, one missing rendering filter, zero errors. Keep that pair of runs. It&#39;s the cleanest argument I own for &amp;ldquo;configure from the docs, not from the decompiler&amp;rdquo;.&lt;/p&gt;
&lt;h3&gt;What does Content Graph actually do with a personalized block?&lt;/h3&gt;
&lt;p&gt;The same page travels to the Next.js head through Optimizely Graph. The personalized item doesn&#39;t:&lt;/p&gt;
&lt;pre style=&quot;background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 6px; padding: 16px; overflow: auto; font-family: Consolas,Monaco,monospace; font-size: 13px; line-height: 1.5;&quot;&gt;variant_a  &amp;rarr;  MainContentArea: []        (the block the MVC head demonstrably renders)
original   &amp;rarr;  MainContentArea: []
control    &amp;rarr;  MinimalPage.HeroArea: [{ &quot;Heading&quot;: &quot;EN Heading&quot; }]   // non-personalized area expands fine&lt;/pre&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center; max-width: 800px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; border: 1px solid #e1e4e8; border-radius: 6px;&quot; src=&quot;https://pino-labs.com/blog/content-variations-cms-13-part-3/graph-void.png&quot; alt=&quot;Optimizely Graph: all three DemoPage arms are in the index with distinct DemoTitle, yet every MainContentArea is empty.&quot; /&gt;
&lt;figcaption style=&quot;font-size: 13px; color: #666; margin-top: 6px;&quot;&gt;&lt;em&gt;Optimizely Graph: all three DemoPage arms (variant_a, variant_b, master) are in the index with distinct DemoTitle, yet every MainContentArea is &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;[]&lt;/span&gt;. The page variation survives; the personalized content-area relation does not.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Not a leak. A void. The VG-personalized item vanishes from the index for &lt;em&gt;every&lt;/em&gt; Graph consumer, including the mobile visitors who would have matched. No API error, no warning in the sync job, nothing for the front-end to even detect. The shape of the void is worth a second look: the &lt;em&gt;block itself&lt;/em&gt; is still in the index, four &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;QuietHeroBlock&lt;/span&gt; documents, each with its own identity and content, but the page&#39;s content-area &lt;strong&gt;relation&lt;/strong&gt; to it is gone. The explanation consistent with every measurement is that indexing evaluates the page without a visitor to personalize for, and an audience-gated item has no honest answer to &amp;ldquo;should this exist?&amp;rdquo;, so it doesn&#39;t. Whatever the internals, the contract you can rely on is the measured one: personalization severs the composition, not the content. The docs say it without decoration: &amp;ldquo;audiences do not work on headless sites.&amp;rdquo; The measurement above is what that sentence costs in practice.&lt;/p&gt;
&lt;p&gt;So what do you actually do when the roadmap says headless? There are three shapes, in order of preference. First, move that personalization up a layer to FX Audiences and Content Variations. That is this article&#39;s whole thesis, and it needs nothing new. Second, take the CMS 13.1.0 Graph Conventions API and customize indexing so the head receives the items plus enough metadata to filter client-side. Workable, but now &lt;em&gt;you&lt;/em&gt; own the audience semantics on every head. Third, stand up a membership endpoint on the CMS that the head consults per request. It works, and it is exactly the coordination code this series exists to avoid. There is no fourth option where the block just shows up.&lt;/p&gt;
&lt;h3&gt;The decision table&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin: 20px 0; font-size: 14px;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;You need&amp;hellip;&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;Engine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;The same personalization on every head, today and after the next re-platform&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;FX Audiences + Content Variations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Criteria that read CMS state: roles, visit history, time of day&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;CMS Audiences&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Measurement: exposures, conversions, significance&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;FX (deliveries excluded, they don&#39;t report)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Kill switch without a deploy&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;FX (latency = datafile propagation; measured at ~75 s here)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Fragment-level personalization, CMS-rendered site, no measurement need&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;CMS Audiences&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Zero marginal license cost&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;CMS Audiences (in the CMS license; FX is usage-billed, though deliveries fire no impressions, only experiment bucketing does)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Per-request evaluation cost approaching zero&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;FX (pure in-memory function; VG criteria may do I/O per content-area item)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Ownership by content editors: instant, local, visual&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;CMS Audiences (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;VisitorGroupAdmins&lt;/span&gt;; no deploy, no datafile, changes land on publish)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Ownership by product managers and analysts&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;FX Audiences, with attribute-key discipline as the price: keys are a code-to-dashboard contract, and once created an attribute stays in the datafile permanently&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A word of architectural restraint: a personalized block inside an experiment variation multiplies your test matrix: arms &amp;times; audiences &amp;times; consent states. The demo does it to prove the layering. Production teams should default to &lt;strong&gt;one personalization axis per page&lt;/strong&gt;, and budget QA explicitly before crossing them.&lt;/p&gt;
&lt;p&gt;ODP deserves its one paragraph: when the attributes should come from customer &lt;em&gt;data&lt;/em&gt; rather than the current request, Real-Time Segments for Feature Experimentation syncs ODP audiences into the same rule slots. Segments stay out of the datafile, qualification typically lands under thirty seconds, and Optimizely&#39;s own docs concede that when you need 100% accuracy you should fall back to plain custom attributes. Different source, same predicate machinery. That&#39;s the whole story here. This demo has no ODP account on purpose.&lt;/p&gt;
&lt;h2&gt;From prose to proof: the day the headless head went dark&lt;/h2&gt;
&lt;p&gt;Here is the embarrassing measurement first, because it earns the rest of the section. The moment the A/B rule&#39;s audience changed from &lt;em&gt;Everyone&lt;/em&gt; to &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;Non-mobile visitors&lt;/span&gt;, the Next.js head, which part 2 proudly demonstrated holding a perfect 33/33/34, went one hundred percent dark:&lt;/p&gt;
&lt;pre style=&quot;background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 6px; padding: 16px; overflow: auto; font-family: Consolas,Monaco,monospace; font-size: 13px; line-height: 1.5;&quot;&gt;default (master)      300  (100.00 %)
FAILED: &quot;300 request(s) were served the master page (no FX decision) - check
that the rule is Running, Traffic Allocation is 100% and the SDK key matches
the rule&#39;s environment.&quot;&lt;/pre&gt;
&lt;p&gt;Rule running, allocation 100%, SDK key correct. The test&#39;s own diagnostic, written by me in part 2, blames everything except the actual cause: the head&#39;s &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;decide()&lt;/span&gt; sent &lt;strong&gt;no attributes&lt;/strong&gt;, so the visitor matched neither audience and fell through to everyone-else-off. No errors, green dashboards, a misleading failure message, and a head silently serving master to every visitor from the minute the audience shipped. Audiences don&#39;t add a feature to your architecture. They add a &lt;em&gt;requirement&lt;/em&gt; to every head you run.&lt;/p&gt;
&lt;p&gt;The fix is a mirror: a &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;buildAttributes()&lt;/span&gt; on the Next side that reproduces the .NET pipeline byte for byte. The parity contract lives in one file with a comment that says exactly that: if the heuristics drift, parity dies at the attribute layer while everyone debugs the hash. The contract itself fits in a table, which is how it should be reviewed:&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin: 20px 0; font-size: 14px;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;Attribute&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;The rule both heads obey&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;device&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Substring tests, case-insensitive: iPad/Tablet &amp;rarr; &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;tablet&lt;/span&gt;, else Mobi &amp;rarr; &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;mobile&lt;/span&gt;, else &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;desktop&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;location&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Header path is strict: &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;^[A-Z]{2}$&lt;/span&gt; and &amp;ne; &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;XX&lt;/span&gt;, &lt;strong&gt;no case folding&lt;/strong&gt; (mirrors the measured CMS resolver); the Accept-Language fallback &lt;em&gt;does&lt;/em&gt; fold case (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;pl-PL&lt;/span&gt; &amp;rarr; &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;PL&lt;/span&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;logged_in&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;cms_role&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Real types, truthful constants on the auth-less head (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;false&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;&quot;none&quot;&lt;/span&gt;): absent and false are different inputs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;consent&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;bool.TryParse&lt;/span&gt; semantics: case-insensitive &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;true&lt;/span&gt;, anything else false&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Upgrading the JS SDK to v6 along the way produced one trap worth its own line. v6 is explicit-opt-in across the board: polling config manager, logger, and ODP all opt-in. So is the &lt;strong&gt;event processor&lt;/strong&gt;: omit &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;createBatchEventProcessor()&lt;/span&gt; and decisions keep flowing while the SDK dispatches &lt;em&gt;no events at all&lt;/em&gt;. Served-but-unmeasured, the exact failure shape this series keeps finding, now available at the initialization layer. (The same modularity quietly fixed an old log nag: no ODP manager, no &amp;ldquo;ODP is not integrated&amp;rdquo; warnings.)&lt;/p&gt;
&lt;p&gt;Two Next.js-specific notes earned their scars. The SDK instance is cached on &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;globalThis&lt;/span&gt;, the same trick Prisma clients use, because &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;next dev&lt;/span&gt;&#39;s Fast Refresh re-evaluates modules. A module-level singleton would leak a fresh polling manager and event processor on every save, and your datafile CDN would meet them all.&lt;/p&gt;
&lt;p&gt;And one asymmetry to disclose rather than hide: the C# head flushes its event queue on shutdown because the DI container disposes the client (part 2&#39;s contract). A Node process has no such container. The batch rides &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;flushInterval&lt;/span&gt;, and what happens at the end of a process&#39;s life depends entirely on what kind of life it had:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Long-running Node&lt;/strong&gt; (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;next start&lt;/span&gt;, a container): safe. The queue drains on the interval, and a tail lost to a SIGTERM is a rounding error.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Serverless / edge&lt;/strong&gt;: risky. The platform can freeze the instance the moment the response returns, and a consented conversion dies in the buffer as a zombie event.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The fix where it matters&lt;/strong&gt;: shrink the batch to the point of synchronicity, or &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;await client.close()&lt;/span&gt; before the invocation ends, and accept the latency as the price of the data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pretending the queue always drains would be exactly the kind of silence this series hunts.&lt;/p&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center; max-width: 800px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; border: 1px solid #e1e4e8; border-radius: 6px;&quot; src=&quot;https://pino-labs.com/blog/content-variations-cms-13-part-3/next-head.png&quot; alt=&quot;The Next.js headless head on a mobile viewport serving variant_a via mobile_delivery.&quot; /&gt;
&lt;figcaption style=&quot;font-size: 13px; color: #666; margin-top: 6px;&quot;&gt;&lt;em&gt;The Next.js headless head on a mobile viewport: the same FX decision serves &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;variant_a&lt;/span&gt; via &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;mobile_delivery&lt;/span&gt;, and the variation&#39;s title arrives through Optimizely Graph, with zero coordination with the MVC head.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Then the green numbers, all in one evening:&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin: 20px 0; font-size: 14px;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;Check&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Mobile, 60 fresh visitors (hard assertion: a delivery rolls no dice)&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;60/60 &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;variant_a&lt;/span&gt; via &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;mobile_delivery&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Desktop distribution, n = 300, against the Next head&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;103 / 105 / 92, &lt;strong&gt;&amp;chi;&amp;sup2; = 0.985&lt;/strong&gt; (critical 13.816 at &amp;alpha; = 0.001, df = 2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Same regression against the MVC head&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;99 / 109 / 92, &amp;chi;&amp;sup2; = 1.465&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Cross-head parity: 4 fixed + 6 random visitors &amp;times; desktop and mobile&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;strong&gt;arm AND rule identical on both heads, 20/20 pairs&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center; max-width: 800px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; border: 1px solid #e1e4e8; border-radius: 6px;&quot; src=&quot;https://pino-labs.com/blog/content-variations-cms-13-part-3/next-distribution.svg&quot; alt=&quot;The desktop chi-square run against the headless Next.js head: 103 / 105 / 92 over n = 300, chi-square 0.985.&quot; /&gt;
&lt;figcaption style=&quot;font-size: 13px; color: #666; margin-top: 6px;&quot;&gt;&lt;em&gt;The desktop chi-square run against the headless Next.js head: 103 / 105 / 92 over n = 300, &amp;chi;&amp;sup2; = 0.985, well under the 13.816 critical value. Every decision came from Optimizely FX in the Node SDK, zero coordination with the MVC head.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The parity test asserts the rule key, not just the arm. &amp;ldquo;mobile got &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;variant_a&lt;/span&gt;&amp;rdquo; and &amp;ldquo;mobile happened to be bucketed into &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;variant_a&lt;/span&gt;&amp;rdquo; are different claims, and only &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;data-rule&lt;/span&gt; in the markup separates them. MurmurHash plus deterministic audience evaluation. Still zero coordination code.&lt;/p&gt;
&lt;p&gt;The arithmetic deserves to be shown once, not just asserted. For the MVC regression (99 / 109 / 92 over n = 300, expected &amp;asymp; 100 per arm):&lt;/p&gt;
&lt;pre style=&quot;background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 6px; padding: 16px; overflow: auto; font-family: Consolas,Monaco,monospace; font-size: 13px; line-height: 1.5;&quot;&gt;&amp;chi;&amp;sup2; = (99&amp;minus;100)&amp;sup2;/100 + (109&amp;minus;100)&amp;sup2;/100 + (92&amp;minus;100)&amp;sup2;/100
   = 0.01 + 0.81 + 0.64 &amp;asymp; 1.46&lt;/pre&gt;
&lt;p&gt;(The suite reports 1.465 because it tests against the configured 33.33/33.33/33.34 rather than exact thirds.) With df = k &amp;minus; 1 = 2 and &amp;alpha; = 0.001, the critical value is 13.816, and 1.465 ≪ 13.816, so the null hypothesis (&amp;ldquo;the split matches the configuration&amp;rdquo;) survives comfortably. In words: after carving the mobile segment out with an audience, the hash shows no distributional anomaly in what remains. The same computation on the Next head&#39;s 103 / 105 / 92 gives 0.985, comfortably inside the spread a fair split throws at n = 300.&lt;/p&gt;
&lt;p&gt;Threats to validity, disclosed as ever: the chi-square run verifies the bucketing distribution, not content delivery (part 2&#39;s caveat stands). The mobile check is exhaustive rather than statistical, because a 100% delivery is deterministic: a single counterexample falsifies it, no &amp;alpha; required. And every test here runs without a consent cookie, which after the egress gate means &lt;strong&gt;the suite sends zero impressions to FX&lt;/strong&gt;. Three hundred requests of load testing used to pollute Results. Now the same suite is invisible to it. That started as a privacy control and turned out to be test hygiene.&lt;/p&gt;
&lt;h2&gt;QA: three override levels, three owners&lt;/h2&gt;
&lt;p&gt;&amp;ldquo;How does QA see the variant for a segment they don&#39;t belong to?&amp;rdquo; That is the question audiences force on every test plan. Three answers, escalating by who owns them:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;New visitor&lt;/strong&gt; (the cookie-reset button, part 2): owned by anyone with a browser. Re-rolls the dice but can&#39;t cross an audience boundary. The button also demonstrates something subtler: rotating the visitor ID re-buckets every &lt;em&gt;experiment&lt;/em&gt;, leaves a &lt;em&gt;delivery&lt;/em&gt; unmoved (its audience is deterministic in the attributes, not the ID), and the CMS Audience doesn&#39;t even notice; its criteria never saw your visitor ID in the first place. One page, three different notions of who &amp;ldquo;you&amp;rdquo; are. QA plans that conflate them chase ghosts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Allowlist&lt;/strong&gt; (dashboard, per rule, up to fifty IDs): owned by the marketer, pins a visitor ID to an arm. Measured here: an allowlisted ID came back &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;variant_b&lt;/span&gt; with the SDK reason &amp;ldquo;is forced in variation&amp;rdquo;. Caveat: allowlisted traffic still fires impressions, so your QA session pollutes Results.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;?fx_force=demo_ab_test:variant_b&lt;/span&gt;&lt;/strong&gt; (this part): owned by the developer, a Development-only query parameter mapped to the SDK&#39;s forced-decision API. It bypasses audiences &lt;em&gt;and&lt;/em&gt; allocation (forced mobile, forced desktop, doesn&#39;t matter), and, deliberately unlike the allowlist, a forced request also suppresses its decision events. QA that leaves no fingerprints on the data it&#39;s there to protect.&lt;/li&gt;
&lt;/ol&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center; max-width: 800px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; border: 1px solid #e1e4e8; border-radius: 6px;&quot; src=&quot;https://pino-labs.com/blog/content-variations-cms-13-part-3/ab-rule-allowlist.png&quot; alt=&quot;The A/B Test rule: audience Non-mobile visitors, 100%, the split, and the allowlist pinning a QA visitor ID to variant_b.&quot; /&gt;
&lt;figcaption style=&quot;font-size: 13px; color: #666; margin-top: 6px;&quot;&gt;&lt;em&gt;The A/B Test rule: audience Non-mobile visitors, 100%, the 33.33 / 33.33 / 33.34 split, and the allowlist pinning a QA visitor ID to &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;variant_b&lt;/span&gt;.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The whole mechanism is one small source behind one small interface, so the SDK types stay where part 2 put them:&lt;/p&gt;
&lt;pre style=&quot;background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 6px; padding: 16px; overflow: auto; font-family: Consolas,Monaco,monospace; font-size: 13px; line-height: 1.5;&quot;&gt;var forced = forcedDecisionSource.GetForcedDecisions();   // [] outside Development
foreach (var (flagKey, variationKey) in forced)
{
    userContext.SetForcedDecision(
        new OptimizelyDecisionContext(flagKey),
        new OptimizelyForcedDecision(variationKey));
}
_eventsAllowed &amp;amp;= forced.Count == 0;   // QA traffic never reaches Results&lt;/pre&gt;
&lt;p&gt;While we&#39;re holding the decide call open, the full option set gets a reference table (part 2 named these in prose):&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin: 20px 0; font-size: 14px;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;Decide option&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;What it does&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;Where this series uses it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;INCLUDE_REASONS&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Returns the evaluation trace: which rule matched, why audiences failed&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Every call; feeds the diagnostics panel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;DISABLE_DECISION_EVENT&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Evaluates without sending an impression&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;No-consent requests; forced QA requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;ENABLED_FLAGS_ONLY&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Filters &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;DecideAll&lt;/span&gt; to enabled flags&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;IGNORE_USER_PROFILE_SERVICE&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Skips UPS stickiness for this call&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;n/a (this demo runs without UPS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;EXCLUDE_VARIABLES&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Omits variable payloads for cheaper decisions&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The CMS side has its own counterpart, &lt;strong&gt;View as Audience&lt;/strong&gt; in the editor, with one boundary note: it previews visitor groups, not FX arms, and the docs scope it to CMS-rendered sites only. Pair the two in your test plan. Neither substitutes for the other.&lt;/p&gt;
&lt;p&gt;Forced decisions don&#39;t persist. They clear with the user context, which in this architecture means they last exactly one request. That&#39;s not a limitation. Per-request is the only scope that can&#39;t leak into someone&#39;s real session.&lt;/p&gt;
&lt;h2&gt;Runbook: &amp;ldquo;the page always serves master&amp;rdquo;, audience edition&lt;/h2&gt;
&lt;p&gt;Part 2 ended with a checklist for the all-master symptom. Audiences roughly double it. The developer&#39;s mental map first: &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;data-rule&lt;/span&gt; on the pill is the fork in the road.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;data-rule is empty&lt;/strong&gt;, no decision happened at all: SDK key, datafile or flag off (part 2&#39;s checklist).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;data-rule = default-rollout-&amp;hellip;&lt;/strong&gt;, &amp;ldquo;everyone else&amp;rdquo; fired, so no audience matched:
&lt;ul&gt;
&lt;li&gt;Attributes row wrong or missing &amp;rarr; context pipeline gap (the dark-head failure: a head sending nothing).&lt;/li&gt;
&lt;li&gt;Attributes row correct &amp;rarr; condition-side defect (whitespace, case, type); open the audience in Code Mode, where quotes make it visible.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;data-rule = a real rule, arm = original&lt;/strong&gt;, working as designed: the control arm serves master via fallback.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And the full ordered checklist, where each step assumes the previous ones passed:&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin: 20px 0; font-size: 14px;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;#&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;Check&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;Where the truth shows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;SDK key matches the rule&#39;s environment; ruleset Running; variation toggles ON&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Part 2&#39;s checklist (dashboard)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;The decision reached a rule at all&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;data-rule&lt;/span&gt; on the pill: &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;default-rollout-&amp;hellip;&lt;/span&gt; means everyone-else fired&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;The attributes you think you send are the attributes you send&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Diagnostics panel, Attributes row, wrong name, wrong case, wrong type, missing key are all visible here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;The audience condition matches those attributes byte-for-byte&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Reasons: &amp;ldquo;collectively evaluated to FALSE&amp;rdquo; while the panel shows the right value means a condition-side defect; open the audience in Code Mode, quotes make it visible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;5&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Type mismatches&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;SDK log (not reasons): &amp;ldquo;evaluated to UNKNOWN because a value of type &amp;hellip; was passed&amp;rdquo;, requires the part-2 logger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;6&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;The head sends attributes at all&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;The dark-head failure: &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;decide()&lt;/span&gt; without attributes matches nothing, and the test diagnostics will blame the rule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;7&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Datafile freshness&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Audience edits propagate on the polling interval like every other change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;8&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;(CMS side) the personalized block&#39;s &lt;em&gt;layer&lt;/em&gt; is reachable&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;A block personalized in a version the visitor never receives is invisible by construction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;9&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;(CMS side) the rendering layer is registered&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Audiences screen working &amp;ne; filter running, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;AddVisitorGroupsMvc()&lt;/span&gt;, then the startup self-check proves it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Sharp edges, part 3: ranked by blood actually drawn&lt;/h2&gt;
&lt;p&gt;Same tradition, same ordering rule. Every one of these happened on this instance, this week.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. A trailing space in an audience condition.&lt;/strong&gt; The audience said &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;device = &quot;mobile &quot;&lt;/span&gt;. The builder keeps whatever the clipboard delivered, and exact-match fields are not trimmed. Exact match means exact: every mobile visitor silently fell through two rules to master while every dashboard stayed green. The only witness was the diagnostics panel: attributes showed &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;device=mobile&lt;/span&gt;, reasons showed &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;Audiences for rule &quot;mobile_delivery&quot; collectively evaluated to FALSE&lt;/span&gt;, and the contradiction between those two lines &lt;em&gt;is&lt;/em&gt; the diagnosis. Code Mode confirms it fastest. JSON quotes make whitespace visible. Cost: the better part of an evening, and it produced this list&#39;s ordering.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. One &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;[OutputCache]&lt;/span&gt; attribute defeats both engines and privacy at once.&lt;/strong&gt; Swap the demo&#39;s &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;no-store&lt;/span&gt; guard for platform output caching, and the measurement reads like an incident report: a desktop visitor received the &lt;em&gt;mobile&lt;/em&gt; visitor&#39;s page from cache, wrong arm, wrong personalization, and the other person&#39;s visitor ID rendered in the panel, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;Age: 0&lt;/span&gt; confirming the hit. The CMS-aware output cache has been gone since the CMS 12 rewrite, and CMS 13 still gives the platform layer nothing that knows your pages are personal (verified: no output-cache type exists anywhere in the 13.1.0 assemblies). The fix is not a smarter cache key. It is classifying pages: personalized means &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;no-store&lt;/span&gt;, full stop. (The CDN analog is &amp;ldquo;ignore cookies in the cache key&amp;rdquo;, which produces the same leak at planetary scale.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Type mismatch evaluates to UNKNOWN, and only the logger says so.&lt;/strong&gt; Send &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;device&lt;/span&gt; as a boolean and &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;INCLUDE_REASONS&lt;/span&gt; reports a flat &amp;ldquo;collectively evaluated to FALSE&amp;rdquo;. The &lt;em&gt;why&lt;/em&gt; lives one layer down, in the SDK log the part-2 adapter made visible: &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;Audience condition {&quot;match&quot;:&quot;exact&quot;,&quot;name&quot;:&quot;device&quot;,&quot;value&quot;:&quot;mobile&quot;} evaluated to UNKNOWN because a value of type &quot;Boolean&quot; was passed&lt;/span&gt;. Wire the logger on day one was part 2&#39;s advice. This is the day it pays. (Note the &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;&quot;match&quot;&lt;/span&gt; spelling: the datafile serialization, not the dashboard&#39;s &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;match_type&lt;/span&gt;.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. A missing attribute is quieter than a wrong one.&lt;/strong&gt; Drop &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;device&lt;/span&gt; from the dictionary and the fall-through looks identical. But this time there is no WARN anywhere, and the SDK mentions missing attributes only at debug verbosity. Absent isn&#39;t false, and absent doesn&#39;t log. The gradient of silence: wrong type warns (if you wired logs), wrong name says nothing, missing says nothing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Case sensitivity, now in three places.&lt;/strong&gt; Part 2 had the case-sensitive &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;VariationKey&lt;/span&gt;. Part 3 adds attribute &lt;em&gt;names&lt;/em&gt; (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;Device&lt;/span&gt; &amp;ne; &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;device&lt;/span&gt;), attribute &lt;em&gt;values&lt;/em&gt; (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;Mobile&lt;/span&gt; &amp;ne; &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;mobile&lt;/span&gt;; our custom criterion deliberately Trim+lowercases editor input as a courtesy FX won&#39;t extend), and the geo header&#39;s &lt;em&gt;value&lt;/em&gt; (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;pl&lt;/span&gt; resolves to nothing). That last one is also the closing argument for the unification: part 2&#39;s hand-rolled parser folded case and would have accepted &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;pl&lt;/span&gt;. The native resolver doesn&#39;t. Two readers of one header with different tolerances is exactly the divergence class that breaks cross-engine and cross-head consistency: one resolver on the CMS, one written-down contract for the second head.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. A personalized item doesn&#39;t leak to Graph: it ceases to exist.&lt;/strong&gt; The coexistence section holds the measurement. The dangerous half is that nothing tells you: the sync succeeds, the schema is fine, the array is just empty. If your roadmap says &amp;ldquo;headless next year&amp;rdquo;, every CMS Audience you ship today is a block that will silently vanish in the migration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;7. One header steers two engines.&lt;/strong&gt; &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;AddCmsClientGeolocation&lt;/span&gt; is genuinely elegant. And it concentrates trust in a single header that, exposed without an edge to strip client values, both engines will believe. The part-2 caveat, squared.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;8. The datafile is still the clock.&lt;/strong&gt; Editing an audience is a datafile change like any other. This instance picked the fix from edge #1 up in about 75 seconds on the default polling. Not a repeat of part 2&#39;s lifecycle section. Just a reminder that audience edits ride the same train, and so does your kill switch.&lt;/p&gt;
&lt;h2&gt;FAQ, the questions as people actually type them&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Can I use Visitor Groups / CMS Audiences with a headless frontend?&lt;/strong&gt; No. Evaluation needs the CMS runtime, and the measurement here is blunter than the docs: a personalized content-area item isn&#39;t filtered for Graph consumers. It is &lt;em&gt;absent from the index entirely&lt;/em&gt;. Personalize whole page versions with FX Audiences instead. They evaluate in any SDK.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Targeted delivery or A/B test: which one do I want?&lt;/strong&gt; Deliveries deploy, experiments measure. A delivery produces no Results page at all, so the question is really &amp;ldquo;do I need to learn anything from this traffic?&amp;rdquo; If yes, experiment; if you already know and just want control and ramp, delivery.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why does my FX audience never match?&lt;/strong&gt; In observed order of likelihood: a stray character in the condition value, the wrong value case, the wrong attribute-name case, the wrong value &lt;em&gt;type&lt;/em&gt;, the attribute not sent at all. The runbook above walks the diagnostics. The short version: the panel shows what you sent, and the SDK log shows what the evaluator thought of it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can a delivery run above an experiment?&lt;/strong&gt; No. The ruleset evaluates experiments first, and the UI encodes that as fixed sections rather than a sortable list. Carve segments out of an experiment with audiences, not with rule order.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do I need ODP to use audiences?&lt;/strong&gt; No. Everything in this article runs on request-derived attributes. ODP (Real-Time Segments) is the path when targeting needs &lt;em&gt;customer data&lt;/em&gt; (past purchases, lifecycle stage) rather than facts about the current request.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How fast is the kill switch?&lt;/strong&gt; One datafile propagation. This instance picked up an audience edit in ~75 seconds on default polling. The guarantee is your polling interval (or webhook latency), not the dashboard click.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Does QA and test traffic pollute my results?&lt;/strong&gt; Dashboard allowlists do: they fire impressions like any decision. This build&#39;s &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;?fx_force&lt;/span&gt; deliberately doesn&#39;t, and the consent gate has a pleasant side effect: the entire automated suite (hundreds of requests per run) sends zero events, because test traffic never carries a consent cookie.&lt;/p&gt;
&lt;h2&gt;The take&lt;/h2&gt;
&lt;p&gt;The model that survives all of the above is short enough to memorize. The CMS owns &lt;strong&gt;what&lt;/strong&gt;: versioned, publishable content variations, plus fragment-level personalization wherever the CMS itself renders. FX rules own &lt;strong&gt;who and whether&lt;/strong&gt;: deterministic assignment, deliveries for rollout, experiments for measurement. Audiences, both kinds, own &lt;strong&gt;for whom&lt;/strong&gt;, and the kind you pick decides where the personalization can exist at all. CMS Audiences see everything and travel nowhere. FX Audiences see only what you send and run everywhere you do.&lt;/p&gt;
&lt;p&gt;For the business reader who skipped to the end: the mobile personalization in this demo shipped without a deploy, measured itself before an audience of three hundred synthetic visitors, survived a re-platform to a second rendering stack with zero integration code, and can be killed from a dashboard in about the time it takes to refresh a datafile. The block-level personalization shipped too. And it stopped at the CMS&#39;s edge, invisible to the headless head, unmeasured by anything but page-view counters. Both behaviors are by design. The architecture decision is choosing which design your roadmap can live with.&lt;/p&gt;
&lt;p&gt;The fragile parts haven&#39;t changed character since part 2: every failure in this article degraded to &amp;ldquo;someone quietly sees master&amp;rdquo; or &amp;ldquo;someone quietly sees too much&amp;rdquo;, and not one of them threw. The integration is still strings and dictionaries. The engineering is still making the silence loud: a diagnostics panel, an SDK logger, a startup self-check, and tests that assert the rule, not just the arm.&lt;/p&gt;
&lt;p&gt;Quiet hero, part 3: now it knows who you are. Still quiet, that&#39;s still the problem to engineer around.&lt;/p&gt;
&lt;h2&gt;Glossary&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin: 20px 0; font-size: 14px;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;Term&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;In one sentence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Goodness_of_fit&quot;&gt;Chi-square goodness-of-fit test&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Tests whether observed arm counts are consistent with the configured split; &amp;chi;&amp;sup2; = &amp;Sigma; (observed &amp;minus; expected)&amp;sup2; / expected.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Degrees_of_freedom_(statistics)&quot;&gt;Degrees of freedom&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Category counts free to vary; k arms give df = k &amp;minus; 1, so our three arms test at df = 2.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Statistical_significance&quot;&gt;Significance level &amp;alpha;&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;The false-alarm rate the test tolerates; this series uses &amp;alpha; = 0.001 so a healthy split almost never red-flags live.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Critical_value_(statistics)&quot;&gt;Critical value&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;The rejection threshold for the statistic, 13.816 for df = 2 at &amp;alpha; = 0.001.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Standard_error&quot;&gt;Sampling error&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Per-arm share fluctuates by &amp;asymp; &amp;radic;(p(1&amp;minus;p)/n), about 2.7 percentage points at n = 300, p = ⅓.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/MurmurHash&quot;&gt;MurmurHash&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;The deterministic hash every FX SDK applies to visitor ID + experiment; the reason parity needs no coordination.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Pure_function&quot;&gt;Pure function&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Same input, same output, no side effects, FX audience evaluation in one phrase.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Predicate_(mathematical_logic)&quot;&gt;Predicate&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;A boolean-valued function; both audience kinds are predicates over different inputs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Eventual_consistency&quot;&gt;Eventual consistency&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Distributed state converges after a delay, the datafile (~75 s observed) and the Graph index both live here.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/feature-experimentation/docs/run-flag-deliveries&quot;&gt;Targeted delivery&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;A rule serving one variation to an audience; deployment, not measurement, no Results page.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/feature-experimentation/docs/manage-config-datafile&quot;&gt;Datafile&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Configuration-as-data: the JSON snapshot of flags, rules and audiences every SDK evaluates locally.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;a href=&quot;https://samnewman.io/patterns/architectural/bff/&quot;&gt;Backend for Frontend&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;The server-side layer that keeps SDK and Graph keys out of the browser on the headless head.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Defense_in_depth_(computing)&quot;&gt;Defense in depth&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;Layered safeguards: consent gate, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;no-store&lt;/span&gt;, antiforgery, origin checks, none trusted alone.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/feature-experimentation/docs/run-a-b-tests&quot;&gt;Allowlist&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;A per-rule list (up to fifty user IDs) pinning specific visitors to specific arms; fires impressions like normal decisions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/feature-experimentation/docs/forced-decision-methods-csharp&quot;&gt;Forced decision&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;An SDK-level override bypassing audiences and allocation; not persistent, it lives and dies with the user context.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/v13.0.0-CMS/docs/audiences&quot;&gt;Create audiences (CMS 13)&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/v13.0.0-CMS/docs/personalize-a-digital-experience-with-audiences&quot;&gt;Personalize content with audiences&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/v13.0.0-CMS/docs/breaking-changes-in-cms-13&quot;&gt;CMS 13 breaking changes&lt;/a&gt;, the &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;AddVisitorGroupsMvc().AddVisitorGroupsUI()&lt;/span&gt; registration &amp;middot; &lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/v13.0.0-CMS/docs/install-cms13&quot;&gt;Install CMS 13&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/feature-experimentation/docs/interactions-between-flag-rules&quot;&gt;Interactions between flag rules&lt;/a&gt;, experiments before deliveries; roll-down vs jump&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/feature-experimentation/docs/run-flag-deliveries&quot;&gt;Run flag deliveries&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://support.optimizely.com/hc/en-us/articles/38816521665933&quot;&gt;Define attributes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/feature-experimentation/docs/forced-decision-methods-csharp&quot;&gt;Forced decision methods (C#)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/feature-experimentation/docs/upgrade-the-javascript-sdk-from-v5-to-v6&quot;&gt;Upgrade the JavaScript SDK from v5 to v6&lt;/a&gt;, the explicit &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;eventProcessor&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/feature-experimentation/docs/real-time-audiences-for-feature-experimentation&quot;&gt;Real-Time Segments for Feature Experimentation&lt;/a&gt;, the ODP path&lt;/li&gt;
&lt;li&gt;Part 1: &lt;a href=&quot;https://pino-labs.com/blog/content-variations-cms-13-quiet-hero/&quot;&gt;Content Variations in CMS 13, the quiet hero&lt;/a&gt; &amp;middot; Part 2: &lt;a href=&quot;/link/06d8f538fc754cb9b4362f2b31071f31.aspx&quot;&gt;Unlock Experimentation with Content Variations in CMS 13&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</id><updated>2026-06-14T17:46:48.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Hiding Pages in the Optimizely CMS 13 Page Tree</title><link href="http://parwissmark.wordpress.com/?p=306" /><id>When working with large Optimizely CMS solutions, the page tree can quickly become one of the biggest sources of editor frustration. This is especially true in multisite installations where many editors share the same platform, but only work with a limited part of the content structure. The access rights may be configured correctly, but the [&amp;#8230;]</id><updated>2026-06-13T20:54:18.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Four database surprises when upgrading from CMS 11 to CMS 13</title><link href="https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/6/four-database-surprises-when-upgrading-from-cms-11-to-cms-13/" /><id>&lt;div&gt;
&lt;div&gt;We&#39;re in the middle of migrating a fairly large site from CMS 11 / .NET Framework to CMS 13 / .NET 10. The code migration is one thing, but the database is where the real surprises live. Everything worked fine against our dev and stage databases &amp;mdash; and then we pointed the upgrade at a copy of the production database.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;Here are four things that bit us. Two of them are reported to Optimizely (one verified as a bug and fixed in the latest release, one in triage), so depending on when you read this you might be spared. The other two are by design in CMS 13 and you could hit them if your solution is old enough.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&lt;strong&gt;1. NULL values in tblSiteDefinition stop the site from booting&lt;/strong&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;CMS 13 replaces `SiteDefinition` with the new `Application` model, and on first boot a schema migration moves your rows from `tblSiteDefinition` to `tblApplication` with a plain `INSERT ... SELECT`. The catch: `Saved` and `SavedBy` are `NOT NULL` in the new table, and in our production database some of the old rows had NULL in those columns. The migration rolls back with an INSERT NULL violation and the site doesn&#39;t start at all.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;The kicker is that our stage databases didn&#39;t have this problem &amp;mdash; only production did. So if your upgrade rehearsals all run against stage copies, you can sail through every test and still faceplant on go-live night.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;The fix is a one-off backfill before the first CMS 13 boot:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;UPDATE tblSiteDefinition
SET Saved   = ISNULL(Saved, GETUTCDATE()),
    SavedBy = ISNULL(SavedBy, &#39;system&#39;)
WHERE Saved IS NULL OR SavedBy IS NULL;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;&lt;strong&gt;2. Tab names must be plain ASCII&lt;/strong&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;CMS 13 validates tab definition names at model sync, and it&#39;s strict: no non-ASCII characters, no spaces, no hyphens. Being a Swedish project we had tab names like &quot;S&amp;ouml;kinst&amp;auml;llningar&quot;, &quot;SEO-Settings&quot; and &quot;Externa l&amp;auml;nkar&quot; &amp;mdash; and the app refused to boot with:&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;```&lt;/div&gt;
&lt;div&gt;ValidationException: The Name &#39;X&#39; is not a valid format.&lt;/div&gt;
&lt;div&gt;```&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;So all tab names need to be plain ASCII identifiers. They&#39;re internal keys though &amp;mdash; if you want nicer editor-facing labels (or anything with &amp;aring;&amp;auml;&amp;ouml;), that&#39;s handled with tab translations in your localization files.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&lt;strong&gt;3. &quot;Content&quot; and &quot;Settings&quot; are reserved tab names&lt;/strong&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;This one is sneaky. CMS 13 reserves the tab names `Content` and `Settings` for its built-in tabs, and if you have those exact strings as your own tab names &amp;mdash; which we did, in a perfectly innocent-looking GroupNames class &amp;mdash; model sync throws a `ConflictingResourceException` against a freshly upgraded database.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;The confusing part is that `SystemTabNames.Content` isn&#39;t the string &quot;Content&quot; at all &amp;mdash; it&#39;s &quot;Information&quot;. And `SystemTabNames.Settings` is &quot;Advanced&quot;. So the constants never collide, only the literals do. The fix is simply to point your own constants at `SystemTabNames`:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public const string Content  = SystemTabNames.Content;   // &quot;Information&quot;
public const string Settings = SystemTabNames.Settings;  // &quot;Advanced&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;
&lt;div&gt;
&lt;div&gt;&lt;strong&gt;4. NULLs in tblSynchedUser break edit mode for everyone&lt;/strong&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;The site authenticates with WS-Federation against an external ADFS, and not every identity comes with `email`, `givenname` or `surname` claims. After user sync those rows end up with NULL in the corresponding columns in `tblSynchedUser` &amp;mdash; including one literally named `Metadata`. The columns are nullable, so we assumed that was fine.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;It isn&#39;t. As soon as anyone opens edit mode, the CMS shell tries to subscribe the current user to feature notifications, reads through the user table, and dies:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;System.Data.SqlTypes.SqlNullValueException: Data is Null.
   at EPiServer.DataAccess.Internal.SynchronizeUsersDB.&amp;lt;FindUsersAsync&amp;gt;g__CreateUser|18_0
   ...
   at FeatureNotificationService.SubscribeAsync&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;A single NULL row anywhere in the table is enough, and it doesn&#39;t have to belong to the user logging in. The public site keeps working &amp;mdash; only edit mode breaks &amp;mdash; which makes it genuinely confusing to troubleshoot.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;The workaround is backfilling the nullable string columns with empty strings. The good news: we reported this one, Optimizely verified it as a bug, and a fix ships in the latest release. If you&#39;re on an earlier 13.x, the backfill is:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;UPDATE tblSynchedUser SET Email            = &#39;&#39; WHERE Email IS NULL;
UPDATE tblSynchedUser SET GivenName        = &#39;&#39; WHERE GivenName IS NULL;
UPDATE tblSynchedUser SET LoweredGivenName = &#39;&#39; WHERE LoweredGivenName IS NULL;
UPDATE tblSynchedUser SET Surname          = &#39;&#39; WHERE Surname IS NULL;
UPDATE tblSynchedUser SET LoweredSurname   = &#39;&#39; WHERE LoweredSurname IS NULL;
UPDATE tblSynchedUser SET Metadata         = &#39;&#39; WHERE Metadata IS NULL;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&lt;strong&gt;Wrapping up&lt;/strong&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;The common thread: none of this shows up on a clean database. It&#39;s the years of accumulated production data &amp;mdash; old site definitions, legacy users synced before claims were complete, tab names from a more liberal era &amp;mdash; that trip the upgrade. So rehearse against a fresh copy of *production*, not stage, before the real cutover.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</id><updated>2026-06-12T10:02:44.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Designing ODP Real-Time Audiences for CMS Personalization and Experimentation</title><link href="https://wseweryn.dev/blog/2026-06-11-odp-real-time-audience-design/" /><id>A practical look at when to use ODP Real-Time Audiences, how to build them, and how they fit into CMS personalization and Feature Experimentation.</id><updated>2026-06-11T20:58:07.0000000Z</updated><summary type="html">Blog post</summary></entry></feed>