# Klaviyo Flow Experiments

Neon Blue integrates with [Klaviyo Flows](https://help.klaviyo.com/hc/en-us/articles/115002774932) to run experiments on flow-triggered emails. Experiment variants are assigned at send time, ensuring each user receives personalized content based on their assignment.

A Klaviyo flow experiment consists of two parts:

1. **Custom Action** — fetches the variant assignment from Neon Blue and writes it to the user's profile
2. **Email Template** — renders the assigned variant content using Klaviyo's template language

***

### Custom Action Setup

Add a [Custom Action](https://developers.klaviyo.com/en/docs/add_a_custom_action_to_a_flow) node to your flow and name it `nb_assignment`. This node calls the Neon Blue assignment API, retrieves the variant content for the user, and stores it on their Klaviyo profile.

#### Environment Variables

| Variable        | Description                                         |
| --------------- | --------------------------------------------------- |
| `API_KEY`       | Your Neon Blue API key (found in the Neon Blue app) |
| `EXPERIMENT_ID` | The UUID or slug identifying your experiment        |

#### Outputs

| Output          | Type     | Default                                | Description                                              |
| --------------- | -------- | -------------------------------------- | -------------------------------------------------------- |
| `assignment_id` | `string` | `00000000-0000-0000-0000-000000000000` | The unique identifier for this user's variant assignment |

#### Code

```javascript
import { Profiles } from 'klaviyo'

const EXPERIMENT_ID = process.env.EXPERIMENT_ID;
const API_KEY = process.env.API_KEY;

export default async (event, profile, context) => {
  // construct assignment payload
  const payload = {
    profile: {
      type: profile.data.type,
      id: profile.data.id,
      attributes: profile.data.attributes,
      properties: profile.data.properties
    },
    event: {
      type: event.data.type,
      id: event.data.id,
      attributes: event.data.attributes
    },
    context: {
      trigger: context.trigger,
      function_id: context.function_id
    }
  };

  // fetch assignment
  const res = await fetch(
    `https://api.aws.neonblue.ai/v1/render/${EXPERIMENT_ID}?user_id=${profile.data.id}`,
    {
      method: 'POST',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'X-Partner': 'coterie',
        'X-Api-Key': API_KEY,
        'X-Preview': context.trigger.type == "MANUAL_RUN"
      },
      body: JSON.stringify(payload)
    }
  );
  const assignment = await res.json();
  const assignment_id =
    assignment.$metadata.assignment_id ||
    '00000000-0000-0000-0000-000000000000';

  // update user profile with variant content
  const $neonblue = profile.data.attributes.$neonblue || {};
  $neonblue[assignment_id] = assignment.value;
  await Profiles.updateProfile(profile.data.id, {
    data: {
      type: "profile",
      id: profile.data.id,
      attributes: {
        properties: {
          $neonblue: $neonblue
        }
      }
    }
  });

  return {
    assignment_id: assignment_id
  };
}
```

#### How It Works

When the flow triggers, the Custom Action sends the user's profile and event context to the Neon Blue assignment API. The API returns a response in the following shape:

```json
{
  "value": {
    "subject": "...",
    "preview": "...",
    "{FIELD_0}": "{VALUE_0}"
  },
  "$metadata": {
    "action": "render",
    "assignment_id": "{ASSIGNMENT_UUID}"
  }
}
```

The `value` object contains the variant content fields configured in your experiment. The Custom Action writes this content to the user's [custom profile properties](https://help.klaviyo.com/hc/en-us/articles/115000250912) under the `$neonblue` key, indexed by `assignment_id`.

> **Why write to the profile?** Klaviyo Custom Action nodes are limited to 5 return values. Storing variant content on the profile allows experiments with any number of content fields.

The `X-Preview` header is automatically set to `true` when the flow is triggered via a manual test run, preventing test sends from being recorded as exposures.

***

### Template Setup

Email templates retrieve variant content from the user's profile using the `assignment_id` returned by the `nb_assignment` Custom Action. A Neon Blue variant can contain content for 1 or more flow nodes (to allow for consistency across the user journey). For the specific message node template setup, you will need to know the `SLOT_KEY` that corresponds to that moment of the user journey.

#### Subject and Preview Text

Set the email **Subject** and **Preview text** to reference variant content via the user's profile:

**Subject:**

{% @neonblue-snippet/snippet-block template="{%with assignment\_id=outputs.nb\_assignment.assignment\_id%}{{person|lookup:"$neonblue"|lookup:assignment\_id|lookup:"{{SLOT\_KEY}}"|lookup:"subject"}}{%endwith%}" %}

**Preview text:**

{% @neonblue-snippet/snippet-block template="{%with assignment\_id=outputs.nb\_assignment.assignment\_id%}{{person|lookup:"$neonblue"|lookup:assignment\_id|lookup:"{{SLOT\_KEY}}"|lookup:"preview"}}{%endwith%}" %}

Replace `"subject"` and `"preview"` with the field names configured in your experiment if they differ.

#### Email Body

To access variant content fields in the email body, wrap your template content with the Neon Blue experiment header and footer blocks.

The **first** content block in the email must be the Universal Content Block `[Neon Blue] Experiment Header`:

{% @neonblue-snippet/snippet-block template="<!--{%with nb=person|lookup:"$neonblue"|lookup:outputs.nb_assignment.assignment_id|lookup:"{{SLOT_KEY}}"%}-->" %}

The **last** content block in the email must be Universal Content Block `[Neon Blue] Experiment Footer`:

```django
<!--{%endwith%}-->
```

These blocks populate the `nb` template variable with the full variant content object. You can then reference any experiment field using `{{nb.<field_name>}}` anywhere in the email body between the header and footer.

For example, if your experiment is configured with a field called `h1`:

```django
<h1>{{nb.h1}}</h1>
```

#### Template Checklist

* [ ] Subject line references variant content via `outputs.nb_assignment.assignment_id`
* [ ] Preview text references variant content via `outputs.nb_assignment.assignment_id`
* [ ] First content block is `[Neon Blue] Experiment Header`
* [ ] Last content block is `[Neon Blue] Experiment Footer`
* [ ] All dynamic content fields use the `{{nb.<field_name>}}` syntax between the header and footer blocks
