---
title: @storyblok/experiments
description: @storyblok/experiments provides framework-agnostic, server-first, dependency-free runtime A/B testing utilities for Storyblok Experiments.
url: https://www.storyblok.com/docs/libraries/js/experiments
---

# @storyblok/experiments

[@storyblok/experiments](https://github.com/storyblok/monoblok/tree/main/packages/experiments) provides runtime A/B testing utilities for [Storyblok Experiments](/docs/concepts/experiments). It assigns each visitor to a variant, decides which story to render, and tracks exposure and conversion events, reading the payload from the [Content Delivery API experiments endpoint](/docs/api/content-delivery/v2/experiments/retrieve-running-experiments). The package is composable, framework-agnostic, and server-first.

In Storyblok, each experiment variant is a separate story. Therefore, the package’s primary function is to resolve which story slug to render. To learn more about how variants map to a main story, refer to the [experiments concept](/docs/concepts/experiments). The API client fetches the story and the experiments payload separately.

## Installation

Add the package to a project:

```bash
npm install @storyblok/experiments@latest
```

## API

The package exposes several standalone utilities. Each one works independently and can be combined as needed.

### `resolveExperiment`

Maps the experiments payload, a slug, and an assignment to the slug that should be rendered, plus an exposure descriptor. Control resolves to the original slug, and an assigned variant resolves to its mapped slug.

```ts
import { resolveExperiment } from "@storyblok/experiments";

const { slug, variant, exposure } = resolveExperiment({ experiments, slug: "home", assignment });

// Fetch the correct story variant with your existing API client. This example uses @storyblok/api-client@alpha.
const story = await client.stories.get(`cdn/stories/${slug}`);
```

When no experiment matches the slug, or the assignment is missing, the slug passes through unchanged and `exposure` is `undefined`.

### `assignVariant`

Deterministic weighted bucketing. For a given experiment, the same `visitorId` always lands on the same variant, with no storage. The `visitorId` can be any stable identifier, such as a cookie or a header. You are responsible for persisting it.

```ts
import { assignVariant } from "@storyblok/experiments";

// sticky: same visitorId always maps to the same variant
const assignment = assignVariant({ experiment, visitorId });
```

`assignVariant` hashes `visitorId` plus the experiment ID into a `0..99` bucket and walks the variants’ cumulative weights, so the same visitor resolves to the same variant on every request.

### Adapters

Adapters deliver experiment events (exposure and conversion) to an analytics destination. An adapter is any `(event) => void | Promise<unknown>` function: it takes an event and delivers it to the destination. Call the adapter directly to send an event.

```ts
import { fetchAdapter } from "@storyblok/experiments/adapters";

const adapter = fetchAdapter("https://my-sink.example/events");
adapter(exposure);

const myAdapter = (event) => myAnalytics.track(event);
myAdapter({ type: "conversion", experiment, variant, name: "signup" });
```

`fetchAdapter` POSTs each event as JSON and rejects on a non-2xx response (`fetch` itself only rejects on a network failure), so delivery failures are observable. When you call an adapter directly, handle the returned promise. When the `createExperiments` factory fires events, pass `onError` to observe failures.

> [!NOTE]
> Adapters live in their own entrypoint, `@storyblok/experiments/adapters`, which is the source of truth for adapter imports. They are intentionally not re-exported from the package root: most users bring their own destination, and a separate entrypoint keeps adapters out of the bundle when they go unused.

## Complete example

```ts
import { assignVariant, resolveExperiment } from "@storyblok/experiments";
import { fetchAdapter } from "@storyblok/experiments/adapters";

const adapter = fetchAdapter("https://my-sink.example/events");

// 1. Fetch the experiment config (cacheable, same for every visitor). This example uses @storyblok/api-client@alpha.
const { experiments } = await client.experiments.list();

// 2. Identify the visitor (you own this: cookie, header, etc.).
const visitorId = getCookie("visitor_id") ?? setCookie("visitor_id", crypto.randomUUID());

// 3. Assign: deterministic bucketing, same visitorId maps to the same variant.
const experiment = experiments.find((candidate) => candidate.name === "homepage_hero");
const assignment = assignVariant({ experiment, visitorId });

// 4. Resolve: which slug does this visitor get? (pure, no I/O)
const { slug, exposure } = resolveExperiment({ experiments, slug: "home", assignment });

// 5. Fetch and render the resolved story with your existing API client. This example uses @storyblok/api-client@alpha.
const story = await client.stories.get(`cdn/stories/${slug}`);
render(story);

// 6. Exposure: fire when the visitor actually saw the variant.
//    `exposure` is undefined when no experiment applied, so guard it.
if (exposure) {
  adapter(exposure);
}

// 7. Conversion: later, wherever the goal happens.
adapter({ type: "conversion", experiment, variant: assignment.variant, name: "signup" });
```

## Factory

`createExperiments` pre-binds the config and adapters, auto-fires exposure on resolve, and remembers assignments so conversions attribute without re-passing context. It is designed for server-side, per-request use.

```ts
import { createExperiments } from "@storyblok/experiments";
import { fetchAdapter } from "@storyblok/experiments/adapters";

const exp = createExperiments({ experiments, adapters: [fetchAdapter(url)] });

const { slug } = exp.resolveExperiment({ slug: "home", visitorId }); // exposure auto-fired

// Fetch the correct story variant with your existing API client. This example uses @storyblok/api-client@alpha.
const story = await client.stories.get(`cdn/stories/${slug}`);

// later, on the page:
exp.track("signup", { plan: "pro" }); // conversion
```

The factory fires events on your behalf, so you cannot await a failing adapter. Adapter errors are swallowed by default to keep the request resilient: a downed analytics destination must never break rendering. Pass `onError` to observe them:

```ts
const exp = createExperiments({
  experiments,
  adapters: [fetchAdapter(url)],
  onError: (error, event) => logger.warn("experiment event delivery failed", { error, event }),
});
```

## Further resources

[Developer Concept: Experiments](/docs/concepts/experiments)

[User Manual: Experiments](/docs/manuals/experiments)

[Content Delivery API Reference: Retrieve Running Experiments](/docs/api/content-delivery/v2/experiments/retrieve-running-experiments)

[Content Delivery API Reference: The Experiments Object](/docs/api/content-delivery/v2/experiments/the-experiments-object)

## Pagination

-   [Previous: Migration Utilities](/docs/libraries/js/migrations)
-   [Next: Region Helper](/docs/libraries/js/region-helper)
