> ## Documentation Index
> Fetch the complete documentation index at: https://launchdarkly.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Sanity CMS integration

<View title="Developer" />

<View title="Federal docs" />

<View title="EU docs" />

This topic explains how to use the integration between LaunchDarkly and Sanity CMS. The integration connects LaunchDarkly feature flags to Sanity Studio so that content editors can manage A/B/n test variations on individual content fields directly inside Sanity, while LaunchDarkly handles experiment targeting and traffic allocation.

Use the integration to:

* Add A/B/n test variations to individual content fields in Sanity Studio
* Sync feature flags and their variations automatically from LaunchDarkly
* Let content editors author variant content without developer intervention
* Serve personalized content based on LaunchDarkly targeting rules at runtime
* Filter which flags appear in the Studio using LaunchDarkly tags

The integration stores variant content in Sanity entries alongside each field's default value. Your application uses a LaunchDarkly SDK to evaluate the active variation and queries Sanity with that variation to retrieve the correct content.

## How the integration works

The integration uses the `@sanity/personalization-plugin`, a Sanity Studio plugin that enables A/B/n experiments on individual content fields. When you install and configure the plugin, content editors can assign LaunchDarkly feature flags to fields and author variant content directly in the Studio.

When configured, the plugin:

* Registers new schema types for each field type you enable for experiments. For example, enabling the `string` type creates `flagString` and `variantString` types.
* Fetches available feature flags and their variations from LaunchDarkly using a Reader API token you configure in the Studio
* Lets editors assign a flag to a field, then author content for each variation
* Stores the default value and all variant content on the Sanity entry

All flag targeting rules and traffic allocation remain managed in LaunchDarkly. The integration reads flags but does not create or modify them.

## Prerequisites

To use the integration, you must have:

* A Sanity Studio v3 project
* A LaunchDarkly account with access to API tokens, projects, and environments
* Node.js 18 or later
* A LaunchDarkly API access token with the **Reader** role, or a custom role with read access to flags

Create or manage API access tokens on the Authorization page. To learn more, read [API access tokens](/docs/home/account/api).

## Set up the Sanity integration

Setting up the Sanity integration involves three steps:

