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

# setEmbeddedData

> Attach context from your app to the next Formbricks response, without tying it to a trigger. The JavaScript SDK reference for setEmbeddedData and clearEmbeddedData.

In an app survey the integrator usually does not know which action will open a survey: your app fires a dozen of them and Formbricks decides. Passing context on `track()` therefore means repeating the same values on every possible call.

`setEmbeddedData()` decouples the two. You push context whenever you have it, and every survey displayed from then on picks it up — until you remove the key, or the page loads again.

```js theme={null}
formbricks.setEmbeddedData({ pageType: "product", plan: "pro" });
```

<Note>
  This is for **website & app surveys** only, where the JavaScript SDK runs. Link surveys have no SDK on the
  page and are filled from [URL parameters](/docs/surveys/general-features/hidden-fields#set-hidden-field-via-url)
  instead. For React Native, iOS, Android and Flutter see [Mobile SDKs](/docs/surveys/embedded-data/mobile-sdks).
</Note>

## What It Writes

The bag reaches a response only through the survey's **declared ingested fields**, which are the entries in the survey editor's Hidden Fields card. A key nothing declares is dropped and logged; so is a key naming a variable, a reserved field or a locked field. Nothing here is ever fatal, and a rejected key never blocks a response.

That means the workflow is always: declare the field on the survey first, then push a value under the same name.

Values are scoped to the response. They are **never** written to the contact record, so this is not a back door into contact attributes.

## Merge Semantics

`setEmbeddedData()` merges into the current bag rather than replacing it, and per key the last write wins:

```js theme={null}
formbricks.setEmbeddedData({ plan: "pro", pageType: "product" });
formbricks.setEmbeddedData({ pageType: "checkout" });
// bag is now { plan: "pro", pageType: "checkout" }
```

That is what lets you set stable fields once and refresh volatile ones as the user moves, without the second call wiping the first.

### `null` Removes, `undefined` Does Nothing

The two are deliberately different, and the difference is the whole reason the "pass every field unconditionally" idiom is safe:

```js theme={null}
formbricks.setEmbeddedData({ pageType: null });      // removes pageType
formbricks.setEmbeddedData({ pageType: undefined }); // no-op — pageType keeps its value
```

A tag manager or a shared helper typically reads keys off a data layer and passes all of them on every page. On a page where `pageType` does not exist the key arrives as `undefined`, and it must not clear the value the previous page set. So `undefined` is a documented no-op, not an accident.

### Accepted Values

`string`, `number`, `boolean` and `Date`, plus `null` to remove a key and `undefined` to skip it. Dates are serialized as ISO 8601, which is what a `date` field accepts.

Anything else is refused rather than mangled. Passing a primitive, an array or a missing object logs an error and writes nothing, so `setEmbeddedData(dataLayerObj)` on a page where that object does not exist is a survivable mistake rather than a broken tag. Arrays in particular would otherwise spread into junk numeric keys.

## Clearing

```js theme={null}
formbricks.clearEmbeddedData("pageType"); // remove one key
formbricks.clearEmbeddedData();           // clear the whole bag
```

Only a literal zero-argument call clears everything. `clearEmbeddedData(someValue)` where `someValue` turned out to be `undefined` is a logged no-op, for the same reason as above: code that reads the key name from its own state must not wipe the bag when that state is empty.

## Lifetime

<Steps>
  <Step title="In memory, never persisted">
    The bag lives in SDK memory for the page load. It is not written to `localStorage`, so a full page load
    starts empty and your app re-pushes. A classic multi-page app does that for free on every navigation.
  </Step>

  <Step title="Snapshot at display, then frozen">
    When a survey is shown, the bag is copied onto it. A `setEmbeddedData()` call made while that survey is
    on screen reaches the **next** response, never the one being answered.
  </Step>

  <Step title="Cleared on an identity switch">
    Calling `formbricks.logout()`, or `formbricks.setUserId()` with a **different** id, clears the bag: one
    user's context must not ride onto the next user's responses on a shared device. Identifying for the first
    time keeps the bag, because pushing context before you know who the user is is a legitimate order.
  </Step>
</Steps>

## Stable vs. Volatile Fields

Two patterns, and mixing them up is the one failure mode worth designing against.

**Stable** context (`plan`, `role`, `accountType`) changes rarely. Set it once after setup and let the next page load re-push it.

**Volatile** context (`pageType`, `screen`, `channel`) is only true for the page you are on. In a single-page app one page load spans many routes, so nothing re-pushes on your behalf and a value set on `/pricing` is still in the bag when a survey opens on `/settings`.

```js theme={null}
// Call this from your router's navigation hook
function onRouteChange(route) {
  // `null` clears the key when the new route has no page type,
  // so the previous route's value cannot leak onto the next response
  formbricks.setEmbeddedData({ pageType: route.pageType ?? null });
  formbricks.registerRouteChange();
}
```

Setting the key to `null` when the new route has no page type is what stops the previous route's value from leaking. `setEmbeddedData()` is a synchronous memory write with no network call, so doing this on every route change costs nothing.

## Calling It Before Setup

`setEmbeddedData()` is synchronous and does not go through the SDK's command queue, so a call made before `setup()` finishes is kept rather than dropped. Two things do have to be true first, and they have different answers.

**The script must have loaded.** `window.formbricks` does not exist until then, and neither does anything on it — `setEmbeddedData` and `on` alike. Push your context from the script tag's own `onload`, or from a Google Tag Manager tag: the dataLayer buffers across load order, so a GTM trigger cannot be too early. Do not poll for the global.

**`setup()` may be delayed, and that is what the readiness event is for.** When the script has loaded but `setup()` waits on something like a consent banner, subscribe first and push when it resolves:

```js theme={null}
// the script has loaded; setup() has not been called yet
formbricks.on("formbricks_setup_successful", () => {
  formbricks.setEmbeddedData({ plan: currentUser.plan });
});
```

The order matters. Events are delivered live rather than replayed, so a handler registered after `setup()` has already finished never fires and logs nothing. See [Lifecycle Events](/docs/surveys/website-app-surveys/lifecycle-events) for the full list and the subscription API, and [Google Tag Manager](/docs/surveys/website-app-surveys/google-tag-manager#set-embedded-data) for the tag-manager wiring.

## Checking What Is in the Bag

The bag is invisible: it is memory, so there is nothing in devtools storage, and the API has no getter. Add `?formbricksDebug=true` to your URL and every write prints:

```text theme={null}
setEmbeddedData: set [pageType], removed [plan] — the bag now holds [pageType, accountType].
Keys land on a response only if the survey declares them as ingested Embedded Data fields.
```

`clearEmbeddedData()` traces too, including the "that key was not in the bag" case, which is usually the answer to "why is my value not there".

<Note>
  The trace prints **keys only, never values**, and only at debug level, so a respondent's console stays
  clean and nothing you pushed is echoed into it.
</Note>

## Precedence Against `track()`

The older `formbricks.track("action", { hiddenFields: { … } })` form still works and still wins. An explicit per-trigger value beats the ambient bag for the same field, whatever the casing on either side, so migrating one call site at a time is safe.

<Note>
  Two ways to load the SDK, two ways to pick this up. The script served by your Formbricks instance
  (`/js/formbricks.umd.cjs`) carries it as soon as the instance is updated, because that bundle is served
  rather than installed. The npm package proxies the SDK's methods explicitly, so there you get it by bumping
  `@formbricks/js`.
</Note>
