<?xml version="1.0" encoding="utf-8"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><language>en</language><title>Blogs</title> <link>https://world.optimizely.com/blogs/</link><description></description><ttl>60</ttl><generator>Optimizely World</generator><item> <title>Building UI Extensions for Optimizely SaaS CMS</title>            <link>https://world.optimizely.com/blogs/mark-stott/dates/2026/10/building-ui-extensions-for-optimizely-saas-cms/</link>            <description>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Developers have been extending the functionality of Optimizely PaaS CMS for years using addons built in C# and deployed as NuGet packages. Where you have control of the code for the CMS, this is the optimum approach for seamless integration. As Optimizely SaaS CMS becomes a more common approach for composable architecture, the way we extend the CMS has had to change. To fill this gap, Optimizely have created a new feature for Optimizely Connect Platform (OCP) and SaaS CMS called CMS UI Extensions which is in beta at the time of writing.&lt;/p&gt;
&lt;p&gt;A CMS UI Extension is an application built in &lt;strong&gt;Node.js&lt;/strong&gt; and &lt;strong&gt;JavaScript&lt;/strong&gt; that is deployed into the OCP. From there, your app can be installed into any SaaS CMS that is within your organisation. There are currently three ways that your CMS UI Extension can manifest within your SaaS CMS:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sidebar&lt;/strong&gt;: Appears within the CMS editor interface within a panel mounted to the right-hand side of the screen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;View&lt;/strong&gt;: Mounts a standalone full-width page that is accessed via the Extensions option in the left-hand menu.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Property Editor&lt;/strong&gt;: Replaces the built-in editor for a single content property of a declared type.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Setting Up Your Environment&lt;/h2&gt;
&lt;p&gt;To build apps for OCP, you need to be an Optimizely Partner or Customer with Developer access to an OCP environment. You may need to raise a ticket with Optimizely Support to activate the developer functionality for OCP for the first time. From there additional members of your team can be granted developer access by assigning the Developer role for OCP in the Admin Centre.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.stott.pro/assets/ocp-developer-portal.png&quot; alt=&quot;A screen shot of the OCP Developer Portal&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Once you have access to the OCP Developer Portal, generate an API key for the OCP CLI. Save the key to a credentials file at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.ocp/credentials.json&lt;/code&gt; and then install the CLI:&lt;/p&gt;
&lt;p&gt;Windows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-Powershell&quot;&gt;# Create the folder
New-Item -Path &quot;~/.ocp&quot; -Name &quot;home dir&quot; -ItemType &quot;directory&quot;

# Create the credentials file
Write-Output &#39;{&quot;apiKey&quot;: &quot;&amp;lt;your-api-key&amp;gt;&quot;}&#39; | out-file &quot;~/.ocp/credentials.json&quot; -encoding utf8

# Install OCP CLI
iwr -useb https://cli.ocp.optimizely.com/install.ps1 | iex
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mac:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-Bash&quot;&gt;# Create the folder
mkdir ~/.ocp (home dir)

# Create the credentials file
echo &#39;{&quot;apiKey&quot;: &quot;&amp;lt;your-api-key&amp;gt;&quot;}&#39; &amp;gt; ~/.ocp/credentials.json

# Install OCP CLI
curl -fsSL https://cli.ocp.optimizely.com/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should then be able to verify access with the whoami command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-Bash&quot;&gt;ocp accounts whoami
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Before Building Your Solution&lt;/h2&gt;
&lt;p&gt;Before you get started with building your application you should look at installing the &lt;a href=&quot;https://github.com/optimizely/ocp-skills/&quot;&gt;Optimizely OCP Skills&lt;/a&gt; for your AI coding assistant. Whether you clone these into your own repo or install them as a plugin is going to be your own decision. It does come with 4 skills, 2 of which are migration skills. If you are new to OCP app development, you can focus on the &lt;strong&gt;ocp-app-development&lt;/strong&gt; and &lt;strong&gt;ocp-local-testing&lt;/strong&gt; skills.&lt;/p&gt;
&lt;p style=&quot;padding-left: 20pt;&quot;&gt;&#128161; &lt;strong&gt;Tip:&lt;/strong&gt; These AI skills know how to build your app, how to register it, how to publish it for developer testing, beta testing and for production review. It is highly recommended that you use them.&lt;/p&gt;
&lt;p&gt;Optimizely have also created a sample OCP CMS UI Extension called &lt;a href=&quot;https://github.com/optimizely/unsplash-viewer-cms-ui-extension-ocp-app&quot;&gt;Unsplash Viewer&lt;/a&gt; that has both a &lt;strong&gt;sidebar&lt;/strong&gt; panel and a full-page &lt;strong&gt;view&lt;/strong&gt;. It is worth looking at this sample repository to see how the two entry points are configured and to understand the general architecture of a CMS UI Extension.&lt;/p&gt;
&lt;h2&gt;Thinking About Data&lt;/h2&gt;
&lt;p&gt;When building add-ons for Optimizely PaaS CMS, you had access to your own tables within the CMS database through tooling such as Entity Framework or by using the &lt;a href=&quot;https://docs.optimizely.com/cms-12/docs/dynamic-data-store&quot;&gt;Dynamic Data Store&lt;/a&gt;. These options are not available to us when building CMS UI Extensions. OCP does however provide access to the following data stores which are key-value pairs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Secrets Store&lt;/strong&gt;: Allows the storing of primitive data types only and is your general purpose storage for sensitive data such as API keys. This store is accessible from the backend of your OCP application.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Settings Store&lt;/strong&gt;: Allows the storing of primitive data types only and is the preferred place to store application settings, including sensitive data such as API keys. This data store can be accessed either by the backend of your application or as the settings form directly with the application settings within OCP.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Key Value Store&lt;/strong&gt;: Allows the storing of primitive data types and complex JSON objects for short-term and long-term storage. Do not store sensitive data here. This data store is typically accessed by the backend of your application.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shared Key Value Store&lt;/strong&gt;: Works like the Key Value Store, but is accessible by both your application and third-party applications.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you are planning to store a lot of data to the Key Value Store, consider co-locating the data as a JSON object. Each entry in the Key Value Store can be up to 400 KB in size and storing data together can reduce the number of API calls needed by your application.&lt;/p&gt;
&lt;p&gt;If the data you are storing to the Key Value Store is intended to have a limited lifespan, e.g. 1 hour, then data should be saved with a TTL (Time to Live). Data saved without a TTL will have a lifespan limited to the application installation.&lt;/p&gt;
&lt;p&gt;If you require data permenance, then you need to consider an alternative place to store your data. The Key Value Store is expected to be rebuildable in the same way that the Content Graph index is expected to be rebuildable. Any value without a TTL will be persisted for the life span of the application. If you need to reinstall or update the application or move it from one regional store to another, then the Key Value Store is completely reset.&lt;/p&gt;
&lt;p&gt;Options for permance should include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Using SDKs to store your data as content in the CMS or CMP.&lt;/li&gt;
&lt;li&gt;Using a third-party data store accessible via API using an API key stored within the Settings Store.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Reviewing the Stott Security for SaaS CMS Solution&lt;/h2&gt;
&lt;p&gt;Stott Security for SaaS CMS is a CMS UI Extension port of the popular &lt;a href=&quot;https://www.stott.pro/article/list/stott-security&quot;&gt;Stott Security&lt;/a&gt; add-on for Optimizely PaaS CMS. This has been built as a full-width page with a publicly accessible endpoint to be consumed by a separate headless frontend.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.stott.pro/assets/stott-security-saas-response-headers-screen.png&quot; alt=&quot;The Response headers tab of Stott Security for SaaS CMS, showing standard security headers with behaviour, value and preview options.&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Solution Structure&lt;/h3&gt;
&lt;p&gt;The following is a folder and file structure for the Stott Security for SaaS CMS solution. I&amp;rsquo;m only including core files here to prevent information overload, but do check out my solution or the Optimizely Unsplash Viewer example.&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;
&lt;div class=&quot;highlight&quot;&gt;
&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;- assets
  - icon.svg
  - logo.png
  - directory
    - overview.md
- forms
  - settings.yml
- src
  - backend
    - functions
      - CmsExtension.ts
      - CompiledHeaders.ts
  - cms-ui-extensions
    - SecurityConsole.view.tsx
- app.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;You may notice that the code is split into two separate runtimes, the backend and the CMS UI Extensions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Frontend
&lt;ul&gt;
&lt;li&gt;Runs within an iFrame hosted within Optimizely CMS&lt;/li&gt;
&lt;li&gt;Code lives in &lt;strong&gt;src/cms-ui-extensions/&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Talks to the backend through &lt;strong&gt;context.extension.invokeFunction()&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Exposes endpoints through &lt;strong&gt;ExtensionContext&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Backend
&lt;ul&gt;
&lt;li&gt;Runs on OCP in the &lt;strong&gt;node22-cms-ext&lt;/strong&gt; context&lt;/li&gt;
&lt;li&gt;Code lives in &lt;strong&gt;src/backend/&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Talks to
&lt;ul&gt;
&lt;li&gt;External APIs over HTTPS&lt;/li&gt;
&lt;li&gt;OCP storage&lt;/li&gt;
&lt;li&gt;Lifecycle APIs&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Understanding the app.yml&lt;/h3&gt;
&lt;p&gt;The app.yml file contains the manifest for your CMS UI Extension. You can see the full YAML file for this solution on &lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely.SaaS&quot;&gt;GitHub&lt;/a&gt;. The metadata for the application and all of its entry points and functions are declared within this file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-YML&quot;&gt;meta:
  app_id: stott_security_saas
  display_name: Stott Security
  vendor: mark_stott
  summary: Manage Content Security Policy and response headers for Optimizely CMS (SaaS).
  categories:
    - Content Management
  availability:
    - all

runtime: node22-cms-ext

ui_extensions:
  view:
    - name: stott-security-console
      entry_point: SecurityConsole
      display_name: Stott Security

functions:
  cms_extension:
    entry_point: CmsExtension
    description: Backend for the Stott Security console (draft CRUD and publish).
    accepts: cms_ui_extension

  compiled_headers:
    entry_point: CompiledHeaders
    description: Serves compiled security headers to the site head.

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;app_id&lt;/strong&gt;: This must be a globally unique name for your application and it is permanent. Changing this name is essentially publishing the application a second time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;vendor&lt;/strong&gt;: This is your vendor name and must match the vendor associated with your partner or customer organisation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;runtime&lt;/strong&gt;: This must be &lt;strong&gt;node22-cms-ext&lt;/strong&gt; for a CMS UI Extension and tells OCP that this application will be run in the CMS Extensions runtime operating on Node.js 22.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ui_extensions&lt;/strong&gt;: This is where you declare your entry points into the CMS and can be declared as:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;view&lt;/strong&gt;: A full-width embedded page.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;sidebar&lt;/strong&gt;: A sidebar embedded in a panel on the right-hand side of the editor interface.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;property-editor&lt;/strong&gt;: A replacement UI for a specific property type.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;functions&lt;/strong&gt;: This is where you declare your backend functions.
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;entry_point&lt;/strong&gt;: This is the name of your entry point.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;accepts&lt;/strong&gt;: If you set this to &lt;strong&gt;cms_ui_extension&lt;/strong&gt;, then it means the endpoint has no public URL and can only be accessed by the frontend using the &lt;strong&gt;invokeFunction()&lt;/strong&gt; method. Omit this if you want to provide a public endpoint.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;SDKs&lt;/h3&gt;
&lt;p&gt;A CMS UI Extension uses three Optimizely packages, plus a design system for the UI:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.npmjs.com/package/@zaiusinc/app-sdk&quot;&gt;@zaiusinc/app-sdk&lt;/a&gt;&lt;/strong&gt; (Backend): The OCP App SDK, which provides the framework for functions, storage and application lifecycle.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.npmjs.com/package/@optimizely/ocp-cms-ui-extensions-sdk&quot;&gt;@optimizely/ocp-cms-ui-extensions-sdk&lt;/a&gt;&lt;/strong&gt; (Build and Backend): A plugin for the OCP App SDK that adds support for defining and validating your app.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.npmjs.com/package/@optimizely/cms-extensibility-sdk&quot;&gt;@optimizely/cms-extensibility-sdk&lt;/a&gt;&lt;/strong&gt; (Frontend): The SDK for the CMS side of your extension that provides registration, delivery and access to your backend.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://optimizely-axiom.github.io/optiaxiom/&quot;&gt;@optiaxiom/react&lt;/a&gt;&lt;/strong&gt; (Frontend): The React implementation of Optimizely&amp;rsquo;s Axiom design system, which helps your extension look and feel like part of the CMS.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The plugin must be registered within &lt;strong&gt;ocp-app.config.mjs&lt;/strong&gt;, otherwise your &lt;strong&gt;ui_extensions&lt;/strong&gt; declarations will not be recognised and &lt;strong&gt;ocp app validate&lt;/strong&gt; will fail:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-JavaScript&quot;&gt;import { defineConfig } from &#39;@zaiusinc/app-sdk&#39;;
import { cmsUiExtensions } from &#39;@optimizely/ocp-cms-ui-extensions-sdk&#39;;

