> Part of the walkerOS documentation. Project overview and full index: <https://www.walkeros.io/llms.txt>

# Consent management

walkerOS checks consent before events leave a flow. You record the user's choices in the collector, and each source and destination declares which consent it needs. Events that lack consent are either held or dropped, depending on where you set the requirement.

warning

This page is not legal advice. Consult a legal expert about the laws and regulations that apply to your business.

## Record consent[​](#record-consent "Direct link to Record consent")

A Consent Management Platform (CMP) usually collects the user's choice. That happens asynchronously: the CMP has to load, and the user may decide late or not at all. walkerOS provides [CMP source packages](/preview/pr-749/docs/sources/web/cmps.md) that listen for CMP events and send the consent command for you.

Without a CMP source, send the `walker consent` command yourself:

```javascript
elb('walker consent', { functional: true, marketing: false });
```

Consent keys are free-form. Common groups are `functional`, `analytics`, and `marketing`, and you can also use one key per vendor. The command stores each value as a boolean and only updates the keys it contains, so `elb('walker consent', { marketing: true })` leaves an existing `functional` state as it is.

## Configure consent in a flow[​](#configure-consent-in-a-flow "Direct link to Configure consent in a flow")

This web flow uses a CMP source to record consent, allows the gtag destination to receive events only with analytics or marketing consent, and maps the CMP source's `analytics` group to Google's `analytics_storage`:

```json
{
  "version": 4,
  "flows": {
    "default": {
      "config": { "platform": "web" },
      "sources": {
        "cmp": {
          "package": "@walkeros/web-source-cmp-cookiefirst"
        },
        "browser": {
          "package": "@walkeros/web-source-browser",
          "config": {
            "settings": { "pageview": true }
          }
        }
      },
      "destinations": {
        "gtag": {
          "package": "@walkeros/web-destination-gtag",
          "config": {
            "consent": { "analytics": true, "marketing": true },
            "settings": {
              "como": {
                "marketing": ["ad_storage", "ad_user_data", "ad_personalization"],
                "analytics": "analytics_storage"
              },
              "ga4": { "measurementId": "G-XXXXXXXXXX" }
            }
          }
        }
      }
    }
  }
}
```

### Gate a destination with `consent`[​](#gate-a-destination-with-consent "Direct link to gate-a-destination-with-consent")

A destination's `config.consent` lists the consent keys it needs. The collector checks every event against it before the destination sees the event:

