Syncing External Data into Optimizely Graph: A Step-by-Step Guide

Optimizely Graph is built to serve content fast. But most real-world projects don’t run on CMS content alone. You often need data from other systems — a PIM, a CRM, a DAM, or a custom API — available alongside your CMS content.

This is where external data sync comes in. Instead of calling multiple APIs from the front end, you bring external data into Optimizely Graph itself. One query. One schema. One source of truth for your content layer.

In this post, I’ll walk through how to sync external data into Optimizely Graph, the setup involved, and the issues you’re likely to run into along the way.

Pushing the data to Optimizely Graph

Setting this up takes three steps: authenticate, define the schema, then push the content.

Step 1: Authentication

For pushing external data, use your App Key and App Secret. Encode them as a Base64 string in the format appKey:appSecret, then pass it as a Basic Authorization header:

Use this token in the Authorization header for both the schema and content endpoints:

Authorization: Basic <base64-encoded-token>

For production, HMAC authentication is preferable to Basic, since it verifies message integrity without ever transmitting the secret in a decryptable form.

Step 2: Define the schema

Before you can push PIM data in, Optimizely Graph needs to know its shape. You define your external content types as JSON and send them to Optimizely Graph, which generates the matching GraphQL schema.

curl --location --request PUT 'https://cg.optimizely.com/api/content/v3/types?id=opck' \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Basic <base64-encoded-token>' \
  --data @content_type.json

A sample content_type.json would look as below.

{
    "languages": ["en"],
    "contentTypes":{
       "FeedArticleOPCK": {
       "contentType": ["_Item"],
       "properties": {
          "Title":   { "type": "String", "searchable": true },
          "Body":    { "type": "String"}
        }
     }
   }
}

A few things worth knowing before you run this:

  • The source name — opck in our case — must be 4 characters or fewer.
  • Content type names — FeedArticleOPCK in our case — must be unique across all sources and must not conflict with existing CMS content types.
  • To make a field searchable, add "searchable": true to its definition.

Step 3: Push the content

With the schema in place, you can now ingest actual content records. Each record you send must match a content type you defined in Step 2.

curl --location --request POST 'https://cg.optimizely.com/api/content/v2/data?id=opck' \
  --header 'Content-Type: text/plain' \
  --header 'Authorization: Basic <base64-encoded-token>' \
  --data-binary @content_data.ndjson

A sample content_data.ndjson would look as below.

{"index": { "_id": "a3d8e21c-5f6b-4e9a-9c2d-1b7e4f0a8d3c", "language_routing": "en"    }}
{"Title___searchable":"Stainless Steel Insulated Water Bottle","Body":"32 oz double-wall vacuum insulated bottle, keeps drinks cold for 24 hours or hot for 12 hours.","ContentType":["FeedArticleOPCK", "_Item"], "_itemMetadata": { "type": "FeedArticleOPCK","key":"a3d8e21c-5f6b-4e9a-9c2d-1b7e4f0a8d3c","displayName___searchable":"Stainless Steel Insulated Water Bottle","lastModified":"2026-06-24T10:00:00Z" },  "Status": "Published", "RolesWithReadAccess":"Everyone"}
{"index": { "_id": "b7f19a3d-2e8c-4d5f-a1b6-9c3e5d7f2a4b", "language_routing": "en"    }}
{"Title___searchable":"Ergonomic Mesh Office Chair","Body":"Adjustable lumbar support, breathable mesh back, and height-adjustable armrests for all-day comfort.","ContentType":["FeedArticleOPCK","_Item"],"_itemMetadata": { "type": "FeedArticleOPCK","key":"b7f19a3d-2e8c-4d5f-a1b6-9c3e5d7f2a4b","displayName___searchable":"Ergonomic Mesh Office Chair","lastModified":"2026-06-24T10:00:00Z" },"Status": "Published", "RolesWithReadAccess":"Everyone"}

A few things worth knowing:

  • The id query parameter must match the source ID you used when defining the content type in Step 2 — opck in our case.
  • The payload must be NDJSON — each index action line is immediately followed by its content line.
  • Batch multiple records into a single NDJSON file and push them in one request. This keeps large syncs fast and avoids hitting rate limits.
  • If content with the same _id is pushed again, it overwrites the existing record — useful for keeping data in sync on a recurring basis.
  • Since Title is defined as searchable: true in the schema but Body is not, only Title needs the ___searchable suffix during ingestion. Body is sent as a plain field name, with no suffix.
  • You also need to include _metadata.type in the payload, set to the content type name (e.g. FeedArticleOPCK). This tells Optimizely Graph which schema type the record belongs to during ingestion.

Once the sync completes, confirm the records landed correctly by querying them back through GraphiQL or your GraphQL client:

{
  FeedArticleOPCK {
    total
    items {
      Title
      Body
    }
  }
}

If a field doesn’t return data, double-check that its name and ___searchable suffix match exactly what you defined in the schema — a mismatch here is the most common reason a query returns empty results.

Connecting External Sources in Optimizely

Once your data is ingested and verified in Graph, the next step is bringing it into the CMS itself — so editors can use it inside pages, experiences, and blocks.

  1. Connect from Graph

In CMS, go to Admin > Content Types > Create new… > Connect from Graph. In the Connect from Graph window, select the Source — this is the external source you defined in Graph (opck in our case).

CMS reads the schema from that source and creates a connected content type. This connected type mirrors your external schema (FeedArticleOPCK) and appears in the Content Type list alongside your regular CMS content types. It’s read-only — you can’t edit its structure in CMS, since it reflects what’s defined in Graph.

Once the connected content type is created, you can verify the ingested content inside Content Manager as below.

2. Create a block type

Next, create a standard CMS block type — the one editors will actually use inside pages and experiences. Define properties on this block that correspond to the fields you want to surface from the connected type, such as Title and Body.

Also enable this block type as a section, so editors can add it directly into an experience via Visual Builder.

This block is what your editors will actually work with — the connected type stays behind the scenes.

3. Map the connected type to the block

With both in place, go to Create Mappings. Select your connected content type (FeedArticleOPCK) as the source, and your new block type as the target.

For each property on the block (target), select the matching property from the connected type (source) — for example, map Title to Title and Body to Body. From here, editors can drop this block into any page or experience, with the external data flowing through automatically.

Adding the Connected Content to a Page

With the mapping in place, editors can now bring this external content into an actual page.

Open the page or experience in the Optimizely Edit UI, select the FeedArticleOPCKBlockType block. Once the block is added, click Connect Content. This opens a popup as shown below.

The popup displays the data ingested into Graph. Selecting an item from the list populates the block automatically with the mapped fields — Title and Body.

A link badge on the block confirms it’s tied to an external source, so editors can tell at a glance that this content isn’t authored directly in CMS.

Final Thoughts

Once your external data is authenticated, schema-defined, and pushed into Optimizely Graph, it behaves just like any other content — you can query it, connect it, and use it in CMS.

The pattern holds regardless of where your data comes from — a PIM, a CRM, or any other external system. Authenticate, define the schema, push the data, connect it in CMS, map it to a block, then let editors place it on a page. From there, your editors and your front end don’t need to know or care where the content originated — it’s all just content in Optimizely.

Happy Optimizing!!!

Leave a comment