# Code

Every file in a Transformation is an async function that receives a `params` object and returns an object describing what EdgeTag should do with the event. This page is the reference: what's in `params` for each file, what you can return, and what each return shape actually does.

You never write wrappers, imports, or exports — only the function body.

### The return shape

Every file returns a plain object. Any combination of these fields is valid:

```javascript
return {}                                     // no-op (pass through)
return { payload }                            // modify the event
return { skipEvent: true }                    // drop the event
return { additionalEvents: [event1, ...] }    // fan out extra events
return { user }                               // update user data (edge only)
return { payload, additionalEvents }          // combine any of the above
```

Returning nothing at all is equivalent to `return {}`.

### `tagRoot`

Runs on the edge **once per event**, before any channel routing. Use for global rules — drop an event everywhere, enrich every payload, and emit an additional event based on attribution.

Available on `params`:

* `payload` — the event: `eventName`, `eventId`, `sdkVersion`, `locale`, `search`, `referrer`, `data` (standard event fields).
* `user` — hashable PII (`email`, `phone`, `firstName`, `lastName`, `zip`, …), or `null`.
* `variables` — your [Secrets](/playground/transformation/secrets.md) as a plain object.
* `configuration` — any configuration attached to the event.
* `platform` — e.g. `SHOPIFY`, `CUSTOM`.
* `userCustomData` — custom tags attached via the SDK.
* `hostData` — request metadata (`userAgent`, `ip`, `country`, `city`, `region`, `timezone`, …).
* `logger.log(...)`, `logger.error(...)`.
* `userKey.getFirstClick()`, `userKey.getLastClick()` — paid-media attribution helpers.
* `providerData` — raw click IDs and UTMs captured for this user.

```javascript
if (params.payload.eventName === 'Purchase') {
  const payload = structuredClone(params.payload)
  payload.data = { ...payload.data, source: 'website' }
  return { payload }
}
return {}
```

### `tagChannel`

Runs on the edge **once per provider channel**. Same params as `tagRoot`, plus:

* `providerId` — the channel being processed (e.g. `'facebook'`, `'google'`, `'tiktok'`).

Use for provider-specific rules.

```javascript
if (params.providerId === 'facebook') {
  const payload = structuredClone(params.payload)
  payload.data = { ...payload.data, fb_custom_field: 'my_value' }
  return { payload }
}
return {}
```

### `tagInstance`

Runs on the edge **once per provider instance** (for example `facebook||pixel1`), the most granular edge level. Same params as `tagChannel`, including `providerId`. Use when you have multiple pixels or accounts inside a provider and need different behavior for each.

### `clientTagRoot`

Runs in the browser **once per event**, before any provider tag fires.

Available on `params`:

* `payload` — `eventName`, `eventId`, `data`.
* `user` — reserved for browser user data (currently empty).
* `settings` — `userId`, `sessionId`, `geoCountry`, `geoRegion`, `isEURequest`, `ip`, `consent`, `consentCategories`, `userProperties`.
* `variables` — only the [Secrets](/playground/transformation/secrets.md) you've flagged as client-side.

```javascript
return {
  payload: {
    ...params.payload,
    data: { ...params.payload.data, screenWidth: window.innerWidth },
  },
}
```

### `clientTagChannel` / `clientTagInstance`

Same as `clientTagRoot`, plus `providerId`. Use these for browser-side provider- or instance-specific rules — for example, skipping a provider's browser pixel for EU traffic.

### Paid-media helpers

Two helpers on edge params make paid-media attribution easy. Both are easy to misuse, so read carefully.

**`params.userKey.getFirstClick()` and `getLastClick()`**

Returns a **provider ID** string or `null`. The provider ID tells you which paid channel drove this user; it is **not** a click ID, a URL, or a query string.

```javascript
const firstClickProvider = await params.userKey.getFirstClick()
const lastClickProvider = await params.userKey.getLastClick()

if (firstClickProvider === 'facebook') {
  // user's first paid-media touch was a Facebook click
}
if (lastClickProvider === null) {
  // organic/direct — no paid-media click on record
}
```

Valid return values are literal strings from a fixed set: `'facebook'`, `'googleAdsClicks'`, `'bing'`, `'snapchat'`, `'tiktok'`, `'twitter'`, `'linkedIn'`, `'pinterest'`, `'reddit'`, `'appLovin'`, `'taboola'`, `'outbrain'`, `'trybe'`, or `null`.

**`params.providerData`**

