Skip to main content
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.
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 instead. For React Native, iOS, Android and Flutter see Mobile SDKs.

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:
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:
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

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

1

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.
2

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.
3

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.

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.
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:
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 for the full list and the subscription API, and Google Tag Manager 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:
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”.
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.

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.
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.