export default defineConfig({
  plugins: [cmsUiExtensions()]
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Backend&lt;/h3&gt;
&lt;p&gt;Each function declared within the &lt;strong&gt;functions &lt;/strong&gt;section of your &lt;strong&gt;app.yml&lt;/strong&gt; has a matching entry file within &lt;strong&gt;src/backend/functions/&lt;/strong&gt;. The &lt;strong&gt;entry_point &lt;/strong&gt;must match the name of the exported class, and that class must extend &lt;strong&gt;App.Function&lt;/strong&gt; from the &lt;strong&gt;@zaiusinc/app-sdk&lt;/strong&gt; package and implement a &lt;strong&gt;perform()&lt;/strong&gt; method that returns an &lt;strong&gt;App.Response&lt;/strong&gt;. One thing to watch out for is that the status code is the first argument for &lt;strong&gt;App.Response&lt;/strong&gt;, which is the opposite of the standard web Response object.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-TypeScript&quot;&gt;import * as App from &#39;@zaiusinc/app-sdk&#39;;

export class HelloWorld extends App.Function {
  public async perform(): Promise&amp;lt;App.Response&amp;gt; {
    return new App.Response(200, { message: &#39;Hello World&#39; });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Stott Security for SaaS CMS has two backend functions, one private and one public.&lt;/p&gt;
&lt;h4&gt;CmsExtension.ts&lt;/h4&gt;
&lt;p&gt;This is the private backend for the security console. Because it is declared with &lt;strong&gt;accepts: cms_ui_extension&lt;/strong&gt;, it has no public URL and can only be called by the frontend using &lt;strong&gt;context.extension.invokeFunction()&lt;/strong&gt;. Rather than creating a function per operation, the convention is to have a single function that accepts an action and a set of params and then switches on the action:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-TypeScript&quot;&gt;export class CmsExtension extends App.Function {
  public async perform(): Promise&amp;lt;App.Response&amp;gt; {
    const body = (this.request.bodyJSON ?? {}) as RequestBody;
    const action = typeof body.action === &#39;string&#39; ? body.action : &#39;&#39;;
    const params = body.params ?? {};

    switch (action) {
      case Actions.GetDraft:
        return ok(await this.getDraft(params));

      case Actions.SaveDraft:
        return ok(await this.saveDraft(params));

      case Actions.Publish:
        return ok(await this.publish(params));

      // ... additional actions

      default:
        return fail(400, ErrorCodes.UnknownAction, `Unknown action &#39;${action}&#39;.`);
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function is responsible for reading and writing the draft configuration, validating it, compiling it into a set of headers and then publishing the compiled headers. All of the data is held as JSON documents within the Key Value Store using &lt;strong&gt;storage.kvStore&lt;/strong&gt; from the app SDK. Saving a draft and publishing are deliberately separate actions as an incorrect Content Security Policy can break a website, so making changes live should be an explicit act by the user.&lt;/p&gt;
&lt;p&gt;Every response is wrapped in a consistent envelope of &lt;strong&gt;{ ok: true, result }&lt;/strong&gt; or &lt;strong&gt;{ ok: false, error, message }&lt;/strong&gt; so that the frontend has a single and predictable way of handling success and failure.&lt;/p&gt;
&lt;p&gt;The function also uses &lt;strong&gt;functions.getEndpoints()&lt;/strong&gt; to resolve the public URL of the &lt;strong&gt;compiled_headers&lt;/strong&gt; function for the current installation. This allows the console to show the editor exactly which URL their website should consume.&lt;/p&gt;
&lt;h4&gt;CompiledHeaders.ts&lt;/h4&gt;
&lt;p&gt;This is the public endpoint that the head of the website calls in order to retrieve the published security headers. As &lt;strong&gt;accepts &lt;/strong&gt;is omitted within the &lt;strong&gt;app.yml&lt;/strong&gt;, OCP gives this function a public URL which contains a unique identifier for the installation. This matters because a regular &lt;strong&gt;App.Function&lt;/strong&gt; resolves the installation from that URL and can therefore read the same Key Value Store that &lt;strong&gt;CmsExtension &lt;/strong&gt;writes to. If this were declared as a global function, it would have no installation context and could only reach the Shared Key Value Store.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-TypeScript&quot;&gt;export class CompiledHeaders extends App.Function {
  public async perform(): Promise&amp;lt;App.Response&amp;gt; {
    if (this.request.method !== &#39;GET&#39;) {
      return new App.Response(405, { error: &#39;Only GET is supported.&#39; });
    }

    try {
      const compiled = await readCompiled(scope);

      return new App.Response(200, {
        headers: compiled?.headers ?? [],
        publishedAt: compiled?.publishedAt ?? null,
        cacheSeconds: CACHE_SECONDS
      });
    } catch (error) {
      logger.error(`Failed to read compiled headers: ${String(error)}`);

      return new App.Response(503, { error: &#39;Compiled headers are temporarily unavailable.&#39; });
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A few design decisions are worth highlighting here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The endpoint only reads the &lt;strong&gt;compiled&lt;/strong&gt; headers, all of the work to compile the configuration happens once on publish rather than on every request.&lt;/li&gt;
&lt;li&gt;The response includes a &lt;strong&gt;cacheSeconds &lt;/strong&gt;value to encourage the consuming website to cache the response. Without this, every page request on the head would result in a request to OCP.&lt;/li&gt;
&lt;li&gt;Having nothing published yet returns an empty array with a &lt;strong&gt;200 &lt;/strong&gt;status so the head can apply its own defaults, while a genuine failure returns a &lt;strong&gt;503 &lt;/strong&gt;so the head can tell the difference and fall back gracefully.&lt;/li&gt;
&lt;li&gt;The response contract is identical to the compiled headers endpoint in the PaaS version of Stott Security, allowing a single consumer implementation to work with either product.&lt;/li&gt;
&lt;li&gt;The endpoint is anonymous by design. These headers are visible on every response from the website anyway, but this is a deliberate decision and you should consider carefully what data you expose from any public function. If your data has privacy needs, then consider requiring an API Key.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&#128240; Please note that I will cover consuming this endpoint in a headless solution in a future article so as to not detract from the topic of this article.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Frontend&lt;/h3&gt;
&lt;p&gt;Each entry within the &lt;strong&gt;ui_extensions &lt;/strong&gt;section of your &lt;strong&gt;app.yml&lt;/strong&gt; has a matching entry file within &lt;strong&gt;src/cms-ui-extensions/&lt;/strong&gt;. For Stott Security for SaaS CMS, this is a single view with an entry point of &lt;strong&gt;SecurityConsole&lt;/strong&gt;, which maps to &lt;strong&gt;SecurityConsole.view.tsx&lt;/strong&gt;. The frontend is built with React and is rendered within an iFrame inside of the CMS.&lt;/p&gt;
&lt;p&gt;Your CMS UI Extension may have multiple entry points that manifest within the CMS in different ways. The naming convention for a UI entry file is as follows:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;
&lt;div class=&quot;highlight&quot;&gt;
&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;lt;Name&amp;gt;.&amp;lt;injection-point&amp;gt;.{ts,tsx,js,jsx}

// Examples
MyExtension.view.tsx
MyExtension.sidebar.ts
MyExtension.property-editor.js
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4&gt;SecurityConsole.view.tsx&lt;/h4&gt;
&lt;p&gt;The most important line in this file is at the very bottom. The &lt;strong&gt;register() &lt;/strong&gt;method from the &lt;strong&gt;@optimizely/cms-extensibility-sdk&lt;/strong&gt; package is how your extension is mounted into the CMS. This must be called exactly once at the top level of the module and never inside of a component or effect. The CMS passes an &lt;strong&gt;ExtensionContext &lt;/strong&gt;into the callback, which you then pass down into your components:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-TypeScript&quot;&gt;import { register, type ExtensionContext } from &#39;@optimizely/cms-extensibility-sdk&#39;;

function SecurityConsole({ context }: { context: ExtensionContext }): React.JSX.Element {
  useEffect(() =&amp;gt; {
    void context.extension.setReady();
  }, [context]);

  // ... render the console
}

register((context) =&amp;gt; &amp;lt;SecurityConsole context={context} /&amp;gt;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;strong&gt;ExtensionContext &lt;/strong&gt;is your bridge between the frontend and both the CMS and your backend. Within the console, this is used for two key purposes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;context.extension.setReady()&lt;/strong&gt; is called once the component has mounted to let the CMS know that the extension has loaded.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;context.extension.invokeFunction() &lt;/strong&gt;is used to call the private &lt;strong&gt;CmsExtension &lt;/strong&gt;backend function.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All backend calls are wrapped within a small typed client, so the components never need to know about the transport:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-TypeScript&quot;&gt;const result = await context.extension.invokeFunction(&#39;cms_extension&#39;, { action, params });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A nice benefit of this approach is that there are no URLs, authentication cookies, CORS rules or CSRF tokens to manage within the frontend. The call never leaves the CMS host and OCP takes care of routing it to your backend function. It is worth noting that &lt;strong&gt;invokeFunction()&lt;/strong&gt; will only reject on a transport failure, application errors are returned as a normal response with a non-2xx status code, so you will want to handle both.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;ExtensionContext &lt;/strong&gt;offers more than the console needs. Which members are available depends on where your extension manifests within the CMS:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;context.extension&lt;/strong&gt;: Available to all extensions.
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;setReady()&lt;/strong&gt; tells the CMS that your extension has finished loading. The CMS may show a loading indicator until this is called.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;getDefinition()&lt;/strong&gt; returns the id, display name, type and configuration of your extension.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;invokeFunction(functionId, parameters)&lt;/strong&gt; calls a backend function. The &lt;strong&gt;functionId &lt;/strong&gt;is the key under &lt;strong&gt;functions &lt;/strong&gt;in your &lt;strong&gt;app.yml&lt;/strong&gt; (e.g. &lt;strong&gt;cms_extension&lt;/strong&gt;) and not the class name.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;context.content&lt;/strong&gt;: Only meaningful within a &lt;strong&gt;sidebar&lt;/strong&gt;.
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;get()&lt;/strong&gt; returns a snapshot of the content currently being edited, including its key, version, locale and public URL.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;subscribe(callback)&lt;/strong&gt; notifies you when the content being edited changes.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;context.property&lt;/strong&gt;: Only bound within a &lt;strong&gt;property-editor&lt;/strong&gt;.
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;get()&lt;/strong&gt; returns the current value of the property, whether it is read-only and any property settings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;set(value)&lt;/strong&gt; updates the value of the property. This resolves to &lt;strong&gt;{ success, error } &lt;/strong&gt;rather than throwing an error, so make sure you check success.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;subscribe(callback)&lt;/strong&gt; notifies you when the value of the property changes.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For both &lt;strong&gt;content &lt;/strong&gt;and &lt;strong&gt;property&lt;/strong&gt;, the &lt;strong&gt;subscribe()&lt;/strong&gt; method only fires on subsequent changes. You should call &lt;strong&gt;get()&lt;/strong&gt; to read the initial value and then &lt;strong&gt;subscribe()&lt;/strong&gt; to receive later changes, otherwise your component will remain empty until something changes. The &lt;strong&gt;subscribe()&lt;/strong&gt; method also returns a function that you should call to unsubscribe when your component unmounts.&lt;/p&gt;
&lt;p&gt;The rest of the file is a fairly standard React component. The UI is built with &lt;a href=&quot;https://www.npmjs.com/package/@optiaxiom/react&quot;&gt;Optimizely Axiom&lt;/a&gt; (&lt;strong&gt;@optiaxiom/react&lt;/strong&gt;), which is Optimizely&amp;rsquo;s own design system, and helps the extension look and feel like a native part of the CMS. Every Axiom component reads its design tokens from the &lt;strong&gt;AxiomProvider&lt;/strong&gt;, so this needs to wrap your entire extension or the components will render unstyled.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-TypeScript&quot;&gt;function Shell({ children }: { children: React.ReactNode }): React.JSX.Element {
  return (
    &amp;lt;AxiomProvider&amp;gt;
      &amp;lt;main&amp;gt;{children}&amp;lt;/main&amp;gt;
    &amp;lt;/AxiomProvider&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are a few other things to be aware of when building a &lt;strong&gt;view&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;view &lt;/strong&gt;is a single mounted bundle. For my multi-tab interface I manage what is visible through component state rather than using a client side router.&lt;/li&gt;
&lt;li&gt;The CMS already renders a title for your extension, so there is no need to repeat it within your own UI.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;ExtensionContext &lt;/strong&gt;does not currently carry the identity of the user, so you cannot rely on it for attributing actions to a named editor.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;App Directory Information&lt;/h3&gt;
&lt;p&gt;The assets folder of your repository is used to provide additional information for when your app is added to the app directory:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;assets/logo.png&lt;/strong&gt; and &lt;strong&gt;assets/icon.svg&lt;/strong&gt; provide a logo to be used both within the card on the App Directory and within the summary card on the app page.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;assets/directory/overview.md&lt;/strong&gt; provides the content to be displayed in the Overview tab within OCP.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://www.stott.pro/assets/ocp-cms-ui-extensions-1.png&quot; alt=&quot;The CMS UI Extensions application detail screen within OCP.&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Application Settings&lt;/h3&gt;
&lt;p&gt;Your app must have a &lt;strong&gt;forms/settings.yml&lt;/strong&gt; which is used to declare the settings form within OCP for your app. This allows you to manage data within the Settings Store for your installation of the app directly within OCP.&lt;/p&gt;
&lt;p&gt;As Stott Security for SaaS CMS does not require any data from the Settings Store, my entry contains a single information entry:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-YML&quot;&gt;sections:
  - key: about
    label: Stott Security
    elements:
      - type: instructions
        text: &amp;gt;
          Everything is configured in the security console...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Other options you can add to your settings.yml include the following, you can see a full description and examples of each option by viewing &lt;a href=&quot;https://docs.optimizely.com/optimizely-connect-platform/docs/form-elements-ocp2&quot;&gt;OCP Form Elements&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Toggle&lt;/strong&gt;: An option you can toggle on or off.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Text&lt;/strong&gt;: A plain input box, additional options can be set to change the input type to text, email, number or phone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secret&lt;/strong&gt;: A secure field for sensitive data that will be displayed as asterisks. Data added to one of these fields cannot be retrieved through the settings form interface.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Select&lt;/strong&gt;: A single-select field that renders differently based on the number of options provided.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-Select&lt;/strong&gt;: A multi-select field that renders differently based on the number of options provided.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Button&lt;/strong&gt;: Allows for an action to be triggered within the form e.g. &amp;ldquo;Begin Integration&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Link Button&lt;/strong&gt;: Allows for a link to an external URL and supports Mustache syntax for URL variation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OAuth Button&lt;/strong&gt;: Supports authentication with a third party in order to support integrations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Divider&lt;/strong&gt;: Renders a simple divider mid-form.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instructions&lt;/strong&gt;: Supports instructions to be displayed to the user written in Markdown.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;CMS UI Extensions are a significant leap forward for Optimizely SaaS CMS. For the first time, we can extend the SaaS editor interface in the same way that we have been extending PaaS CMS with add-ons for years, whether that is a full-width tool, a sidebar that works alongside the editor or a custom property editor.&lt;/p&gt;
&lt;p&gt;The technology choice also makes a lot of sense. CMS UI Extensions are built with Node.js &amp;amp; JavaScript potentially with frameworks such as TypeScript and React. These are the same tools that developers building headless solutions on SaaS CMS are already using every day. This means the barrier to entry is low for the teams most likely to want to build them.&lt;/p&gt;
&lt;p&gt;There are still some challenges to work through. Data storage is limited to key value stores that are tied to the lifespan of the application installation, so anything that needs permenance requires some careful thought. As this is a new feature, best practices are also still emerging.&lt;/p&gt;
&lt;p&gt;Overall, CMS UI Extensions are a really good addition to Optimizely SaaS CMS, and I&amp;rsquo;m looking forward to seeing what the community builds with them. If you are about to build your first CMS UI Extension, here are a few tips to help you get started:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Install the &lt;a href=&quot;https://github.com/optimizely/ocp-skills/&quot;&gt;Optimizely OCP Skills&lt;/a&gt; as this will help your AI assistant to structure your application correctly.&lt;/li&gt;
&lt;li&gt;Review Optimizely&amp;rsquo;s example CMS UI Extension: &lt;a href=&quot;https://github.com/optimizely/unsplash-viewer-cms-ui-extension-ocp-app&quot;&gt;Unsplash Viewer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Consider app ownership and naming very carefully. App names must be unique and are permanent.&lt;/li&gt;
&lt;li&gt;Consider whether you need data permanence and where that data will live.&lt;/li&gt;
&lt;li&gt;Build for performance and leverage caching wherever possible.&lt;/li&gt;
&lt;li&gt;If you are using the Key Value Store, co-locate your data as JSON to reduce the number of API calls.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Appendix&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.optimizely.com/optimizely-connect-platform/docs/cms-ui-extensions-ocp2&quot;&gt;Optimizely Docs : CMS UI extensions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.optimizely.com/optimizely-connect-platform/docs/get-started-with-the-ocp2-developer-platform&quot;&gt;Optimizely Docs : Get started with the OCP developer platform&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.optimizely.com/optimizely-connect-platform/docs/configure-your-development-environment-ocp2&quot;&gt;Optimizely Docs : Configure your OCP development environment&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.optimizely.com/optimizely-connect-platform/docs/form-elements-ocp2&quot;&gt;Optimizely Docs : OCP Form Elements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/optimizely/ocp-skills/&quot;&gt;GitHub : Optimizely OCP Skills&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/optimizely/unsplash-viewer-cms-ui-extension-ocp-app&quot;&gt;GitHub : Unsplash Viewer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely.SaaS&quot;&gt;GitHub : Stott Security for SaaS CMS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;I&#39;m a Platinum Optimizely MVP and the maintainer of two open source add-ons: Stott Security for &lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely.SaaS&quot;&gt;SaaS CMS&lt;/a&gt; and &lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely&quot;&gt;PaaS CMS 12 &amp;amp; 13&lt;/a&gt;, and &lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Optimizely.RobotsHandler&quot;&gt;Stott Robots Handler&lt;/a&gt; for PaaS CMS 12 &amp;amp; 13. All of my writing is collated at &lt;a href=&quot;https://www.stott.pro/&quot;&gt;https://www.stott.pro&lt;/a&gt;.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/mark-stott/dates/2026/10/building-ui-extensions-for-optimizely-saas-cms/</guid>            <pubDate>Thu, 15 Oct 2026 16:59:39 GMT</pubDate>           <category>Blog post</category></item><item> <title>Cheapskate AI Translator: Because Translation Should Not Require a Second Mortgage</title>            <link>https://world.optimizely.com/blogs/linh-doan-cuu/dates/2026/10/cheapskate-ai-translator-because-translation-should-not-require-a-second-mortgage/</link>            <description>&lt;p&gt;There is a particular kind of software project that begins with a detailed architecture document, a product roadmap, and a carefully assembled budget.&lt;/p&gt;
&lt;p&gt;This is not that kind of project.&lt;/p&gt;
&lt;p&gt;This project began with a simpler question:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Can I create language versions in Optimizely CMS without paying more for an LLM than I absolutely have to?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The answer is now an open source add-on: &lt;strong&gt;Cheapskate.AITranslator&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;You can find the source code on &lt;em&gt;&lt;a href=&quot;https://github.com/birgitzprinz/Cheapskate.AITranslator&quot;&gt;GitHub&lt;/a&gt; &lt;/em&gt;and install the published package from the&lt;em&gt; &lt;a href=&quot;https://nuget.optimizely.com/packages/cheapskate.aitranslator/&quot;&gt;Optimizely NuGet feed&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;The honest motivation&lt;/h2&gt;
&lt;p&gt;I am a cheapskate.&lt;/p&gt;
&lt;p&gt;Not in the charming, coupon-clipping sense. In the practical engineering sense. If a free tier, a generous quota, or a small amount of careful request batching can do the job, I would prefer that to adding another monthly AI bill to the household budget.&lt;/p&gt;
&lt;p&gt;The goal is not to avoid paying for good services forever. The goal is to pay as little as I can get away with while still delivering useful translations and a maintainable CMS workflow.&lt;/p&gt;
&lt;p&gt;That means the add-on is designed around choice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Google Gemini-compatible chat completions&lt;/li&gt;
&lt;li&gt;GroqCloud-compatible chat completions&lt;/li&gt;
&lt;li&gt;Microsoft Azure Translator&lt;/li&gt;
&lt;li&gt;DeepL API Free&lt;/li&gt;
&lt;li&gt;Custom OpenAI-compatible providers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some providers offer free tiers, some offer free quotas, and some are inexpensive enough to be useful when a free option is not appropriate. The add-on does not assume that one provider, one pricing model, or one API will remain the best choice forever.&lt;/p&gt;
&lt;h2&gt;What the add-on does&lt;/h2&gt;
&lt;p&gt;Optimizely content editors can create draft language versions from the editor and choose whether to translate them with AI after the drafts are created.&lt;/p&gt;
&lt;p&gt;The add-on can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create all missing language branches or one selected target language&lt;/li&gt;
&lt;li&gt;Keep the new versions as drafts rather than publishing them&lt;/li&gt;
&lt;li&gt;Optionally follow linked content in &lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;ContentArea&lt;/span&gt; properties&lt;/li&gt;
&lt;li&gt;Translate language-specific text and Rich Text content&lt;/li&gt;
&lt;li&gt;Preserve HTML structure while translating visible text&lt;/li&gt;
&lt;li&gt;Run translation in the background after draft creation&lt;/li&gt;
&lt;li&gt;Use different providers through one CMS settings screen&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The workflow is intentionally modest. It creates drafts, starts translation work, and reports the job status. It does not pretend that an automated translation should bypass editorial review.&lt;/p&gt;
&lt;p&gt;That last part is important. A translation engine can be very good and still occasionally produce something that makes a human editor stare at the screen for a while. Drafts are a useful place for that staring to happen.&lt;/p&gt;
&lt;h2&gt;The small trick that saves API calls&lt;/h2&gt;
&lt;p&gt;The add-on can combine multiple translatable properties into one JSON request instead of making one API request per property.&lt;/p&gt;
&lt;p&gt;Without batching, a page with a title, teaser, body, call to action, and several blocks can turn into a small parade of API calls. That is inefficient, costs more, and can hit provider rate limits faster than necessary.&lt;/p&gt;
&lt;p&gt;With batching enabled, the worker groups translation entries into bounded requests according to configured input and output token limits. The provider receives a structured payload, translates the values, and returns the translated entries for the background worker to write back to the draft.&lt;/p&gt;
&lt;p&gt;In plain English:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Fewer trips to the API means fewer opportunities for the API to say, &quot;Please slow down.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Batching does not remove provider limits, and it does not make an unlimited service out of a limited one. It simply uses the available quota more sensibly. The request-size limits remain configurable because different providers have different capabilities and pricing rules.&lt;/p&gt;
&lt;h2&gt;Why support several providers?&lt;/h2&gt;
&lt;p&gt;Because depending on one provider is an excellent way to discover that provider&#39;s outage page at the worst possible moment.&lt;/p&gt;
&lt;p&gt;More seriously, different projects have different constraints:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A project may already have an Azure subscription and prefer Azure Translator.&lt;/li&gt;
&lt;li&gt;A developer may want to use a free Gemini or GroqCloud quota.&lt;/li&gt;
&lt;li&gt;A team may prefer DeepL for a particular language pair.&lt;/li&gt;
&lt;li&gt;An organization may have its own OpenAI-compatible gateway.&lt;/li&gt;
&lt;li&gt;A provider&#39;s model, pricing, quota, or terms may change.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The add-on keeps the CMS workflow stable while allowing the translation backend to change. Provider credentials and options live in CMS settings rather than in source control, and the custom provider option gives developers room to connect a compatible internal service.&lt;/p&gt;
&lt;h2&gt;This is not Optimizely Language Manager&lt;/h2&gt;
&lt;p&gt;A very important clarification: &lt;strong&gt;Cheapskate.AITranslator is not intended to replace Optimizely&#39;s official Language Manager add-on.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Language Manager is the official product for teams that need its supported translation-management workflow, integrations, governance, and product support. This add-on has a narrower and more developer-oriented purpose.&lt;/p&gt;
&lt;p&gt;Cheapskate.AITranslator brings the implementation into the developer&#39;s control. The source code is available, the behavior can be inspected, and the add-on can be customized for a particular Optimizely CMS solution.&lt;/p&gt;
&lt;p&gt;That may mean changing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which content properties are translated&lt;/li&gt;
&lt;li&gt;How linked content is traversed&lt;/li&gt;
&lt;li&gt;How prompts are written&lt;/li&gt;
&lt;li&gt;Which provider is called&lt;/li&gt;
&lt;li&gt;How batches are sized&lt;/li&gt;
&lt;li&gt;How translation jobs are queued or reported&lt;/li&gt;
&lt;li&gt;Which editorial safeguards are required&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your project needs an officially supported translation-management product, use Language Manager. If your project needs a small, inspectable starting point that your team can adapt, this add-on may be useful.&lt;/p&gt;
&lt;p&gt;It is not a product-versus-product argument. It is a choice between a supported official solution and a source-controlled developer tool with a smaller scope.&lt;/p&gt;
&lt;h2&gt;Open source is the point&lt;/h2&gt;
&lt;p&gt;The most valuable part of this add-on is not the skate icon or the settings page. It is the ability to see what happens between clicking &lt;strong&gt;Create Language Versions&lt;/strong&gt; and saving a translated draft.&lt;/p&gt;
&lt;p&gt;Developers can review the code, change the translation rules, add provider behavior, adjust batching, and fit the workflow to their own content model. The add-on is deliberately not a sealed box that asks everyone to accept the same assumptions.&lt;/p&gt;
&lt;p&gt;That also means responsibility comes with the freedom:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Review generated translations before publishing.&lt;/li&gt;
&lt;li&gt;Protect provider credentials.&lt;/li&gt;
&lt;li&gt;Check provider terms and quotas.&lt;/li&gt;
&lt;li&gt;Test prompts and content types in a non-production environment.&lt;/li&gt;
&lt;li&gt;Keep the add-on aligned with the CMS version used by the project.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Cheap is good. Cheap and mysterious is less good.&lt;/p&gt;
&lt;h2&gt;Getting started&lt;/h2&gt;
&lt;p&gt;The project targets Optimizely CMS 13 and .NET 10 on the &lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;main&lt;/span&gt; branch. The legacy CMS 12 implementation remains on the&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;CMS-12-base&lt;/span&gt; branch.&lt;/p&gt;
&lt;p&gt;Install the CMS 13 package with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;dotnet add .\YourCmsProject.csproj package Cheapskate.AITranslator --version 2.0.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then configure a provider under &lt;strong&gt;Settings &amp;gt; AI Translator&lt;/strong&gt;, open localizable content in the editor, and use the &lt;strong&gt;Create Language Versions&lt;/strong&gt; command.&lt;/p&gt;
&lt;p&gt;The resulting versions are drafts. The AI translation runs in the background when enabled, and the editor remains responsible for the final content review.&lt;/p&gt;
&lt;h2&gt;Final accounting&lt;/h2&gt;
&lt;p&gt;The philosophy is simple:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use the provider that fits the project.&lt;/li&gt;
&lt;li&gt;Use free quotas where they are enough.&lt;/li&gt;
&lt;li&gt;Batch requests so the quota lasts longer.&lt;/li&gt;
&lt;li&gt;Keep the workflow in developer-controlled source code.&lt;/li&gt;
&lt;li&gt;Spend money when spending money is actually justified.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That is the entire cheapskate strategy. It is not glamorous, but neither is discovering a surprisingly large AI invoice after a bulk translation job.&lt;/p&gt;
&lt;p&gt;Cheapskate.AITranslator is open source under the MIT license: a small Optimizely add-on for teams that want draft language creation, optional AI translation, multiple provider choices, and a little more control over where the money goes.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/linh-doan-cuu/dates/2026/10/cheapskate-ai-translator-because-translation-should-not-require-a-second-mortgage/</guid>            <pubDate>Thu, 15 Oct 2026 08:06:38 GMT</pubDate>           <category>Blog post</category></item><item> <title>Computed Fields in CMS 13 Content Contracts with Optimizely Graph</title>            <link>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/computed-fields-in-cms-13-content-contracts-a-practical-guide-with-optimizely-graph-c-sdk/</link>            <description>&lt;p&gt;Content Contracts in Optimizely CMS 13 allow different content types to share a common structure when querying Optimizely Graph.&lt;/p&gt;
&lt;p&gt;But what if the shared field doesn&#39;t exist as a stored CMS property, and its value needs to be calculated differently depending on the content type?&lt;/p&gt;
&lt;p&gt;For example, imagine a search feature that includes articles, events, and images:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Articles use &lt;strong&gt;Heading&lt;/strong&gt; as their search title.&lt;/li&gt;
&lt;li&gt;Events use &lt;strong&gt;EventName&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Images use &lt;strong&gt;ImageTitle&lt;/strong&gt;, falling back to their name.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We want all three to expose a common &lt;strong&gt;SearchTitle&lt;/strong&gt; field in Graph, without adding another persisted property to each content type.&lt;/p&gt;
&lt;p&gt;Here&#39;s a working approach tested with &lt;strong&gt;Optimizely CMS 13.3.0&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;1. Define a Content Contract&lt;/h2&gt;
&lt;p&gt;First, define a Content Contract with a method that each implementing content type uses to provide its search title.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ContentType]
public interface ISearchableContent : IContentData
{
    string GetSearchTitle();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each content type implements the same contract but calculates the search title using its own properties. For example, an article page uses &lt;strong&gt;Heading&lt;/strong&gt;, while an event page uses &lt;strong&gt;EventName&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ArticlePage : PageData, ISearchableContent
{
    public virtual string Heading { get; set; }

    public string GetSearchTitle() =&amp;gt;
        string.IsNullOrWhiteSpace(Heading) ? Name : Heading;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class EventPage : PageData, ISearchableContent
{
    public virtual string EventName { get; set; }

    public string GetSearchTitle() =&amp;gt;
        string.IsNullOrWhiteSpace(EventName) ? Name : EventName;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both content types implement the same contract but calculate &lt;strong&gt;SearchTitle&lt;/strong&gt; differently. The same approach works for media types, such as images, using their own properties.&lt;/p&gt;
&lt;p&gt;Make sure the relevant content types implement or are associated with the Content Contract in CMS so they can be queried together in Graph.&lt;/p&gt;
&lt;p&gt;This keeps the calculation close to each content type and avoids maintaining a central switch statement.&lt;/p&gt;
&lt;h2&gt;2. Expose the Computed Field in Graph&lt;/h2&gt;
&lt;p&gt;Next, create an extension method and register it using Graph indexing conventions.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static class SearchExtensions
{
    public static string SearchTitle(this ISearchableContent content)
        =&amp;gt; content.GetSearchTitle();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Register the computed field:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services.AddContentGraph(configureConventions: conventions =&amp;gt;
{
    conventions.ForInstancesOf&amp;lt;ISearchableContent&amp;gt;()
        .IncludeField(
            x =&amp;gt; x.SearchTitle(),
            IndexingType.Searchable);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important detail is that &lt;strong&gt;SearchTitle is computed during indexing&lt;/strong&gt;. We don&#39;t need to add a persisted SearchTitle property to our CMS models.&lt;/p&gt;
&lt;p&gt;The resulting Graph schema exposes the field through &lt;strong&gt;ISearchableContent&lt;/strong&gt;, regardless of the underlying content type.&lt;/p&gt;
&lt;h2&gt;3. Query It with the Graph C# SDK&lt;/h2&gt;
&lt;p&gt;The interesting part is that the computed field can also be used in strongly typed SDK queries.&lt;/p&gt;
&lt;p&gt;First, define a lightweight result model:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class SearchResult
{
    public string SearchTitle { get; set; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then query the Content Contract:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var query = graphClient
    .QueryContent&amp;lt;ISearchableContent&amp;gt;()
    .Where(x =&amp;gt; x.SearchTitle().Contains(&quot;Optimizely&quot;))
    .Limit(20)
    .IncludeTotal()
    .Fields&amp;lt;SearchResult&amp;gt;(x =&amp;gt; x.SearchTitle);

var result = await query.GetAsync();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In our test, the SDK translated the filtering expression into GraphQL:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;query ISearchableContentQuery {
  ISearchableContent(
    where: { SearchTitle: { contains: &quot;Optimizely&quot; } }
    limit: 20
  ) {
    items {
      SearchTitle
    }
    total(all: true)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The query returned one matching event with &lt;strong&gt;SearchTitle&lt;/strong&gt; equal to &lt;strong&gt;Optimizely Developer Webinar&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Here&#39;s the interesting detail: &lt;strong&gt;SearchTitle()&lt;/strong&gt; is a C# extension method, not a stored CMS property. Graph indexing conventions expose it as a searchable field, and the Graph C# SDK translates the expression into a server-side GraphQL filter rather than filtering an in-memory collection.&lt;/p&gt;
&lt;p&gt;We also verified sorting by the computed field, language-specific values, and recalculation after updating and publishing the underlying content.&lt;/p&gt;
&lt;h2&gt;Key Takeaway&lt;/h2&gt;
&lt;p&gt;By combining &lt;strong&gt;CMS 13 Content Contracts, Graph indexing conventions, and the Graph C# SDK&lt;/strong&gt;, we can expose a shared searchable field across different content types while keeping the calculation specific to each type.&lt;/p&gt;
&lt;p&gt;No additional persisted CMS properties are required.&lt;/p&gt;
&lt;p&gt;Keep computed field calculations lightweight and deterministic, since they run as part of the indexing process.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.optimizely.com/cms-13/docs/indexing-conventions&quot;&gt;Optimizely CMS 13 &amp;mdash; Indexing Conventions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/link/5ffa0c0d7b184f049358e62d7fcad94f.aspx&quot;&gt;Introducing Optimizely CMS 13 Graph SDK &amp;mdash; Jake Minard&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>            <guid>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/computed-fields-in-cms-13-content-contracts-a-practical-guide-with-optimizely-graph-c-sdk/</guid>            <pubDate>Wed, 14 Oct 2026 05:01:48 GMT</pubDate>           <category>Blog post</category></item><item> <title>Opal Workflow Agents And The Graph Webhook Trap</title>            <link>https://world.optimizely.com/blogs/ritu-madan/dates/2026/10/opal-workflow-agents-and-the-graph-webhook-trap/</link>            <description>&lt;h2&gt;What we did?&lt;/h2&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;&lt;span style=&quot;mso-bookmark: what-we-were-building;&quot;&gt;We built a compliance review workflow agent in Opal. The job was simple: when an editor submits a page for review in Optimizely CMS 13, the agent reads the content, checks it against defined compliance rules, and writes its findings back onto the item so a human reviewer sees them before approving.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;&lt;span style=&quot;mso-bookmark: what-we-were-building;&quot;&gt;One editorial action in, one review out. That was the whole contract. A complete run costs 100&amp;ndash;130 credits and takes 2-4 minutes. This makes a single run fairly expensive on credits. Now picture that same run firing over and over, several at once, and the consumption climbs fast.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;The trigger was the only open question. &lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;span style=&quot;color: rgb(35, 111, 161); text-decoration: underline;&quot;&gt;&lt;a style=&quot;color: rgb(35, 111, 161); text-decoration: underline;&quot; title=&quot;Opal workflow agents can fire a few ways&quot; href=&quot;https://support.optimizely.com/hc/en-us/articles/39865892823693-Workflow-agent-triggers&quot;&gt;Opal workflow agents can fire a few ways&lt;/a&gt;&lt;/span&gt;&lt;/span&gt; &amp;mdash; chat input, a schedule, email, or a webhook &amp;mdash; but for a run that kicks off automatically when something happens in the CMS, the webhook is the one that fits. So something in Optimizely CMS had to notice &amp;ldquo;this page entered review&amp;rdquo; and POST to the trigger URL. The obvious source was &lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;span style=&quot;color: rgb(35, 111, 161); text-decoration: underline;&quot;&gt;&lt;a style=&quot;color: rgb(35, 111, 161); text-decoration: underline;&quot; title=&quot;Optimizely Graph&quot; href=&quot;https://docs.developers.optimizely.com/platform-optimizely/docs/manage-webhooks&quot;&gt;Optimizely Graph&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;. It&amp;rsquo;s pure configuration &amp;mdash; register a webhook through the API, no CMS code to deploy &amp;mdash; and it already models content changes, which is more or less what we were reacting to. So we started there: a Graph subscription pointed at the Opal trigger, filtered to the InReview status.&lt;/p&gt;
&lt;h2&gt;What we didn&amp;rsquo;t see coming?&lt;/h2&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;&lt;span style=&quot;mso-bookmark: what-we-didnt-see-coming;&quot;&gt;The first run worked exactly as intended. An editor submitted a page, the status went to InReview, Graph synced, the webhook fired, and the agent ran its compliance check.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;&lt;span style=&quot;mso-bookmark: what-we-didnt-see-coming;&quot;&gt;Then the agent did the one thing it was built to do: it wrote its findings back onto the page.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;&lt;span style=&quot;mso-bookmark: what-we-didnt-see-coming;&quot;&gt;That write-back was a save. The save synced to Graph. The content was still InReview &amp;mdash; writing findings doesn&amp;rsquo;t change an approval status &amp;mdash; so Graph delivered a webhook for an InReview document, which is exactly the event we&amp;rsquo;d subscribed to. The agent fired again. Checked again. Wrote back again. Synced again.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;&lt;span style=&quot;mso-bookmark: what-we-didnt-see-coming;&quot;&gt;By the time we were looking at it, we had four pages of execution logs: the same agent, on the same page, running over and over, several instances going at once. We started cancelling runs manually to stop them burning through credits. The agent was triggering itself, and nothing in the loop was going to stop it.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;&lt;span style=&quot;mso-bookmark: what-we-didnt-see-coming;&quot;&gt;Nothing was broken, which is what made it hard. Graph was doing exactly what Graph does.&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;Why it looped?&lt;/h2&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;&lt;span style=&quot;mso-bookmark: why-it-looped;&quot;&gt;Seeing the behaviour was easy. Finding the cause took a while. Two facts about Graph, neither of them a bug, combined into the loop:&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;&lt;span style=&quot;mso-bookmark: why-it-looped;&quot;&gt;&lt;a title=&quot;Graph syncs on save, not on intent.&quot; href=&quot;https://docs.developers.optimizely.com/platform-optimizely/docs/synchronize-content-events-to-optimizely-graph&quot;&gt;&lt;strong&gt;&lt;span style=&quot;text-decoration: underline; color: rgb(35, 111, 161);&quot;&gt;Graph syncs on save, not on intent.&lt;/span&gt;&lt;/strong&gt;&lt;/a&gt; It re-indexes whenever content is saved, published, moved, deleted, or expired. Our agent&amp;rsquo;s write-back is a save like any other, so every write re-syncs.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;&lt;span style=&quot;mso-bookmark: why-it-looped;&quot;&gt;&lt;strong&gt;Status is part of a document&amp;rsquo;s identity.&lt;/strong&gt; Graph identifies an indexed document as &lt;/span&gt;&lt;span style=&quot;mso-bookmark: why-it-looped;&quot;&gt;&lt;em&gt;&lt;span class=&quot;VerbatimChar&quot; style=&quot;font-size: 10pt;&quot;&gt;{contentGuid}_{version}_{locale}_{status}&lt;/span&gt;&lt;/em&gt;. Our write-back changed the content but left the status at InReview &amp;mdash; so every re-sync produced the &lt;em&gt;same&lt;/em&gt; InReview document, indistinguishable from the submit event we&amp;rsquo;d subscribed to. There was no status change to mark the work as handled, and no state anywhere saying &amp;ldquo;this one&amp;rsquo;s done.&amp;rdquo; So each sync looked like a fresh reason to run.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;&lt;span style=&quot;mso-bookmark: why-it-looped;&quot;&gt;Underneath it, the mistake was conceptual: we were inferring an editorial decision from index activity. But the index was churning every time our own agent touched the page &amp;mdash; and the agent never stopped touching the page.&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;The fix: subscribe to the decision, not the side effect&lt;/h2&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;&lt;span style=&quot;mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;&quot;&gt;Optimizely CMS already raises an event for the exact moment we cared about. The &lt;span style=&quot;text-decoration: underline; color: rgb(35, 111, 161);&quot;&gt;&lt;a style=&quot;color: rgb(35, 111, 161); text-decoration: underline;&quot; title=&quot;approval engine&quot; href=&quot;https://docs.developers.optimizely.com/content-management-system/v13.0.0-CMS/docs/content-approvals&quot;&gt;approval engine&lt;/a&gt;&lt;/span&gt; exposes &lt;/span&gt;&lt;span style=&quot;mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;&quot;&gt;&lt;em&gt;&lt;span class=&quot;VerbatimChar&quot; style=&quot;font-size: 10pt;&quot;&gt;IApprovalEngineEvents&lt;/span&gt;&lt;/em&gt;, and its &lt;/span&gt;&lt;span style=&quot;mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;&quot;&gt;&lt;em&gt;&lt;span class=&quot;VerbatimChar&quot; style=&quot;font-size: 10pt;&quot;&gt;Started&lt;/span&gt;&lt;/em&gt; event fires once, when an approval instance begins &amp;mdash; i.e.&amp;nbsp;when a human submits for review. It&amp;rsquo;s raised by the approval lifecycle, not the persistence layer. Background saves, forced writes, and our own agent&amp;rsquo;s write-back don&amp;rsquo;t start approval instances, so they don&amp;rsquo;t raise it.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;&lt;span style=&quot;mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;&quot;&gt;So instead of inferring the decision from Graph, we subscribed to it directly in the CMS solution and POSTed to the Opal trigger ourselves:&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;span style=&quot;color: rgb(35, 111, 161);&quot;&gt;&lt;code&gt;&lt;span style=&quot;mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;&quot;&gt;&lt;span class=&quot;OperatorTok&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;InitializableModule&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;span class=&quot;FunctionTok&quot;&gt;ModuleDependency&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;KeywordTok&quot;&gt;typeof&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;EPiServer&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;Web&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;InitializationModule&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;))]&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;KeywordTok&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;KeywordTok&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; ComplianceReviewTriggerModule &lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; IInitializableModule&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;&lt;/span&gt;&lt;span class=&quot;KeywordTok&quot;&gt;private&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; IApprovalEngineEvents _approvalEvents&lt;/span&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;&lt;/span&gt;&lt;span class=&quot;CommentTok&quot;&gt;// IContentLoader, IHttpClientFactory, config resolved in Initialize()&lt;/span&gt;&lt;br /&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;&lt;/span&gt;&lt;span class=&quot;KeywordTok&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;DataTypeTok&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;FunctionTok&quot;&gt;Initialize&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;InitializationEngine context&lt;/span&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;&lt;/span&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;_approvalEvents &lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; context&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;Services&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;GetRequiredService&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;IApprovalEngineEvents&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;_approvalEvents&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;Started&lt;/span&gt; &lt;span class=&quot;OperatorTok&quot;&gt;+=&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; OnApprovalStarted&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;&lt;span style=&quot;mso-spacerun: yes;&quot;&gt;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;CommentTok&quot;&gt;// raised once, on submit&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;&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;}&lt;/span&gt;&lt;br /&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;&lt;/span&gt;&lt;span class=&quot;KeywordTok&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;DataTypeTok&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;FunctionTok&quot;&gt;OnApprovalStarted&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;DataTypeTok&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; sender&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; ApprovalEventArgs e&lt;/span&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;&lt;/span&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;KeywordTok&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;OperatorTok&quot;&gt;(!&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;IsContentApproval&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;KeywordTok&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;;&lt;/span&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;CommentTok&quot;&gt;// approvals are generic&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;DataTypeTok&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; contentLink &lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; e&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;GetContentLink&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;();&lt;/span&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;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;CommentTok&quot;&gt;// extension method&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;KeywordTok&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;OperatorTok&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;ContentReference&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;contentLink&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;KeywordTok&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;;&lt;/span&gt;&lt;br /&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;KeywordTok&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;OperatorTok&quot;&gt;(!&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;_contentLoader&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;TryGet&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;MyCustomPage&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&amp;gt;(&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;contentLink&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;KeywordTok&quot;&gt;out&lt;/span&gt; &lt;span class=&quot;DataTypeTok&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; page&lt;/span&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;KeywordTok&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;;&lt;/span&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;CommentTok&quot;&gt;// scope to our content type&lt;/span&gt;&lt;br /&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;CommentTok&quot;&gt;// Mirror Graph&#39;s docId shape so the workflow is source-agnostic.&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;DataTypeTok&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; docId &lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; $&lt;/span&gt;&lt;span class=&quot;StringTok&quot;&gt;&quot;{page.ContentGuid}_{contentLink.WorkID}_{page.Language.Name}_InReview&quot;&lt;/span&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;&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;NormalTok&quot;&gt; Task&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;Run&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;OperatorTok&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;FunctionTok&quot;&gt;TriggerOpalAsync&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;docId&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;));&lt;/span&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;&lt;/span&gt;&lt;span class=&quot;CommentTok&quot;&gt;// fire and forget, with a timeout&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;&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;/span&gt;&lt;/code&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;&lt;span style=&quot;mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;&quot;&gt;A few things that mattered in practice:&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;Compact&quot; style=&quot;margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;&quot;&gt;&lt;span style=&quot;mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;em&gt;&lt;span class=&quot;VerbatimChar&quot; style=&quot;font-size: 10pt;&quot;&gt;&lt;strong&gt;ApprovalEventArgs&lt;/strong&gt;&lt;/span&gt;&lt;/em&gt;&lt;strong&gt; has no &lt;/strong&gt;&lt;/span&gt;&lt;span style=&quot;mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;&quot;&gt;&lt;em&gt;&lt;span class=&quot;VerbatimChar&quot; style=&quot;font-size: 10pt;&quot;&gt;&lt;strong&gt;ContentLink&lt;/strong&gt;&lt;/span&gt;&lt;/em&gt;&lt;strong&gt;.&lt;/strong&gt; Approvals are a general mechanism; content approvals are one implementation. You reach the item through the &lt;/span&gt;&lt;span style=&quot;mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;&quot;&gt;&lt;em&gt;&lt;span class=&quot;VerbatimChar&quot; style=&quot;font-size: 10pt;&quot;&gt;IsContentApproval()&lt;/span&gt;&lt;/em&gt; and &lt;/span&gt;&lt;span style=&quot;mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;&quot;&gt;&lt;em&gt;&lt;span class=&quot;VerbatimChar&quot; style=&quot;font-size: 10pt;&quot;&gt;GetContentLink()&lt;/span&gt;&lt;/em&gt; extension methods in &lt;/span&gt;&lt;span style=&quot;mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;&quot;&gt;&lt;em&gt;&lt;span class=&quot;VerbatimChar&quot; style=&quot;font-size: 10pt;&quot;&gt;EPiServer.Approvals.ContentApprovals&lt;/span&gt;&lt;/em&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;Compact&quot; style=&quot;margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;&quot;&gt;&lt;span style=&quot;mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;strong&gt;The Opal trigger wants auth.&lt;/strong&gt; The webhook trigger takes an Auth Key and sends it as a header (e.g.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;&quot;&gt;&lt;em&gt;&lt;span class=&quot;VerbatimChar&quot; style=&quot;font-size: 10pt;&quot;&gt;Authorization: Bearer {token}&lt;/span&gt;&lt;/em&gt;). Our POST sets that header from config; a request that doesn&amp;rsquo;t match is rejected.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;Compact&quot; style=&quot;margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;&quot;&gt;&lt;span style=&quot;mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;strong&gt;We mirrored Graph&amp;rsquo;s &lt;/strong&gt;&lt;/span&gt;&lt;span style=&quot;mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;&quot;&gt;&lt;em&gt;&lt;span class=&quot;VerbatimChar&quot; style=&quot;font-size: 10pt;&quot;&gt;&lt;strong&gt;docId&lt;/strong&gt;&lt;/span&gt;&lt;/em&gt;&lt;strong&gt;&lt;em&gt;&lt;span style=&quot;font-size: 10pt;&quot;&gt; &lt;/span&gt;&lt;/em&gt;shape&lt;/strong&gt; in the payload, so the downstream workflow didn&amp;rsquo;t need to change. If we ever switch sources again, the agent doesn&amp;rsquo;t care.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;Compact&quot; style=&quot;margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;&quot;&gt;&lt;span style=&quot;mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;strong&gt;Fire-and-forget has a cost.&lt;/strong&gt; Blocking the editor&amp;rsquo;s submit on an outbound call is a bad idea, so we don&amp;rsquo;t. The trade is that if Opal is unreachable the moment &lt;/span&gt;&lt;span style=&quot;mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;&quot;&gt;&lt;em&gt;&lt;span style=&quot;font-size: 10pt;&quot;&gt;&lt;span class=&quot;VerbatimChar&quot;&gt;Started&lt;/span&gt; &lt;/span&gt;&lt;/em&gt;fires, that trigger is lost &amp;mdash; at-most-once. For a review flow that&amp;rsquo;s fine, because an editor can resubmit. If it weren&amp;rsquo;t, we&amp;rsquo;d add retries or a durable queue.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;&lt;span style=&quot;mso-bookmark: X8862362e12c60d9a9215084724db4b43eeea817;&quot;&gt;Result: one submit, one execution. The write-back still syncs to Graph, but we&amp;rsquo;re no longer listening to Graph for this trigger, and writing content doesn&amp;rsquo;t start an approval &amp;mdash; so there&amp;rsquo;s nothing left to re-fire. The loop was gone, not because we out-guarded it, but because we stopped listening to the wrong layer.&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;So when is Graph the right source?&lt;/h2&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;&lt;span style=&quot;mso-bookmark: so-when-is-graph-the-right-source;&quot;&gt;The lesson isn&amp;rsquo;t &amp;ldquo;Graph is wrong.&amp;rdquo; Graph is excellent &amp;mdash; for the thing it actually reports, which is &lt;em&gt;content as the published world sees it&lt;/em&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;&lt;span style=&quot;mso-bookmark: so-when-is-graph-the-right-source;&quot;&gt;&lt;strong&gt;Reach for Graph when:&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;Compact&quot; style=&quot;margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;&quot;&gt;&lt;span style=&quot;mso-bookmark: so-when-is-graph-the-right-source;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;You&amp;rsquo;re reacting to &lt;strong&gt;content state&lt;/strong&gt;, not editorial intent &amp;mdash; a page got published, purge the CDN; rebuild the static site. This is Graph&amp;rsquo;s canonical use case, and duplicate deliveries are harmless because the second purge is a no-op.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;Compact&quot; style=&quot;margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;&quot;&gt;&lt;span style=&quot;mso-bookmark: so-when-is-graph-the-right-source;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;The consumer is &lt;strong&gt;idempotent&lt;/strong&gt; &amp;mdash; ISR cache invalidation, deploy hooks, downstream sync. Firing twice costs nothing.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;Compact&quot; style=&quot;margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;&quot;&gt;&lt;span style=&quot;mso-bookmark: so-when-is-graph-the-right-source;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;You &lt;strong&gt;don&amp;rsquo;t own the CMS codebase&lt;/strong&gt;, or you&amp;rsquo;re on &lt;strong&gt;SaaS CMS&lt;/strong&gt;. Graph is configuration, no deploy required.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;Compact&quot; style=&quot;margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;&quot;&gt;&lt;span style=&quot;mso-bookmark: so-when-is-graph-the-right-source;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;The consuming system is &lt;strong&gt;decoupled from CMS&lt;/strong&gt; and you don&amp;rsquo;t want to couple to its internals.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;&lt;span style=&quot;mso-bookmark: so-when-is-graph-the-right-source;&quot;&gt;&lt;strong&gt;Reach for a CMS server-side event (the code path) when:&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;Compact&quot; style=&quot;margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;&quot;&gt;&lt;span style=&quot;mso-bookmark: so-when-is-graph-the-right-source;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;The trigger is an &lt;strong&gt;editorial decision&lt;/strong&gt; &amp;mdash; submitted, approved, rejected. That lives in the approval engine, and Graph can&amp;rsquo;t model it.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;Compact&quot; style=&quot;margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;&quot;&gt;&lt;span style=&quot;mso-bookmark: so-when-is-graph-the-right-source;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;Your workflow &lt;strong&gt;writes back to the item that triggered it&lt;/strong&gt;. This is the one that bit us; any index-based trigger risks the same loop.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;Compact&quot; style=&quot;margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;&quot;&gt;&lt;span style=&quot;mso-bookmark: so-when-is-graph-the-right-source;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;&lt;strong&gt;Each execution is expensive.&lt;/strong&gt; When a run costs real credits and minutes, precise firing matters far more than easy setup.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;Compact&quot; style=&quot;margin-left: .5in; text-indent: -24.0pt; mso-list: l0 level1 lfo1;&quot;&gt;&lt;span style=&quot;mso-bookmark: so-when-is-graph-the-right-source;&quot;&gt;&lt;!-- [if !supportLists]--&gt;&lt;span style=&quot;mso-bidi-font-family: Cambria; mso-bidi-theme-font: minor-latin;&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;!--[endif]--&gt;You need &lt;strong&gt;business rules before firing&lt;/strong&gt; &amp;mdash; content type, language, site. Strongly typed C# beats filtering on payload shape.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;&lt;span style=&quot;mso-bookmark: so-when-is-graph-the-right-source;&quot;&gt;For us it was a compliance review agent that wrote back to the page it reviewed, at 100+ credits a run. Three of those four boxes. Graph was the wrong tool for this job. We kept Graph for delivery-side concerns and moved the editorial trigger to the approval event. They answer different questions.&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;The takeaway&lt;/h2&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;Both sources are honest. Graph faithfully reports that an indexed document changed. The approval engine faithfully reports that a person made a decision. Neither is broken.&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;The real question is which of those you care about &amp;mdash; &lt;em&gt;a document changing&lt;/em&gt; or &lt;em&gt;a person deciding&lt;/em&gt; &amp;mdash; and subscribing at that layer instead of inferring it from the one below. When a workflow fires more often than the business event behind it, the instinct is to bolt on a guard. Sometimes you need one. But first ask what your event source is actually reporting.&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;Ours was reporting every save. Our agent just happened to be the thing doing the saving.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/ritu-madan/dates/2026/10/opal-workflow-agents-and-the-graph-webhook-trap/</guid>            <pubDate>Wed, 14 Oct 2026 01:06:16 GMT</pubDate>           <category>Blog post</category></item><item> <title>cms13-scan: A Deterministic CMS 13 Readiness Scanner</title>            <link>https://www.adnanzameer.com/2026/10/cms13-scan-optimizely-cms-13-readiness-scanner.html</link>            <description>&lt;!--excerpt: A free, open-source, deterministic .NET global tool that scans an Optimizely CMS 12 codebase for CMS 13 breaking changes before you touch a single line of upgrade code. No AI, no tokens, no API keys, just static analysis you can run in CI.--&gt;

Every CMS 12 to CMS 13 upgrade I have looked at starts the same way. Someone opens the solution and starts hunting. A text search for the APIs</description>            <guid>https://www.adnanzameer.com/2026/10/cms13-scan-optimizely-cms-13-readiness-scanner.html</guid>            <pubDate>Mon, 12 Oct 2026 10:09:07 GMT</pubDate>           <category>Blog post</category></item><item> <title>Setting Up an Optimizely CMS 13 MCP Server Locally with Codex</title>            <link>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/getting-started-with-optimizely-cms-13-mcp-server-in-vs-code-using-codex/</link>            <description>&lt;p&gt;Optimizely CMS 13 supports &lt;a href=&quot;https://modelcontextprotocol.io/docs/2026-07-28/getting-started/intro&quot;&gt;Model Context Protocol (MCP)&lt;/a&gt;, allowing AI tools such as Claude and Codex to interact with CMS content and metadata through an MCP server.&lt;/p&gt;
&lt;p&gt;In this post, I&#39;ll show how I configured the MCP server in my local CMS 13 Alloy project, connected it to Codex in Visual Studio Code, and used it to explore content types, create pages, and build a Visual Builder experience.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Optimizely CMS 13 (I&#39;m using Alloy with CMS 13.3.0).&lt;/li&gt;
&lt;li&gt;An MCP-compatible AI client (I&#39;m using Codex in VS Code).&lt;/li&gt;
&lt;li&gt;A local CMS instance running over HTTPS, for example, https://localhost:5000.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Configure the MCP Server&lt;/h2&gt;
&lt;p&gt;Install the &lt;strong&gt;Optimizely.Cms.Opal.Tools&lt;/strong&gt; NuGet package (version 13.2.0 or later):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dotnet add package Optimizely.Cms.Opal.Tools --version 13.2.0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Register the MCP server in &lt;strong&gt;Startup.cs&lt;/strong&gt; using client-secret authentication:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services.AddCmsMcpServer()
    .WithClientSecretAuthentication();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then map the MCP endpoint and require client-secret authorization:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.UseEndpoints(endpoints =&amp;gt;
{
    endpoints.MapContent();

    endpoints.MapMcp(&quot;/mcp&quot;)
        .RequireClientSecretAuthorization();
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The MCP endpoint is now available at &lt;strong&gt;https://localhost:5000/mcp&lt;/strong&gt;, or your application&#39;s configured HTTPS address.&lt;/p&gt;
&lt;h2&gt;Configure Client-Secret Authentication&lt;/h2&gt;
&lt;p&gt;Make sure a client secret is configured for your local CMS application. We&#39;ll use it to authenticate Codex when connecting to the MCP server.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security note:&lt;/strong&gt; Never commit your client secret to source control or expose it in screenshots.&lt;/p&gt;
&lt;h2&gt;Connect Codex in VS Code&lt;/h2&gt;
&lt;p&gt;Open &lt;strong&gt;Codex Settings &amp;rarr; MCP servers &amp;rarr; Add server&lt;/strong&gt; and select &lt;strong&gt;Streamable HTTP&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Name:&lt;/strong&gt; optimizely-cms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;URL:&lt;/strong&gt; https://localhost:5000/mcp&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bearer token env var:&lt;/strong&gt; Leave empty.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Header:&lt;/strong&gt; Authorization, with your configured client secret as the value.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Alternatively, configure the server in Codex&#39;s &lt;strong&gt;config.toml&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mcp_servers.optimizely-cms]
type = &quot;http&quot;
url = &quot;https://localhost:5000/mcp&quot;

[mcp_servers.optimizely-cms.headers]
Authorization = &quot;your-secret-value&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace the placeholder with your actual secret and enable the server in Codex Settings. The secret is passed directly in the Authorization header, without a Bearer prefix.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 500px; height: auto;&quot; src=&quot;/link/1b35b3874d3149f7a6065da7c50869d3.aspx&quot; alt=&quot;Codex MCP server configuration&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;em&gt;Configuring the local Optimizely MCP server in Codex.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Verify the Integration&lt;/h2&gt;
&lt;p&gt;With the CMS running, open a new Codex chat in VS Code and try:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;List the available content types in my local Optimizely CMS instance using the MCP server.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Codex retrieved the registered content types, including &lt;strong&gt;StandardPage&lt;/strong&gt; and &lt;strong&gt;StandardExperienceData&lt;/strong&gt;, and could inspect their properties, locales, and available Visual Builder components.&lt;/p&gt;
&lt;p&gt;You can also retrieve existing content:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Find the published content items of type ProductTestPage and show their properties.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;These read-only operations confirm that Codex can access CMS content and metadata through MCP.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 650px; height: auto;&quot; src=&quot;/link/7ff3dbdcef3d48c59233445efb77db39.aspx&quot; alt=&quot;Codex discovering Optimizely CMS content types&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;em&gt;Codex retrieving content types from the local CMS instance.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Create Content Using Codex&lt;/h3&gt;
&lt;p&gt;Next, I asked Codex to create an unpublished page:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Create &quot;MCP Demo Page&quot; under Start using StandardPage. Add a heading and paragraph to MainBody, and save it as an English draft.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Codex created the page and updated its content through the MCP server.&lt;/p&gt;
&lt;p&gt;I also tested Visual Builder:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Create an unpublished &quot;Alloy Demo Experience&quot; using StandardExperienceData, with a BlankSection containing a VisualBuilderTestBlock.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Codex created the experience using the existing CMS content types and Visual Builder components. Both items were saved as drafts without publishing.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 750px; height: auto;&quot; src=&quot;/link/058a28868a6d4b519226eaefffb487d1.aspx&quot; alt=&quot;MCP Demo Page created in Optimizely CMS&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;em&gt;The MCP Demo Page created through Codex and saved as a draft.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Troubleshooting Content Permissions&lt;/h3&gt;
&lt;p&gt;During content creation, I encountered an authorization error:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Access denied: You do not have permission to create content in this location.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The MCP client authenticated successfully with a &lt;strong&gt;CmsAdmins&lt;/strong&gt; role claim, which was mapped to &lt;strong&gt;WebAdmins&lt;/strong&gt; in the application configuration. Although the Start page granted Create permission to WebAdmins, the MCP client was still denied access.&lt;/p&gt;
&lt;p&gt;The issue was resolved by explicitly granting the &lt;strong&gt;MCP Client&lt;/strong&gt; permissions on the Start page through CMS access rights. Afterward, Codex could create and update content.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 750px; height: auto;&quot; src=&quot;/link/28ac25ea3076497792a6660ed63434cc.aspx&quot; alt=&quot;MCP Client permissions on the Start page&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;em&gt;Granting the MCP Client access on the Start page resolved the create denial.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security note:&lt;/strong&gt; For this local demo, all permissions were enabled, including Publish and Administer. In a real environment, grant only the permissions needed for the intended operations.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;With Codex connected to Optimizely CMS 13 through MCP, we were able to explore content types, create pages, and work with Visual Builder directly from VS Code.&lt;/p&gt;
&lt;p&gt;One thing worth noting is that connecting successfully doesn&#39;t necessarily mean the MCP client has permission to create or update content. You&#39;ll still need to configure the appropriate access rights in CMS.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/getting-started-with-optimizely-cms-13-mcp-server-in-vs-code-using-codex/</guid>            <pubDate>Mon, 12 Oct 2026 04:41:05 GMT</pubDate>           <category>Blog post</category></item><item> <title>Handling SixLabors.ImageSharp vulnerability warnings in Optimizely CMS 12 (unofficial)</title>            <link>https://world.optimizely.com/blogs/cuong-nguyen-inh/dates/2026/10/handling-sixlabors.imagesharp-vulnerability-warnings-in-optimizely-cms-12-unofficial</link>            <description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;&amp;nbsp;&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;css-1fd6doq&quot;&gt;
&lt;div&gt;
&lt;div class=&quot;_2O5Vij7dHp _3YEZNO7dHp _1bHrqEN91d _4ouGXIdnbC _1Uk34SOLYo&quot;&gt;
&lt;div class=&quot;_39xV02N91d _0u2WMmrU95&quot;&gt;
&lt;div class=&quot;css-3qfej8&quot;&gt;
&lt;div class=&quot;ak-renderer-wrapper is-comment css-pw7jst&quot;&gt;
&lt;div class=&quot;css-14y9ydk&quot;&gt;
&lt;div class=&quot;ak-renderer-document&quot;&gt;
&lt;p&gt;&lt;strong&gt;2nd UPDATED: we&amp;rsquo;ve gotten a SixLabors license file, CMS team is working on update ImageSharp to a safe version (both SaaS and Paas CMS) in this story CMS-57368&lt;/strong&gt;&lt;/p&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;p&gt;&lt;em&gt;&lt;br /&gt;Written by Claude Opus 5.5 (Anthropic) on behalf of Cuong Nguyen, Optimizely Technical Support.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A note from Cuong:&lt;/strong&gt; Thanks to Optimizely for giving me access to Claude, which made this kind of deep dive possible within a support ticket.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SixLabors.ImageSharp&lt;/strong&gt; is one of the most popular image libraries in the .NET world. Chances are it&#39;s in your solution too, even if you never added it yourself. Then one morning you open the project, run a build like any other day, and the output is suddenly full of &lt;strong&gt;NU1902&lt;/strong&gt;/&lt;strong&gt;NU1903&lt;/strong&gt; warnings. Nothing changed in your code. If &lt;strong&gt;TreatWarningsAsErrors&lt;/strong&gt; is on, the build simply fails.&lt;/p&gt;
&lt;p&gt;That&#39;s the story behind a lot of tickets we&#39;ve been getting in Optimizely Support lately. This post covers where the dependency comes from, why &quot;just upgrade ImageSharp&quot; doesn&#39;t work, and the four options you have today, including a SkiaSharp-based &lt;strong&gt;IImageService&lt;/strong&gt; sample.&lt;/p&gt;
&lt;h2&gt;1. What you&#39;ll see&lt;/h2&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code&gt;$ dotnet list package --include-transitive --vulnerable

   Transitive Package          Resolved   Severity   Advisory URL
   &amp;gt; SixLabors.ImageSharp      3.1.11     Moderate   https://github.com/advisories/GHSA-gwg2-r3hj-4w44
                                          High       https://github.com/advisories/GHSA-j3p4-wp97-rph4
                                          High       https://github.com/advisories/GHSA-j9gm-c75j-xc9q
                                          Moderate   https://github.com/advisories/GHSA-wmxv-xphr-5c9g
                                          High       https://github.com/advisories/GHSA-jjfr-hcj7-qf5w
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You&#39;ll also get &lt;strong&gt;NU1902&lt;/strong&gt;/&lt;strong&gt;NU1903&lt;/strong&gt; during restore and build. With &lt;strong&gt;TreatWarningsAsErrors&lt;/strong&gt; enabled, the build fails.&lt;/p&gt;
&lt;h2&gt;2. Where the dependency comes from&lt;/h2&gt;
&lt;p&gt;The CMS uses &lt;strong&gt;IImageService&lt;/strong&gt; for thumbnails and the image editor. The default implementation lives in &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt;, which depends on &lt;strong&gt;SixLabors.ImageSharp&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code&gt;EPiServer.CMS 12.34.x
└── EPiServer.ImageLibrary.ImageSharp  (&amp;gt;= 1.0.6)   &amp;larr; NuGet picks the lowest matching version
    └── SixLabors.ImageSharp 2.1.11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;NuGet resolves dependencies with the &lt;em&gt;lowest applicable version&lt;/em&gt; rule (&lt;a href=&quot;https://learn.microsoft.com/nuget/concepts/dependency-resolution#lowest-applicable-version&quot;&gt;docs&lt;/a&gt;). So &lt;strong&gt;EPiServer.CMS&lt;/strong&gt; 12.34.8 resolves &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt; &lt;strong&gt;1.0.6&lt;/strong&gt;, which brings in ImageSharp &lt;strong&gt;2.1.11&lt;/strong&gt;. This is standard NuGet behavior, not something specific to Optimizely packages.&lt;/p&gt;
&lt;p&gt;To get a newer version, reference the CMS components directly instead of the &lt;strong&gt;EPiServer.CMS&lt;/strong&gt; metapackage, and pin &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt; to 2.0.6:&lt;/p&gt;
&lt;pre class=&quot;language-xml&quot;&gt;&lt;code&gt;&amp;lt;PackageReference Include=&quot;EPiServer.CMS.UI&quot; Version=&quot;12.34.6&quot; /&amp;gt;
&amp;lt;PackageReference Include=&quot;EPiServer.Hosting&quot; Version=&quot;12.24.1&quot; /&amp;gt;
&amp;lt;!-- ... --&amp;gt;
&amp;lt;PackageReference Include=&quot;EPiServer.ImageLibrary.ImageSharp&quot; Version=&quot;2.0.6&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That moves you to ImageSharp 3.1.11, which clears the older advisories but not the five listed above.&lt;/p&gt;
&lt;h2&gt;3. Why not force a newer ImageSharp?&lt;/h2&gt;
&lt;p&gt;The advisories above are fixed in ImageSharp 4.x. Six Labors changed the licensing model in 4.x, and Release builds require a license key (&lt;a href=&quot;https://github.com/SixLabors/ImageSharp/issues/3205#issuecomment-6062121101&quot;&gt;discussion&lt;/a&gt;). Forcing 4.x through &lt;strong&gt;Directory.Packages.props&lt;/strong&gt; therefore gives you a build error unless you have a Six Labors license. The 3.1.x line, the last one usable without a license, still carries the advisories.&lt;/p&gt;
&lt;h2&gt;4. Your options&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Remove &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No code, warnings disappear&lt;/td&gt;
&lt;td&gt;No image processing: thumbnails and the image editor stop working&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Provide your own &lt;a href=&quot;/CsClassLibraries/cms/EPiServer.ImageLibrary.IImageService?version=12&quot;&gt;&lt;strong&gt;IImageService&lt;/strong&gt;&lt;/a&gt;, e.g. with &lt;a href=&quot;https://github.com/mono/SkiaSharp&quot;&gt;SkiaSharp&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Clean audit, features kept&lt;/td&gt;
&lt;td&gt;Custom code that your team owns and maintains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Suppress the audit warnings&lt;/td&gt;
&lt;td&gt;Quick&lt;/td&gt;
&lt;td&gt;The vulnerable package stays in your solution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Wait for an updated &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No custom code&lt;/td&gt;
&lt;td&gt;No committed timeline today&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;On risk:&lt;/strong&gt; the image service only processes media that authenticated editors upload through the CMS. It doesn&#39;t handle arbitrary anonymous input. That lowers the exposure, and editors validating images on upload lowers it further. Whether that&#39;s acceptable is a decision for your own security policy and threat model.&lt;/p&gt;
&lt;h2&gt;5. Option 2: a custom &lt;strong&gt;IImageService&lt;/strong&gt; with SkiaSharp&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;IImageService&lt;/strong&gt; is a public extension point with a single method:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public interface IImageService
{
    void RenderImage(Stream input, Stream output, RenderRequest request);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The default implementation maps each &lt;strong&gt;ImageEditorCommand&lt;/strong&gt; like this, and a replacement should do the same:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;ImageEditorCommand&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;Default (ImageSharp)&lt;/th&gt;
&lt;th&gt;SkiaSharp equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Grayscale&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Grayscale()&lt;/strong&gt; (BT.709)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;SKColorFilter.CreateColorMatrix&lt;/strong&gt; with BT.709 coefficients&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Crop&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Crop(rect)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;DrawBitmap(src, -left, -top)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FlipX&lt;/strong&gt; / &lt;strong&gt;FlipY&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Flip(Horizontal/Vertical)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;canvas.Scale(-1, 1, cx, 0)&lt;/strong&gt; / &lt;strong&gt;Scale(1, -1, 0, cy)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Resize&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ResizeMode.Stretch&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;DrawBitmap(src, destRect, Mitchell)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ResizeKeepScale&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ResizeMode.BoxPad&lt;/strong&gt; + &lt;strong&gt;BackgroundColor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;fit the box, &lt;strong&gt;no upscaling&lt;/strong&gt;, center, &lt;strong&gt;Clear(bg)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rotate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Rotate(deg)&lt;/strong&gt;, canvas expands&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Translate&lt;/strong&gt; + &lt;strong&gt;RotateDegrees&lt;/strong&gt; on an expanded canvas&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Packages&lt;/h3&gt;
&lt;p&gt;Remove &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt; and add SkiaSharp. If you reference the &lt;strong&gt;EPiServer.CMS&lt;/strong&gt; metapackage, switch to individual component references first (see section 2). The metapackage brings &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt; in transitively, so you can&#39;t remove it otherwise.&lt;/p&gt;
&lt;pre class=&quot;language-xml&quot;&gt;&lt;code&gt;&amp;lt;!-- remove: &amp;lt;PackageReference Include=&quot;EPiServer.ImageLibrary.ImageSharp&quot; Version=&quot;...&quot; /&amp;gt; --&amp;gt;
&amp;lt;PackageReference Include=&quot;SkiaSharp&quot; Version=&quot;4.153.1&quot; /&amp;gt;
&amp;lt;!-- Required on Linux, containers and DXP: --&amp;gt;
&amp;lt;PackageReference Include=&quot;SkiaSharp.NativeAssets.Linux.NoDependencies&quot; Version=&quot;4.153.1&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The service&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;ResizeKeepScale&lt;/strong&gt; matters most, because thumbnail generation calls it with &lt;strong&gt;BackgroundColor = &quot;#00000000&quot;&lt;/strong&gt;. The full implementation:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;using EPiServer.ImageLibrary;
using SkiaSharp;

namespace YourSite.Business.Imaging;

/// &amp;lt;summary&amp;gt;
/// SkiaSharp-based replacement for EPiServer.ImageLibrary.ImageSharp&#39;s DefaultImageService,
/// which pulls in vulnerable SixLabors.ImageSharp versions. Mirrors the default behavior:
/// same commands, ResizeKeepScale = ImageSharp &quot;BoxPad&quot; + background color, ZoomFactor applied last.
/// &amp;lt;/summary&amp;gt;
/// &amp;lt;remarks&amp;gt;
/// Skia only encodes PNG, JPEG and WebP. GIF/BMP/TIFF output is not supported (NotSupportedException),
/// and animated GIF input is flattened to its first frame.
/// &amp;lt;/remarks&amp;gt;
public class SkiaImageService : IImageService
{
    private static readonly SKSamplingOptions ResizeSampling = new(SKCubicResampler.Mitchell);
    private static readonly SKSamplingOptions RotateSampling = new(SKFilterMode.Linear);
    private static readonly SKSamplingOptions CopySampling = new(SKFilterMode.Nearest);

    // ITU-R BT.709 luma, same coefficients as ImageSharp&#39;s Grayscale().
    private static readonly float[] GrayscaleMatrix =
    [
        0.2126f, 0.7152f, 0.0722f, 0, 0,
        0.2126f, 0.7152f, 0.0722f, 0, 0,
        0.2126f, 0.7152f, 0.0722f, 0, 0,
        0, 0, 0, 1, 0,
    ];

    public void RenderImage(Stream input, Stream output, RenderRequest request)
    {
        ArgumentNullException.ThrowIfNull(input);
        ArgumentNullException.ThrowIfNull(output);
        ArgumentNullException.ThrowIfNull(request);

        var image = Decode(input);
        try
        {
            foreach (var operation in request.Operations ?? [])
            {
                image = Replace(image, Apply(image, operation));
            }

            if (request.ZoomFactor != 1f)
            {
                var width = (int)Math.Floor(image.Width * request.ZoomFactor);
                var height = (int)Math.Floor(image.Height * request.ZoomFactor);
                image = Replace(image, Resize(image, width, height));
            }

            Encode(image, output, request);
        }
        finally
        {
            image.Dispose();
        }
    }

    private static SKBitmap Decode(Stream input)
    {
        using var codec = SKCodec.Create(input)
            ?? throw new NotSupportedException(&quot;The source image format is not supported.&quot;);

        var info = codec.Info
            .WithColorType(SKImageInfo.PlatformColorType)
            .WithAlphaType(SKAlphaType.Premul);

        return SKBitmap.Decode(codec, info)
            ?? throw new NotSupportedException(&quot;The source image could not be decoded.&quot;);
    }

    private static SKBitmap Replace(SKBitmap current, SKBitmap next)
    {
        current.Dispose();
        return next;
    }

    private static SKBitmap Apply(SKBitmap source, ImageOperation operation) =&amp;gt; operation.Command switch
    {
        ImageEditorCommand.Grayscale =&amp;gt; Grayscale(source),
        ImageEditorCommand.Crop =&amp;gt; Crop(source, operation),
        ImageEditorCommand.FlipX =&amp;gt; Draw(source.Width, source.Height, canvas =&amp;gt;
        {
            canvas.Scale(-1, 1, source.Width / 2f, 0);
            canvas.DrawBitmap(source, 0, 0, CopySampling);
        }),
        ImageEditorCommand.FlipY =&amp;gt; Draw(source.Width, source.Height, canvas =&amp;gt;
        {
            canvas.Scale(1, -1, 0, source.Height / 2f);
            canvas.DrawBitmap(source, 0, 0, CopySampling);
        }),
        ImageEditorCommand.Resize =&amp;gt; Resize(source, operation.Width, operation.Height),
        ImageEditorCommand.ResizeKeepScale =&amp;gt; ResizeKeepScale(source, operation),
        ImageEditorCommand.Rotate =&amp;gt; Rotate(source, operation.Angle),
        _ =&amp;gt; throw new InvalidOperationException($&quot;Unknown command {operation.Command}&quot;),
    };

    private static SKBitmap Draw(int width, int height, Action&amp;lt;SKCanvas&amp;gt; draw)
    {
        var bitmap = new SKBitmap(width, height, SKImageInfo.PlatformColorType, SKAlphaType.Premul);
        using var canvas = new SKCanvas(bitmap);
        canvas.Clear(SKColors.Transparent);
        draw(canvas);
        return bitmap;
    }

    private static SKBitmap Grayscale(SKBitmap source)
    {
        using var filter = SKColorFilter.CreateColorMatrix(GrayscaleMatrix);
        using var paint = new SKPaint { ColorFilter = filter };
        return Draw(source.Width, source.Height, canvas =&amp;gt; canvas.DrawBitmap(source, 0, 0, CopySampling, paint));
    }

    private static SKBitmap Crop(SKBitmap source, ImageOperation operation)
    {
        var area = SKRectI.Create(operation.Left, operation.Top, operation.Width, operation.Height);
        if (area.IsEmpty || !SKRectI.Create(source.Width, source.Height).Contains(area))
        {
            throw new ArgumentOutOfRangeException(nameof(operation), $&quot;Crop area {area} is outside the image bounds.&quot;);
        }

        return Draw(area.Width, area.Height, canvas =&amp;gt; canvas.DrawBitmap(source, -area.Left, -area.Top, CopySampling));
    }

    private static SKBitmap Resize(SKBitmap source, int width, int height)
    {
        var size = TargetSize(source, width, height);
        return Draw(size.Width, size.Height, canvas =&amp;gt;
            canvas.DrawBitmap(source, SKRect.Create(size.Width, size.Height), ResizeSampling));
    }

    /// &amp;lt;summary&amp;gt;ImageSharp BoxPad: fit inside the box without upscaling, center, fill the rest with the background color.&amp;lt;/summary&amp;gt;
    private static SKBitmap ResizeKeepScale(SKBitmap source, ImageOperation operation)
    {
        var box = TargetSize(source, operation.Width, operation.Height);
        var scale = Math.Min(1f, Math.Min(box.Width / (float)source.Width, box.Height / (float)source.Height));
        var width = Math.Max(1, (int)MathF.Round(source.Width * scale));
        var height = Math.Max(1, (int)MathF.Round(source.Height * scale));
        var destination = SKRect.Create((box.Width - width) / 2, (box.Height - height) / 2, width, height);
        var background = ParseColor(operation.BackgroundColor);

        return Draw(box.Width, box.Height, canvas =&amp;gt;
        {
            canvas.Clear(background);
            canvas.DrawBitmap(source, destination, ResizeSampling);
        });
    }

    private static SKBitmap Rotate(SKBitmap source, double degrees)
    {
        var radians = degrees * Math.PI / 180;
        var cos = Math.Abs(Math.Cos(radians));
        var sin = Math.Abs(Math.Sin(radians));
        // Round before Ceiling so right angles don&#39;t grow by a pixel from floating point noise (50.0000001 -&amp;gt; 51).
        var width = (int)Math.Ceiling(Math.Round(source.Width * cos + source.Height * sin, 3));
        var height = (int)Math.Ceiling(Math.Round(source.Width * sin + source.Height * cos, 3));

        return Draw(width, height, canvas =&amp;gt;
        {
            canvas.Translate(width / 2f, height / 2f);
            canvas.RotateDegrees((float)degrees);
            canvas.DrawBitmap(source, -source.Width / 2f, -source.Height / 2f, RotateSampling);
        });
    }

    /// &amp;lt;summary&amp;gt;Same as ImageSharp: a 0 dimension is derived from the other one, keeping the aspect ratio.&amp;lt;/summary&amp;gt;
    private static SKSizeI TargetSize(SKBitmap source, int width, int height)
    {
        if (width == 0 &amp;amp;&amp;amp; height &amp;gt; 0)
        {
            width = Math.Max(1, (int)MathF.Round(source.Width * height / (float)source.Height));
        }

        if (height == 0 &amp;amp;&amp;amp; width &amp;gt; 0)
        {
            height = Math.Max(1, (int)MathF.Round(source.Height * width / (float)source.Width));
        }

        if (width &amp;lt;= 0 || height &amp;lt;= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(width), $&quot;Invalid target size {width}x{height}.&quot;);
        }

        return new SKSizeI(width, height);
    }

    /// &amp;lt;summary&amp;gt;Parses ImageSharp-style hex colors (#RGB, #RGBA, #RRGGBB, #RRGGBBAA). Empty means white.&amp;lt;/summary&amp;gt;
    private static SKColor ParseColor(string value)
    {
        if (string.IsNullOrEmpty(value))
        {
            return SKColors.White;
        }

        var hex = value.TrimStart(&#39;#&#39;);
        // Skia expects the alpha first (#ARGB / #AARRGGBB).
        var argb = hex.Length switch
        {
            4 =&amp;gt; hex[3..] + hex[..3],
            8 =&amp;gt; hex[6..] + hex[..6],
            _ =&amp;gt; hex,
        };

        return SKColor.TryParse(argb, out var color)
            ? color
            : throw new ArgumentException($&quot;Invalid background color &#39;{value}&#39;.&quot;, nameof(value));
    }

    private static void Encode(SKBitmap image, Stream output, RenderRequest request)
    {
        using var pixmap = image.PeekPixels();
        var quality = Math.Clamp(request.Quality, 0, 100);

        var encoded = request.MimeType switch
        {
            &quot;image/png&quot; or &quot;image/x-icon&quot; =&amp;gt;
                pixmap.Encode(output, new SKPngEncoderOptions(SKPngEncoderFilterFlags.AllFilters, PngZLibLevel(request.Quality))),
            &quot;image/jpg&quot; or &quot;image/jpe&quot; or &quot;image/jpeg&quot; or &quot;image/pjpeg&quot; =&amp;gt;
                pixmap.Encode(output, new SKJpegEncoderOptions(quality)),
            &quot;image/webp&quot; =&amp;gt;
                pixmap.Encode(output, new SKWebpEncoderOptions(SKWebpEncoderCompression.Lossy, quality)),
            _ =&amp;gt; throw new NotSupportedException($&quot;Not supported image format [{request.MimeType}] when creating images.&quot;),
        };

        if (!encoded)
        {
            throw new InvalidOperationException($&quot;Failed to encode image as [{request.MimeType}].&quot;);
        }
    }

    /// &amp;lt;summary&amp;gt;Same quality -&amp;gt; compression mapping as the default service: 100 = level 0 ... &amp;amp;lt;20 = level 9.&amp;lt;/summary&amp;gt;
    private static int PngZLibLevel(int quality) =&amp;gt; quality &amp;lt; 0 ? 6 : Math.Clamp(10 - quality / 10, 0, 9);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two details that are easy to miss:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Channel order&lt;/strong&gt; (&lt;strong&gt;ParseColor&lt;/strong&gt;). ImageSharp parses &lt;strong&gt;#RRGGBBAA&lt;/strong&gt;, Skia parses &lt;strong&gt;#AARRGGBB&lt;/strong&gt;. Rotate the hex string before parsing, or &lt;strong&gt;#00FF0080&lt;/strong&gt; comes out as a different color.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;90&amp;deg; rotation&lt;/strong&gt; (&lt;strong&gt;Rotate&lt;/strong&gt;). &lt;strong&gt;cos(90&amp;deg;)&lt;/strong&gt; evaluates to &lt;strong&gt;6e-17&lt;/strong&gt;, not 0, so &lt;strong&gt;Math.Ceiling(50.00000000000001)&lt;/strong&gt; gives 51 and the output gains an extra pixel. Round before taking the ceiling.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Registration&lt;/h3&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;using EPiServer.ImageLibrary;
using YourSite.Business.Imaging;

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services
            .AddCmsAspNetIdentity&amp;lt;ApplicationUser&amp;gt;()
            .AddCms();

        // Replaces EPiServer.ImageLibrary.ImageSharp for thumbnails and the CMS image editor.
        services.AddSingleton&amp;lt;IImageService, SkiaImageService&amp;gt;();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The CMS only registers a fallback &lt;strong&gt;IImageService&lt;/strong&gt; when none exists (&lt;strong&gt;TryAddSingleton&lt;/strong&gt;), so your implementation takes precedence. Without any registration, that fallback throws &lt;strong&gt;InvalidOperationException&lt;/strong&gt; the first time it&#39;s resolved.&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code&gt;$ dotnet list package --include-transitive --vulnerable
The given project `Alloy` has no vulnerable packages given the current sources.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Known limitations&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Skia encodes PNG, JPEG and WebP only. GIF, BMP or TIFF output throws &lt;strong&gt;NotSupportedException&lt;/strong&gt;, so the image editor won&#39;t work for those formats. Thumbnails aren&#39;t affected, because the CMS always stores them as &lt;strong&gt;.png&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Skia can&#39;t decode TIFF, so TIFF uploads get no thumbnail. The upload itself still succeeds, because the CMS handles thumbnail errors.&lt;/li&gt;
&lt;li&gt;Animated GIFs are flattened to the first frame.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; this sample is not an officially supported Optimizely package. It&#39;s provided as guidance, and you own it once it&#39;s in your solution. Test it against your own media library before you deploy.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;6. Option 3: suppress the warnings&lt;/h2&gt;
&lt;pre class=&quot;language-xml&quot;&gt;&lt;code&gt;&amp;lt;ItemGroup&amp;gt;
  &amp;lt;NuGetAuditSuppress Include=&quot;https://github.com/advisories/GHSA-gwg2-r3hj-4w44&quot; /&amp;gt;
  &amp;lt;NuGetAuditSuppress Include=&quot;https://github.com/advisories/GHSA-j3p4-wp97-rph4&quot; /&amp;gt;
  &amp;lt;NuGetAuditSuppress Include=&quot;https://github.com/advisories/GHSA-j9gm-c75j-xc9q&quot; /&amp;gt;
  &amp;lt;NuGetAuditSuppress Include=&quot;https://github.com/advisories/GHSA-wmxv-xphr-5c9g&quot; /&amp;gt;
  &amp;lt;NuGetAuditSuppress Include=&quot;https://github.com/advisories/GHSA-jjfr-hcj7-qf5w&quot; /&amp;gt;
&amp;lt;/ItemGroup&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Suppressing only hides the warning, the vulnerability is still there. Record the decision (reason, owner, review date) wherever your team tracks accepted risks.&lt;/p&gt;
&lt;h2&gt;7. Option 4: wait for an updated package&lt;/h2&gt;
&lt;p&gt;An updated &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt; would remove the problem at the source. We can&#39;t share a timeline for that yet. If you go this route:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Watch the Optimizely release notes for a new &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt; version, or check from the command line:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;dotnet list package --outdated --include-transitive | grep -i imagelibrary
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Combine it with option 3 in the meantime, and add a review date so the suppression gets removed once the update ships:&lt;/p&gt;
&lt;pre class=&quot;language-xml&quot;&gt;&lt;code&gt;&amp;lt;!-- TEMP: waiting for an EPiServer.ImageLibrary.ImageSharp update without the vulnerable ImageSharp.
     Review by: 2026-12-31. Owner: @team-cms --&amp;gt;
&amp;lt;NuGetAuditSuppress Include=&quot;https://github.com/advisories/GHSA-j3p4-wp97-rph4&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If you&#39;re on option 2, moving to the official package later means deleting your &lt;strong&gt;IImageService&lt;/strong&gt; implementation and its registration.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;SixLabors/ImageSharp, 4.x license discussion: &lt;a href=&quot;https://github.com/SixLabors/ImageSharp/issues/3205#issuecomment-6062121101&quot;&gt;https://github.com/SixLabors/ImageSharp/issues/3205#issuecomment-6062121101&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Optimizely &lt;strong&gt;IImageService&lt;/strong&gt; API: &lt;a href=&quot;/CsClassLibraries/cms/EPiServer.ImageLibrary.IImageService?version=12&quot;&gt;https://world.optimizely.com/CsClassLibraries/cms/EPiServer.ImageLibrary.IImageService?version=12&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;SkiaSharp: &lt;a href=&quot;https://github.com/mono/SkiaSharp&quot;&gt;https://github.com/mono/SkiaSharp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;NuGet dependency resolution: &lt;a href=&quot;https://learn.microsoft.com/nuget/concepts/dependency-resolution&quot;&gt;https://learn.microsoft.com/nuget/concepts/dependency-resolution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;NuGet Audit: &lt;a href=&quot;https://learn.microsoft.com/nuget/concepts/auditing-packages&quot;&gt;https://learn.microsoft.com/nuget/concepts/auditing-packages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Advisories: &lt;a href=&quot;https://github.com/advisories/GHSA-gwg2-r3hj-4w44&quot;&gt;GHSA-gwg2-r3hj-4w44&lt;/a&gt;, &lt;a href=&quot;https://github.com/advisories/GHSA-j3p4-wp97-rph4&quot;&gt;GHSA-j3p4-wp97-rph4&lt;/a&gt;, &lt;a href=&quot;https://github.com/advisories/GHSA-j9gm-c75j-xc9q&quot;&gt;GHSA-j9gm-c75j-xc9q&lt;/a&gt;, &lt;a href=&quot;https://github.com/advisories/GHSA-wmxv-xphr-5c9g&quot;&gt;GHSA-wmxv-xphr-5c9g&lt;/a&gt;, &lt;a href=&quot;https://github.com/advisories/GHSA-jjfr-hcj7-qf5w&quot;&gt;GHSA-jjfr-hcj7-qf5w&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>            <guid>https://world.optimizely.com/blogs/cuong-nguyen-inh/dates/2026/10/handling-sixlabors.imagesharp-vulnerability-warnings-in-optimizely-cms-12-unofficial</guid>            <pubDate>Sat, 10 Oct 2026 06:01:41 GMT</pubDate>           <category>Blog post</category></item><item> <title>Optimizely CMS 12 on .NET 10: Fixing a Siteimprove TypeMismatchException</title>            <link>https://madhuanbalagan.com/?p=5093</link>            <description>&lt;p&gt;Recently, while upgrading an Optimizely CMS 12 solution from .NET 8 to .NET 10, we encountered a frustrating exception that initially looked like a CMS&amp;#46;&amp;#46;&amp;#46;&lt;/p&gt;
&lt;p&gt;The post &lt;a href=&quot;https://madhuanbalagan.com/optimizely-cms-12-on-net-10-fixing-a-siteimprove-typemismatchexception&quot;&gt;Optimizely CMS 12 on .NET 10: Fixing a Siteimprove TypeMismatchException&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;
</description>            <guid>https://madhuanbalagan.com/?p=5093</guid>            <pubDate>Fri, 09 Oct 2026 21:58:00 GMT</pubDate>           <category>Blog post</category></item><item> <title>What made Scheduled Jobs faster in Optimizely CMS 13.3.0</title>            <link>https://www.gulla.net/en/blog/what-made-scheduled-jobs-faster-in-optimizely-cms-13.3.0/</link>            <description>A look under the hood at the update that made scheduled jobs faster.</description>            <guid>https://www.gulla.net/en/blog/what-made-scheduled-jobs-faster-in-optimizely-cms-13.3.0/</guid>            <pubDate>Fri, 09 Oct 2026 06:00:00 GMT</pubDate>           <category>Blog post</category></item><item> <title>New DDS explorer addon for Optimizely CMS</title>            <link>https://www.gulla.net/en/blog/new-dds-explorer-addon-for-optimizely-cms/</link>            <description>I wanted to be able to manage Dynamic Data Store from within Optimizely CMS, so I created another addon.</description>            <guid>https://www.gulla.net/en/blog/new-dds-explorer-addon-for-optimizely-cms/</guid>            <pubDate>Thu, 08 Oct 2026 18:27:59 GMT</pubDate>           <category>Blog post</category></item><item> <title>Queryable or Searchable? How indexing type changes your Optimizely Graph query</title>            <link>http://optimizelyfoundation.com/?p=694</link>            <description>The Property Indexing Type on a CMS field decides whether Optimizely Graph will return it, filter it, or run full-text search on it. Default, Queryable, Searchable, and Disabled each change the GraphQL query.</description>            <guid>http://optimizelyfoundation.com/?p=694</guid>            <pubDate>Thu, 08 Oct 2026 13:06:39 GMT</pubDate>           <category>Blog post</category></item><item> <title>How to Build Optimizely CMS 13 with Visual Builder in .NET</title>            <link>https://www.technicaldogsbody.com/blog/how-to-build-optimizely-cms-13-with-visual-builder-in-net</link>            <description>A code-first guide for .NET developers moving from Optimizely CMS 12 to CMS 13, covering Visual Builder: ExperienceData, Sections, Elements, tag helpers and styles.</description>            <guid>https://www.technicaldogsbody.com/blog/how-to-build-optimizely-cms-13-with-visual-builder-in-net</guid>            <pubDate>Thu, 08 Oct 2026 11:30:15 GMT</pubDate>           <category>Blog post</category></item><item> <title>Supercharging Optimizely DXP Troubleshooting with the Azure Copilot Observability Agent</title>            <link>https://world.optimizely.com/blogs/scott-reed/dates/2026/10/supercharging-optimizely-dxp-troubleshooting-with-the-azure-copilot-observability-agent/</link>            <description>&lt;p&gt;If you&amp;rsquo;ve logged into the Azure portal recently to check the Application Insights for your Optimizely DXP environment, you might have noticed a new toggle in the Logs UI labelled simply:&amp;nbsp;&lt;strong&gt;&quot;Agent.&quot;&lt;/strong&gt; And as you create a new query, being prompted with help from the Observability Agent&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;img src=&quot;/link/e079e0af11884b5991b56be2b21969dc.aspx&quot; width=&quot;698&quot; height=&quot;326&quot; /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Application Insights gives us many very useful tools, including recent updates such as Grafana dashboards that can help us view key information about our application. We technical specialists in the Optimizely DXP stack should be familiar with working within the investigate, monitoring, and usage areas.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;However, one key pain point has been working with and remembering Kusto Query Language (KQL) but with recent updates in Application Insights and this default enabled on the Optimise DXP, this can now be a lot easier.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Instead of writing complex Kusto Query Language (KQL) to figure out why your modern Optimizely Commerce 15 checkout is dragging, or why your CMS 12 editors are experiencing slow load times, you can now ask plain-English questions.&lt;/p&gt;
&lt;p&gt;In this post, we&amp;rsquo;ll explore how to leverage this AI agent specifically for web apps running on Optimizely DXP, focusing on the exact types of telemetry, anomalies, and performance metrics that matter most for modern CMS 12/13 and Commerce 14/15 solutions.&lt;/p&gt;
&lt;h2&gt;The Modern Optimizely DXP Telemetry Landscape&lt;/h2&gt;
&lt;p&gt;Before we talk to the AI, we need to understand the data. Optimizely DXP environments (Integration, Preproduction, Production) automatically wire up Application Insights. For applications running on modern .NET (like CMS 12/13 and Commerce 14/15), this captures:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Requests:&lt;/strong&gt; Server-side ASP.NET Core page loads, Content Delivery API calls, and Optimizely Graph queries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dependencies:&lt;/strong&gt; SQL Database queries, external API calls (payment gateways, ERPs), and internal Optimizely services.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exceptions:&lt;/strong&gt; Server-side errors and handled exceptions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom Events:&lt;/strong&gt; Business logic milestones captured via &lt;span style=&quot;font-family: Consolas,Menlo,monospace; color: #0b6e4f;&quot;&gt;TelemetryClient&lt;/span&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Historically, correlating a spike in &lt;span style=&quot;font-family: Consolas,Menlo,monospace; color: #0b6e4f;&quot;&gt;Exceptions&lt;/span&gt; with a slowdown in &lt;span style=&quot;font-family: Consolas,Menlo,monospace; color: #0b6e4f;&quot;&gt;Dependencies&lt;/span&gt; required mastery of KQL &lt;span style=&quot;font-family: Consolas,Menlo,monospace; color: #0b6e4f;&quot;&gt;join&lt;/span&gt; statements. Now, the Observability Agent handles the translation. Let&#39;s look at how this plays out across the platform.&lt;/p&gt;
&lt;h2&gt;Scenario 1: Debugging Commerce 14/15 Checkout and Asynchronous Operations&lt;/h2&gt;
&lt;p&gt;Optimizely Commerce 14 and 15 fully embrace asynchronous programming. A modern checkout process involves validating a serialised cart, recalculating promotions, calling an external tax API, communicating with a payment provider, and finally using &lt;span style=&quot;font-family: Consolas,Menlo,monospace; color: #0b6e4f;&quot;&gt;SaveAsPurchaseOrderAsync()&lt;/span&gt; to convert the cart.&lt;/p&gt;
&lt;p&gt;If conversion rates drop, the culprit is often a silent performance degradation in one of these external or asynchronous steps.&lt;/p&gt;
&lt;h3&gt;The Old Way (KQL)&lt;/h3&gt;
&lt;p&gt;To find the average duration of the order saving operation over the last 24 hours, you&amp;rsquo;d have to write:&lt;/p&gt;
&lt;pre style=&quot;background: #f4f6f8; color: #1a1a1a; border: 1px solid #d0d7de; border-radius: 4px; padding: 12px; overflow-x: auto; font-family: Consolas,Menlo,monospace; font-size: 14px; white-space: pre;&quot;&gt;dependencies
| where name contains &quot;SaveAsPurchaseOrder&quot; or name contains &quot;IOrderRepository.SaveAsync&quot;
| where timestamp &amp;gt; ago(24h)
| summarise AvgDuration = avg(duration), Count = count() by bin(timestamp, 1h)
| render timechart
&lt;/pre&gt;
&lt;h3&gt;The Copilot Way&lt;/h3&gt;
&lt;p&gt;Toggle the &quot;Agent&quot; switch to &lt;strong&gt;On&lt;/strong&gt; and simply type:&lt;/p&gt;
&lt;blockquote style=&quot;border-left: 4px solid #0b6e4f; margin: 16px 0; padding: 8px 16px; background: #f7faf9;&quot;&gt;
&lt;p&gt;&lt;em&gt;&quot;Show me a timechart of the average duration for dependency calls related to IOrderRepository.SaveAsync over the last 24 hours, grouped by hour.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;What to Look For in Commerce 14/15:&lt;/h3&gt;
&lt;p&gt;When using the agent, focus your queries on the known bottlenecks of a modern e-commerce platform:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Cart Serialisation Latency:&lt;/strong&gt; Commerce 14/15 stores active carts as JSON in the &lt;span style=&quot;font-family: Consolas,Menlo,monospace; color: #0b6e4f;&quot;&gt;SerializableCart&lt;/span&gt; table. Ask the agent: &lt;em&gt;&quot;Are there any SQL dependencies related to the SerializableCart table taking longer than 500ms today compared to yesterday?&quot;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Third-Party Payment Delays:&lt;/strong&gt; &lt;em&gt;&quot;Show me the failure rate and average response time for external HTTP dependencies calling &#39;api.stripe.com&#39; (or your payment provider) over the last 7 days.&quot;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Promotion Engine Overhead:&lt;/strong&gt; &lt;em&gt;&quot;Did the average request duration of the /checkout API endpoint increase after our deployment at 2 PM today?&quot;&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can also ask the agent to find root causes: &lt;em&gt;&quot;Why did checkout requests fail more frequently between 2 PM and 4 PM today?&quot;&lt;/em&gt; The Copilot will analyse exceptions and dependency failures during that window and summarise the findings.&lt;/p&gt;
&lt;h2&gt;Scenario 2: Diagnosing CMS 12/13 Rendering and Headless Delivery&lt;/h2&gt;
&lt;p&gt;With CMS 12 and 13 running on ASP.NET Core, performance is incredibly fast out of the box. However, bottlenecks usually occur around custom block rendering, complex tag helpers, or heavy utilisation of Optimizely Graph and Content Delivery APIs in headless architectures.&lt;/p&gt;
&lt;h3&gt;Uncovering Headless and Search Bottlenecks&lt;/h3&gt;
&lt;p&gt;If your frontend Next.js application relies heavily on Optimizely Graph or Search &amp;amp; Navigation, a poorly optimised GraphQL query or &lt;span style=&quot;font-family: Consolas,Menlo,monospace; color: #0b6e4f;&quot;&gt;.Filter()&lt;/span&gt; statement will cause cascading slowdowns.&lt;/p&gt;
&lt;p&gt;Try prompting the Observability Agent with:&lt;/p&gt;
&lt;blockquote style=&quot;border-left: 4px solid #0b6e4f; margin: 16px 0; padding: 8px 16px; background: #f7faf9;&quot;&gt;
&lt;p&gt;&lt;em&gt;&quot;Identify the top 5 slowest outgoing HTTP requests to our Optimizely Graph or Search endpoints over the last 3 days. What CMS pages or API routes were users requesting when these slow calls happened?&quot;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The agent will automatically correlate the &lt;span style=&quot;font-family: Consolas,Menlo,monospace; color: #0b6e4f;&quot;&gt;dependencies&lt;/span&gt; table with the &lt;span style=&quot;font-family: Consolas,Menlo,monospace; color: #0b6e4f;&quot;&gt;requests&lt;/span&gt; table, saving you the hassle of writing complex cross-table joins.&lt;/p&gt;
&lt;h3&gt;Investigating ASP.NET Core Memory and Caching&lt;/h3&gt;
&lt;p&gt;CMS 12/13 utilises heavily optimised memory caching in .NET Core. If the cache is invalidated too frequently, you&#39;ll see a spike in SQL queries to the core CMS database.&lt;/p&gt;
&lt;blockquote style=&quot;border-left: 4px solid #0b6e4f; margin: 16px 0; padding: 8px 16px; background: #f7faf9;&quot;&gt;
&lt;p&gt;&lt;em&gt;&quot;Has there been an unexpected spike in dependency calls to the core CMS SQL database in the last 12 hours compared to the previous week? If so, what are the most common SQL commands being executed?&quot;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If an editor publishes a massive content change that clears a large portion of the cache, the Copilot can help you correlate that spike in database traffic directly to the time the editor hit &quot;Publish.&quot;&lt;/p&gt;
&lt;h2&gt;Scenario 3: Post-Deployment Verification on DXP&lt;/h2&gt;
&lt;p&gt;Deploying modern .NET applications to Optimizely DXP involves swapping slots in Production. The moments immediately following a slot swap are critical. Instead of staring at a raw stream of logs, you can use the Copilot as an active monitor.&lt;/p&gt;
&lt;h3&gt;The Post-Deployment Checkup&lt;/h3&gt;
&lt;p&gt;After a deployment finishes, open the App Insights logs, activate the Agent, and ask:&lt;/p&gt;
&lt;blockquote style=&quot;border-left: 4px solid #0b6e4f; margin: 16px 0; padding: 8px 16px; background: #f7faf9;&quot;&gt;
&lt;p&gt;&lt;em&gt;&quot;Summarise the health of the application over the last 60 minutes. Have any new exception types appeared that were not present in the 24 hours prior?&quot;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The AI will parse the &lt;span style=&quot;font-family: Consolas,Menlo,monospace; color: #0b6e4f;&quot;&gt;exceptions&lt;/span&gt; table, filter out the normal background noise, and highlight genuinely new errors&amp;mdash;like a missed dependency injection in your &lt;span style=&quot;font-family: Consolas,Menlo,monospace; color: #0b6e4f;&quot;&gt;Startup.cs&lt;/span&gt; or a misconfigured &lt;span style=&quot;font-family: Consolas,Menlo,monospace; color: #0b6e4f;&quot;&gt;appsettings.json&lt;/span&gt; binding introduced by your latest Commerce 15 release.&lt;/p&gt;
&lt;p&gt;If it finds something, you can follow up conversationally:&lt;/p&gt;
&lt;blockquote style=&quot;border-left: 4px solid #0b6e4f; margin: 16px 0; padding: 8px 16px; background: #f7faf9;&quot;&gt;
&lt;p&gt;&lt;em&gt;&quot;For that new InvalidOperationException related to dependency injection, what was the exact stack trace?&quot;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Best Practices for AI-Assisted Telemetry&lt;/h2&gt;
&lt;p&gt;While the Azure Copilot Observability Agent is a game-changer for Optimizely developers, it works best when you guide it properly:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Enrich Your Telemetry:&lt;/strong&gt; Use &lt;span style=&quot;font-family: Consolas,Menlo,monospace; color: #0b6e4f;&quot;&gt;ITelemetryInitializer&lt;/span&gt; in your CMS 12/13 codebase to inject custom properties. Add the &lt;span style=&quot;font-family: Consolas,Menlo,monospace; color: #0b6e4f;&quot;&gt;CurrentLanguageBranch&lt;/span&gt; or &lt;span style=&quot;font-family: Consolas,Menlo,monospace; color: #0b6e4f;&quot;&gt;CustomerGroup&lt;/span&gt; to every request. You can then ask the agent: &lt;em&gt;&quot;Are authenticated B2B users experiencing slower API response times than anonymous users?&quot;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Track Custom Events:&lt;/strong&gt; Log specific business events like &lt;span style=&quot;font-family: Consolas,Menlo,monospace; color: #0b6e4f;&quot;&gt;TrackEvent(&quot;ItemAddedToCart&quot;)&lt;/span&gt; or &lt;span style=&quot;font-family: Consolas,Menlo,monospace; color: #0b6e4f;&quot;&gt;TrackEvent(&quot;GraphQueryExecuted&quot;)&lt;/span&gt;. This allows you to ask the agent business-centric questions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Be Specific with Timeframes:&lt;/strong&gt; Vague questions yield vague answers. Always specify the time window (e.g., &quot;between 10 AM and noon UTC yesterday&quot;).&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The addition of the Azure Copilot Observability Agent to Application Insights drastically lowers the barrier to entry for deep telemetry analysis in modern Optimizely environments. By focusing your natural language queries on the specific architectural patterns of CMS 12/13 (like ASP.NET Core caching and Graph API calls) and Commerce 14/15 (like asynchronous cart processing), you can dramatically reduce your mean time to resolution (MTTR) and ensure a blazingly fast digital experience.&lt;/p&gt;
&lt;h2&gt;How can Niteco help you?&lt;/h2&gt;
&lt;p&gt;Niteco is the largest global certified Optimizely CMS and Commerce partner, and we are experts and specialists in the Optimizely agent platform. We have recently attended Opticon New York 2026 as a sponsored partner, and we&#39;ll be coming back next week to London. Come and find us at our stand, or by looking for our yellow trousers.&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;margin: 20px 0 8px 0;&quot;&gt;
&lt;div style=&quot;display: inline-block; vertical-align: top; box-sizing: border-box; width: 23%; min-width: 190px; margin: 0 1% 16px 0; padding: 16px; background: #f7f7f5; border-top: 4px solid #F8D749; border-radius: 4px;&quot;&gt;
&lt;p style=&quot;margin: 0 0 4px 0; font-size: 44px; line-height: 1.1; font-weight: 300; color: #16171a;&quot;&gt;#1&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 10px 0; font-size: 13px; font-weight: bold; letter-spacing: 3px; text-transform: uppercase; color: #16171a;&quot;&gt;Optimizely&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.5; color: #444;&quot;&gt;The largest global partner. Powered by technical excellence and innovation. Accelerated by AI.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;display: inline-block; vertical-align: top; box-sizing: border-box; width: 23%; min-width: 190px; margin: 0 1% 16px 0; padding: 16px; background: #f7f7f5; border-top: 4px solid #F8D749; border-radius: 4px;&quot;&gt;
&lt;p style=&quot;margin: 0 0 4px 0; font-size: 44px; line-height: 1.1; font-weight: 300; color: #16171a;&quot;&gt;400+&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 10px 0; font-size: 13px; font-weight: bold; letter-spacing: 3px; text-transform: uppercase; color: #16171a;&quot;&gt;AI-trained experts&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.5; color: #444;&quot;&gt;Certified by leading platforms like Optimizely, Microsoft, Contentful &amp;amp; Adobe.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;display: inline-block; vertical-align: top; box-sizing: border-box; width: 23%; min-width: 190px; margin: 0 1% 16px 0; padding: 16px; background: #f7f7f5; border-top: 4px solid #F8D749; border-radius: 4px;&quot;&gt;
&lt;p style=&quot;margin: 0 0 4px 0; font-size: 44px; line-height: 1.1; font-weight: 300; color: #16171a;&quot;&gt;2000+&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 10px 0; font-size: 13px; font-weight: bold; letter-spacing: 3px; text-transform: uppercase; color: #16171a;&quot;&gt;Successful projects&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.5; color: #444;&quot;&gt;We have vast experience from years of delivering digital platforms for clients in diverse sectors.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;display: inline-block; vertical-align: top; box-sizing: border-box; width: 23%; min-width: 190px; margin: 0 1% 16px 0; padding: 16px; background: #f7f7f5; border-top: 4px solid #F8D749; border-radius: 4px;&quot;&gt;
&lt;p style=&quot;margin: 0 0 4px 0; font-size: 44px; line-height: 1.1; font-weight: 300; color: #16171a;&quot;&gt;9&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 10px 0; font-size: 13px; font-weight: bold; letter-spacing: 3px; text-transform: uppercase; color: #16171a;&quot;&gt;Optimizely MVPs&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.5; color: #444;&quot;&gt;Largest team with OMVPs. Meaning the best &amp;amp; most experienced Optimizely minds will work with you.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;As Optimizely&#39;s largest certified partner in Optimizely CMS, Commerce, and the agent platform, we can help you with any of your strategic initiatives globally around content, commerce, or your organisational AI.&lt;/p&gt;
&lt;p&gt;If you would like help getting more out of your Optimizely DXP telemetry, get in touch with us at Niteco: &lt;a href=&quot;https://niteco.com/contact-us/&quot;&gt;https://niteco.com/contact-us/&lt;/a&gt;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/scott-reed/dates/2026/10/supercharging-optimizely-dxp-troubleshooting-with-the-azure-copilot-observability-agent/</guid>            <pubDate>Thu, 08 Oct 2026 09:10:07 GMT</pubDate>           <category>Blog post</category></item><item> <title>Visual Builder Without Graph: Server-Side MVC Rendering in CMS 13</title>            <link>https://www.adnanzameer.com/2026/10/visual-builder-without-graph-mvc-rendering-cms-13.html</link>            <description>&lt;!--excerpt: Many teams assume moving to Optimizely CMS 13 requires rebuilding their frontend as a headless application or querying Optimizely Graph to render Visual Builder layouts. That is a myth. CMS 13 provides native ASP.NET Core MVC tag helpers that render Visual Builder experiences and sections server-side without a single Graph query. Here is how to configure and use them.--&gt;

When teams </description>            <guid>https://www.adnanzameer.com/2026/10/visual-builder-without-graph-mvc-rendering-cms-13.html</guid>            <pubDate>Wed, 07 Oct 2026 09:51:36 GMT</pubDate>           <category>Blog post</category></item><item> <title>Writing a Catalog Entry the Low-Level Way. Part 2: The Writer</title>            <link>https://szolkowski.github.io/2026/10/07/writing-a-catalog-entry-the-low-level-way-part-2-the-writer</link>            <description>Upserting a catalog entry through CatalogContext and MetaDataPlus: ContentGuid, the meta class seam, response groups, and a create path that rolls back.</description>            <guid>https://szolkowski.github.io/2026/10/07/writing-a-catalog-entry-the-low-level-way-part-2-the-writer</guid>            <pubDate>Wed, 07 Oct 2026 08:00:00 GMT</pubDate>           <category>Blog post</category></item><item> <title>Migrate Optimizely DAM Assets from CMS 12 to CMS 13</title>            <link>https://world.optimizely.com/blogs/bartosz-sekula/dates/2026/10/dam-migration-from-cms12-to-cms13/</link>            <description>&lt;p&gt;Sites that used Optimizely DAM in CMS 12 need their asset references moved to the new Graph-backed content sources after upgrading. CMS 13.3 includes a scheduled job for this. This post covers how to configure it, run it and later on verify the results.&lt;/p&gt;
&lt;p&gt;So, in CMS12 we supported DAM in the following properties:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/5d757ebab1ca48c18b10288b9e19ff34.aspx&quot; alt=&quot;&quot; width=&quot;828&quot; height=&quot;985&quot; /&gt;&lt;/p&gt;
&lt;p&gt;After upgrading your site to CMS you will see all those images, videos and files that came from DAM as broken or empty, both in edit mode and on the live site:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/998b02cfd18d4815a5c963d9aa35b3b3.aspx&quot; alt=&quot;&quot; width=&quot;1059&quot; height=&quot;1006&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This happens because the content still points to the old dam content provider. In CMS 13, DAM assets come through Optimizely Graph instead. The old references need to be moved over to the new format.&lt;/p&gt;
&lt;p&gt;In CMS &lt;a title=&quot;DAM Migration package&quot; href=&quot;https://nuget.optimizely.com/packages/episerver.cms.dammigration/13.3.0&quot;&gt;13.3.0&lt;/a&gt; we added a scheduled job that does this for you. This post explains how to set it up and what to check along the way.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What the job does&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The job is called &lt;strong&gt;Optimizely DAM Legacy Asset Migration&lt;/strong&gt;. It works in three steps:&lt;/p&gt;
&lt;p&gt;1. It reads all old DAM references from the database and works out the new ID for each asset.&lt;br /&gt;2. It goes through all content and updates the references. This covers published content, drafts, old versions, all languages and the trash.&lt;br /&gt;3. It removes the old tracking records in CMP and deletes the old mapping rows from the database.&lt;/p&gt;
&lt;p&gt;These property types are updated:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ContentReference&lt;/li&gt;
&lt;li&gt;IList&amp;lt;ContentReference&amp;gt;&lt;/li&gt;
&lt;li&gt;ContentArea, including inline blocks&lt;/li&gt;
&lt;li&gt;Url&lt;/li&gt;
&lt;li&gt;LinkItem and LinkItemCollection&lt;/li&gt;
&lt;li&gt;XhtmlString&lt;/li&gt;
&lt;li&gt;Local blocks and block lists&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So all the properties defined in the below page type will be migrated successfully:&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;pre&gt;[ContentType(&lt;br /&gt;    GUID = &quot;BA5FBE47-E44A-44CD-BFAF-3CC21B0A2F9C&quot;,&lt;br /&gt;    DisplayName = &quot;DAM references test page&quot;,&lt;br /&gt;    Description = &quot;Contains every property type that can hold a reference to a DAM asset&quot;,&lt;br /&gt;    GroupName = Globals.GroupNames.Specialized)]&lt;br /&gt;public class DamReferencesTestPage : PageData, IDamReferences&lt;br /&gt;{&lt;br /&gt;    [Display(Name = &quot;Content Reference&quot;, GroupName = SystemTabNames.Content, Order = 10)]&lt;br /&gt;    public virtual ContentReference ContentReference1 { get; set; }&lt;br /&gt;&lt;br /&gt;    [Display(Name = &quot;Content Area&quot;, GroupName = SystemTabNames.Content, Order = 20)]&lt;br /&gt;    public virtual ContentArea ContentArea1 { get; set; }&lt;br /&gt;&lt;br /&gt;    [Display(Name = &quot;Content Area Item&quot;, GroupName = SystemTabNames.Content, Order = 30)]&lt;br /&gt;    [BackingType(typeof(PropertyContentAreaItem))]&lt;br /&gt;    public virtual ContentAreaItem ContentAreaItem1 { get; set; }&lt;br /&gt;&lt;br /&gt;    [Display(Name = &quot;Link item&quot;, GroupName = SystemTabNames.Content, Order = 40)]&lt;br /&gt;    [BackingType(typeof(PropertyLinkItem))]&lt;br /&gt;    public virtual LinkItem LinkItem1 { get; set; }&lt;br /&gt;&lt;br /&gt;    [Display(Name = &quot;Link item collection&quot;, GroupName = SystemTabNames.Content, Order = 50)]&lt;br /&gt;    public virtual LinkItemCollection LinkItemCollection1 { get; set; }&lt;br /&gt;&lt;br /&gt;    [Display(Name = &quot;Url&quot;, GroupName = SystemTabNames.Content, Order = 60)]&lt;br /&gt;    public virtual Url Url1 { get; set; }&lt;br /&gt;&lt;br /&gt;    [Display(Name = &quot;Url to image&quot;, GroupName = SystemTabNames.Content, Order = 70)]&lt;br /&gt;    [BackingType(typeof(PropertyImageUrl))]&lt;br /&gt;    public virtual Url ImageUrl1 { get; set; }&lt;br /&gt;&lt;br /&gt;    [Display(Name = &quot;Image&quot;, GroupName = SystemTabNames.Content, Order = 80)]&lt;br /&gt;    [UIHint(UIHint.Image)]&lt;br /&gt;    public virtual ContentReference Image1 { get; set; }&lt;br /&gt;    &lt;br /&gt;    [Display(Name = &quot;Block as property&quot;, GroupName = SystemTabNames.Content, Order = 90)]&lt;br /&gt;    public virtual DamReferencesTestBlock BlockAsProperty { get; set; }&lt;br /&gt;    &lt;br /&gt;    [Display(Name = &quot;Block list as property&quot;, GroupName = SystemTabNames.Content, Order = 100)]&lt;br /&gt;    public virtual IList&amp;lt;DamReferencesTestBlock&amp;gt; BlocksAsProperty { get; set; }&lt;br /&gt;}&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Images, videos, documents and image and video renditions are all supported.&lt;/p&gt;
&lt;p&gt;The job updates versions in place. &lt;strong&gt;It does not create new versions, so your version history stays the same.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before you start&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Make sure these things are in place first.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You are on CMS 13.3.0 or later.&lt;/li&gt;
&lt;li&gt;DAM features are activated&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;/link/3ece23070fdd448f9c35aa5ce9861950.aspx&quot; alt=&quot;&quot; width=&quot;973&quot; height=&quot;400&quot; /&gt;&lt;/li&gt;
&lt;li&gt;DAM assets have finished syncing from CMP to Graph. If Graph is not ready, editors cannot see or pick the assets after the migration.&lt;/li&gt;
&lt;li&gt;The Graph content sources for images, videos and files exist.&lt;/li&gt;
&lt;li&gt;CMP credentials are set up. Without them, the job still moves the references but skips the cleanup step.&lt;/li&gt;
&lt;li&gt;The Optimizely DAM Tracking Assets job runs without errors.&lt;/li&gt;
&lt;li&gt;You have a full database backup. The job cannot be undone from the admin UI.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Setup&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Add the packages to your project.&lt;/p&gt;
&lt;p&gt;&amp;lt;PackageReference Include=&quot;EPiServer.Cms.DamMigration&quot; Version=&quot;13.3.0&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;Register the services in Program.cs.&lt;/p&gt;
&lt;p&gt;builder.Services.AddDamMigration();&lt;/p&gt;
&lt;p&gt;Add the settings to appsettings.json. The job is turned off by default, so you need to turn it on.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;{&lt;br /&gt;&amp;nbsp; &quot;Optimizely&quot;: {&lt;br /&gt;&amp;nbsp; &amp;nbsp; &quot;Cmp&quot;: {&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;Client&quot;: {&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;ClientId&quot;: &quot;your-client-id&quot;,&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;ClientSecret&quot;: &quot;your-client-secret&quot;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;br /&gt;&amp;nbsp; &amp;nbsp; },&lt;br /&gt;&amp;nbsp; &amp;nbsp; &quot;Cms&quot;: {&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;DamUI&quot;: {&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;TrackingEnabled&quot;: true&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; },&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;DamMigration&quot;: {&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;Enabled&quot;: true&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;br /&gt;&amp;nbsp; &amp;nbsp; }&lt;br /&gt;&amp;nbsp; }&lt;br /&gt;}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;There are more settings for batch sizes, concurrency and retries. The defaults should work for most sites.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Running the job&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;img src=&quot;/link/d365c8eb5cda4e30bcb4d6b6d8e7ce6a.aspx&quot; alt=&quot;&quot; width=&quot;813&quot; height=&quot;648&quot; /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1. Go to CMS Admin &amp;gt; Scheduled Jobs.&lt;br /&gt;2. Run Optimizely DAM Tracking Assets once to check that CMS can talk to CMP.&lt;br /&gt;3. Open Optimizely DAM Legacy Asset Migration and click Start.&lt;br /&gt;4. Wait for it to finish and read the message in the job history.&lt;/p&gt;
&lt;p&gt;A good result looks something like this.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/94119f48cad64433b083ecc6e28ef136.aspx&quot; alt=&quot;&quot; width=&quot;1041&quot; height=&quot;313&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If some assets could not be moved, the message lists how many and why. The most common reasons are missing settings for an asset type or broken data in the old mapping table.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Check your content&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When the job is done, open a few pages and blocks in edit mode. Look at images, videos, content areas and rich text. Check drafts and other languages too.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/748ed0d3b0c54f218020e62315568c7b.aspx&quot; alt=&quot;&quot; width=&quot;978&quot; height=&quot;876&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When the job stops early&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The job is safe to run more than once. References that are already moved get skipped. If you stop the job halfway, everything done so far is kept, and the next run picks up the rest.&lt;/p&gt;
&lt;p&gt;Cleanup can stop on its own in a few cases.&lt;/p&gt;
&lt;p&gt;- No CMP credentials are configured.&lt;br /&gt;- Some content belongs to an external content provider. The job skips that content and leaves the old records alone, because they might still be used.&lt;br /&gt;- CMP returns an auth error or too many rate limit errors.&lt;/p&gt;
&lt;p&gt;In all of these cases the content changes stay in place. Only the cleanup is skipped or stopped.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;After the migration&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Set Optimizely:Cms:DamMigration:Enabled back to false. This hides the job so nobody runs it by mistake.&lt;/p&gt;
&lt;p&gt;Keep TrackingEnabled set to true. The Optimizely DAM Tracking Assets job then runs once a week and tells CMP which assets are used on your site.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A note on the trash&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;There is no setting to skip the trash, and that is on purpose. Editors can restore content from the trash at any time. If the job skipped it, the cleanup step would delete old records that restored content still points to. Those references would then be broken for good.&lt;/p&gt;
&lt;p&gt;Please bear in mind that migration might take some time as we need to scan each content item version which holds a reference to a DAM asset.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/bartosz-sekula/dates/2026/10/dam-migration-from-cms12-to-cms13/</guid>            <pubDate>Tue, 06 Oct 2026 12:04:10 GMT</pubDate>           <category>Blog post</category></item><item> <title>Scheduler Delegation: Using Optimizely DXP Infrastructure for Queued Processing</title>            <link>https://world.optimizely.com/blogs/mike/dates/2026/10/scheduler-delegation-using-optimizely-dxp-infrastructure-for-queued-processing/</link>            <description>&lt;div&gt;
&lt;div&gt;At SYZYGY Techsolutions, we support Optimizely DXP projects at scale, so continuously identifying unique ways to optimize the application is an essential part of how we work. One pattern that has paid off repeatedly is&amp;nbsp;&lt;strong&gt;scheduler delegation&lt;/strong&gt;: moving expensive, recurring or bursty background work off user-facing instances and onto the dedicated scheduler instance, using infrastructure that DXP already provides.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;This post walks through the problem, the architecture, the reasoning behind it, and the&amp;nbsp;code needed to apply it in your own solution.&lt;/div&gt;
&lt;h2&gt;The problem: expensive work triggered from the request path&lt;/h2&gt;
Imagine thousands of visitors performing a similar action at the same time, spread across several instances of your application. They might be clicking through a car configurator, where every choice of model, colour or engine asks which options can still be combined and which are available. Or they might simply be opening pages, each of which needs the website&#39;s translated texts in their language.&lt;br /&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;Resolving that action takes time. The answer depends on several data sources, each with its own rules: product rules, production planning and supplier stock for the configurator; the CMS, a translation management system and other applications for the texts. Yet the number of distinct questions is limited, and the answer does not need to be exact to the second. The final order confirms availability anyway, and a text that is a minute old is harmless.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;So you cache the result. That solves the speed, but introduces new problems:&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;1.&amp;nbsp;&lt;strong&gt;Cold starts.&lt;/strong&gt;&amp;nbsp;After a deployment, a restart or an expiry, the cache is empty and the first visitors get nothing or wait.&lt;/div&gt;
&lt;div&gt;2. &lt;strong&gt;Repeated work.&lt;/strong&gt;&amp;nbsp;Many visitors hit the same empty entry at once, and each request calculates the same answer, overloading the underlying systems.&lt;/div&gt;
&lt;div&gt;3.&amp;nbsp;&lt;strong&gt;Instances out of sync.&lt;/strong&gt; Every web instance has its own cache, recalculates on its own schedule, and may show a different answer than its neighbour.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;Together these produce a cache stampede: the same heavy work done many times over, competing with visitor traffic for CPU, memory and connections to the source systems. Making visitors wait for the calculation only moves the problem onto them. The goal is to keep&amp;nbsp;&lt;strong&gt;the request path fast, whatever the state of the cache&lt;/strong&gt;.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;Optimizely&#39;s built-in &lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;ISynchronizedObjectInstanceCache&lt;/span&gt; is a great fit for data that changes infrequently. Under high load in a multi-instance setup, however, we observed clear limits of its caching model for this kind of workload. Each instance keeps its own in-memory copy, and only invalidations travel between instances. Every instance that recalculates an entry and stores it sends an invalidation to all the others, which drop their copies and recalculate in turn. The result is a stream of cache-update events from every side, the same stampede repeated on every instance, and moments when instances serve different values or overwrite each other&#39;s results.&lt;/div&gt;
&lt;h2&gt;What DXP gives you: a scheduler instance&lt;/h2&gt;
&lt;div&gt;On Preproduction and Production, Optimizely DXP customers can request a dedicated &lt;strong&gt;scheduler instance&lt;/strong&gt;, one per environment, next to the web instances that serve visitors. This instance:&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;does not receive user-facing requests;&lt;/li&gt;
&lt;li&gt;is intended for backend work: scheduled jobs, background tasks and back-office operations performed by content editors;&lt;/li&gt;
&lt;li&gt;is part of the same deployment, so it runs your code, your configuration and your initialization modules.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;In other words, DXP already hands you a place where heavy work runs away from visitor traffic. Most teams use it only for classic scheduled jobs on a fixed timetable. Scheduler delegation extends it to&amp;nbsp;&lt;strong&gt;event-driven, queued work&lt;/strong&gt;: user-facing instances report that something is needed, and the scheduler instance does it.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;The rest of the pattern relies on platform features that every DXP project already has:&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Capability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Provided by&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Role in the pattern&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-instance messaging&lt;/td&gt;
&lt;td&gt;The CMS event system (&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;IEventRegistry&lt;/span&gt;), which uses Azure Service Bus on DXP&lt;/td&gt;
&lt;td&gt;Carries a small &quot;something is missing&quot; message from any instance to all instances&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduler awareness&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;SchedulerOptions.Enabled&lt;/span&gt; (EPiServer.Scheduler), which is only &lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;true&lt;/span&gt; on the scheduler instance&lt;/td&gt;
&lt;td&gt;Lets a handler decide whether it should act on a message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduled jobs&lt;/td&gt;
&lt;td&gt;The CMS scheduled job engine (&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;ScheduledJobBase&lt;/span&gt;, &lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;[ScheduledPlugIn]&lt;/span&gt;)&lt;/td&gt;
&lt;td&gt;Provides a recurring, restartable execution loop for batch processing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared state&lt;/td&gt;
&lt;td&gt;Blob storage (&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;IBlobFactory&lt;/span&gt;), the CMS/Commerce databases, or a persisted synchronized cache&lt;/td&gt;
&lt;td&gt;Carries the finished result back to user-facing instances&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div&gt;&lt;br /&gt;
&lt;div&gt;Because all of this ships with DXP, the pattern runs entirely on infrastructure you already have.&lt;/div&gt;
&lt;h2&gt;Architecture overview&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/link/a6ead68713a0439c98e910ab124199f6.aspx&quot; width=&quot;1233&quot; height=&quot;790&quot; /&gt;&lt;/p&gt;
&lt;div&gt;The flow, step by step:&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;1. &lt;strong&gt;Raise.&lt;/strong&gt; A user-facing instance reads the calculated result from shared storage. If it is missing or stale, it returns what it has (stale data or a fallback) and&amp;nbsp;&lt;strong&gt;raises an event&lt;/strong&gt;&amp;nbsp;that identifies what is needed. Raising is fire-and-forget and must never throw into the request.&lt;/div&gt;
&lt;div&gt;2. &lt;strong&gt;Deliver.&lt;/strong&gt;&amp;nbsp;The event travels over the platform&#39;s event bus and reaches every instance.&lt;/div&gt;
&lt;div&gt;3. &lt;strong&gt;Enqueue.&lt;/strong&gt;&amp;nbsp;Every instance receives the event, but only the scheduler instance acts on it: it places the item into an in-memory queue that keeps one entry per unique input.&lt;/div&gt;
&lt;div&gt;4. &lt;strong&gt;Batch.&lt;/strong&gt;&amp;nbsp;A scheduled job runs at a fixed interval (for example every minute), drains the queue, and processes each unique input once.&lt;/div&gt;
&lt;div&gt;5. &lt;strong&gt;Write back.&lt;/strong&gt;&amp;nbsp;The result is written to shared storage.&lt;/div&gt;
&lt;div&gt;6. &lt;strong&gt;Read.&lt;/strong&gt;&amp;nbsp;User-facing instances pick up the result on their next read, with no direct communication between the scheduler and the web instances.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;&lt;img src=&quot;/link/0e2a878fd9394d50bf63984746603a0e.aspx&quot; width=&quot;1233&quot; height=&quot;764&quot; /&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;From the visitor&#39;s perspective, every request is answered immediately. The expensive work runs later on the scheduler instance, once per unique input.&lt;/div&gt;
&lt;h2&gt;Design decisions and the reasoning behind them&lt;/h2&gt;
&lt;div&gt;The code samples below are simplified C# against the Optimizely CMS 12 APIs. Logging, null checks and dependency injection details are trimmed for readability.&lt;/div&gt;
&lt;h3&gt;1. Events carry intent; data stays at the source&lt;/h3&gt;
&lt;div&gt;The event payload is only an identifier (for example &lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;{ model, options }&lt;/span&gt;). It is small, cheap to serialize and well within message size limits. The scheduler fetches whatever data it needs from the source of truth when it processes the item. This avoids stale payloads and keeps the bus lightweight.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;The payload must be serializable by the event system, and its type must be registered as a known event type:&lt;/div&gt;
&lt;pre&gt;&lt;code&gt;using System.Runtime.Serialization;
using EPiServer.Events;

[DataContract]
[EventsServiceKnownType]
public class RecalculationRequest
{
    [DataMember] public string Model { get; set; } = &quot;&quot;;
    [DataMember] public string Options { get; set; } = &quot;&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. An in-memory queue on the scheduler is enough&lt;/h3&gt;
&lt;div&gt;A durable queue (a dedicated Service Bus queue, a database table) is the obvious place for pending work. Here the work is &lt;strong&gt;idempotent and re-calculable&lt;/strong&gt;, so a simple in-memory queue on the scheduler instance does the job:&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;If the scheduler restarts and loses its queue, the next request that sees a missing or stale result raises the event again.&lt;/li&gt;
&lt;li&gt;Duplicate events are harmless because the queue keeps one entry per input.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;Self-healing through the request path replaces durability. That removes persistence, retries, dead-lettering and cleanup from the design. This only holds if the work really is idempotent; for non-recoverable work, use a durable queue instead.&lt;/div&gt;
&lt;h3&gt;3. Batching and de-duplication give you load control&lt;/h3&gt;
&lt;div&gt;&lt;img src=&quot;/link/812931f526124590b55800d55924edf8.aspx&quot; width=&quot;1234&quot; height=&quot;617&quot; /&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;Because events are queued and drained on a timer, the cost of the system depends only on the number of &lt;strong&gt;unique inputs&lt;/strong&gt;. Request volume and instance count drop out of the equation: a burst of thousands of misses collapses into a handful of resource (re)generations. The job interval becomes a single dial that trades refresh latency against background load.&lt;/div&gt;
&lt;h3&gt;4. A de-duplicating queue keeps producers fast and memory bounded&lt;/h3&gt;
&lt;div&gt;The queue is written by event handler threads (producers) and drained by the job thread (consumer). Keying it by the inputs means a burst of identical events occupies a single entry, so memory stays bounded during a miss storm. Draining swaps in a fresh dictionary atomically, so producers never wait for the batch. The queue is registered as a singleton, so the event handler and the scheduled job share the same instance:&lt;/div&gt;
&lt;pre&gt;&lt;code&gt;using System.Collections.Concurrent;
using EPiServer.ServiceLocation;

[ServiceConfiguration(Lifecycle = ServiceInstanceScope.Singleton)]
public class RecalculationQueue
{
    private ConcurrentDictionary&amp;lt;(string Model, string Options), RecalculationRequest&amp;gt; _pending = new();

    public void Add(RecalculationRequest request) =&amp;gt;
        _pending.TryAdd((request.Model, request.Options), request);

    public IReadOnlyCollection&amp;lt;RecalculationRequest&amp;gt; Drain()
    {
        // A write racing the swap can be lost; the next miss raises it again.
        var drained = Interlocked.Exchange(ref _pending, new());
        return drained.Values.ToList();
    }

    public bool IsEmpty =&amp;gt; _pending.IsEmpty;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. One event rail, two routing modes&lt;/h3&gt;
&lt;div&gt;Local development, CI and single-node environments usually run without a scheduler instance. A configuration flag routes the same event differently, so one code path covers both setups:&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;&lt;img src=&quot;/link/28e7cbdd7e704261a5ae2f3ed33c25e5.aspx&quot; width=&quot;1236&quot; height=&quot;710&quot; /&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Delegation on&lt;/strong&gt; (Preproduction, Production): only the scheduler instance reacts, and it queues the item.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Delegation off&lt;/strong&gt; (local, simple setups): only the instance that raised the event reacts, and it processes the item immediately on a background thread.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;Both modes share the same read path and the same processing logic. Only the &quot;who handles it, and when&quot; decision changes, and that sits behind a configuration flag. This keeps the pattern easy to test locally and makes it safe to switch off.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;The routing is encapsulated in an abstract initialization module. Concrete features only supply the event identity and the processing logic (template method pattern):&lt;/div&gt;
&lt;pre&gt;&lt;code&gt;using EPiServer.Events;
using EPiServer.Events.Clients;
using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.Scheduler;
using EPiServer.ServiceLocation;
using Microsoft.Extensions.Options;

public abstract class DelegatingEventModule&amp;lt;TRequest&amp;gt; : IInitializableModule
    where TRequest : class
{
    protected abstract Guid EventId { get; }
    protected abstract Guid RaiserId { get; }
    protected abstract void HandleOnScheduler(TRequest request);
    protected abstract void HandleLocally(TRequest request);

    private bool DelegateToScheduler =&amp;gt;
        ServiceLocator.Current.GetInstance&amp;lt;IOptions&amp;lt;DelegationOptions&amp;gt;&amp;gt;().Value.Enabled;

    private static bool IsScheduler =&amp;gt;
        ServiceLocator.Current.GetInstance&amp;lt;IOptions&amp;lt;SchedulerOptions&amp;gt;&amp;gt;().Value.Enabled;

    public void Initialize(InitializationEngine context)
    {
        var evt = ServiceLocator.Current.GetInstance&amp;lt;IEventRegistry&amp;gt;().Get(EventId);
        evt.Raised += DelegateToScheduler ? OnSchedulerRoute : OnLocalRoute;
    }

    public void Uninitialize(InitializationEngine context)
    {
        var evt = ServiceLocator.Current.GetInstance&amp;lt;IEventRegistry&amp;gt;().Get(EventId);
        evt.Raised -= DelegateToScheduler ? OnSchedulerRoute : OnLocalRoute;
    }

    private void OnSchedulerRoute(object sender, EventNotificationEventArgs e)
    {
        if (!IsScheduler) return;                  // web instances ignore it
        if (e.Param is not TRequest request) return;
        HandleOnScheduler(request);                // typically: queue.Add(request)
    }

    private void OnLocalRoute(object sender, EventNotificationEventArgs e)
    {
        if (e.RaiserId != RaiserId) return;        // only handle what this instance raised
        if (e.Param is not TRequest request) return;
        Task.Run(() =&amp;gt; HandleLocally(request));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;A concrete module is discovered by the initialization engine through &lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;[ModuleDependency]&lt;/span&gt;:&lt;/div&gt;
&lt;pre&gt;&lt;code&gt;[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class ConfiguratorRecalculationModule : DelegatingEventModule&amp;lt;RecalculationRequest&amp;gt;
{
    internal static readonly Guid Id = new(&quot;00000000-0000-0000-0000-000000000001&quot;);
    internal static readonly Guid Raiser = Guid.NewGuid();

    protected override Guid EventId =&amp;gt; Id;
    protected override Guid RaiserId =&amp;gt; Raiser;

    protected override void HandleOnScheduler(RecalculationRequest request) =&amp;gt;
        ServiceLocator.Current.GetInstance&amp;lt;RecalculationQueue&amp;gt;().Add(request);

    protected override void HandleLocally(RecalculationRequest request) =&amp;gt;
        ServiceLocator.Current.GetInstance&amp;lt;ConfiguratorCalculator&amp;gt;().Recalculate(request);
}&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;Four details matter here:&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;IsScheduler&lt;/span&gt; reads &lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;SchedulerOptions.Enabled&lt;/span&gt;, which the platform sets per instance. On DXP the scheduler is enabled only on the scheduler instance, so the check follows the real topology.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;DelegationOptions&lt;/span&gt; is your own options class, bound from &lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;appsettings.json&lt;/span&gt;. It is the configuration flag that switches between the two routing modes.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;RaiserId&lt;/span&gt; is generated once per process (&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;Guid.NewGuid()&lt;/span&gt; in a static field). It lets an instance recognize its own events when delegation is off.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;EventId&lt;/span&gt; is a fixed GUID. Every instance must use the same value to subscribe to the same event.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6. The read path is defensive&lt;/h3&gt;
&lt;div&gt;Reading the result and requesting a recalculation live in one service on the web instances. It follows a simple rule, &lt;strong&gt;stale data beats no data&lt;/strong&gt;, and a failure to raise the event never reaches the visitor:&lt;/div&gt;
&lt;pre&gt;&lt;code&gt;using EPiServer.Events.Clients;

public class ConfiguratorResultService(
    IEventRegistry eventRegistry,
    IResultStorage storage,
    ILogger&amp;lt;ConfiguratorResultService&amp;gt; logger)
{
    private static readonly TimeSpan Ttl = TimeSpan.FromMinutes(2);

    public ConfiguratorResult GetResult(RecalculationRequest request)
    {
        var entry = storage.TryRead(request);

        if (entry is null || entry.CalculatedAt &amp;lt; DateTime.UtcNow - Ttl)
        {
            RequestRecalculation(request);
        }

        return entry?.Value ?? ConfiguratorResult.Fallback;
    }

    private void RequestRecalculation(RecalculationRequest request)
    {
        try
        {
            eventRegistry.Get(ConfiguratorRecalculationModule.Id)
                ?.Raise(ConfiguratorRecalculationModule.Raiser, request);
        }
        catch (Exception ex)
        {
            // A failed raise only delays the refresh; the visitor still gets an answer.
            logger.LogWarning(ex, &quot;Recalculation request for {Model} could not be raised&quot;, request.Model);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7. The scheduled job is the batch processor&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;using EPiServer.PlugIn;
using EPiServer.Scheduler;

[ScheduledPlugIn(
    DisplayName = &quot;Recalculate configurator results on demand&quot;,
    GUID = &quot;00000000-0000-0000-0000-000000000002&quot;,
    IntervalType = ScheduledIntervalType.Minutes,
    IntervalLength = 1,
    DefaultEnabled = true,
    Restartable = true)]
public class RecalculateOnDemandJob : ScheduledJobBase
{
    private readonly RecalculationQueue _queue;
    private readonly ConfiguratorCalculator _calculator;
    private readonly ILogger&amp;lt;RecalculateOnDemandJob&amp;gt; _logger;
    private bool _stopRequested;

    public RecalculateOnDemandJob(
        RecalculationQueue queue,
        ConfiguratorCalculator calculator,
        ILogger&amp;lt;RecalculateOnDemandJob&amp;gt; logger)
    {
        _queue = queue;
        _calculator = calculator;
        _logger = logger;
        IsStoppable = true;
    }

    public override void Stop() =&amp;gt; _stopRequested = true;

    public override string Execute()
    {
        if (_queue.IsEmpty) return &quot;Nothing to recalculate&quot;;

        var batch = _queue.Drain();

        var done = 0;
        foreach (var request in batch)
        {
            if (_stopRequested) break;
            try
            {
                _calculator.Recalculate(request);   // calculate and write to shared storage
                done++;
            }
            catch (Exception ex)
            {
                // One failing item must not abandon the batch; the next miss re-raises it.
                _logger.LogError(ex, &quot;Recalculation failed for {Model}&quot;, request.Model);
            }
        }

        return $&quot;Recalculated {done} of {batch.Count} items&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;Restartable = true&lt;/span&gt; makes the scheduler start the job again if the instance shuts down while it is running, and &lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;IsStoppable&lt;/span&gt; together with &lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;Stop()&lt;/span&gt; lets an operator end a long batch from the admin interface. Items dropped by a stop are simply requested again by the next miss.&lt;/div&gt;
&lt;h2&gt;Results in production&lt;/h2&gt;
&lt;div&gt;In one production implementation, the read path serves about 1,022,000 requests per day (absolute figures are scaled for confidentiality; the ratios are real):&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;1,000,000 hits&lt;/strong&gt; (about 98%), answered from a fresh entry;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;22,000 stale reads&lt;/strong&gt; (about 2%), answered immediately with the previous result while the scheduler refreshes the entry;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;0 misses. &lt;/strong&gt;Misses only occur before the storage is first populated. After that, visitors receive the previous value of an expired entry until it is refreshed.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;Before the pattern, each of those 22,000 stale reads would have been a miss: a visitor waiting for a synchronous recalculation, with several instances often recalculating the same entry at once. Stale reads now absorb that work, and the recalculation runs once per unique input on the scheduler instance. The exact numbers depend on traffic, the feature and the chosen TTL, but the shape stays the same: misses disappear and stale reads take their place.&lt;/div&gt;
&lt;h2&gt;What enables this architecture&lt;/h2&gt;
&lt;div&gt;The building blocks are the platform capabilities listed earlier. Three properties make them fit together:&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Every instance runs the same deployment.&lt;/strong&gt; Initialization modules subscribe to the event on all instances, and a single platform setting decides which one acts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The control path and the data path are separate.&lt;/strong&gt; Events travel over the bus, results travel through shared storage, and the scheduler and web instances never call each other. Either path can be replaced independently.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;There is a single writer.&lt;/strong&gt; Only the scheduler instance calculates and stores results, so every web instance reads the same value. Eventual consistency is confined to one place: the time until the next job tick.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The return path is a free choice&lt;/h2&gt;
&lt;div&gt;Any storage that every instance can read works as the return path. Blob storage is the simplest starting point and handles large results well. The CMS or Commerce database suits small structured results but adds load to a shared database. A synchronized cache gives the lowest read latency, provided the scheduler instance stays its only writer; an eviction then simply raises a new recalculation request. Whichever you choose, keep a short-lived local cache on the web instances so that shared storage is read once per interval rather than on every request.&lt;/div&gt;
&lt;h2&gt;Trade-offs and failure modes&lt;/h2&gt;
&lt;div&gt;Restarts, duplicate events, failing items and environments without a scheduler are covered by the design decisions above. Three trade-offs remain:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concern&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Behavior&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eventual consistency&lt;/td&gt;
&lt;td&gt;A first-time miss is answered with a fallback until the next job tick&lt;/td&gt;
&lt;td&gt;Choose an interval that matches business tolerance; pre-warm after deployments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event delivery is best-effort&lt;/td&gt;
&lt;td&gt;A message may be missed in rare cases&lt;/td&gt;
&lt;td&gt;The next miss or stale read raises it again; the TTL bounds the impact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared scheduler capacity&lt;/td&gt;
&lt;td&gt;The scheduler instance also runs every other scheduled job and the editors&#39; back-office work, so a long batch competes with them&lt;/td&gt;
&lt;td&gt;Keep per-item work bounded, watch batch duration, and move truly heavy work to a dedicated service&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;br /&gt;
&lt;div&gt;Observability is worth building from day one: log every miss, every raise, and the batch size per tick. The ratio between events received and unique inputs processed is a direct measure of how much work the pattern is saving.&lt;/div&gt;
&lt;h2&gt;When to use it&lt;/h2&gt;
&lt;div&gt;The problem section already describes the shape that fits: a limited set of repeated questions, slow answers from several sources, tolerance for slightly outdated results, and work that can be recalculated at any time. Besides configurators and translated texts, the same shape appears in accessory compatibility per model, dealer and store details per region, and filter options per product category.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;Two further conditions are easy to overlook:&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The answer contains no financial values, personal data or access decisions.&lt;/strong&gt; Such data must be accurate and current on every request.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Visitors ask only a fraction of all possible questions.&lt;/strong&gt; If every answer is needed anyway, a plain scheduled job that calculates everything is simpler.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;Cases that look similar but fail one of these conditions:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Business case&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Why it does NOT fit&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prices, discounts, taxes, delivery costs&lt;/td&gt;
&lt;td&gt;Financial values shown to customers must be accurate at all times. An outdated value creates legal and reputational risk.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stock or slot reservation at checkout&lt;/td&gt;
&lt;td&gt;Visitors compete for the same item, which needs a synchronous, locked check.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personal recommendations or account data&lt;/td&gt;
&lt;td&gt;Inputs are unique per visitor, so de-duplication has nothing to merge, and personal data should stay out of shared storage.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Navigation or content filtered by permissions&lt;/td&gt;
&lt;td&gt;Access decisions must be current. A stale answer can expose content.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sitemaps and product feeds&lt;/td&gt;
&lt;td&gt;Every answer is needed and read by machines on a timetable. A plain scheduled job is simpler.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image renditions&lt;/td&gt;
&lt;td&gt;Serving the original as a fallback defeats the purpose, and the number of variants is large. Resize on demand at the CDN.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Getting started checklist&lt;/h2&gt;
&lt;div&gt;1. Request a scheduler instance for Preproduction and Production through Optimizely support.&lt;/div&gt;
&lt;div&gt;2. Identify one expensive, re-calculable operation currently done on the request path.&lt;/div&gt;
&lt;div&gt;3. Define a small event payload and a stable event identifier.&lt;/div&gt;
&lt;div&gt;4. Implement the read path: raise on miss or stale, never throw, always return something.&lt;/div&gt;
&lt;div&gt;5. Implement the delegating module with both routing modes behind a configuration flag.&lt;/div&gt;
&lt;div&gt;6. Add the de-duplicating queue and the scheduled batch job.&lt;/div&gt;
&lt;div&gt;7. Pick the return path and a TTL that match your consistency needs.&lt;/div&gt;
&lt;div&gt;8. Add logging and metrics for misses, raises and batch size.&lt;/div&gt;
&lt;div&gt;9. Disable delegation locally; enable it in Preproduction and Production.&lt;/div&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;div&gt;Scheduler delegation turns an under-used part of DXP into a small background processing system. Web instances report what they need over the event bus, the scheduler instance collects those requests and processes them in batches, and shared storage hands the results back. The request path stays fast, each heavy calculation runs once per unique input, and everything runs on infrastructure DXP already provides.&lt;/div&gt;
&lt;/div&gt;</description>            <guid>https://world.optimizely.com/blogs/mike/dates/2026/10/scheduler-delegation-using-optimizely-dxp-infrastructure-for-queued-processing/</guid>            <pubDate>Tue, 06 Oct 2026 08:46:00 GMT</pubDate>           <category>Blog post</category></item><item> <title>Thomas in the AI Discovery Era</title>            <link>https://world.optimizely.com/blogs/ritu-madan/dates/2026/10/thomas-in-the-ai-discovery-era/</link>            <description>&lt;p&gt;&lt;span class=&quot;ckymss ckya1h ckyjz7 ckymt4&quot;&gt;Its been a while, but hope you haven&#39;t forgotten Thomas, our most loyal visitor, the one who suddenly stops showing up.&lt;br /&gt;Not because he lost interest. Because he asked an AI tool the question he used to Google, got his answer in thirty seconds, and never came to the site at all.&lt;br /&gt;That&#39;s the shift most brands haven&#39;t clocked yet. The front door moved. Search isn&#39;t the only way in anymore, and a lot of the traffic you&#39;re losing to AI never shows up as a loss in your analytics. It just quietly isn&#39;t there.&lt;br /&gt;But here&#39;s the part worth sitting with: AI isn&#39;t only taking visits. It&#39;s also sending them, to the brands it decides are worth citing. Which means the game isn&#39;t over. It just changed.&lt;br /&gt;I pulled apart what actually earns those citations, and the one tactic everyone&#39;s doubling down on that the 2026 data says barely moves the needle. Plus what it means for the visitor who lands on your site already halfway through a thought.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;ckymss ckya1h ckyjz7 ckymt4&quot;&gt;Read the full article &lt;a href=&quot;https://www.linkedin.com/pulse/thomass-new-entry-point-ritu-madan-t1pze/&quot;&gt;here&lt;/a&gt;.&lt;/span&gt;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/ritu-madan/dates/2026/10/thomas-in-the-ai-discovery-era/</guid>            <pubDate>Tue, 06 Oct 2026 02:48:33 GMT</pubDate>           <category>Blog post</category></item><item> <title>Gated Content on Optimizely Graph, Part 2: Keeping Graph in Step with the CMS</title>            <link>https://pino-labs.com/blog/gated-content-optimizely-graph-part-2-in-step/</link>            <description>A correct ACL in the CMS is not enough: Graph evaluates access against the state available to it. That state can lag behind the CMS, and every cache between Graph and the browser adds a window of its own.</description>            <guid>https://pino-labs.com/blog/gated-content-optimizely-graph-part-2-in-step/</guid>            <pubDate>Tue, 06 Oct 2026 00:00:00 GMT</pubDate>           <category>Blog post</category></item><item> <title>Optimizely Agent Platform - October 2026 State of Play</title>            <link>https://world.optimizely.com/blogs/scott-reed/dates/2026/10/optimizely-agent-platform---october-2025-state-of-play/</link>            <description>&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;A month ago I wrote up my takeaways from Opticon New York (&lt;a href=&quot;/link/fecc835a60ef4678891f74e1fcbf06ea.aspx&quot;&gt;Opticon New York &amp;amp; The Strategy on Upgrading to CMS 13 &amp;amp; Commerce 15&lt;/a&gt;). The headline was that Opal has grown up: the platform as a whole is now the &lt;strong&gt;Optimizely Agent Platform&lt;/strong&gt;, and the AI chat you work with day to day is now &lt;strong&gt;Mark&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The trouble is that the pace of delivery has been relentless. The 2026 release notes alone run to well over an hour of reading, with something shipping almost every week, and features have been renamed, moved, merged and superseded along the way. If you are a solution architect or technical lead trying to answer the simple question &lt;em&gt;&quot;what does the platform actually do today, and how is it put together?&quot;&lt;/em&gt;, it is genuinely hard to get a clean picture.&lt;/p&gt;
&lt;p&gt;So this post is my attempt at a &lt;strong&gt;state-of-play roundup as of early October 2026&lt;/strong&gt;, broken down by the core building blocks of the platform, with a focus on the architecture, configuration and governance details that matter when you are designing real solutions. Wherever I can, I call out &lt;strong&gt;what changed and when&lt;/strong&gt; using the official &lt;a href=&quot;https://docs.optimizely.com/agent-platform/release-notes&quot;&gt;release notes&lt;/a&gt;, and I flag the places where the documentation is still catching up.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;FYI, I know this is quite a long document, and it is mostly AI-generated, with my review added in. It should be a really good update for anyone who&#39;s used the platform, trying to keep up with all the changes. I&#39;ll probably do one of these every 6 months.&amp;nbsp;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on sources.&lt;/strong&gt; Everything here is based on the &lt;a href=&quot;https://docs.optimizely.com/agent-platform/docs&quot;&gt;Agent Platform documentation&lt;/a&gt;, the &lt;a href=&quot;https://docs.optimizely.com/agent-platform/release-notes&quot;&gt;2026&lt;/a&gt; and &lt;a href=&quot;https://docs.optimizely.com/agent-platform/release-notes/2025-agent-platform-release-notes&quot;&gt;2025&lt;/a&gt; release notes, and Optimizely&#39;s Opticon announcements, as they stood on 5 October 2026. Screenshots marked as from the documentation are reproduced with permission. Things move fast, so always double-check the docs before you commit to a design.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Also &lt;a href=&quot;https://drive.google.com/file/d/1VLziEiYH4mSGEkiC3VrGJOTP_pSIwF0-/view?usp=sharing&quot;&gt;https://drive.google.com/file/d/1VLziEiYH4mSGEkiC3VrGJOTP_pSIwF0-/view?usp=sharing&lt;/a&gt; is a version of this document as a nice infographic for easier digestion&amp;nbsp;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;TL;DR for architects&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; background: rgb(232, 241, 236); font-weight: bold; white-space: nowrap;&quot;&gt;Area&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left; background: #e8f1ec; font-weight: bold;&quot;&gt;Where we are today&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Naming&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Opal &amp;rarr; &lt;strong&gt;Optimizely Agent Platform&lt;/strong&gt; (effective 1 September 2026). &lt;strong&gt;Mark&lt;/strong&gt; is the personified AI; &lt;strong&gt;Mark-1 / Mark AI&lt;/strong&gt; is the post-trained model family; &lt;strong&gt;Mark-IQ&lt;/strong&gt; is the context layer; &lt;strong&gt;Mark-Bench&lt;/strong&gt; is the open-source benchmark. The docs still say &quot;Opal&quot; almost everywhere.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Models&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Six inference levels (Quick &amp;rarr; Code) mapped to Gemini or Claude models. Admins pick a default &lt;strong&gt;model provider&lt;/strong&gt; per instance; specialised agents can override both provider and level. Mark&#39;s own post-trained models are announced as &quot;coming soon&quot;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Orchestration&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Chat automatically delegates to &lt;strong&gt;sub-agents&lt;/strong&gt; (Mini / Fast / Deep tiers) in isolated contexts, with parallel fan-out, no recursion and a per-turn dispatch budget.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Context&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;&lt;strong&gt;Skills&lt;/strong&gt; (formerly Instructions), &lt;strong&gt;Brands&lt;/strong&gt;, &lt;strong&gt;Memory&lt;/strong&gt;, &lt;strong&gt;Personal files&lt;/strong&gt; and &lt;strong&gt;RAG&lt;/strong&gt; (CMP, Graph, and now your own artifacts).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Tools&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;System tools, connector tools, &lt;strong&gt;remote MCP servers&lt;/strong&gt; (bring your own as of July) and &lt;strong&gt;custom tools&lt;/strong&gt; via SDKs for TypeScript, Python and C#. A large CMS 13 (PaaS) tool set landed from May onwards.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Agents&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Agent Directory (70+ prebuilt), Agent Library (curated launcher), specialised agents, workflow agents (now with Code steps, parallel loops, nested workflows, trigger priority and version history).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Autonomy&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;&lt;strong&gt;Virtual Teammates&lt;/strong&gt; with their own Opti ID, memory and scheduled jobs, plus &lt;strong&gt;Team Messages&lt;/strong&gt; as the collaboration layer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Governance&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Input guardrails, the &lt;strong&gt;Quality&lt;/strong&gt; tab (Output Evaluation, Execution Guardrails, Execution Advisor), Safe URL Browsing, agent sharing/visibility, custom roles, exportable audit logs and version history.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Commercials&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Credits pooled across products, fixed complexity bands since March 2026, 200 complimentary credits per instance per month until the end of 2026.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;1. What&#39;s in a name? Opal, Agent Platform and Mark&lt;/h2&gt;
&lt;p&gt;Let&#39;s get the naming sorted first, because it is the single biggest source of confusion right now.&lt;/p&gt;
&lt;p&gt;Optimizely&#39;s support centre now labels the category as &quot;Agent Platform (previously Opal)&quot; and states that the rename to &lt;strong&gt;Optimizely Agent Platform&lt;/strong&gt; took effect on 1 September 2026, with &lt;strong&gt;Mark&lt;/strong&gt; (for Marketing) as the personification of the platform. At Opticon the brand was split into four parts:&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; background: rgb(232, 241, 236); font-weight: bold; white-space: nowrap;&quot;&gt;Name&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left; background: #e8f1ec; font-weight: bold;&quot;&gt;What it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Optimizely Agent Platform&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;The overall platform: agents, workflows, tools, skills, governance and the admin app.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Mark&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;The personified AI you talk to. In practice, this is what we used to call Opal Chat and the in-product assistant.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Mark AI / Mark-1&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;A family of post-trained, open-weight-based models built specifically for marketing tasks. Optimizely positions these as roughly 10x more cost-efficient than frontier models on the same marketing work. The press release states they are &lt;strong&gt;coming soon&lt;/strong&gt; to all Agent Platform customers, so treat these as announced rather than generally available.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Mark-IQ&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;The data and context layer the models draw on, including your experimentation history and web analytics.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Mark-Bench&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;An open-source benchmark of 285 tasks across 15 marketing functions with more than 6,000 grading criteria.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;On Mark-Bench, it is worth being precise about the numbers, because two sets are in circulation. The Opticon press release reports Agent Platform at a 67% all-pass rate against 60% for Claude Code, at half the cost, on default configurations. Optimizely&#39;s own Camp Opticon write-up quotes 58% versus 52% at 2.1x cheaper. Both come from Optimizely running its own benchmark, so if you are using them in a business case, cite which figure and the configuration behind it.&lt;/p&gt;
&lt;h3&gt;The docs are mid-migration&lt;/h3&gt;
&lt;p&gt;As of today (early October 2026) the documentation still uses &quot;Optimizely Opal&quot; in nearly every page title, the release notes are still called &quot;Optimizely Opal release notes&quot;, and the product UI in the doc screenshots still says Opal.&amp;nbsp;So you will see three names for the same thing depending on where you look. In this post I use &lt;strong&gt;Agent Platform&lt;/strong&gt; for the platform and &lt;strong&gt;Mark (Opal Chat)&lt;/strong&gt; for the chat experience, and I keep the documentation&#39;s feature names where they have not changed.&lt;/p&gt;
&lt;h3&gt;Other renames and retirements in 2026&lt;/h3&gt;
&lt;p&gt;The platform rename is not the only one. These are the changes I have seen trip people up:&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; background: rgb(232, 241, 236); font-weight: bold; white-space: nowrap;&quot;&gt;When&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left; background: #e8f1ec; font-weight: bold;&quot;&gt;What changed&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left; background: #e8f1ec; font-weight: bold;&quot;&gt;Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;14 Jan 2026&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;The &lt;strong&gt;Connections&lt;/strong&gt; tab moved into &lt;strong&gt;Settings &amp;rarr; Product Connections&lt;/strong&gt;.&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Old guides point to the wrong place.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;14 Jan 2026&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;New tools and registries are &lt;strong&gt;disabled by default&lt;/strong&gt;; admins must enable them.&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;New system tools will not appear in agents until switched on.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;5 Feb 2026&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Skills (then &quot;Instructions&quot;) became &lt;strong&gt;inactive by default&lt;/strong&gt; on creation.&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;You must toggle &lt;strong&gt;Active&lt;/strong&gt; on.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;24 Feb 2026&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;&quot;Threads&quot; and &quot;conversations&quot; standardised to &lt;strong&gt;chats&lt;/strong&gt;.&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Cosmetic, but affects training material.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;18 Mar 2026&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;The &lt;strong&gt;creativity&lt;/strong&gt; (temperature) setting was removed from specialised agents because newer models no longer support it.&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Existing values are ignored, not errors.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;7 May 2026&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;&lt;strong&gt;Instructions renamed to Skills&lt;/strong&gt;. Existing instructions now live under &lt;strong&gt;Context &amp;rarr; Skills&lt;/strong&gt;, with no behaviour change.&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;The biggest terminology change before the rebrand.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;7 May 2026&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;The 128-tool cap on dynamic tool lookups was removed.&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Bigger tool catalogues are now practical.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;4 Jun 2026&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;cms_geo_apply and cms_geo_analysis (CMS SaaS) were&amp;nbsp;&lt;strong&gt;decommissioned&lt;/strong&gt;, replaced by the general CMS content tools.&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Update any agents or workflows that reference them.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;2. How the platform processes a request&lt;/h2&gt;
&lt;p&gt;Before going into features, it helps to understand the runtime model, because almost every feature plugs into one of its stages. Each prompt goes through roughly the following pipeline:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Prompt and intent extraction.&lt;/strong&gt; Mark works out what you are actually asking for.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skills.&lt;/strong&gt; Relevant skills are selected and injected into the system prompt.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contextual intelligence.&lt;/strong&gt; Product data, content knowledge and workspace context are added.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tool selection.&lt;/strong&gt; The right tools and agents are chosen for the job.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Input generation and validation.&lt;/strong&gt; Parameters are built and checked. If they are invalid, Mark asks you for clarification instead of calling the model, which saves credits.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM interaction.&lt;/strong&gt; Foundation models are called through business accounts, and your data is not used for training.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Execute and refine.&lt;/strong&gt; Tools and agents run, and the output is re-evaluated and iterated on.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Response.&lt;/strong&gt; The result comes back, often as a canvas or an action card.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://cdn.images.archbee.com/BrbvVORKJ5iIeSMZye5kI/E5TkJugsaS9HVfUjUaaVK_how-opal-works-diagram.png?format=webp&quot; alt=&quot;How Opal processes a request, from prompt to tailored response&quot; /&gt; &lt;em&gt;Source: Optimizely Agent Platform documentation&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The way the docs describe the relationship between the building blocks is still the best mental model:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Skills&lt;/strong&gt; define &lt;em&gt;how&lt;/em&gt; the AI behaves and uses tools.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tools&lt;/strong&gt; provide &lt;em&gt;capabilities&lt;/em&gt;, such as reading data or taking action.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Specialised agents&lt;/strong&gt; package tools and skills into one well-defined task.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workflow agents&lt;/strong&gt; chain agents together behind a trigger.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Virtual Teammates&lt;/strong&gt; (new in August) sit on top as persistent, autonomous roles that own jobs over time.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. Models and inference&lt;/h2&gt;
&lt;p&gt;This is the area that has changed most under the bonnet in 2026, and it has direct cost and quality implications.&lt;/p&gt;
&lt;h3&gt;Multi-model by design&lt;/h3&gt;
&lt;p&gt;The Agent Platform launched on Google Gemini and from&amp;nbsp;&lt;strong&gt;15 March 2026&lt;/strong&gt; Anthropic&#39;s Claude models (accessed via Google Vertex AI) joined the line-up, with &lt;strong&gt;Anthropic becoming a sub-processor&lt;/strong&gt; under the Optimizely DPA. If you are working with a client&#39;s legal or procurement team, that is the notice to point them to. Optimizely also states that the choice of model does not change how credits are consumed.&lt;/p&gt;
&lt;p&gt;On &lt;strong&gt;24 June 2026&lt;/strong&gt; model provider selection arrived at both the instance level and the specialised-agent level. Admins now choose a &lt;strong&gt;Default Model Provider&lt;/strong&gt; in &lt;strong&gt;Settings &amp;rarr; Chat&lt;/strong&gt;. The docs position Gemini as fast, cost-effective and good for creative writing, and Claude as stronger for complex tool orchestration and code generation. Specialised agents can override the instance default.&lt;/p&gt;
&lt;h3&gt;Inference levels&lt;/h3&gt;
&lt;p&gt;The inference level controls both the model and how much &quot;thinking&quot; it does before responding. &lt;strong&gt;Balanced&lt;/strong&gt; is the default.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; background: rgb(232, 241, 236); font-weight: bold; white-space: nowrap;&quot;&gt;Inference level&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left; background: #e8f1ec; font-weight: bold;&quot;&gt;Best for&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left; background: #e8f1ec; font-weight: bold;&quot;&gt;Google model&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left; background: #e8f1ec; font-weight: bold;&quot;&gt;Claude model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;Quick&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Simple, fast tasks&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;gemini-3.5-flash-lite (medium thinking)&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;claude-haiku-4-5 (medium thinking)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;Standard&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Everyday tasks without deep reasoning&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;gemini-3.8-flash (low thinking)&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;claude-sonnet-5 (no thinking)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Balanced&lt;/strong&gt; (default)&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Everyday tasks that benefit from reasoning&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;gemini-3.8-flash (medium thinking)&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;claude-sonnet-5 (high thinking)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;Complex&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Multi-step planning and analysis&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;gemini-3.8-flash (high thinking)&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;claude-sonnet-5 (x-high thinking)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;Pro&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Nuanced evaluation and strategy&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;gemini-3.1-pro (medium thinking)&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;claude-opus-5 (high thinking)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;Code&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Code, plus anything needing maximum reasoning&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;gemini-3.1-pro (high thinking)&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;claude-opus-5 (x-high thinking)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;em&gt;Model mapping as published in the &lt;a href=&quot;https://docs.optimizely.com/agent-platform/docs/inference-level&quot;&gt;Inference level&lt;/a&gt; documentation (updated 2 October 2026).&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The mapping is managed by Optimizely and changes without any action on your side. Two recent examples: on 14 August the Complex and Code levels were upgraded to claude-opus-5, and on 13 September the Standard, Balanced and Complex levels moved to Gemini 3.8 Flash. Note that the current table shows Complex on claude-sonnet-5 with x-high thinking rather than Opus, so the August note appears to have been superseded. &lt;strong&gt;Treat the inference-level page as the source of truth, not the release notes.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Admins set the organisation-wide default for chat; agent creators set a level per specialised agent that overrides it.&lt;/p&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: 368px;&quot; src=&quot;https://cdn.images.archbee.com/BrbvVORKJ5iIeSMZye5kI/ishgeJd8GwqaqGXYYFgF3_inference-level.png?format=webp&quot; alt=&quot;Setting the default inference level in Opal settings&quot; width=&quot;860&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;em&gt;Source: Optimizely Agent Platform documentation&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Sub-agents (released 17 September)&lt;/h3&gt;
&lt;p&gt;This is one of the most architecturally interesting releases of the year, and it has gone a bit under the radar. When chat (or a specialised agent) hits a self-contained sub-task, it now hands it off to a &lt;strong&gt;sub-agent&lt;/strong&gt; that runs in its own isolated context and returns only the final result. Intermediate tool calls and documents never enter your conversation.&lt;/p&gt;
&lt;p&gt;The motivation is classic context engineering: avoiding context bloat (re-reading huge tool outputs every turn) and split attention (one model juggling unrelated objectives).&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; background: rgb(232, 241, 236); font-weight: bold; white-space: nowrap;&quot;&gt;Tier&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left; background: #e8f1ec; font-weight: bold;&quot;&gt;Inference level&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left; background: #e8f1ec; font-weight: bold;&quot;&gt;Extended thinking&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left; background: #e8f1ec; font-weight: bold;&quot;&gt;Typical use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Mini&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Quick&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;No&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Extraction, classification, reformatting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Fast&lt;/strong&gt; (default)&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Balanced&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Yes&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;General delegation needing judgement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Deep&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Pro&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Yes, with a higher token budget&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Sustained multi-step reasoning&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The key technical behaviours:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Parallel fan-out.&lt;/strong&gt; Independent sub-tasks are dispatched concurrently; dependent ones run in sequence.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-turn continuation.&lt;/strong&gt; A sub-agent can be resumed with its context intact.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No recursion.&lt;/strong&gt; Delegation tools are stripped from sub-agents, so there are no unbounded chains.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Per-turn dispatch budget.&lt;/strong&gt; Individual calls and fan-out slots share one cap per turn.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instance default provider.&lt;/strong&gt; Sub-agents never override the configured model provider.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero configuration.&lt;/strong&gt; They are installed and managed by Optimizely, cannot be edited or deleted, and do not appear in the Agent Directory.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The practical gotcha is in your logs. One chat message can now produce several rows in &lt;strong&gt;Agent Activity Logs&lt;/strong&gt; (search for &quot;sub&quot;), each with its own credit total, attributed to the user who started the chat, with the model provider shown as &lt;strong&gt;Instance Default&lt;/strong&gt;. Rows sharing the same execution time indicate fan-out. If a client asks why one question produced five log entries, this is why, and Optimizely&#39;s position is that it is the same work routed more cheaply rather than extra work.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/172d765d80e040ed85c50dc98299aba6.aspx&quot; width=&quot;1271&quot; height=&quot;445&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. The chat experience (Mark / Opal Chat)&lt;/h2&gt;
&lt;p&gt;Chat is still where most users meet the platform. It is available in the global navigation across Optimizely One and at &lt;a href=&quot;https://opal.optimizely.com&quot;&gt;opal.optimizely.com&lt;/a&gt;. (likely to be changed soon)&lt;/p&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;img src=&quot;/link/605e7e540ce44ee1b0d59f5c0f7ed5d7.aspx&quot; width=&quot;1061&quot; height=&quot;291&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;img src=&quot;/link/c03135f0b4bf4c3da60a929b9d0bd0d3.aspx&quot; width=&quot;1062&quot; height=&quot;391&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;What&#39;s new in chat in 2026&lt;/h3&gt;
&lt;p&gt;The chat has matured from a simple prompt box into a proper agent console. In rough chronological order:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;January.&lt;/strong&gt; CSV uploads, Markdown in the input box, a toggle for the history panel, and dark mode and font-size settings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;February.&lt;/strong&gt; Upload of .xlsx and .docx files, and an immediate &quot;thinking&amp;hellip;&quot; indicator.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;March.&lt;/strong&gt; &lt;strong&gt;Skill mentions&lt;/strong&gt; (type &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;/&lt;/span&gt; to apply a skill) and &quot;save this as a skill&quot; directly from a conversation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;April.&lt;/strong&gt; &lt;strong&gt;Message queuing&lt;/strong&gt; (keep typing while Mark works), pinned conversations, recency-sorted history and undo for pasted content.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;May.&lt;/strong&gt; &lt;strong&gt;Chat modes&lt;/strong&gt; to match output depth to the task, &lt;strong&gt;action cards&lt;/strong&gt; (rich, interactive inline components instead of plain text), &lt;strong&gt;chat pills&lt;/strong&gt; (one-click starter prompts), &lt;strong&gt;memory&lt;/strong&gt;, and &lt;strong&gt;multi-turn mode&lt;/strong&gt; for specialised agents.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;June.&lt;/strong&gt; A live &lt;strong&gt;activity stream&lt;/strong&gt; with inline tool status and &quot;context pickup pills&quot; showing what context was pulled in, Escape to stop a response, PowerPoint and Word generation (via the &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;code_execution&lt;/span&gt; tool) and an &lt;strong&gt;image editor&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;June 30.&lt;/strong&gt; The &lt;strong&gt;Agent Library&lt;/strong&gt; on the chat homepage and &lt;strong&gt;Personalize Opal&lt;/strong&gt;, a pinned onboarding agent that walks new users through connecting tools and building skills.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;July.&lt;/strong&gt; &lt;strong&gt;Conversation compaction&lt;/strong&gt; (automatic and manual checkpoints for long chats), &lt;strong&gt;editing sent messages&lt;/strong&gt;, rotating contextual tips, &lt;strong&gt;Agent Builder&lt;/strong&gt; and &lt;strong&gt;Skill Builder&lt;/strong&gt; inside chat, and &lt;strong&gt;Safe URL Browsing&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;September.&lt;/strong&gt; &lt;strong&gt;RAG for artifacts&lt;/strong&gt;, so chat can draw on your own canvases.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For architects, compaction and sub-agents together are the important pair: they are Optimizely&#39;s answer to long-running, tool-heavy conversations getting slower, dumber and more expensive over time.&lt;/p&gt;
&lt;h3&gt;Chat everywhere: in-product and third-party surfaces&lt;/h3&gt;
&lt;p&gt;Chat is embedded across the stack, and that list keeps growing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CMS 13.&lt;/strong&gt; The Optimizely.Cms.OpalChat adds Opal to the platform under verson 13.2.0 but 13.2.0 and onwards have Mark chat is built directly in to the CMS (requires DXP and Opti ID)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CMS 12&lt;/strong&gt; (since October 2025, requires DXP and Opti ID), &lt;strong&gt;CMS SaaS&lt;/strong&gt;, &lt;strong&gt;Commerce Connect&lt;/strong&gt; (since September 2025), &lt;strong&gt;Configured Commerce&lt;/strong&gt;, &lt;strong&gt;PIM&lt;/strong&gt; (January 2026), &lt;strong&gt;CMP&lt;/strong&gt;, &lt;strong&gt;Web and Feature Experimentation&lt;/strong&gt;, &lt;strong&gt;Personalization&lt;/strong&gt;, &lt;strong&gt;Analytics&lt;/strong&gt;, &lt;strong&gt;ODP&lt;/strong&gt; and &lt;strong&gt;Product Recommendations&lt;/strong&gt; (beta from May 2026).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Slack.&lt;/strong&gt; Now a verified app in the Slack Marketplace (February 2026).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Microsoft Copilot&lt;/strong&gt; and &lt;strong&gt;Google Agentspace / Gemini Enterprise&lt;/strong&gt;, the latter using the Agent-to-Agent (A2A) protocol for cross-vendor agent collaboration (both since September 2025).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;5. The context layer: Skills, Brands, Memory, Personal files and RAG&lt;/h2&gt;
&lt;p&gt;If tools are what the platform can &lt;em&gt;do&lt;/em&gt;, this layer is what it &lt;em&gt;knows&lt;/em&gt;. It is also the part that has evolved the most conceptually this year, and it effectively forms the user-facing side of what Optimizely now calls Mark-IQ.&lt;/p&gt;
&lt;h3&gt;Skills (formerly Instructions)&lt;/h3&gt;
&lt;p&gt;Skills are the reusable rules, context and behavioural guidelines that shape output, such as brand voice, product taxonomies, personas, compliance language or output formats. They come in two scopes: &lt;strong&gt;organisation skills&lt;/strong&gt; (admin-managed) and &lt;strong&gt;personal skills&lt;/strong&gt; (user-managed, since 30 March).&lt;/p&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: 1110px;&quot; src=&quot;https://cdn.images.archbee.com/BrbvVORKJ5iIeSMZye5kI/l_stRVPc54VVzstPTlMGQ_components-of-a-skill.png?format=webp&quot; alt=&quot;The components of a skill&quot; width=&quot;752&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;em&gt;Source: Optimizely Agent Platform documentation&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Each skill has a name, watchers, version history, an active flag, a &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;/shortcut&lt;/span&gt;, a scope, a &lt;strong&gt;When to use&lt;/strong&gt; activation trigger, the &lt;strong&gt;core skill&lt;/strong&gt; body (rich text or Markdown), attached reference files, and a &lt;strong&gt;Where to use&lt;/strong&gt; setting to scope it to a specific product instance.&lt;/p&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://cdn.images.archbee.com/BrbvVORKJ5iIeSMZye5kI/FmfPTLEdueHxalNaUtjnp_core-skill.png?format=webp&quot; alt=&quot;Editing the core skill in Markdown or rich text&quot; /&gt; &lt;em&gt;Source: Optimizely Agent Platform documentation&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How selection actually works&lt;/strong&gt; matters a lot when you are designing a skills library:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;All active skills available to the user are gathered.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tool-triggered skills&lt;/strong&gt; are applied automatically whenever their tools are in play, with no further evaluation.&lt;/li&gt;
&lt;li&gt;The remaining skills are judged by a relevancy model that reads your latest message, the conversation history and each skill&#39;s &lt;strong&gt;Keywords or phrases&lt;/strong&gt; field.&lt;/li&gt;
&lt;li&gt;Matched skills are added to the system prompt.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Two consequences are easy to miss. First, the relevancy model looks &lt;strong&gt;only&lt;/strong&gt; at the &lt;em&gt;When to use&lt;/em&gt; field and ignores the skill&#39;s name and body during selection. Second, it is deliberately &lt;strong&gt;inclusive&lt;/strong&gt;: it would rather include a borderline skill than miss a relevant one, and it matches semantically. A skill about &quot;marketing promotions&quot; can fire for a prompt about strawberries on sale. The fix is to write tight activation criteria, using &quot;ONLY use for&amp;hellip;&quot;, explicit &quot;Do NOT use for&amp;hellip;&quot; exclusions, synonyms for narrow concepts, and mutually exclusive boundaries between related skills. Leaving &lt;em&gt;When to use&lt;/em&gt; blank makes a skill shortcut-only.&lt;/p&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://cdn.images.archbee.com/BrbvVORKJ5iIeSMZye5kI/pe0Ve4qJMd5M5dXpWor55_tool-skills.png?format=webp&quot; alt=&quot;Configuring a tool-triggered skill&quot; /&gt; &lt;em&gt;Source: Optimizely Agent Platform documentation&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The skills toolchain has also grown up considerably:&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; background: rgb(232, 241, 236); font-weight: bold; white-space: nowrap;&quot;&gt;When&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left; background: #e8f1ec; font-weight: bold;&quot;&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;5 Feb&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Status column added; skills inactive by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;30 Mar&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Personal skills; create skills from a chat; &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;/&lt;/span&gt; skill mentions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;14 Apr&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Redesigned &lt;strong&gt;Activation Trigger&lt;/strong&gt; section; better disambiguation when editing from chat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;7 May&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;&lt;strong&gt;Instructions &amp;rarr; Skills&lt;/strong&gt; rename&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;26 May&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;File-explorer UI for skill files; PPTX/DOCX support in skills (with code execution)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;27 May&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Skills section in specialised agents, with a &lt;strong&gt;Choose skills&lt;/strong&gt; picker to insert skills at exact points in a prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;24 Jun&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Skill watching; &lt;strong&gt;import/export&lt;/strong&gt; across instances; scope switcher between personal and organisation; skill preview in the agent editor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;30 Jun / 29 Jul&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;&lt;strong&gt;Skills builder&lt;/strong&gt; GA, then Skill Builder inside chat&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Brands (25 August) - Not fully out yet&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Brands&lt;/strong&gt; are a structured record of a brand&#39;s assets, colours, typefaces and usage rules, managed under &lt;strong&gt;Context &amp;rarr; Brand&lt;/strong&gt;. The clever bit architecturally is that &lt;strong&gt;publishing a brand turns it into a skill&lt;/strong&gt;, so you get a single source of truth that image generation, decks, documents and social images all read from, instead of each feature holding its own copy. Brands move through Draft, Published and Default states, and an organisation can hold several brands with one default. Marketers can also bootstrap a brand on the fly when none exists.&lt;/p&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://cdn.images.archbee.com/BrbvVORKJ5iIeSMZye5kI/u99QJ0i969KoCcnlVnKgv_brand-overview.png?format=webp&quot; alt=&quot;The Brand page under Context&quot; /&gt; &lt;em&gt;Source: Optimizely Agent Platform documentation&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;For multi-brand clients this is a significant change. Previously you had to emulate this with a pile of brand-voice skills.&lt;/p&gt;
&lt;h3&gt;Memory (7 May) and Personal files (21 September)&lt;/h3&gt;
&lt;p&gt;Memory and personal files are now one system, and the implementation will look very familiar to anyone who has worked with coding agents: &lt;strong&gt;it is file-based.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Personal files&lt;/strong&gt; are a persistent, per-user document store under &lt;strong&gt;Context &amp;rarr; Personal Files&lt;/strong&gt;, with a browser-based editor for text and Markdown files.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory&lt;/strong&gt; lives in that same store. &lt;strong&gt;FACTS.md&lt;/strong&gt; holds short durable facts, named notes files hold broader context, and &lt;strong&gt;RECENTS.md&lt;/strong&gt; is a running summary of recent conversations and agent activity that Mark maintains. &lt;strong&gt;MEMORY.md&lt;/strong&gt; is generated by Mark and is read-only.&lt;/li&gt;
&lt;li&gt;Memories are captured either automatically or on request (&quot;remember that&amp;hellip;&quot;).&lt;/li&gt;
&lt;li&gt;When &lt;strong&gt;Enable personal context&lt;/strong&gt; is turned on for a specialised agent, context files such as &lt;strong&gt;AGENTS.md&lt;/strong&gt; and &lt;strong&gt;MEMORY.md&lt;/strong&gt; are made available to the agent before it starts. You can restrict the agent to a &lt;strong&gt;working directory&lt;/strong&gt; (for example &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;/context/agents/support&lt;/span&gt;) rather than exposing the whole store.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://cdn.images.archbee.com/BrbvVORKJ5iIeSMZye5kI/T3Mw2AkhKTW61HyS_Wt2p_view-memories.png?format=webp&quot; alt=&quot;Viewing memory files in Personal Files&quot; /&gt; &lt;em&gt;Source: Optimizely Agent Platform documentation&lt;/em&gt;&lt;/p&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://cdn.images.archbee.com/BrbvVORKJ5iIeSMZye5kI/6TlhobVIXskXcKoj725RO_pesonal-files.png?format=webp&quot; alt=&quot;The Personal Files editor&quot; /&gt; &lt;em&gt;Source: Optimizely Agent Platform documentation&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The distinction the docs draw is useful when you explain it to clients: &lt;strong&gt;skills capture how work gets done; memory captures who you are and what you are working on.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Retrieval-augmented generation (RAG)&lt;/h3&gt;
&lt;p&gt;RAG has expanded in three steps this year:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;7 May. RAG for CMP.&lt;/strong&gt; Permission-aware semantic and keyword retrieval across campaigns, tasks, assets, work requests and briefs, exposed as &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;rag_retrieve_from_cmp&lt;/span&gt; (structured) and &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;rag_retrieve_from_cmp_as_csv&lt;/span&gt; (tabular). Alongside it came &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;rag_retrieve_from_optimizely_graph&lt;/span&gt; for CMS and CMP content via &lt;strong&gt;Optimizely Graph&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;24 June.&lt;/strong&gt; Folder and path filtering for CMP assets, &lt;strong&gt;relevancy explanations&lt;/strong&gt; for suggested assets, and &lt;strong&gt;EU-region RAG&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;17 September. RAG for artifacts.&lt;/strong&gt; Chat can now search your own canvases, respecting artifact permissions and including artifacts shared with you.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Retrieval, embedding and indexing all happen inside Optimizely&#39;s infrastructure, and every query respects the user&#39;s roles and permissions. Admins can turn RAG on or off for direct chat under &lt;strong&gt;Settings &amp;rarr; RAG Configuration&lt;/strong&gt;, but note that &lt;strong&gt;agents can use RAG regardless of that setting&lt;/strong&gt;, which is important if you are relying on the toggle for governance.&lt;/p&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: 386px;&quot; src=&quot;https://cdn.images.archbee.com/BrbvVORKJ5iIeSMZye5kI/2r8-QfX7E328za8VmjsrV_enable-rag-in-chat.png?format=webp&quot; alt=&quot;The Enable RAG in Chat setting&quot; width=&quot;1062&quot; /&gt; &lt;em&gt;Source: Optimizely Agent Platform documentation&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is exactly where the CMS 13 upgrade story from my last post comes back in: &lt;strong&gt;Graph is the bridge&lt;/strong&gt; that gets CMS and Commerce content into the agent platform&#39;s retrieval layer.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. Tools: system, connector, remote MCP and custom&lt;/h2&gt;
&lt;p&gt;Tools are the actions. There are three families, and the year&#39;s big story is the arrival of &lt;strong&gt;MCP&lt;/strong&gt; as a first-class citizen.&lt;/p&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: 1172px;&quot; src=&quot;https://cdn.images.archbee.com/BrbvVORKJ5iIeSMZye5kI/w0geSGrNPUffwHrKbpJC3_tools-tab-numbered-list.png?format=webp&quot; alt=&quot;The Tools tab, with registries, filters and chat enablement&quot; width=&quot;903&quot; /&gt; &lt;em&gt;Source: Optimizely Agent Platform documentation&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The Tools area is now split into four tabs: &lt;strong&gt;Connectors&lt;/strong&gt; (each user links their own third-party accounts), &lt;strong&gt;Tools&lt;/strong&gt;, &lt;strong&gt;Registries&lt;/strong&gt; and &lt;strong&gt;External Providers&lt;/strong&gt; (remote MCP servers and auth providers). You can filter tools by type (&lt;strong&gt;Remote MCP&lt;/strong&gt; or &lt;strong&gt;Opal Tools&lt;/strong&gt;), control per registry whether tools are active and whether they are enabled in chat, and restore chat defaults. Note that agents started from chat can use a registry&#39;s tools even when that registry is disabled for chat.&lt;/p&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: 510px;&quot; src=&quot;https://cdn.images.archbee.com/BrbvVORKJ5iIeSMZye5kI/C5D5n7xk_DuEjYKRPyp7l_registeries-tab-numbered-list.png?format=webp&quot; alt=&quot;The Registries tab with discovery URLs and sync&quot; width=&quot;1053&quot; /&gt; &lt;em&gt;Source: Optimizely Agent Platform documentation&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;System tools for Optimizely products&lt;/h3&gt;
&lt;p&gt;Optimizely-maintained tools now cover practically every product. The ones that matter most for CMS and Commerce architects:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CMS 13 (PaaS)&lt;/strong&gt;, which arrived from 22 May:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Content types:&lt;/strong&gt; &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;paas_cms_list_content_types&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;paas_cms_get_content_type_details&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;paas_cms_create_content_type&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;paas_cms_update_content_type&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;paas_cms_delete_content_type&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Property groups:&lt;/strong&gt; &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;paas_cms_list_property_groups&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;paas_cms_create_property_group&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content:&lt;/strong&gt; &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;paas_cms_get_content_data&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;paas_cms_update_content_item&lt;/span&gt; (creates a draft if no key is given), &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;paas_cms_delete_content_item&lt;/span&gt; (moves to the waste basket), &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;paas_cms_publish_content_item&lt;/span&gt; (immediate or scheduled), &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;paas_cms_create_content_variation&lt;/span&gt; (locale variations)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Preview:&lt;/strong&gt; &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;paas_cms_get_content_preview_url&lt;/span&gt;, which lets the agent fetch and analyse rendered HTML&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SEO and GEO:&lt;/strong&gt; &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;paas_cms_seo_analysis&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;paas_cms_seo_edit&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;paas_cms_geo_analysis&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;paas_cms_geo_apply&lt;/span&gt; (writes JSON-LD into a &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;JsonLdTemplates&lt;/span&gt; property as a draft)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Display templates and media&lt;/strong&gt; (29 July): &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;paas_cms_list_display_templates&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;paas_cms_get_display_template&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;paas_cms_create_display_template&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;paas_cms_update_display_template&lt;/span&gt;, and &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;paas_cms_upload_media&lt;/span&gt; (pulls a file from a public URL into CMS media)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note that the GEO tools were &lt;strong&gt;removed from CMS SaaS&lt;/strong&gt; in June but &lt;strong&gt;exist for CMS 13 PaaS&lt;/strong&gt;. Do not assume parity between the two tool sets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Optimizely Graph&lt;/strong&gt; (CMS SaaS, CMS 12 and CMS 13): &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;graph_content_type_schema&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;graph_content_graphql_executor&lt;/span&gt; and &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;graph_content_search_tool&lt;/span&gt; (January), plus &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;graph_pinned_result&lt;/span&gt; and &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;graph_synonyms&lt;/span&gt; (14 August), so agents can now &lt;strong&gt;tune search relevance&lt;/strong&gt;, not just query it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Commerce:&lt;/strong&gt; Commerce Connect became a &lt;strong&gt;product connection&lt;/strong&gt; on 13 March, alongside the &lt;strong&gt;Product Promotion&lt;/strong&gt; agent for Commerce Connect; Configured Commerce has system tools and a &lt;strong&gt;Restriction Group Creation&lt;/strong&gt; agent; and Product Recommendations has a growing &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;pr_*&lt;/span&gt; tool set for category, search, trend and algorithm performance reports and page management.&lt;/p&gt;
&lt;p&gt;Beyond that there are tool sets for CMP (work requests, campaigns, milestones, structured content, asset updates), Experimentation and Personalization (including ten &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;exp_ve_*&lt;/span&gt; Visual Editor DOM tools that power the AI variation development agent), Analytics, ODP, Campaign, PIM and Content Recommendations.&lt;/p&gt;
&lt;h3&gt;Connector tools and remote MCP&lt;/h3&gt;
&lt;p&gt;The connector catalogue exploded this year. Highlights include Microsoft 365 (Teams, Outlook, and from 18 September OneNote and &lt;strong&gt;sensitivity labels&lt;/strong&gt; on OneDrive and SharePoint), Google Workspace (Docs, Sheets, Slides, Calendar and Gmail), Google Ads, Google Search Console, GA4, LinkedIn Ads, OpenAI Ads, Adobe Analytics, Salesforce CRM, Amplitude, Mixpanel, WordPress, Zoom, Crunchbase and Figma.&lt;/p&gt;
&lt;p&gt;Remote MCP arrived in stages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;30 March.&lt;/strong&gt; Remote MCP connector tools (starting with Atlassian and Conductor), with &lt;strong&gt;user-level authentication&lt;/strong&gt;: the admin enables the server for the organisation, and each user authenticates their own account so actions carry their own identity and permissions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;7 May.&lt;/strong&gt; Notion, Contentsquare, Contentful, Gamma, GitHub, Sanity and ClickUp.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;29 July.&lt;/strong&gt; &lt;strong&gt;Bring your own remote MCP server&lt;/strong&gt;, plus HubSpot and ZoomInfo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;18 September.&lt;/strong&gt; Braze and Fullstory, and &lt;strong&gt;Contentful Global and EU endpoints&lt;/strong&gt; for data residency.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;User-level authentication is the most important architectural point here. It gives you proper action attribution and per-user data scoping for third-party systems, rather than a single shared service account.&lt;/p&gt;
&lt;h3&gt;Custom tools&lt;/h3&gt;
&lt;p&gt;When nothing off the shelf fits, you build a custom tool. The contract is straightforward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;tool manifest&lt;/strong&gt; describing name, description, typed parameters, the execution endpoint and auth requirements.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;discovery endpoint&lt;/strong&gt; (typically &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;/discovery&lt;/span&gt;) that returns the manifest. If it is unreachable or malformed, the tools simply will not appear.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;execution endpoint&lt;/strong&gt;, typically a POST with a JSON body returning JSON.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authentication.&lt;/strong&gt; Notably, the docs state that &lt;strong&gt;only Opti ID is supported for tool authentication&lt;/strong&gt; at the moment, with other providers in the works.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Since 1 April, parameters support &lt;strong&gt;nested schemas&lt;/strong&gt; (objects within objects, arrays of objects and enums), which makes richer integrations far less awkward. Tools are hosted either on &lt;strong&gt;Optimizely Connect Platform (OCP)&lt;/strong&gt; or anywhere publicly reachable, and they are registered as a registry with a discovery URL that you can re-sync.&lt;/p&gt;
&lt;p&gt;Official SDKs:&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; background: rgb(232, 241, 236); font-weight: bold; white-space: nowrap;&quot;&gt;Language&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left; background: #e8f1ec; font-weight: bold;&quot;&gt;Install&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left; background: #e8f1ec; font-weight: bold;&quot;&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;TypeScript / JavaScript&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;npm install @optimizely-opal/opal-tools-sdk&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Express middleware, &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;@tool()&lt;/span&gt; decorators, parameter validation, auth helpers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;Python&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;pip install optimizely-opal.opal-tools-sdk&lt;/span&gt; (import as &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;opal_tools_sdk&lt;/span&gt;)&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;FastAPI integration, auto &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;/discovery&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;C# / .NET&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;See note below&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;ASP.NET Core integration, &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;[Tool]&lt;/span&gt; and &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;[RequiresAuth]&lt;/span&gt; attributes, DI support&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A small but real gotcha for .NET teams: the docs currently show the install command as &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;Install-Package OptimizelyOpal.OpalToolsSDK&lt;/span&gt;, but link to a NuGet listing named &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;Optimizely.Opal.Tools&lt;/span&gt;. Check NuGet for the current package ID before you pin it in a project.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. Agents&lt;/h2&gt;
&lt;h3&gt;The four kinds of agent&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; background: rgb(232, 241, 236); font-weight: bold; white-space: nowrap;&quot;&gt;Type&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left; background: #e8f1ec; font-weight: bold;&quot;&gt;Who builds it&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left; background: #e8f1ec; font-weight: bold;&quot;&gt;What it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Agent Directory agents&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Optimizely&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Prebuilt, installable agents. There are now 70+ listed in the credits reference alone. Since &lt;strong&gt;25 August they auto-update&lt;/strong&gt; when Optimizely publishes a new version (on by default).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Agent Library&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Optimizely (curated)&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;A browsable launcher on the chat homepage with more than 45 agents runnable in one click, without installation (30 June).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Specialised agents&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;You&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;One well-defined task, with your prompt template, tools, skills, variables, model provider and inference level.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Workflow agents&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;You&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Multi-step automations of triggers, logic and agents. Still described in the docs as &lt;strong&gt;private GA and not available on all instances&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: 572px;&quot; src=&quot;https://cdn.images.archbee.com/BrbvVORKJ5iIeSMZye5kI/riQIZv2U-uUAyVJt84u0R_agents-reusable-content-block.png?format=webp&quot; alt=&quot;The Agents page in the Opal app&quot; width=&quot;1208&quot; /&gt; &lt;em&gt;Source: Optimizely Agent Platform documentation&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Directory agents worth knowing as a CMS or Commerce architect&lt;/h3&gt;
&lt;p&gt;The Directory has shifted from generic content helpers towards agents that act directly on the platform:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CMS 13 (PaaS):&lt;/strong&gt; Content Model Creation, Page Builder, Content Analysis, GEO Schema Optimization and SEO Metadata Implementation. The Content Analysis agent produces a Markdown instruction set for a content type that the Page Builder agent then uses, which is a neat pattern for on-model content generation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CMS SaaS&lt;/strong&gt; equivalents, plus Content Refresh Analysis and FAQ Creation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Commerce:&lt;/strong&gt; Product Promotion (Commerce Connect) and Restriction Group Creation (Configured Commerce).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Experimentation:&lt;/strong&gt; Experiment Conflict Checker, Backlog Prioritization (PIE scoring), Value Estimator, Program Overview, Program Health Review, Feature Experimentation Governance, and the &lt;strong&gt;Feature Flag Implementation&lt;/strong&gt; agent, which outputs a portable &lt;strong&gt;SKILL.md&lt;/strong&gt; for implementing a flag in your codebase. That is a nice nod towards developer agent tooling.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI search and AEO:&lt;/strong&gt; GEO Auditor, the Conductor AEO Gap Finder, AI Brand Visibility and Competitive Share of Voice agents, Profound Citation Gap Analysis, and Agent Visibility Analytics Insights.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Specialised agents: what&#39;s new&lt;/h3&gt;
&lt;p&gt;The agent editor has quietly become quite sophisticated over 2026:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Versioning&lt;/strong&gt; with view, restore and branch-from-version (January).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prompt template shortcuts&lt;/strong&gt; for inserting variables, tools and skills inline; &lt;strong&gt;case-insensitive, unique variables&lt;/strong&gt;; &lt;strong&gt;array/list inputs&lt;/strong&gt; (May).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Example shots&lt;/strong&gt; in the prompt template (February) and &lt;strong&gt;preferred output examples&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JSON export and import&lt;/strong&gt; for moving agents between instances (April).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-turn interaction mode&lt;/strong&gt; in chat (May). Note that specialised agents always run single-turn inside workflows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Model provider and inference level per agent&lt;/strong&gt; (June).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sharing and visibility&lt;/strong&gt; (30 June): private, named people or the whole organisation, with view, run, edit and delete permission levels. New agents default to &lt;strong&gt;private&lt;/strong&gt; where the feature is enabled, and it is not available on all tiers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent Builder&lt;/strong&gt; in chat (29 July), so agents can be created and iterated on conversationally.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Personal context and working directory&lt;/strong&gt; settings (September).&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;60-minute timeout&lt;/strong&gt; (since November 2025) to stop runaway executions burning credits.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Workflow agents: what&#39;s new&lt;/h3&gt;
&lt;p&gt;Workflow agents are made up of &lt;strong&gt;triggers&lt;/strong&gt; (chat, schedule, webhook and email), &lt;strong&gt;logic&lt;/strong&gt; (conditions, loops and code) and &lt;strong&gt;agents&lt;/strong&gt;.&lt;/p&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://cdn.images.archbee.com/BrbvVORKJ5iIeSMZye5kI/qRVER8BLyJNTg0gYmDQkh_review-workflow-components.png?format=webp&quot; alt=&quot;The structure of a workflow agent&quot; /&gt; &lt;em&gt;Source: Optimizely Agent Platform documentation&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This has been the busiest area for releases:&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; background: rgb(232, 241, 236); font-weight: bold; white-space: nowrap;&quot;&gt;When&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left; background: #e8f1ec; font-weight: bold;&quot;&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;Jan&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Add a trigger first when building&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;Mar&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Builder-first creation flow; &lt;strong&gt;multi-branch conditions&lt;/strong&gt; in a single node; email trigger improvements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;Apr&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Webhook trigger redesign with product instance selection and default CMP auth headers; &lt;strong&gt;parameter generation logs&lt;/strong&gt;; helper messages for chat triggers; JSON download and copy; case-sensitive conditions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;May&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Workflow &lt;strong&gt;import/export&lt;/strong&gt;; loop validation; schedule start time and &lt;strong&gt;no end date&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;Jun&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Execution-log export to CSV/Excel with date ranges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;29 Jul&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;&lt;strong&gt;Nested workflows&lt;/strong&gt;; &lt;strong&gt;static versus predicted parameters&lt;/strong&gt; (lock a variable or let the LLM fill it); &lt;strong&gt;Agent Update Warning&lt;/strong&gt; before editing an agent used by a workflow; &lt;strong&gt;Version History&lt;/strong&gt; for workflows; a new &lt;strong&gt;Execution Status Taxonomy&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;17 Sep&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;&lt;strong&gt;Code step&lt;/strong&gt; (Python, JavaScript or Bash, with no external service needed); &lt;strong&gt;Run iterations in parallel&lt;/strong&gt; on loops; &lt;strong&gt;trigger priority&lt;/strong&gt; (Standard versus Background) so batch runs do not delay interactive ones&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;/link/016a9f6001bc4762a3718cf3cb8653a2.aspx&quot; width=&quot;696&quot; height=&quot;1110&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The Code step is a big deal. Previously, any deterministic transformation, such as parsing, maths or reshaping JSON between agents, meant either burning an LLM call or standing up a custom tool. Now it can live inline in the workflow. Together with nested workflows and static parameters, workflow agents are becoming a reasonable low-code orchestration layer rather than just a chain of prompts.&lt;/p&gt;
&lt;p&gt;A credit note: a workflow&#39;s cost is &lt;strong&gt;the sum of the specialised agents it invokes, with no overhead for the workflow itself&lt;/strong&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. Virtual Teammates and Team Messages&lt;/h2&gt;
&lt;p&gt;This was the headline Opticon launch, released on &lt;strong&gt;25 August (Not availible to everyone yet, especially partners)&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;What a Virtual Teammate is&lt;/h3&gt;
&lt;p&gt;A Virtual Teammate is a persistent AI worker built from the same components you already have, namely &lt;strong&gt;tools&lt;/strong&gt;, &lt;strong&gt;agents and workflows&lt;/strong&gt;, and &lt;strong&gt;jobs&lt;/strong&gt; (workflows it runs on its own, on a schedule or on events), wrapped in a persistent identity with memory, a persona and a defined role. The distinction from an agent is &lt;strong&gt;continuity&lt;/strong&gt;: an agent runs when you invoke it, whereas a teammate keeps working, holds context, improves from feedback and collaborates with other teammates.&lt;/p&gt;
&lt;p&gt;The launch roster:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Chief of Staff:&lt;/strong&gt; daily and weekly briefs, meeting prep and recaps, and competitive intelligence.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Marketing Analyst:&lt;/strong&gt; scheduled, decision-ready GA4 reports.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SEO &amp;amp; AI Search Analyst:&lt;/strong&gt; reasons across Search Console, GA4, Conductor and AI Visibility Analytics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CRO Manager:&lt;/strong&gt; monitors behaviour, analyses competitor UX and &lt;strong&gt;stages experiment configurations&lt;/strong&gt; in Optimizely.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Personalization Strategist:&lt;/strong&gt; builds and runs personalisation campaigns end to end.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Identity and access: the architect&#39;s checklist&lt;/h3&gt;
&lt;p&gt;This is the bit to get right in any implementation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each teammate gets &lt;strong&gt;its own Opti ID account&lt;/strong&gt; (its own login and email, flagged as a virtual user). It shows up alongside people and fits into existing access and audit processes, and &lt;strong&gt;its usage is tracked separately from human usage&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;does not consume a product seat&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;It gets &lt;strong&gt;only the access you grant it&lt;/strong&gt;, with nothing inherited automatically from the person who hired it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inside Optimizely products it acts as itself. Outside Optimizely it acts through accounts a human connects on its behalf&lt;/strong&gt;, usually the person who hired it. It cannot authorise third-party connections on its own.&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;CMS 13 and CMS SaaS&lt;/strong&gt; there is an extra step: after granting the teammate a role in the Opti ID Admin Center, you must also add it to the relevant content under &lt;strong&gt;Settings &amp;rarr; Set Access Rights&lt;/strong&gt; in CMS, with only the rights its jobs need.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: 439px;&quot; src=&quot;https://cdn.images.archbee.com/BrbvVORKJ5iIeSMZye5kI/rTp0e_qht88QPTmqoQQ70_access-rights.png?format=webp&quot; alt=&quot;Granting a Virtual Teammate access rights in CMS&quot; width=&quot;908&quot; /&gt; &lt;em&gt;Source: Optimizely Agent Platform documentation&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;That last point is another reason &lt;strong&gt;Opti ID is non-negotiable in CMS 13&lt;/strong&gt;: Virtual Teammates are first-class principals in the identity layer, so the same governance you apply to people applies to them.&lt;/p&gt;
&lt;h3&gt;Autonomy controls&lt;/h3&gt;
&lt;p&gt;By default a teammate &lt;strong&gt;drafts and proposes, and a human approves&lt;/strong&gt; via Team Messages. As confidence builds you can allow specific actions to run automatically within boundaries you set, and you configure its persona, values, who it may talk to, and which tools it can read, write or run. Admins enable teammates for the organisation and can pause or deactivate them; users and agent builders hire and configure their own. You can even switch identity to work in the platform &lt;em&gt;as&lt;/em&gt; the teammate.&lt;/p&gt;
&lt;h3&gt;Team Messages&lt;/h3&gt;
&lt;p&gt;Team Messages is the real-time collaboration layer for people, teammates and agents, offering direct messages, group chats and channels, with sharing of files, agents and artifacts, mentions and persistent history. It follows you across products (an unread badge appears on the IM icon in CMP, Experimentation and elsewhere), and teammates also join meetings and share transcript summaries in Teams and Slack.&lt;/p&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://cdn.images.archbee.com/BrbvVORKJ5iIeSMZye5kI/Nf41dmYPbtQnknCTcpzNT_team-messages.png?format=webp&quot; alt=&quot;Team Messages&quot; /&gt; &lt;em&gt;Source: Optimizely Agent Platform documentation&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Alongside it sit the &lt;strong&gt;Tracker&lt;/strong&gt; and &lt;strong&gt;Notifications&lt;/strong&gt; (in-app tray, Notification Center, browser notifications since May, and skill-update and guardrail notification categories since June).&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;9. Quality, safety and governance&lt;/h2&gt;
&lt;p&gt;If Virtual Teammates are the accelerator, this is the brake, and it is what makes autonomous operation defensible to a client&#39;s risk team.&lt;/p&gt;
&lt;h3&gt;Input guardrails (29 July)&lt;/h3&gt;
&lt;p&gt;You define rules in plain language about what users may ask, and the platform &lt;strong&gt;trains a classifier&lt;/strong&gt; to enforce them. This is useful for keeping usage on-topic and compliant without hand-maintaining block lists.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/9602487681be43c885477790206c12d5.aspx&quot; width=&quot;925&quot; height=&quot;755&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;The Quality tab for specialised agents (1 June)&lt;/h3&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://cdn.images.archbee.com/BrbvVORKJ5iIeSMZye5kI/VU6IVwwlUlIrd7XwYC9En_quality-tab-overview.png?format=webp&quot; alt=&quot;The Quality tab for a specialised agent&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;em&gt;Source: Optimizely Agent Platform documentation&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The Quality tab gives you three mechanisms:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Output Evaluation.&lt;/strong&gt; An AI judge scores every run against &lt;strong&gt;up to 10 criteria&lt;/strong&gt;, a &lt;strong&gt;baseline score between 75% and 95%&lt;/strong&gt; (in 5% steps) and &lt;strong&gt;up to five examples&lt;/strong&gt;. This builds on the preferred-output examples from December 2025 and the ability (January 2026) to promote past runs to evals.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Execution Guardrails.&lt;/strong&gt; These learn what &quot;normal&quot; looks like for an agent and progress through &lt;strong&gt;Learning &amp;rarr; Watching &amp;rarr; Enforcing&lt;/strong&gt; as successful runs accumulate. You control sensitivity and whether flagged runs are stopped automatically.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Execution Advisor.&lt;/strong&gt; This has no configuration screen; it acts on what the other two detect. It &lt;strong&gt;recovers flagged runs&lt;/strong&gt; with corrective guidance, &lt;strong&gt;retries failed tool calls&lt;/strong&gt; with adjusted input, and &lt;strong&gt;reviews sensitive tool calls&lt;/strong&gt; (email, publish, delete) with inline &lt;strong&gt;Approved&lt;/strong&gt; or &lt;strong&gt;Blocked&lt;/strong&gt; verdicts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each run therefore carries two independent verdicts, an &lt;strong&gt;Evaluation Score&lt;/strong&gt; and a &lt;strong&gt;Guardrail Status&lt;/strong&gt;, and a run can pass one and fail the other. The docs suggest starting with Output Evaluation and adding guardrails once behaviour has stabilised, which matches how I would roll it out.&lt;/p&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://cdn.images.archbee.com/BrbvVORKJ5iIeSMZye5kI/JvGh28VWtIFjb4mIDrVNQ_access-the-quality-tab-4.png?format=webp&quot; alt=&quot;Accessing Output Evaluation and Execution Guardrails&quot; /&gt; &lt;em&gt;Source: Optimizely Agent Platform documentation&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Safe URL Browsing (29 July)&lt;/h3&gt;
&lt;p&gt;Every URL is screened before the platform browses it. &lt;strong&gt;Standard&lt;/strong&gt; (always on) checks Google Web Risk for malware, social engineering and unwanted software. &lt;strong&gt;Advanced&lt;/strong&gt; (opt-in per instance under &lt;strong&gt;Settings &amp;rarr; Chat&lt;/strong&gt;) adds a &lt;strong&gt;Cloudflare Radar deep scan&lt;/strong&gt; in parallel, which can add up to 30 seconds of latency for URLs Cloudflare has not seen recently. Only &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;http&lt;/span&gt; and &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;https&lt;/span&gt; are accepted, &lt;strong&gt;redirect targets are screened too&lt;/strong&gt; (which closes the open-redirect bypass), and &lt;strong&gt;links in emails processed by email triggers are also checked&lt;/strong&gt;. Blocked requests return no partial content, only the threat category.&lt;/p&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://cdn.images.archbee.com/BrbvVORKJ5iIeSMZye5kI/8iZ7543ngHYoQ2I-3fiyJ_advanced-url-safety-checks.png?format=webp&quot; alt=&quot;Advanced URL safety checks setting&quot; /&gt; &lt;em&gt;Source: Optimizely Agent Platform documentation&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If a client&#39;s firewall blocks the platform from scanning their sites, there is a published allowlist of separate &lt;strong&gt;non-EU and EU egress IPs&lt;/strong&gt;, last updated on 22 July. Check the admin guide for the current list rather than copying it from old project documentation.&lt;/p&gt;
&lt;h3&gt;Roles, auditability and change control&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Roles:&lt;/strong&gt; Opal Administrator, &lt;strong&gt;Agent Builder&lt;/strong&gt; (new on 24 February, for people who build agents without being admins) and Opal User, plus &lt;strong&gt;custom roles&lt;/strong&gt; in the Opti ID Admin Center.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Audit:&lt;/strong&gt; Created By, Triggered By and Modified By on agents and skills; credit usage per run in logs; execution-log export with date ranges for specialised and workflow agents; and the Execution Status Taxonomy (July).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Change control:&lt;/strong&gt; version history for specialised agents, skills and workflows; watchers on skills and agents; and the Agent Update Warning before editing an agent a workflow depends on.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Human in the loop for side effects:&lt;/strong&gt; for example, &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;send_email&lt;/span&gt; shows a confirmation &quot;island&quot; in chat (since December 2025), and Execution Advisor can gate sensitive tool calls.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kill switch:&lt;/strong&gt; generative AI can be turned off for an organisation in the Opti ID Admin Center. Be aware that turning it off means contacting Optimizely Support to re-enable it.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;10. Artifacts and canvases&lt;/h2&gt;
&lt;p&gt;Outputs have also grown well beyond chat text:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Canvas&lt;/strong&gt; (December 2025) is the working surface for documents, HTML, React/Next.js, CSV and PowerPoint content, with downloads in the relevant formats (February), an &quot;Edit in canvas&quot; button, a raw Markdown mode (May) and &lt;strong&gt;space-level sharing permissions&lt;/strong&gt; with view, edit and manage access (June).&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Artifacts page&lt;/strong&gt; (May) is a single searchable home for everything you have created, with &lt;strong&gt;sharing&lt;/strong&gt; and, since September, &lt;strong&gt;RAG over artifacts&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PowerPoint and Word&lt;/strong&gt; creation and editing (June), when the &lt;span style=&quot;font-family: Consolas,Menlo,Monaco,monospace; font-size: 0.92em; color: #0b5d3b;&quot;&gt;code_execution&lt;/span&gt; tool is enabled, now on-brand by default thanks to Brands.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;image editor&lt;/strong&gt; (June) offers element detection and selection, brush transforms, batch edits and smart aspect ratios. (below)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limitless 1:1 Personalization&lt;/strong&gt; (June) generates landing pages per target account or segment, which is the capability I covered from the keynote last month.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/link/f6d36291e8c64c828f338e6a63d67219.aspx&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;11. Administration, data residency and credits&lt;/h2&gt;
&lt;h3&gt;Administration essentials&lt;/h3&gt;
&lt;p&gt;The setup order has not changed: &lt;strong&gt;connect your product instances first&lt;/strong&gt; under &lt;strong&gt;Settings &amp;rarr; Product Connections&lt;/strong&gt;. If an instance is not connected, it cannot use the platform&#39;s features.&lt;/p&gt;
&lt;p style=&quot;font-size: 0.85em; color: #555; margin-top: 2px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto;&quot; src=&quot;https://cdn.images.archbee.com/BrbvVORKJ5iIeSMZye5kI/JRB_-VY_6VeSpeT0aSP3p_product-connections.png?format=webp&quot; alt=&quot;Product Connections in Opal settings&quot; /&gt; &lt;em&gt;Source: Optimizely Agent Platform documentation&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Other settings to know about are the default model provider and inference level, RAG in chat, Advanced URL safety, and the &lt;strong&gt;email domain&lt;/strong&gt; used for outbound email. Be careful with the email domain: it is a single subdomain prefix per instance, and &lt;strong&gt;it cannot be changed once set&lt;/strong&gt;. Users without Opti ID can also be enabled (see &quot;Enable Opal for non-Opti ID users&quot; in the docs).&lt;/p&gt;
&lt;h3&gt;Data residency&lt;/h3&gt;
&lt;p&gt;The platform is hosted in the &lt;strong&gt;US and the EU&lt;/strong&gt;, with the region chosen on your order form. Everyone uses the same URL, and routing to your region happens behind the scenes. Customer inputs and outputs are stored long-term in North America or the EU (Belgium), with no per-country selection. However, &lt;strong&gt;prompt processing goes through global model endpoints&lt;/strong&gt; for availability and performance, so a prompt may be processed in any region, although the LLM does not retain it and the output returns to your region. EU app hosting arrived on 4 June and EU RAG on 24 June. If a client has strict residency requirements, that processing nuance is the one to raise early.&lt;/p&gt;
&lt;h3&gt;Credits and cost&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Credits are &lt;strong&gt;pooled across all eligible Optimizely products&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Since &lt;strong&gt;1 March 2026&lt;/strong&gt;, usage falls into &lt;strong&gt;fixed complexity bands&lt;/strong&gt; for predictability.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every instance gets 200 complimentary credits per month from 1 October 2025 until 31 December 2026.&lt;/strong&gt; This is worth noting for 2027 budgeting.&lt;/li&gt;
&lt;li&gt;There are &lt;strong&gt;no per-user credit limits&lt;/strong&gt;. You control spend by controlling who has access.&lt;/li&gt;
&lt;li&gt;Usage is visible in the Opti ID &lt;strong&gt;Usage and billing dashboards&lt;/strong&gt;, with monthly notifications, and in the redesigned &lt;strong&gt;Opal Agent Usage&lt;/strong&gt; dashboard (30 June), which shows executions, credits, top agents and users, and weekly active users.&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; background: rgb(232, 241, 236); font-weight: bold; white-space: nowrap;&quot;&gt;Complexity band&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left; background: #e8f1ec; font-weight: bold;&quot;&gt;Average credits per invocation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;Chat and basic agents&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;XS&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;S&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;M&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;L&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;130&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;XL&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;To give a sense of scale from the published per-agent figures: SEO Metadata Implementation for CMS 13 averages 120&amp;ndash;200 credits, GEO Schema Optimization 40&amp;ndash;60, Content Model Creation 30&amp;ndash;80 and the Conductor AEO Gap Finder 180&amp;ndash;200.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Who pays&lt;/strong&gt; depends on the trigger: for chat and in-product invocations it is the invoking user, but for &lt;strong&gt;schedule, webhook and email triggers it is the user who created the trigger&lt;/strong&gt;. Keep that in mind when you design shared automations. In practice, you probably want a dedicated owner (or, increasingly, a Virtual Teammate) owning scheduled workflows, so usage reporting stays meaningful.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;12. 2026 at a glance&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; background: rgb(232, 241, 236); font-weight: bold; white-space: nowrap;&quot;&gt;Month&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left; background: #e8f1ec; font-weight: bold;&quot;&gt;Headline releases&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Jan&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Specialised agent versioning; prompt template shortcuts; Graph tools; CMS SaaS content tools; Experimentation and Personalization system tools; PIM chat; tools disabled by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Feb&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;&lt;strong&gt;Agent Builder role&lt;/strong&gt; and custom roles; Slack Marketplace app; canvas downloads; example shots; Visual Editor, CMP and Analytics tool sets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Mar&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Claude models join (15 March); fixed-band credits (1 March); &lt;strong&gt;Commerce Connect product connection&lt;/strong&gt;; &lt;strong&gt;personal skills&lt;/strong&gt;; user-level auth for connectors; first &lt;strong&gt;remote MCP&lt;/strong&gt; connectors; credit usage in logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Apr&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;Agent JSON import/export; message queuing; nested parameter schemas for custom tools; &lt;strong&gt;CMS 13 Opal Chat NuGet&lt;/strong&gt;; Idea Builder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;May&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;&lt;strong&gt;Instructions &amp;rarr; Skills&lt;/strong&gt;; &lt;strong&gt;memory&lt;/strong&gt;; Artifacts page; notifications; chat modes, action cards and chat pills; &lt;strong&gt;RAG for CMP and Graph&lt;/strong&gt;; &lt;strong&gt;CMS 13 content tools&lt;/strong&gt;; more remote MCP servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Jun&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;&lt;strong&gt;Quality tab&lt;/strong&gt;; EU hosting and EU RAG; model provider selection; skill import/export; PPTX/DOCX generation; image editor; &lt;strong&gt;Agent Library&lt;/strong&gt;; agent sharing and visibility; Limitless 1:1 Personalization; Teams and Outlook tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Jul&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;&lt;strong&gt;Agent Builder and Skill Builder in chat&lt;/strong&gt;; &lt;strong&gt;input guardrails&lt;/strong&gt;; Safe URL Browsing; conversation compaction; &lt;strong&gt;nested workflows&lt;/strong&gt;; workflow version history; &lt;strong&gt;bring-your-own remote MCP&lt;/strong&gt;; CMS 13 display template and media tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Aug&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;&lt;strong&gt;Virtual Teammates&lt;/strong&gt;; &lt;strong&gt;Team Messages&lt;/strong&gt;; &lt;strong&gt;Brands&lt;/strong&gt;; Directory auto-update; Graph synonyms and pinned results; CMS 13 Page Builder and Content Analysis agents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid rgb(200, 211, 205); padding: 6px 10px; vertical-align: top; text-align: left; white-space: nowrap;&quot;&gt;&lt;strong&gt;Sep&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #c8d3cd; padding: 6px 10px; vertical-align: top; text-align: left;&quot;&gt;&lt;strong&gt;Rename to Agent Platform and Mark&lt;/strong&gt;; Mark-1, Mark-IQ and Mark-Bench announced; &lt;strong&gt;sub-agents&lt;/strong&gt;; &lt;strong&gt;RAG for artifacts&lt;/strong&gt;; workflow &lt;strong&gt;Code step&lt;/strong&gt;, parallel loops and trigger priority; &lt;strong&gt;personal files&lt;/strong&gt;; LinkedIn and OpenAI Ads connectors; Braze and Fullstory MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For context, at the start of the 2025 release notes Opal was essentially a chat assistant embedded in individual products. The platform pieces, namely specialised agents, workflow agents, the Agent Directory, system tools and custom tools, only arrived on &lt;strong&gt;9 September 2025&lt;/strong&gt;. Everything in the table above has been built in the thirteen months since.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;13. What this means for solution architects&lt;/h2&gt;
&lt;p&gt;Pulling this together, these are the points I would take into any design conversation right now:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Upgrade the foundations first.&lt;/strong&gt; Opti ID (identity for humans &lt;em&gt;and&lt;/em&gt; teammates), Graph (retrieval, search tuning and agent access to content) and product connections are the prerequisites for nearly everything above. This is the practical reason the CMS 13 and Commerce 15 upgrade is a strategic decision, not just a version bump.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treat skills as code.&lt;/strong&gt; With import/export, versioning, watchers and a selection model that only reads &lt;em&gt;When to use&lt;/em&gt;, a skills library deserves the same discipline as any shared configuration: naming conventions, scoped activation criteria with explicit exclusions, review, and promotion between instances.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decide your integration pattern deliberately.&lt;/strong&gt; For each external system, the choice is now between an existing connector, a vendor&#39;s remote MCP server, your own remote MCP server, or a custom tool (on OCP or self-hosted). User-level auth on MCP and connectors gives better attribution; custom tools currently authenticate with Opti ID only.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Put deterministic logic in deterministic places.&lt;/strong&gt; Use workflow Code steps and static parameters for anything that does not need an LLM, and keep specialised agents small and single-purpose.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Design for cost visibility.&lt;/strong&gt; Choose the inference level per agent rather than defaulting everything to Pro, expect sub-agent rows in logs, and be deliberate about who owns scheduled and webhook triggers, since they pay.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Layer governance before autonomy.&lt;/strong&gt; Input guardrails, Output Evaluation, then Execution Guardrails, and only then let Virtual Teammates act without approval, scoping their Opti ID roles and CMS access rights to exactly what their jobs need.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use the docs carefully.&lt;/strong&gt; They are excellent but moving fast: naming is mid-migration, workflow agents are still flagged as private GA, release notes can be superseded by reference pages (as with the inference mapping), and at least one SDK package ID is inconsistent. When it matters, verify in your own instance.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Twelve months ago, Opal was a promising chat assistant with a brand-new agent builder. Today the Optimizely Agent Platform has a multi-model runtime with managed sub-agent delegation, a layered context system (skills, brands, memory, files and RAG), an open tool ecosystem built around MCP, low-code workflows with real code execution, autonomous Virtual Teammates with their own identities, and a governance stack to keep it all in check. Mark is the friendly face on top, and Mark&#39;s own post-trained models are next.&lt;/p&gt;
&lt;p&gt;For those of us designing solutions on Optimizely, the platform is no longer an add-on to CMS or Commerce. It is becoming the operational layer that sits across them, and the quality of what it can do depends directly on how well the underlying platform is connected through Opti ID, Graph and the product connections.&lt;/p&gt;
&lt;p&gt;If you would like help shaping your agent platform strategy, building custom tools and MCP integrations, or getting onto CMS 13 and Commerce 15 so you can use all of this, get in touch with us at Niteco: &lt;a href=&quot;https://niteco.com/contact-us/&quot;&gt;https://niteco.com/contact-us/&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Key references&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Agent Platform documentation: &lt;a href=&quot;https://docs.optimizely.com/agent-platform/docs&quot;&gt;https://docs.optimizely.com/agent-platform/docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;2026 release notes: &lt;a href=&quot;https://docs.optimizely.com/agent-platform/release-notes&quot;&gt;https://docs.optimizely.com/agent-platform/release-notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;2025 release notes: &lt;a href=&quot;https://docs.optimizely.com/agent-platform/release-notes/2025-agent-platform-release-notes&quot;&gt;https://docs.optimizely.com/agent-platform/release-notes/2025-agent-platform-release-notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Inference level and model mapping: &lt;a href=&quot;https://docs.optimizely.com/agent-platform/docs/inference-level&quot;&gt;https://docs.optimizely.com/agent-platform/docs/inference-level&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Sub-agents overview: &lt;a href=&quot;https://docs.optimizely.com/agent-platform/docs/sub-agents-overview&quot;&gt;https://docs.optimizely.com/agent-platform/docs/sub-agents-overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Skills overview: &lt;a href=&quot;https://docs.optimizely.com/agent-platform/docs/skills-overview&quot;&gt;https://docs.optimizely.com/agent-platform/docs/skills-overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Tools overview: &lt;a href=&quot;https://docs.optimizely.com/agent-platform/docs/tools-overview&quot;&gt;https://docs.optimizely.com/agent-platform/docs/tools-overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Create custom tools (SDKs): &lt;a href=&quot;https://docs.optimizely.com/agent-platform/docs/create-custom-tools&quot;&gt;https://docs.optimizely.com/agent-platform/docs/create-custom-tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Virtual Teammates overview: &lt;a href=&quot;https://docs.optimizely.com/agent-platform/docs/virtual-teammates-overview&quot;&gt;https://docs.optimizely.com/agent-platform/docs/virtual-teammates-overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Quality tab overview: &lt;a href=&quot;https://docs.optimizely.com/agent-platform/docs/quality-tab-overview&quot;&gt;https://docs.optimizely.com/agent-platform/docs/quality-tab-overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Credits: &lt;a href=&quot;https://docs.optimizely.com/agent-platform/docs/agent-platform-credits&quot;&gt;https://docs.optimizely.com/agent-platform/docs/agent-platform-credits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Administrator guide: &lt;a href=&quot;https://docs.optimizely.com/agent-platform/docs/get-started-with-agent-platform-for-administrators&quot;&gt;https://docs.optimizely.com/agent-platform/docs/get-started-with-agent-platform-for-administrators&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Mark models and Mark-Bench press release (1 September 2026): &lt;a href=&quot;https://www.prnewswire.com/news-releases/optimizely-introduces-purpose-built-ai-models-for-marketing-that-deliver-frontier-level-quality-at-a-fraction-of-the-cost-302865074.html&quot;&gt;https://www.prnewswire.com/news-releases/optimizely-introduces-purpose-built-ai-models-for-marketing-that-deliver-frontier-level-quality-at-a-fraction-of-the-cost-302865074.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;My Opticon New York write-up: &lt;a href=&quot;/link/fecc835a60ef4678891f74e1fcbf06ea.aspx&quot;&gt;https://world.optimizely.com/blogs/scott-reed/dates/2026/9/opticon-new-york--the-strategy-on-upgrading-to-cms-13-/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>            <guid>https://world.optimizely.com/blogs/scott-reed/dates/2026/10/optimizely-agent-platform---october-2025-state-of-play/</guid>            <pubDate>Mon, 05 Oct 2026 10:45:21 GMT</pubDate>           <category>Blog post</category></item></channel>
</rss>