The raw click IDs and UTM values captured for this user, keyed by provider and then by the original query-parameter name:

```javascript
params.providerData.facebook?.fbclid          // Meta click ID, if present
params.providerData.googleAdsClicks?.gclid    // Google Ads click ID
params.providerData.utm?.utm_source           // 'google', 'newsletter', etc.
params.providerData.utm?.utm_medium
params.providerData.utm?.utm_campaign
```

An empty `providerData` means no paid-media clicks or UTMs were ever captured for this user.

{% hint style="warning" %}
`getFirstClick()` and `getLastClick()` return a provider ID like `'facebook'`, not the `fbclid`. Don't try to parse it with `new URL(...)` or `URLSearchParams` — just compare it to a literal provider ID or to `null`.
{% endhint %}

### What a Transformation can do

The four capabilities described at the top — modify, skip, fan out, update user — cover every real use case. Here's one real example of each.

{% stepper %}
{% step %}

### Modify a payload

Enrich every `Purchase` event with a server-side `source` field and a USD-converted value. Always use `structuredClone()` before mutating the payload so you never touch the input.

{% code title="tagRoot — enrich Purchase payloads" %}

```javascript
if (params.payload.eventName === 'Purchase') {
  const payload = structuredClone(params.payload)
  payload.data = {
    ...payload.data,
    source: 'website',
    valueUSD:
      payload.data.currency === 'USD'
        ? payload.data.value
        : payload.data.value * 1.08,
  }
  return { payload }
}
return {}
```

{% endcode %}
{% endstep %}

{% step %}

### Skip an event

Drop `PageView` events so they never reach any channel, and separately skip Facebook's browser pixel for visitors from the EU.

{% code title="tagRoot — drop PageView everywhere" %}

```javascript
if (params.payload.eventName === 'PageView') {
  return { skipEvent: true }
}
return {}
```

{% endcode %}

{% code title="clientTagChannel — skip Facebook in the browser for EU traffic" %}

```javascript
if (params.providerId === 'facebook' && params.settings.isEURequest) {
  return { skipEvent: true }
}
return {}
```

{% endcode %}
{% endstep %}

{% step %}

### Create new events

When a `Purchase` happens, and the user's first or last paid-media click was Facebook, fan out a second `Purchase_Facebook` event alongside the original. This is where `userKey` and `additionalEvents` work together.

{% code title="tagRoot — emit Purchase\_Facebook for Facebook-attributed purchases" %}

```javascript
if (params.payload.eventName === 'Purchase') {
  const firstClick = await params.userKey.getFirstClick()
  const lastClick = await params.userKey.getLastClick()
  if (firstClick === 'facebook' || lastClick === 'facebook') {
    const extra = structuredClone(params.payload)
    extra.eventName = 'Purchase_Facebook'
    return { additionalEvents: [extra] }
  }
}
return {}
```

{% endcode %}

{% hint style="info" %}
`additionalEvents` is honored from `tagRoot` on the edge and `clientTagRoot` in the browser. Each additional event then goes through channel routing like any other event.
{% endhint %}
{% endstep %}
{% endstepper %}

### Combining capabilities

A single return can do more than one thing. This example enriches the payload **and** emits an additional event for Facebook-attributed purchases in the same call:

```javascript
if (params.payload.eventName === 'Purchase') {
  const payload = structuredClone(params.payload)
  payload.data = { ...payload.data, source: 'website' }

  const lastClick = await params.userKey.getLastClick()
  if (lastClick === 'facebook') {
    const extra = structuredClone(payload)
    extra.eventName = 'Purchase_Facebook'
    return { payload, additionalEvents: [extra] }
  }

  return { payload }
}
return {}
```

### Return-value reference

| Field              | Type                   | Effect                                                           |
| ------------------ | ---------------------- | ---------------------------------------------------------------- |
| `payload`          | event object           | Replace the event payload for this level.                        |
| `user`             | user object or `null`  | Replace user data for this level. Edge only.                     |
| `additionalEvents` | array of event objects | Emit extra events. Root-level only (`tagRoot`, `clientTagRoot`). |
| `skipEvent`        | `boolean`              | `true` drops the event at this level.                            |

### What's not available

Transformations are pure event transforms — by design, they don't:

* make outbound HTTP requests (`requestHandler` is not available)
* write to the identity graph (`userSave`, `userGet`, `providerSave` are not available)
* access storage bindings (`infra` is not available)

If you need any of those, use a [Destination](/playground/destination.md).


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.edgetag.io/playground/transformation/code.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