* One granted key is enough. With `{ "analytics": true, "marketing": true }`, the destination receives events if either `analytics` or `marketing` is granted.
* Events without the required consent wait in the destination's queue. Once consent is granted, they are delivered in their original order.
* The collector initializes the destination only after its required consent is granted: at the first event that passes the check or, for a destination with an `on()` handler, at the consent command that grants it.
* The queue holds up to 1000 events per destination by default. When it is full, the oldest events go first. Change the limit with `config.queueMax`. See [buffer bounds](/preview/pr-749/docs/collector.md#buffer-bounds).

`config.queue` does not switch off this consent queue. The collector also keeps the events of the current run, so a destination added later still receives them. `"queue": false` skips only that replay for the destination.

A destination receives the current consent state through its `on('consent')` handler before any event is sent to it. This holds when consent was set before or after `walker run`, when the destination waits for consent with `require`, and when it is added with `walker destination` while the flow is running. The collector waits for the handler to finish, up to the destination's `config.timeout` (10 seconds by default). If the handler does not finish in that time, or fails, the destination receives no events until a later delivery of the state finishes. Its events wait in its queue meanwhile and are discarded at the next `walker run`. The collector delivers the state again when it changes, and with the next event once the timeout has passed.

### Gate a source with `consent`[​](#gate-a-source-with-consent "Direct link to gate-a-source-with-consent")

The collector drops events from a source when they lack the consent in the source's `config.consent`. Sources have no consent queue, so granting consent later does not bring these events back:

```json
"browser": {
  "package": "@walkeros/web-source-browser",
  "config": {
    "consent": { "functional": true },
    "settings": { "pageview": true }
  }
}
```

Use source gating when events must not enter the flow at all without consent. Use destination gating when events should wait for a decision.

### Wait for the CMP with `require`[​](#wait-for-the-cmp-with-require "Direct link to wait-for-the-cmp-with-require")

`"require": ["consent"]` holds a destination back, and holds a source's `on()` delivery back, until the collector has a consent state. Any recorded state counts, whether granted or denied, so `require` does not check values; `config.consent` decides which events the destination receives. See [conditional activation](/preview/pr-749/docs/sources/web/cmps.md#conditional-activation-with-require) for how `require` works.

### Set an initial consent state[​](#set-an-initial-consent-state "Direct link to Set an initial consent state")

The flow's `collector` block (next to `sources` and `destinations`) sets the state before any CMP reports:

```json
"collector": {
  "consent": { "functional": true }
}
```

An initial state is a recorded consent state, so it satisfies `"require": ["consent"]` as soon as the flow starts.

## How queued events are processed[​](#how-queued-events-are-processed "Direct link to How queued events are processed")

The collector re-checks a destination's queue whenever a new event or a state command (such as `walker consent`, `walker user`, or `walker globals`) reaches the destinations. Before the check, each queued event is updated:

* `consent` is replaced with the current collector state. Once the event passes, it carries only the granted keys.
* `user` is merged with the collector's user data. Values already on the event take precedence.
* `globals` keeps the values the event was captured with.

The queues belong to the current run. A `walker run` command starts a new run and clears the destination queues along with the replay buffer, so events still waiting for consent are lost. The collector already runs by default, so do not send `walker run` in reaction to a consent change. Only a bundle you build with `run: false` needs one `elb('walker run')`: call it once, from your CMP's first consent callback, not from an `on consent` handler, which is not called before the collector runs.

## Revoke consent[​](#revoke-consent "Direct link to Revoke consent")

To stop sending events to a destination, set its required keys to `false`:

```javascript
elb('walker consent', { marketing: false });
```

Once none of a destination's required keys is granted, new events are queued for it instead of delivered. If consent is granted again during the same run, those queued events are delivered. Events that were already sent are not affected.

## Opt-out[​](#opt-out "Direct link to Opt-out")

An opt-out beats consent: when a user opts out, no event of theirs is sent, whatever consent they granted. Mark the user with `optout: true` and stop the event at the first step of the chain with a gated stop:

```json
{
  "match": { "key": "event.user.optout", "operator": "eq", "value": "true" },
  "stop": true
}
```

* **Web:** put the stop first in your bundle's `collector.next`. An opted-out browser sends nothing. Sources still run, for example the session source keeps its ids in storage.
* **Server:** put the stop first in the source's `next`, before deduplication. An opted-out event leaves no dedup entry and no fingerprint hash, and reaches no destination.

`eq` compares as strings, so a boolean `true` and the string `"true"` both match.

Set the flag before the first event. A `walker user` command queued in `elbLayer` before your bundle loads applies before anything is sent:

```javascript
// Before your bundle loads
window.elbLayer = window.elbLayer || [];
window.elbLayer.push(['walker user', { optout: true }]);
```

`data-elbuser` is read right before the page view, so it keeps the page view in the browser. Events sent before it is read, such as `session start` and events queued in `elbLayer` before your bundle loaded, are sent.

```html
<body data-elbuser="optout:true">
```

A `walker user` command after your bundle loaded stops every later event, but the page view of that page has already been sent.

The server flow of [`flow-complete.json`](https://github.com/elbwalker/walkerOS/blob/main/packages/cli/examples/flow-complete.json), the tested example flow, stops opted-out events before `dedup`; its web flow uses the same stop first in `collector.next`:

`/flows/server/sources/express/next/0`

```json
{
  "match": {
    "key": "event.user.optout",
    "operator": "eq",
    "value": "true"
  },
  "stop": true
}
```

An opted-out user (user.optout true) is stopped first: on the server before dedup, so no dedup entry, hash or delivery; on the web first in collector.next, so the browser sends nothing. [Guide chapter `consent-privacy`](https://github.com/elbwalker/walkerOS/blob/main/packages/cli/examples/flow-complete.md#consent-privacy-consent-and-personal-data)

## Consent on a single event[​](#consent-on-a-single-event "Direct link to Consent on a single event")

An event can carry its own `consent`. It is merged over the collector state for this event, and the event's values win:

```javascript
elb({ name: 'order complete', consent: { marketing: true } });
```

If the event is still queued, the next check replaces its `consent` with the collector state, as described above.

To send a single field only with consent, use [consent in a mapping value](/preview/pr-749/docs/mapping/value.md#consent).

## React to consent changes[​](#react-to-consent-changes "Direct link to React to consent changes")

Use the `walker on` command with the `consent` type to run code when consent changes. `rules` is an object with one handler per consent key:

```javascript
elb('walker on', {
  type: 'consent',
  rules: {
    marketing: (consent, context) => {
      if (consent.marketing) {
        // marketing was granted
      } else {
        // marketing was denied
      }
    },
  },
});
```

A handler receives the consent state and a context with `collector` and `logger`:

* It runs when its key is part of a `walker consent` command, for a denial as well as a grant, and receives only the values of that command. Check the value inside the handler.
* When it is registered, and when the collector starts, it receives the full consent state and runs if its key is set. Until the collector runs, no handler is called.
* `rules` must be an object keyed by consent name. A bare function has no key and is never called.

To start or stop sending events based on consent, use `config.consent` and `require` instead of handlers. See [on](/preview/pr-749/docs/collector/commands.md#on) for all subscription types.

## Google Consent Mode[​](#google-consent-mode "Direct link to Google Consent Mode")

The [gtag destination](/preview/pr-749/docs/destinations/web/gtag.md) supports Google Consent Mode v2 through its `como` setting, which is on by default. When the destination has a `config.consent` requirement (or `como_advanced` is set), it first sets every mapped parameter to `denied` with `gtag('consent', 'default', ...)` before loading any tool; otherwise it sends that default right before its first update. It turns consent commands into `gtag('consent', 'update', ...)` calls, and holds commands sent before it initializes until initialization. The default mapping is:

| walkerOS key | Consent Mode parameters                            |
| ------------ | -------------------------------------------------- |
| `marketing`  | `ad_storage`, `ad_user_data`, `ad_personalization` |
| `functional` | `analytics_storage`                                |

The CookieFirst and CookiePro sources report measurement consent as `analytics` and, by default, map strictly necessary cookies to `functional`, which is granted as soon as the CMP reports a choice. With the default mapping, `analytics_storage` is then granted even when the user denied analytics. With these sources, map `analytics` instead, as in the example below.

Pass an object to use your own keys:

```json
"gtag": {
  "package": "@walkeros/web-destination-gtag",
  "config": {
    "consent": { "analytics": true, "marketing": true },
    "settings": {
      "como": {
        "marketing": ["ad_storage", "ad_user_data", "ad_personalization"],
        "analytics": "analytics_storage"
      },
      "ga4": { "measurementId": "G-XXXXXXXXXX" }
    }
  }
}
```

Set `"como": false` to turn it off. Do not call `gtag('consent', 'update')` yourself while `como` is on, because Google then receives duplicate or conflicting updates.