1. [Install the plugin](#install-the-plugin)
2. [Configure the plugin](#configure-the-plugin)
3. [Enable experiment fields](#enable-experiment-fields)

After setup, content editors can add experiments to fields directly in Sanity Studio.

### Install the plugin

To install the plugin in your Sanity Studio project, enter the following command in your terminal:

<CodeGroup>
  ```bash title="Install personalization-plugin" lines wrap theme={null}
  npm install @sanity/personalization-plugin
  ```
</CodeGroup>

### Configure the plugin

Before you configure the plugin, configure your LaunchDarkly flags with string variation types and descriptive variation names. You can also apply tags to filter which flags appear in the Studio.

Here is an example flag configuration:

<CodeGroup>
  ```txt title="Example flag configuration" lines wrap theme={null}
  Flag key:   homepage-hero-experiment
  Variations: control, treatment-a, treatment-b
  Tags:       sanity-content
  ```
</CodeGroup>

To configure the plugin in `sanity.config.ts`:

1. Import `fieldLevelExperiments` from `@sanity/personalization-plugin/launchDarkly`.
2. Add the plugin to your Sanity config and provide your LaunchDarkly project key.
3. Specify the field types you want to enable for experiments.
4. Optionally, provide `tags` to filter which flags appear in the Studio.

Here is an example `sanity.config.ts`:

<CodeGroup>
  ```typescript title="Example sanity.config.ts" lines wrap theme={null}
  import { defineConfig } from 'sanity'
  import { fieldLevelExperiments } from '@sanity/personalization-plugin/launchDarkly'

  export default defineConfig({
    // ... your existing Sanity config
    plugins: [
      // ... your other plugins
      fieldLevelExperiments({
        projectKey: process.env.SANITY_STUDIO_LAUNCHDARKLY_PROJECT_KEY!,
        fields: ['string', 'text', 'image'],
        // ... optional filter flags
        tags: ['sanity-content'],
      }),
    ],
  })
  ```
</CodeGroup>

Set the project key in your `.env` file as follows:

<CodeGroup>
  ```bash title="Example .env file entry" lines wrap theme={null}
  SANITY_STUDIO_LAUNCHDARKLY_PROJECT_KEY=your-project-key
  ```
</CodeGroup>

The API token is stored securely in Sanity using `@sanity/studio-secrets` and is never exposed to your frontend application.

<Accordion title="Expand plugin configuration options">
  <table>
    <thead>
      <tr>
        <th>Option</th>
        <th>Type</th>
        <th>Required</th>
        <th>Description</th>
      </tr>
    </thead>

    <tbody>
      <tr>
        <td><code>projectKey</code></td>
        <td>String</td>
        <td>Yes</td>
        <td>Your LaunchDarkly project key</td>
      </tr>

      <tr>
        <td><code>fields</code></td>
        <td>Array</td>
        <td>Yes</td>
        <td>Field types to enable for experiments. Accepts type name strings or complete field definitions.</td>
      </tr>

      <tr>
        <td><code>tags</code></td>
        <td>Array</td>
        <td>No</td>
        <td>Filter flags to only those with all specified tags</td>
      </tr>
    </tbody>
  </table>
</Accordion>

### Enable experiment fields

The plugin registers new schema types for each field type you configure. For each type, the plugin creates two new types: one for the flag field and one for each variant entry.

<Accordion title="Expand field type mappings">
  <table>
    <thead>
      <tr>
        <th>Original type</th>
        <th>Flag type</th>
        <th>Variant type</th>
      </tr>
    </thead>

    <tbody>
      <tr>
        <td><code>string</code></td>
        <td><code>flagString</code></td>
        <td><code>variantString</code></td>
      </tr>

      <tr>
        <td><code>text</code></td>
        <td><code>flagText</code></td>
        <td><code>variantText</code></td>
      </tr>

      <tr>
        <td><code>image</code></td>
        <td><code>flagImage</code></td>
        <td><code>variantImage</code></td>
      </tr>

      <tr>
        <td>Custom types</td>
        <td><code>flag{TypeName}</code></td>
        <td><code>variant{TypeName}</code></td>
      </tr>
    </tbody>
  </table>
</Accordion>

To use experiment-enabled fields, replace the original field type with the corresponding flag type in your schema.

Here is an example schema:

<CodeGroup>
  ```typescript title="schemas/post.ts" expandable lines wrap theme={null}
  import { defineField, defineType } from 'sanity'

  export const post = defineType({
    name: 'post',
    title: 'Post',
    type: 'document',
    fields: [
      defineField({
        name: 'title',
        title: 'Title',
        type: 'flagString', // Enables A/B/n testing on this field
      }),
      defineField({
        name: 'heroImage',
        title: 'Hero Image',
        type: 'flagImage', // Enables A/B/n testing on images
      }),
      defineField({
        name: 'excerpt',
        title: 'Excerpt',
        type: 'text', // Regular field, no experiments
      }),
    ],
  })
  ```
</CodeGroup>

### Add experiments in Sanity Studio

After configuring the plugin and updating your schema, content editors can add experiments to fields directly in the Studio.

To add an experiment to a field:

1. Open a document with experiment-enabled fields.
2. Enter your LaunchDarkly API token when prompted. The Studio stores this token securely. You only need to enter it once.
3. Select the **three-dot** field actions menu on an experiment-enabled field.
4. Select **Add flag**.
5. Choose a flag from the menu, which is populated from your LaunchDarkly project.
6. Add variant content using the quick-add buttons.
7. Use the **Copy default** button to duplicate the default field value to a variant as a starting point.

The Studio displays a visual distinction between the default content and each variant.

## Stored fields and implementation

This section describes how the integration stores experiment data in Sanity and provides examples for retrieving variant content in your application.

### Stored fields on entries

The integration stores the following data on each experiment-enabled field in Sanity:

<Accordion title="Expand stored field properties">
  <table>
    <thead>
      <tr>
        <th>Property</th>
        <th>Type</th>
        <th>Description</th>
      </tr>
    </thead>

    <tbody>
      <tr>
        <td><code>\_type</code></td>
        <td>String</td>
        <td>Schema type, such as <code>flagString</code></td>
      </tr>

      <tr>
        <td><code>default</code></td>
        <td>Varies</td>
        <td>The default/control content value</td>
      </tr>

      <tr>
        <td><code>active</code></td>
        <td>Boolean</td>
        <td>Whether the experiment is active on this field</td>
      </tr>

      <tr>
        <td><code>flagId</code></td>
        <td>String</td>
        <td>LaunchDarkly feature flag key</td>
      </tr>

      <tr>
        <td><code>variants</code></td>
        <td>Array</td>
        <td>Array of variant content objects</td>
      </tr>
    </tbody>
  </table>
</Accordion>

For example, a `flagString` field with two variants is stored as in this example:

<CodeGroup>
  ```json title="Example stored entry structure" expandable lines wrap theme={null}
  {
    "title": {
      "_type": "flagString",
      "default": "Welcome to Our Site",
      "active": true,
      "flagId": "homepage-hero-experiment",
      "variants": [
        {
          "_key": "abc123",
          "_type": "variantString",
          "flagId": "homepage-hero-experiment",
          "variantId": "control",
          "value": "Welcome to Our Site"
        },
        {
          "_key": "def456",
          "_type": "variantString",
          "flagId": "homepage-hero-experiment",
          "variantId": "treatment-a",
          "value": "Discover Something New"
        }
      ]
    }
  }
  ```
</CodeGroup>

Each variant object in the `variants` array contains:

<Accordion title="Expand variant object properties">
  <table>
    <thead>
      <tr>
        <th>Property</th>
        <th>Type</th>
        <th>Description</th>
      </tr>
    </thead>

    <tbody>
      <tr>
        <td><code>\_key</code></td>
        <td>String</td>
        <td>Unique key for the array element</td>
      </tr>

      <tr>
        <td><code>\_type</code></td>
        <td>String</td>
        <td>Schema type, such as <code>variantString</code></td>
      </tr>

      <tr>
        <td><code>flagId</code></td>
        <td>String</td>
        <td>LaunchDarkly feature flag key</td>
      </tr>

      <tr>
        <td><code>variantId</code></td>
        <td>String</td>
        <td>LaunchDarkly variation value</td>
      </tr>

      <tr>
        <td><code>value</code></td>
        <td>Varies</td>
        <td>The variant content value</td>
      </tr>
    </tbody>
  </table>
</Accordion>

### Implementation examples

These examples show how to evaluate feature flags and retrieve the mapped variant content in an application.

#### Resolve variant content using GROQ

Use the `coalesce` function in GROQ (Graph-Relational Object Queries), Sanity's query language, to retrieve the correct variant content for the active variation. If no variant matches, the query falls back to the default value.

Here is an example of a GROQ query with variant resolution:

<CodeGroup>
  ```groq title="GROQ query with variant resolution" lines wrap theme={null}
  *[_type == "post" && slug.current == $slug][0] {
    "title": coalesce(
      title.variants[flagId == $flagKey && variantId == $variantId][0].value,
      title.default
    ),
    "heroImage": coalesce(
      heroImage.variants[flagId == $flagKey && variantId == $variantId][0].value,
      heroImage.default
    ),
    excerpt,
    slug
  }
  ```
</CodeGroup>

#### Evaluate a flag and fetch personalized content

This example shows how a Next.js application retrieves the active variation from LaunchDarkly and passes it to Sanity to fetch the matching content:

<CodeGroup>
  ```typescript title="lib/launchdarkly.ts" expandable lines wrap theme={null}
  import { init, LDClient } from '@launchdarkly/node-server-sdk'

  let client: LDClient | null = null

  export async function getClient(): Promise<LDClient | null> {
    if (!client && process.env.LAUNCHDARKLY_SDK_KEY) {
      client = init(process.env.LAUNCHDARKLY_SDK_KEY)
      await client.waitForInitialization({ timeout: 10 })
    }
    return client
  }

  export async function getVariation(
    flagKey: string,
    context: { key: string; anonymous?: boolean }
  ): Promise<string> {
    const ldClient = await getClient()
    if (!ldClient) return 'control'

    const variation = await ldClient.variation(flagKey, context, 'control')
    return variation.toString()
  }
  ```

  ```typescript title="app/posts/[slug]/page.tsx" expandable lines wrap theme={null}
  import { sanityClient } from '@/lib/sanity'
  import { getVariation } from '@/lib/launchdarkly'
  import { cookies } from 'next/headers'

  async function getPost(slug: string, variantId: string) {
    return sanityClient.fetch(
      `*[_type == "post" && slug.current == $slug][0] {
        "title": coalesce(
          title.variants[flagId == 'homepage-hero-experiment' && variantId == $variantId][0].value,
          title.default
        ),
        "heroImage": coalesce(
          heroImage.variants[flagId == 'homepage-hero-experiment' && variantId == $variantId][0].value,
          heroImage.default
        ),
        excerpt,
        content
      }`,
      { slug, variantId }
    )
  }

  export default async function PostPage({ params }: { params: { slug: string } }) {
    const cookieStore = cookies()
    const userId = cookieStore.get('user-id')?.value || 'anonymous'

    const variantId = await getVariation('homepage-hero-experiment', {
      key: userId,
      anonymous: true,
    })

    const post = await getPost(params.slug, variantId)

    return (
      <article>
        <h1>{post.title}</h1>
        {/* ... */}
      </article>
    )
  }
  ```
</CodeGroup>

#### Handle multiple flags

For documents with multiple experiment fields using different flags, pass each flag key and variant ID separately.

Here is an example:

<CodeGroup>
  ```groq title="Example GROQ query with multiple flags" lines wrap theme={null}
  *[_type == "page"][0] {
    "headline": coalesce(
      headline.variants[flagId == $headlineFlag && variantId == $headlineVariant][0].value,
      headline.default
    ),
    "heroImage": coalesce(
      heroImage.variants[flagId == $heroFlag && variantId == $heroVariant][0].value,
      heroImage.default
    ),
    "ctaText": coalesce(
      ctaText.variants[flagId == $ctaFlag && variantId == $ctaVariant][0].value,
      ctaText.default
    )
  }
  ```
</CodeGroup>

## Troubleshooting

If you encounter issues with the LaunchDarkly Sanity integration, try these solutions.

### Flags do not appear in the Studio menu

If no flags appear when adding an experiment to a field:

1. Verify that your API token has read access to the selected project.
2. Confirm that `projectKey` in the plugin config matches your LaunchDarkly project key, not the SDK key.
3. Verify that the expected flags have all the specified tags applied in LaunchDarkly when using the `tags` option.
4. Refresh the Studio page to re-fetch flags from LaunchDarkly.

### API token errors

If the Studio displays a token error or repeatedly prompts for a token:

1. Navigate to a document with experiment-enabled fields.
2. Enter a new valid LaunchDarkly API token when prompted. The old token is replaced.
3. Confirm that the token has Reader access or a custom role with read access to flags.

### Variant content is not delivered correctly

If your application serves the default content instead of the expected variant:

1. Confirm that the `variantId` passed to your GROQ query matches the variation value defined in LaunchDarkly, not the variation name.
2. Verify that the Sanity document is published. Draft changes are not visible to your frontend.
3. Confirm that the `flagId` on the field matches the flag key in your GROQ query.
4. Clear any CDN caches on your frontend after publishing changes in Sanity.

### Schema types are not available

If `flagString` or other flag types are not available in your schema, confirm that the field type is listed in the `fields` array in the plugin config. Then restart the Sanity Studio dev server after updating the plugin configuration.
