<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Blog posts by Andre Gabriel Coetzee</title><link href="http://world.optimizely.com" /><updated>2026-06-22T13:31:38.0000000Z</updated><id>https://world.optimizely.com/blogs/andre-gabriel-coetzee/</id> <generator uri="http://world.optimizely.com" version="2.0">Optimizely World</generator> <entry><title>Optimizely Content APIs: the Setup the Docs Don&#39;t Walk You Through</title><link href="https://world.optimizely.com/blogs/andre-gabriel-coetzee/dates/2026/6/optimizely-content-apis-the-setup-the-docs-dont-walk-you-through/" /><id>&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;CMS 13 is pushing things firmly in the direction of Optimizely Graph, but plenty of teams are still running on older CMS versions, or have good reasons to stick with the Content Delivery, Content Definitions, or Content Management APIs regardless of what version they&#39;re on. This guide is for those teams.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;What follows is a step-by-step walkthrough to get any of the content APIs up and running, tested locally, and fully understood, in one place. No jumping between API reference pages, no piecing together fragments from several different resources. Just the things the documentation doesn&#39;t make obvious, learned the hard way.&lt;/p&gt;
&lt;hr class=&quot;border-border-200 border-t-0.5 my-3 mx-1.5&quot; /&gt;
&lt;h3 class=&quot;text-text-100 mt-3 -mb-1 text-[1.125rem] font-bold&quot;&gt;&lt;strong&gt;The mental model first&lt;/strong&gt;&lt;/h3&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Optimizely uses OpenID Connect (via a library called OpenIddict under the hood) to issue JWT bearer tokens. When you want a system to be able to call one of the content APIs, you define it as an OpenID Connect application, either in code, in the CMS Admin UI, or both. We&#39;ll cover the specifics of each in the steps below.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&quot;Application&quot; in this context is not referring to your CMS instance or front-end site, it refers to discrete client identities registered specifically for machine-to-machine communication. Your CMS can have as many of these as you need.&lt;/p&gt;
&lt;h4 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&lt;strong&gt;Why grant_type=client_credentials and not something else&lt;/strong&gt;&lt;/h4&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;OAuth 2.0 has several grant types: different flows for getting a token, each suited to a different kind of requester or use case.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;strong&gt;authorization_code&lt;/strong&gt;&amp;nbsp;is for interactive users: a person clicks login, gets redirected, enters their credentials, and the app receives a token on their behalf.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;strong&gt;client_credentials&lt;/strong&gt;&amp;nbsp;is for machine-to-machine communication. Your service acts as itself, not on behalf of a user. There are no redirects, it simply exchanges its credentials directly for a token.&lt;/p&gt;
&lt;h4 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&lt;strong&gt;Why the token endpoint is at /connect/token&lt;/strong&gt;&lt;/h4&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;This is OpenIddict&#39;s default endpoint path, and OpenIddict is the OpenID Connect server library Optimizely uses under the hood. Optimizely didn&#39;t invent this URL, it&#39;s baked into the library. The &lt;strong&gt;/api/episerver&lt;/strong&gt;&amp;nbsp;prefix in front of it is the actual Optimizely part of the route, sitting in the same legacy episerver namespace you&#39;ll see across the older API paths.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;The flow from here is straightforward: you request an access token for one of these applications, then pass that token in subsequent requests to the content APIs. Importantly, the access rights you configure in the CMS are tied to the application identity itself, so when a request comes in using that token, content access is evaluated against whatever permissions that application has been granted.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;/link/97584b3856524d738de8085f065eccdb.aspx&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-6axZYk-gPv/0d7f2a7de1e85c0b2044c8e8f0f8d8592c1549a00a4746a00110d39d57f2a00ea426e35a907df27fefe33102d25f33b8158227cf15f4e51bef79f26d202ef1f636a7da0e82114cb5dc624aaddad115be213b1da5054a9af57d4bfbf223a6c0f0f6565710?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;em&gt;The Add User/Group dropdown showing the Applications section with two OpenID applications listed&lt;/em&gt;&lt;/p&gt;
&lt;hr class=&quot;border-border-200 border-t-0.5 my-3 mx-1.5&quot; /&gt;
&lt;h3 class=&quot;text-text-100 mt-3 -mb-1 text-[1.125rem] font-bold&quot;&gt;&lt;strong&gt;Let&#39;s set it all up&lt;/strong&gt;&lt;/h3&gt;
&lt;h4 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&lt;strong&gt;Step 1: Install the required NuGet packages&lt;/strong&gt;&lt;/h4&gt;
&lt;div class=&quot;relative group/copy bg-bg-000/50 border-0.5 border-border-400 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-100&quot;&gt;
&lt;div class=&quot;overflow-x-auto&quot;&gt;
&lt;pre class=&quot;code-block__code !my-0 !rounded-lg !text-sm !leading-relaxed p-3.5&quot;&gt;&lt;code&gt;dotnet add package EPiServer.OpenIDConnect
dotnet add package EPiServer.OpenIDConnect.UI # Optional&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;You&#39;ll also need the API package for whichever API you&#39;re securing:&lt;/p&gt;
&lt;ul class=&quot;[li_&amp;amp;]:mb-0 [li_&amp;amp;]:mt-1 [li_&amp;amp;]:gap-1 [&amp;amp;:not(:last-child)_ul]:pb-1 [&amp;amp;:not(:last-child)_ol]:pb-1 list-disc flex flex-col gap-1 pl-8 mb-3&quot;&gt;
&lt;li class=&quot;font-claude-response-body whitespace-normal break-words pl-2&quot;&gt;Content Delivery API: &lt;strong&gt;EPiServer.ContentDeliveryApi.Cms&lt;/strong&gt;&lt;/li&gt;
&lt;li class=&quot;font-claude-response-body whitespace-normal break-words pl-2&quot;&gt;Content Management API: &lt;strong&gt;EPiServer.ContentManagementApi&lt;/strong&gt;&lt;/li&gt;
&lt;li class=&quot;font-claude-response-body whitespace-normal break-words pl-2&quot;&gt;Content Definitions API: &lt;strong&gt;EPiServer.ContentDefinitionsApi&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;strong&gt;Version gotcha:&lt;/strong&gt; Make sure your &lt;strong&gt;EPiServer.OpenIDConnect &lt;/strong&gt;version is compatible with your CMS version. Mismatched versions are a common source of silent failures at startup.&lt;/p&gt;
&lt;h4 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&lt;strong&gt;Step 2: Wire up the configuration in code&lt;/strong&gt;&lt;/h4&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;In your &lt;strong&gt;Startup.cs&lt;/strong&gt;, you need to configure OpenID Connect and register whichever content APIs you need:&lt;/p&gt;
&lt;div class=&quot;relative group/copy bg-bg-000/50 border-0.5 border-border-400 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-100&quot;&gt;
&lt;div class=&quot;overflow-x-auto&quot;&gt;
&lt;pre class=&quot;code-block__code !my-0 !rounded-lg !text-sm !leading-relaxed p-3.5&quot;&gt;&lt;code&gt;public void ConfigureServices(IServiceCollection services)
{
    // ASP.NET Identity must be configured before OpenID Connect
    services.AddCmsAspNetIdentity&amp;lt;ApplicationUser&amp;gt;();

    services.AddOpenIDConnect&amp;lt;ApplicationUser&amp;gt;(
        useDevelopmentCertificate: _webHostingEnvironment.IsDevelopment(),
        createSchema: true,
        options =&amp;gt;
        {
            // Seeding an application here is optional: see note below
            options.Applications.Add(new OpenIDConnectApplication
            {
                ClientId = &quot;your-client-id&quot;,
                ClientSecret = &quot;your-client-secret&quot;,
                Scopes =
                {
                    &quot;epi_content_delivery&quot;,
                    &quot;epi_content_management&quot;,
                    &quot;epi_content_definitions&quot;
                }
            });
        });

    services.AddOpenIDConnectUI(); // Optional: adds the OpenID Connect panel in CMS Admin

    services.AddContentDeliveryApi(OpenIDConnectOptionsDefaults.AuthenticationScheme);
    services.AddContentDefinitionsApi(OpenIDConnectOptionsDefaults.AuthenticationScheme);
    services.AddContentManagementApi(OpenIDConnectOptionsDefaults.AuthenticationScheme);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;A few things worth noting here:&lt;/p&gt;
&lt;ul class=&quot;[li_&amp;amp;]:mb-0 [li_&amp;amp;]:mt-1 [li_&amp;amp;]:gap-1 [&amp;amp;:not(:last-child)_ul]:pb-1 [&amp;amp;:not(:last-child)_ol]:pb-1 list-disc flex flex-col gap-1 pl-8 mb-3&quot;&gt;
&lt;li class=&quot;font-claude-response-body whitespace-normal break-words pl-2&quot;&gt;Each API registration method accepts an authentication scheme parameter, don&#39;t leave it out. Without it, the API has no way to validate incoming Bearer tokens and will return a &lt;strong&gt;401&lt;/strong&gt; regardless of whether your token is valid. Pass &lt;strong&gt;OpenIDConnectOptionsDefaults.AuthenticationScheme&lt;/strong&gt; to wire them up correctly.&lt;/li&gt;
&lt;li class=&quot;font-claude-response-body whitespace-normal break-words pl-2&quot;&gt;&lt;strong&gt;useDevelopmentCertificate &lt;/strong&gt;: tying this to &lt;strong&gt;IsDevelopment() &lt;/strong&gt;means it uses a local dev certificate in your local environment and expects a real one in production. On DXP, certificates are provided automatically via &lt;strong&gt;EPiServer.CloudPlatform.Cms&lt;/strong&gt; 1.6.1 or later.&lt;/li&gt;
&lt;li class=&quot;font-claude-response-body whitespace-normal break-words pl-2&quot;&gt;Scopes: each scope string controls which API that client is permitted to call, and must match the scope you pass when requesting a token. There are constants available if you&#39;d prefer not to use raw strings: &lt;strong&gt;ContentDeliveryOptionsDefaults.Scope&lt;/strong&gt;, &lt;strong&gt;ContentDefinitionsApiOptionsDefaults.Scope&lt;/strong&gt;, and &lt;strong&gt;ContentManagementApiOptionsDefaults.Scope&lt;/strong&gt;.&lt;/li&gt;
&lt;li class=&quot;font-claude-response-body whitespace-normal break-words pl-2&quot;&gt;Seeding applications in code is optional. The &lt;strong&gt;options.Applications.Add(...)&lt;/strong&gt; block pre-creates the client on startup, which is convenient for local development. If you&#39;d rather manage clients entirely through the UI, you can omit it, that&#39;s what the &lt;strong&gt;AddOpenIDConnectUI()&lt;/strong&gt; line enables. It adds an OpenID Connect section under Settings in CMS Admin where you can create and manage applications directly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-oki8fxiyA7/5cd57dd6794f296df6654bc4c7aa473b1171a4d8575e67bd58ded795e9c955eead6210f9123ca6f85e7f93ea9de301c69d838208b36c8325f6744862c30110e55fd837c8291ec1448d6c817c7bd3e8ff859e13f6e235447f2ac3d73256f2466a0dfb76c4?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;em&gt;The &quot;OpenID Connect&quot; settings panel can be seen in the screenshot above.&lt;/em&gt;&lt;/p&gt;
&lt;h4 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&lt;strong&gt;Step 3: Request a token&lt;/strong&gt;&lt;/h4&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Once you have an application configured, either in code or the UI, you first need to request an access token for the application before calls to any of the content APIs can be made.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Using curl:&lt;/p&gt;
&lt;div class=&quot;relative group/copy bg-bg-000/50 border-0.5 border-border-400 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-100&quot;&gt;
&lt;div class=&quot;overflow-x-auto&quot;&gt;
&lt;pre class=&quot;code-block__code !my-0 !rounded-lg !text-sm !leading-relaxed p-3.5&quot;&gt;&lt;code&gt;curl -X POST https://your-site.com/api/episerver/connect/token \
  -H &quot;Content-Type: application/x-www-form-urlencoded&quot; \
  -d &quot;grant_type=client_credentials&amp;amp;client_id=your-client-id&amp;amp;client_secret=your-client-secret&amp;amp;scope=epi_content_management&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Using Postman:&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-gPVIKzvOJE/f7bf91e744372274b5dccfdc85dd0dbc9baa8eb91fdee7b93a4b8c26793ee8898b698a77b9c5aa805a5fc3934d04b8642cbc3a6059ffaa92136afd1eaa2eb5f17688bd706702a0fc3613ac6f7c6d74fb7898db17dbd5b35368fd65fae32753032b11c7e6?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Set the request to POST, the URL to your token endpoint, and under the Body tab select &lt;strong&gt;x-www-form-urlencoded&lt;/strong&gt;. Add the four key-value pairs: &lt;strong&gt;grant_type, client_id, client_secret, scope&lt;/strong&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;A successful response looks like this:&lt;/p&gt;
&lt;div class=&quot;relative group/copy bg-bg-000/50 border-0.5 border-border-400 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-100&quot;&gt;
&lt;div class=&quot;overflow-x-auto&quot;&gt;
&lt;pre class=&quot;code-block__code !my-0 !rounded-lg !text-sm !leading-relaxed p-3.5&quot;&gt;&lt;code&gt;{
  &quot;access_token&quot;: &quot;eyJhbGci...&quot;,
  &quot;expires_in&quot;: 3599,
  &quot;token_type&quot;: &quot;Bearer&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Copy the &lt;strong&gt;access_token&lt;/strong&gt; value, you&#39;ll use it in Step 5.&lt;/p&gt;
&lt;h4 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&lt;strong&gt;Step 4: Assign CMS access rights to the client&lt;/strong&gt;&lt;/h4&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;This is the most commonly missed step, and it causes some of the most confusing symptoms, you have a valid token, your requests aren&#39;t getting &lt;strong&gt;401s&lt;/strong&gt;, but you&#39;re getting empty results or unexpected &lt;strong&gt;403s&lt;/strong&gt;.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;The API client you created is effectively a user in the CMS. Like any user, it needs to be granted access rights to the content it&#39;s trying to read or write. There are different levels you could configure access rights, let&#39;s get into them below.&lt;/p&gt;
&lt;h5 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&amp;nbsp;&lt;/h5&gt;
&lt;h5 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&lt;strong&gt;Global access rights&lt;/strong&gt;&lt;/h5&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Global access rights apply at the page or section level in your content tree and cascade down to every item beneath. For instance, if you want to deny a specific OpenID application Read access to a sensitive area, say, an &quot;Admin&quot; page or a &quot;Members Only&quot; section, you set the restriction at that page, and the cascade ensures every sub-page inherits the same rule automatically. You don&#39;t have to configure each one individually.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-wHVz_SRQT6/9a2b8822c3e7ca4442dfc503adbbff00f14ed9b74eedbd81d1487dec35f33061b21d46266513d0c1e8ecbc519ea9c4fe60e0db4cdf5c8676fc955aad580be95add7cc628e35c54434f4363ddf43a528a0c579d79c3413007238721d09e47b0710261574c?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;em&gt;On this level, either all content is returned, or none of it. Either 200-OK, or 403-Forbidden&lt;/em&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Navigate to Settings &amp;rarr; Set Access Rights, select the page or section you want the client to access, and add it using Add User/Group. Your registered applications (from either the code or created in the CMS on the OpenID Connect settings panel) will appear under the Applications group in the dropdown.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Grant it the appropriate permissions: Read is sufficient for Content Delivery, while Content Management will need Read, Change, and Publish depending on what operations you need to perform. Check Apply settings for all subitems to cascade the rights down the content tree.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-G7Oqc_d9dv/65f22110078ca502ef75b1a83a1ec3652580830c21791120f685a2cfde53b42d62737429b8ba88f183d645b66eb5160468ef90752518b2e807fd94ba6fff3b580442c4c65a742453a0e8a73515650aa577db9adad1b41ccb824c0997d8618475f3621c33?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;em&gt;alloy-client application granted Read access on the Start page, cascading to all subitems.&lt;/em&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Skipping this step produces one of two outcomes depending on your setup: either the API returns a &lt;strong&gt;403&lt;/strong&gt; Forbidden response, or it returns content that was never actually restricted to begin with, because the Everyone group already has read access. In the latter case everything appears to be working, but your access rights configuration isn&#39;t doing anything. If you ever tighten down content permissions, your client will silently lose access.&lt;/p&gt;
&lt;h5 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&amp;nbsp;&lt;/h5&gt;
&lt;h5 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&lt;strong&gt;Content-level access rights (per page/block instance in the tree)&lt;/strong&gt;&lt;/h5&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Content-level access rights give you more granular control than the global settings screen, you can restrict access to specific pages or blocks directly in the content tree rather than applying a blanket rule across everything.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;A practical example: imagine you have a pricing page that should only be visible to authenticated partners. Rather than locking down your entire content tree, you can grant your API client access to everything by default and then explicitly restrict access on those specific items, or invert it entirely, grant no global access, and only open up the specific content the client needs to see.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-KagMkB0nHI/84fd374988f4ffa674e61c110c8dd272a70b53a3aaac9b9e69a3c7a2f7904af1773ebb6a4731948214519ccbc5ec37577bafd38a338a450b09e5e0eba3bb6fa71cdd6284c0442e856b1467f4de13af0e9fd093cf7735eb69e73f71e0c32465a8cf3f00c1?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;em&gt;Same request, different response. The restricted client&#39;s &quot;Pricing&quot; entry isn&#39;t returned as a 403 error, it&#39;s simply omitted from the response. This is what makes content-level access rights powerful: the client doesn&#39;t need to know what they can&#39;t see, and the API does the filtering server-side.&lt;/em&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;The result isn&#39;t binary. It&#39;s not just 200 or 403, you can build quite precise access models by combining global and content-level rights.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Content-level access rights are set directly on a specific page or block in the edit interface. To access them, right-click the item in the content area or page tree and select Edit, then look for the Visible to panel at the top of the properties view. If access is unrestricted it will show as Everyone. Click Manage to open the Access Rights dialog and configure it the same way as the global screen.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-qR4ivN8SRr/b1a50ba1bf085a982d94ee754c078da101225a16e25244fc5d9c8cbc736292fe097f308e83577c3f01d0fae8184840d72d542e812eb4618722716dc813d7e5abb3de10c714b93db366a20b4bd0b3c5b580d83a9ddbd9f031491b687fa4d36d3d1d50f4ca?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;em&gt;Right-clicking a content item in the page tree to access its properties&lt;/em&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-WSEoq3HxlV/50e6bbbae79aa96c3d964669a0a78de6d46fef1b38a276d48351652090e0cf572a26a24277995b9aac202280dd6e1a98f69eb35f29ab8c264b53d408910f12febb1db17a875ab48629dde4cf5385c47fd04443d133025250ed1f71e4beddb28d649acdcf?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;em&gt;The &quot;Visible to&quot; panel showing the current access restriction status for a content item, with the Manage link to open the Access Rights dialog&lt;/em&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-gRug85CPa2/a655f207b12969e892ee42f18c06fe4559edc2458edd8eac4fdba97ca66f1b57940a77f4656e4e94deb5d320983ab7ce7b56f93992910a7c1f502243c1294c8c8349eb52f165b0cbd6caddda4cbe605aaf827322729400de2e1d68bc28f44516d684351c?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;em&gt;The Access Rights dialog for a specific content item, with alloy-client granted Read access and &quot;Inherit settings from parent item&quot; unchecked to override the global defaults&lt;/em&gt;&lt;/p&gt;
&lt;h4 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 class=&quot;text-text-100 mt-2 -mb-1 text-base font-bold&quot;&gt;&lt;strong&gt;Step 5: Make your first authenticated request&lt;/strong&gt;&lt;/h4&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;With your token in hand (from Step 3) and access rights set, you can now call the API. Pass the token as a Bearer token in the &lt;strong&gt;Authorization&lt;/strong&gt; header.&lt;/p&gt;
&lt;div class=&quot;relative group/copy bg-bg-000/50 border-0.5 border-border-400 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent-100&quot;&gt;
&lt;div class=&quot;overflow-x-auto&quot;&gt;
&lt;pre class=&quot;code-block__code !my-0 !rounded-lg !text-sm !leading-relaxed p-3.5&quot;&gt;&lt;code&gt;curl -X GET https://your-site.com/api/episerver/v3.0/content/{contentGuid} \
  -H &quot;Authorization: Bearer eyJhbGci...&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-FU_Ev1Aesh/e10d121a20a2164e0dae719606c8455da4b0249791f5133404aa422e9e1305da7a25b2c0c706bad935e45143206bb1573e2b9cc0d2ba6bd276a2b647b477549c2853b76b35191888168aaa288e0c1580f4596e7bf7d5c0283f5f076a95e09b06e0cebea2?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-NXSO-5LtU5/e6aa4fcd051b0c43b89b8c85ad1dce2c781e14eb93a6f63afb206b3780777bf8e57bab1dadce5aa8e54b7a205305f2ba10bfb1a792846708e43936e9ae6a37e80ab382f0de3587e031b5aaa2dc4a88bf2655eece10f86e21527fd89bd1e5f87d155ff6b0?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;em&gt;Success response from the Content Delivery API (200 - OK)&lt;/em&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;However, if I revoke read access from the OpenId Application, I get the following:&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-qxuy8jsqAm/3bb819b8f43ca3a5307fb37c86c495da49bb3b98fede2645dcb989df111a4d3581549553ca3323489dbc9c169cdfbba8cfcf395c34c048d66d598408810c86281c422c293e4bed5605e2a0e7099c49380d8c3bbc2d3725707840509617be42aa044340b0?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-4bM8czjC1c/1f9f13813cdbbf1744799c9aa89168fde0b36192909142daf589b5707d800ec9b9c91de53a2dfe3bf26c28d318cf550e9cedf3f32a18daea6b22f5dd0f884ca723c2ec77b1a9270724da6dc853706a241ee58975287d316102a1a095bef86ee8849e61bb?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;em&gt;Forbidden response from Content Delivery API (403 - Forbidden)&lt;/em&gt;&lt;/p&gt;
&lt;hr class=&quot;border-border-200 border-t-0.5 my-3 mx-1.5&quot; /&gt;
&lt;h3 class=&quot;text-text-100 mt-3 -mb-1 text-[1.125rem] font-bold&quot;&gt;&lt;strong&gt;Common errors and what they actually mean&lt;/strong&gt;&lt;/h3&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;For the last example, the &lt;strong&gt;403 &lt;/strong&gt;is exactly what we&#39;d expect, but it&#39;s worth understanding the difference between a &lt;strong&gt;401&lt;/strong&gt; and a &lt;strong&gt;403&lt;/strong&gt;, since they&#39;re easy to confuse and point to very different problems.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;strong&gt;401 Unauthorized&lt;/strong&gt; means the API doesn&#39;t recognise or can&#39;t validate the token itself. Either it&#39;s malformed, it&#39;s expired, or the API isn&#39;t configured to accept it. Check that you&#39;re passing &lt;strong&gt;OpenIDConnectOptionsDefaults.AuthenticationScheme &lt;/strong&gt;into your API registrations in &lt;strong&gt;Startup.cs&lt;/strong&gt;, that your token hasn&#39;t expired (3599 seconds), and that your &lt;strong&gt;client_id&lt;/strong&gt; and &lt;strong&gt;client_secret&lt;/strong&gt; match exactly what&#39;s registered in CMS Admin.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;strong&gt;403 Forbidden&lt;/strong&gt; means the token is valid and recognised, but the application it belongs to doesn&#39;t have permission to access that content. This is the access rights problem covered in the previous step, the API knows who you are, it just won&#39;t let you in.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;strong&gt;Token endpoint returns 404.&lt;/strong&gt; The OpenID Connect middleware isn&#39;t registered, or the package isn&#39;t installed. Confirm the NuGet package is installed and correctly configured in &lt;strong&gt;Startup.cs&lt;/strong&gt;.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;strong&gt;invalid_client error from the token endpoint.&lt;/strong&gt; The &lt;strong&gt;client_id&lt;/strong&gt; or &lt;strong&gt;client_secret&lt;/strong&gt; doesn&#39;t match what&#39;s registered in CMS Admin (or code). These must be identical, including case.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;strong&gt;invalid_scope error from the token endpoint.&lt;/strong&gt; The scope you&#39;re requesting (&lt;strong&gt;epi_content_management&lt;/strong&gt;, etc.) doesn&#39;t match what was registered for the client in CMS Admin.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;&lt;strong&gt;unsupported_grant_type from the token endpoint.&lt;/strong&gt; The client was not registered with the &lt;strong&gt;client_credentials&lt;/strong&gt; grant type. Check the application configuration in CMS Admin and ensure Client Credentials is selected as a permitted grant type.&lt;/p&gt;
&lt;hr class=&quot;border-border-200 border-t-0.5 my-3 mx-1.5&quot; /&gt;
&lt;h3 class=&quot;text-text-100 mt-3 -mb-1 text-[1.125rem] font-bold&quot;&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/h3&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;Getting the Optimizely content APIs working for the first time involves piecing together information from several different sources, the official docs, community posts, and a fair amount of trial and error. Hopefully this post brings it all into one place and saves someone the same legwork.&lt;/p&gt;
&lt;p class=&quot;font-claude-response-body break-words whitespace-normal&quot;&gt;If you&#39;re already on CMS 13 or considering the move, Optimizely Graph changes the delivery layer significantly and much of the authentication setup described here won&#39;t apply in the same way. But for teams on CMS 12 or those still needing to use any of the content APIs, this remains the path.&lt;/p&gt;</id><updated>2026-06-22T13:31:38.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Optimizely Opal: How to Build Effective Workflow Agents</title><link href="https://world.optimizely.com/blogs/andre-gabriel-coetzee/dates/2026/5/my-optimizely-opal-workflow-agent-experiment-what-worked-what-didnt/" /><id>&lt;div&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;If you&#39;re building workflow agents in Optimizely Opal, this post covers how specialized agents pass context to each other, why keeping agents small and focused matters, and a useful trick for handling situations where you&#39;re not sure how to proceed.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;I recently decided to put Optimizely Opal through its paces. The experiment: build a simple mock API, feed external data into Opal, and see if a workflow agent could take it from there and autonomously publish content to the CMS, with no human intervention required.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;The workflow agent I built pulls data from an external mock API on a schedule, analyzes it, checks it against existing content in the CMS, makes a judgement call on whether a new article is warranted, and if it is, writes and publishes it to the CMS, all without any human intervention.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;It is made up of 5 specialized agents, each with a single responsibility, and a condition that branches the workflow based on whether a new article is warranted.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;My example consists of the following:&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;An agent that retrieves route information from the mock AviationStack API&lt;/li&gt;
&lt;li&gt;An agent that compares the route info with articles published in the CMS, and makes a judgement call on whether the info from the API call warrants a new article in the CMS&lt;/li&gt;
&lt;li&gt;An agent that writes an article based on the info received from the external mock AviationStack API&lt;/li&gt;
&lt;li&gt;An agent that publishes the new article to the CMS&lt;/li&gt;
&lt;li&gt;An agent that gracefully ends the workflow if no new article is needed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br /&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-etsUzv2Dz7/3408a8416b38371b4a0a1011e247e5b4e5e77bb263977247b0959774ff8866ac828fd6d9e6d45d4a68071da6794cd9b31e81b41c9d373cd0f98c28b0fa85bc306cb1f0ba3f63e13021abea570172c64fdace84bb6a029614852e4ee55aec1581d21f86d7?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;&lt;em&gt;&lt;br /&gt;The full workflow agent. A scheduler triggers the process, data flows through each specialized agent in sequence, and a condition branches the workflow: TRUE continues to writing and publishing, FALSE ends the workflow cleanly&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;If you want to build a similar workflow agent yourself and apply some of the tips here in practice, here&amp;rsquo;s what you need:&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li style=&quot;font-size: 14pt;&quot;&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;Optimizely Opal&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;font-size: 14pt;&quot;&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;Access to Optimizely CMS&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;font-size: 14pt;&quot;&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;A mock API with a Custom Tool configured in Opal&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;font-size: 14pt;&quot;&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;Basic familiarity with creating specialized agents in Opal&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;font-size: 36pt;&quot;&gt;Here&#39;s what I learned&lt;/span&gt;&lt;span style=&quot;font-size: 36pt; color: rgb(149, 165, 166);&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/h1&gt;
&lt;hr /&gt;
&lt;h1 style=&quot;text-align: left;&quot;&gt;There&amp;rsquo;s no prompting between specialized agents&lt;/h1&gt;
&lt;div&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;Each agent passes its output directly to the next and runs completely on its own, with no human in the loop. This keeps the workflow consistent and predictable, but it also means you need to be deliberate about what each agent outputs, because the next agent depends on it entirely.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;br /&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;Agent 1 output:&lt;/span&gt;&lt;strong&gt;&lt;br /&gt;&lt;/strong&gt;&lt;br /&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-x86SnAX_nl/04321c3dacc79ff08a0e50ab4ef5d1395bce005a2833c489d7a4b07620421b8a25558afd065d4f84eb5a8f142735d8c7a16f055f22ffdbba483ce02c24b572933eea4059a10bcbd7fc2bacf09d32a9b319a21a261d2e93613658e2309e64058eb5ad7f3d?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&lt;em&gt;&lt;br /&gt;The first specialized agent outputs a simple JSON object containing the route information retrieved from the mock AviationStack API&lt;/em&gt;&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;Agent 2 Input:&lt;/span&gt;&lt;strong&gt;&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-E_3xcxTabY/912d743ab0f4926d01b83641f9962891a9e74717061f1b1627c8f4f13f56130f0d16de0c633d3ad468347b2e40fea7cc98acf787fd96287fe03251229eaec0c03447a6715e5b27610f0a668a3b0460a039269b4d430926f2532d7e4412ac13e3519eb0eb?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;&lt;/strong&gt;&lt;br /&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-QB8U70NVT3/7b84ae2291e4ee54ee6e79af84649770c8f4e0ad6abf05b347e3298ddf6f1c04513c49a97a83df7bf2fd0511b0752f875c57abdafbc7c009a2d1f7342a09b7e88afe36eac29b803c33d28cf09c3b1f31fa9c0de26b2024b9a27bcf879ca1971af303a6cb?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;div&gt;&lt;em&gt;That same &quot;routes&quot; object becomes the input for the next agent, defined as a required variable and referenced directly in the prompt template. This is what &quot;output becomes input&quot; looks like in practice.&lt;/em&gt;&lt;/div&gt;
&lt;hr /&gt;
&lt;h1&gt;Create as many specialized agents as you need&lt;/h1&gt;
&lt;div&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;Don&#39;t hesitate to create as many agents as your workflow needs, and be as granular as necessary. Each agent should do one thing well, not many things adequately.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;
&lt;div&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;Developers will recognize this as the Single Responsibility Principle (SRP), and it applies just as well here. For those less familiar with the term, the idea is simple: don&#39;t create one agent that retrieves external data, analyzes it, writes an article, and publishes it. That&#39;s too much for one agent to handle reliably, and you&#39;ll end up with bloated instructions. Split those responsibilities across separate agents instead, and let them pass context to each other.&lt;/span&gt;&lt;/div&gt;
&lt;br /&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-sMM-Tmyes9/61ea231a49ab6ce082d59c3280cebe49ccf630168f5711d6ac95fd1bf44ae642972dce83161d529b5fba3bffcc92e05f253c88f6455a1b8956db6c2f0a7e6515195ad3317906a8ac44f162bd09dad40ac87b8d156b245522a56d496a965b52e663bfb3e9?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&lt;em&gt;A single agent handling too many responsibilities. This is harder to debug, harder to maintain, and more likely to produce inconsistent results.&lt;/em&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h1&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-gjt814Dg6l/3b1f8e8e38428374d003026b0770899e52979739d84faf1f7befc8627b0c86aafee258fd6aa56c0aed5fd7fb1e290551d996993d788d68c2960f742fce7bc24298bbda5ddd838b27ca48309889ac291c38cf1085afa53fd8139594e9415051527e0eadfa?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/h1&gt;
&lt;div&gt;&lt;em&gt;The same workflow split across three focused agents, each with a single responsibility, and each passing its output to the next.&lt;/em&gt;&lt;/div&gt;
&lt;hr /&gt;
&lt;h1&gt;If you run into a gray area and you&#39;re not sure how to handle it, create a specialized agent&lt;/h1&gt;
&lt;div&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;This is simpler than it sounds. When you hit a question like &quot;How do I get it to write an article?&quot;, you create a specialized agent for it. &quot;How do I get it to publish to the CMS?&quot;, you also create a specialized agent for it. Most problems in a workflow agent have the same answer: a focused, purpose-built specialized agent.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;A good example from my own workflow: when the condition evaluates to false, meaning the external data doesn&#39;t warrant a new article, I needed the workflow to simply stop cleanly. There&#39;s no built-in &quot;do nothing&quot; option, but the answer was straightforward: create an &quot;End Workflow&quot; agent whose only job is to acknowledge the decision and exit gracefully.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-2tv3E7USqI/7715b92663891aeb140f10a67422095138d453cdaa125065284a4f1ba266274aa0a691cb8a78ffa01e8fa74f202faea083499b389d56a38ad325e7d628c2d3fc61dcd1254f253d452d8fb0f0d48db3d644f2eadc9b3eff5646f43bf733d3203320c4ecbe?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&lt;em&gt;&lt;br /&gt;The &quot;End Workflow&quot; agent keeps it simple by design. No tools, no content creation, no external calls. Its only job is to end the workflow cleanly when no action is needed, which is exactly the kind of focused single-responsibility agent your workflow will thank you for later.&lt;/em&gt;&lt;/div&gt;
&lt;hr /&gt;
&lt;h1&gt;Don&#39;t assume an agent has context from earlier in the workflow unless you explicitly pass it&lt;/h1&gt;
&lt;div&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;This is an important one. Just because multiple specialized agents live inside the same workflow agent doesn&#39;t mean they automatically share context. If Agent 4 needs data that Agent 1 retrieved, you need to explicitly pass it through each agent&#39;s inputs and outputs along the way, it won&#39;t be there by default.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;In my workflow, by the time the article-writing agent needs to do its job, it has no automatic awareness of the route data retrieved at the start. If I want it to have that context, I need to explicitly include it in the outputs of the agents before it, and specify it as an input to the agents that follow.&lt;/span&gt;&lt;/div&gt;
&lt;h1&gt;&lt;img src=&quot;https://codaio.imgix.net/docs/-XvL0D-nmX/blobs/bl-arF8aKa8Nu/28333b43056d181ee30677a8a0c36cbc391b8cdd2353caa86ebbcdc85376aab333272045c1b19ab42d79436a11f4d4b7e0a2daf625a1b2fd25f3c49b22df4af72eee1c908366c15327e2f333003cd3800e1aabad69f9f8dbc01cb8fa3eb4c1ab1722a169?fit=max&amp;amp;fm=webp&amp;amp;lossless=true&quot; alt=&quot;image.png&quot; /&gt;&lt;/h1&gt;
&lt;div&gt;&lt;em&gt;The output schema of my &quot;Gap Analyzer&quot; agent explicitly carries the &quot;routes&quot; object forward, alongside its own &quot;reason&quot; and &quot;publishDecision&quot; fields. Without &quot;routes&quot; being defined here as an output, the article-writing agent downstream would have no access to the original API data, even though it was retrieved earlier in the same workflow.&lt;br /&gt;&lt;/em&gt;&lt;hr /&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;That&#39;s about it for now! Hope this helps someone else out. If you&#39;ve built something similar or have any questions or suggestions, I&#39;d love to hear about it in the comments. Thanks for reading!&lt;/span&gt;&lt;/div&gt;
&lt;h1&gt;&lt;br /&gt;&lt;br /&gt;&lt;/h1&gt;</id><updated>2026-05-20T09:04:10.0000000Z</updated><summary type="html">Blog post</summary></entry></feed>