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

# Experiment Center and Auth0 ACUL Integration

> How to read experiment context in Auth0 Custom Universal Login (ACUL) screens and branch rendering based on variation.

export const ReleaseStageNotice = ({feature, stage, plans, contact, terms}) => {
  const stageTextMap = {
    "beta": "Beta",
    "ea": "Early Access"
  };
  const stageText = stageTextMap[stage] || "a product release stage";
  const prsLink = "/docs/troubleshoot/product-lifecycle/product-release-stages";
  const linkify = (text, url) => {
    return <a href={url} target="_blank" rel="noreferrer" class="link">{text}</a>;
  };
  const includeDetails = (plans, contact, terms) => {
    const hasDetails = terms || plans || contact;
    if (!hasDetails) return null;
    return <span data-as="p">
            {plans && <>This feature is available for {linkify(`${plans} plans`, "https://auth0.com/pricing")}. </>}
            {contact && "To participate, contact " + contact + ". "}
            {terms && <>By using this feature, you agree to the applicable Free Trial terms in Okta's {linkify("Master Subscription Agreement", "https://www.okta.com/legal")}.</>}
        </span>;
  };
  return <Warning>
            <span data-as="p">
                <strong>The {feature} feature is in {linkify(stageText, prsLink)}.</strong>
            </span>

            {includeDetails(plans, contact, terms)}
        </Warning>;
};

<ReleaseStageNotice feature="Auth0 Experiment Center" stage="ea" plans="Enterprise" contact="your Account Executive" />

When an experiment is active and a variation is assigned, Experiment Center adds the assigned variation to the context of your [Advanced Customizations for Universal Login (ACUL)](/docs/customize/login-pages/advanced-customizations) screen as `experiment`. You can access it with the [ACUL JS SDK](/docs/libraries/acul/js-sdk) or [ACUL React SDK](/docs/libraries/acul/react-sdk).

The experiment context is only delivered to screens where you have opted in. You opt in per screen by adding `"experiment"` to the `context_configuration` array for that screen.

To opt a screen in to receive experiment context, make a call to the [`PATCH /v2/prompts/{prompt}/screen/{screen}/rendering` endpoint](/docs/api/management/v2/prompts/patch-rendering) with the following in the request body:

```json Example wrap lines theme={null}
{
    "context_configuration": ["experiment"]
}
```

Replace `{prompt}` with the prompt name (for example, `login`) and `{screen}` with the screen name (for example, `login`).

<Callout icon="file-lines" color="#0EA5E9" iconType="regular">
  Experiment resolution and tenant log enrichment always run, regardless of whether a screen has opted in. The opt-in only controls whether `experiment` properties are passed to your ACUL component. This is a data-minimization measure: don't opt in screens that do not need experiment context.
</Callout>

## Access the experiment context with the ACUL SDKs

You can access the experiment context with version 1.9.0 or later of the ACUL JS SDK (`@auth0/auth0-acul-js`) or ACUL React SDK (`@auth0/auth0-acul-react`).

Use the SDK that matches how you build your ACUL screens:

<Tabs>
  <Tab title="React SDK">
    Use the `useExperiment()` hook, which is available from every screen entry point of `@auth0/auth0-acul-react`.

    ```tsx wrap lines theme={null}
    import { useExperiment } from '@auth0/auth0-acul-react/login';

    const experiment = useExperiment();
    ```
  </Tab>

  <Tab title="JS SDK">
    Read the `experiment` property on any screen instance from `@auth0/auth0-acul-js`.

    ```typescript wrap lines theme={null}
    import Login from '@auth0/auth0-acul-js/login';

    const experiment = new Login().experiment;
    ```
  </Tab>
</Tabs>

The SDKs return `null` when no experiment is active or the screen has not opted in.

## The experiment context shape

The [experiment context](/docs/libraries/acul/js-sdk/Screens/interfaces/ExperimentMembers) has the following shape:

```typescript wrap lines theme={null}
interface ExperimentMembers {
    experimentId: string;                 // The active experiment ID
    variationId: string;                  // The assigned variation ID
    variationName: string | null;         // The name of the assigned variation
    variationDescription: string | null;  // The description of the assigned variation
    config: {                             // Merged configuration: baseline + overrides
        [paramName: string]: unknown;     // Each entry is a { value } object
    };
    isControl: boolean;                   // True when this is the control variation
}
```

The ACUL SDKs set `variationName` and `variationDescription` to `null` when they aren't defined, and set `config` to an empty object (`{}`) when the server doesn't send a configuration.

The `config` property contains the merged configuration for the assigned variation. Experiment Center takes the feature flag's baseline parameters and merges the assigned variation's overrides on top. When the server sends a configuration, every parameter defined on the feature flag has a value in `config`.

For example, if your feature flag has a parameter `button_label` with a baseline value of `"Sign in"`, and the assigned variation overrides it to `"Continue"`, then `config.button_label.value` is `"Continue"`.
For the control variation (no overrides), `config.button_label.value` is `"Sign in"`.

## Read a parameter value

Each entry in `config` is an object that wraps the resolved value, for example `{ value: "Continue" }`. Read the resolved value from `config[paramName].value`:

```typescript wrap lines theme={null}
const label = experiment?.config?.button_label?.value;
```

Always read `.value` rather than testing the entry itself. The wrapper object is always truthy, even when the resolved value is `false`:

```typescript wrap lines theme={null}
// Incorrect: { value: false } is still a truthy object, so this branch always runs.
if (experiment?.config?.show_passkey_banner) { /* ... */ }

// Correct: read the resolved value.
if (experiment?.config?.show_passkey_banner?.value === true) { /* ... */ }
```

In the ACUL SDKs, `config` values are typed as `unknown` because parameter types are defined per experiment. In TypeScript, narrow the entry before you read it:

```typescript wrap lines theme={null}
const entry = experiment?.config?.show_passkey_banner as { value?: boolean } | undefined;
const showBanner = entry?.value ?? false;
```

Use optional chaining (`?.`) throughout. The `experiment` property is `null` when no experiment is active.

## Use isControl

The `isControl` property is `true` when the user is in the control group (they received the baseline, no overrides applied). Use it when you need to track which users saw the unmodified experience, or when you want to skip optional processing for control users.

```typescript wrap lines theme={null}
if (experiment && !experiment.isControl) {
    // Only runs for treatment users
    trackExperimentImpression(experiment.experimentId, experiment.variationId);
}
```

Check that `experiment` exists before you check `isControl`. When no experiment is active, `!experiment?.isControl` evaluates to `true`, which runs the treatment branch for users who aren't in an experiment.

For branching rendering, check the parameter value directly (`config.my_param.value`) rather than `isControl`.
Parameter-based checks are more readable and work correctly even when you change which variation is the statistical control in a future experiment.

## Example: copy variant experiment

This example shows a feature flag with two variations that changes button copy. The control variation uses the standard label; the treatment variation uses an alternative.

**Feature flag parameters:**

```json wrap lines theme={null}
{
    "button_label": {
        "type": "string",
        "value": "Sign in",
        "description": "Label for the primary login button"
    }
}
```

**Control variation:** empty overrides (inherits `button_label: "Sign in"`)

**Treatment variation:**

```json wrap lines theme={null}
{
    "overrides": {
        "button_label": { "value": "Continue to your account" }
    }
}
```

**ACUL component reading the parameter:**

```tsx wrap lines theme={null}
// LoginScreen.tsx
import { useExperiment } from '@auth0/auth0-acul-react/login';

export default function LoginScreen() {
    const experiment = useExperiment();
    const entry = experiment?.config?.button_label as { value?: string } | undefined;
    const buttonLabel = entry?.value ?? "Sign in";

    return (
        <div>
            <h1>Welcome back</h1>
            <form>
                <input type="email" name="email" placeholder="Email" />
                <input type="password" name="password" placeholder="Password" />
                <button type="submit">{buttonLabel}</button>
            </form>
        </div>
    );
}
```

The `?? "Sign in"` fallback handles the case where no experiment is active (in which case `experiment` is `null` and `entry` is `undefined`). If you prefer, you can use a separate null check:

```typescript wrap lines theme={null}
const buttonLabel = experiment
    ? (experiment.config.button_label as { value: string }).value
    : "Sign in"; // No active experiment; use default
```

## Example: boolean feature rollout

This example uses a boolean parameter to conditionally show a new UI element.

```tsx wrap lines theme={null}
// LoginScreen.tsx
import { useExperiment } from '@auth0/auth0-acul-react/login';

export default function LoginScreen() {
    const experiment = useExperiment();
    const entry = experiment?.config?.show_passkey_banner as { value?: boolean } | undefined;
    const showPasskeyBanner = entry?.value === true;

    return (
        <div>
            {/* Your existing login form */}
            {showPasskeyBanner && (
                <div className="banner">
                    <p>Set up a passkey for faster sign-in next time.</p>
                </div>
            )}
        </div>
    );
}
```

The `=== true` check (rather than a truthy check) is intentional: it ensures the banner only shows when the parameter is explicitly `true`, not when `experiment` is `null` or `config` is empty.

## Example: control and treatment in the JS SDK

This example uses the JS SDK to render the default experience for users with no experiment or in the control group, and the treatment experience only when the parameter resolves to `true`.

```typescript wrap lines theme={null}
import Login from '@auth0/auth0-acul-js/login';

const experiment = new Login().experiment;

if (!experiment || experiment.isControl) {
    renderDefaultLoginUI();
} else {
    const entry = experiment.config?.show_passkey as { value?: boolean } | undefined;

    if (entry?.value) {
        renderTreatmentLoginUI();
    } else {
        renderDefaultLoginUI();
    }
}
```

`renderDefaultLoginUI()` and `renderTreatmentLoginUI()` are your own render functions. When an experiment has more than two variations, you can also branch on `experiment.variationId`.

## Troubleshooting

The `experiment` property is `null` in three situations:

1. No experiment is currently active for the tenant.
2. The screen has not opted in via `context_configuration`.
3. Experiment Center is not enabled on the tenant.

If `experiment` is present but `config` is empty (`{}`), the server didn't send a configuration for the variation. Read parameters with optional chaining and a default value so your screen still renders.

Always treat `experiment` as potentially `null`. The safest pattern:

```typescript wrap lines theme={null}
const experiment = useExperiment(); // or new Login().experiment
const entry = experiment?.config?.my_param as { value?: string } | undefined;
const myParam = entry?.value ?? "your-default";
```

Never assume the `experiment` property is present. Code that relies on `experiment` being defined will throw errors when no experiment is running.
