Skip to main content
Ask your AI

Mapping.Rule

A Mapping.Rule is the object that sits at config.mapping[entity][action] on a source or destination. It decides whether an event applies, renames it, reshapes its data, and can emit side effects (batching, custom settings). The shape is defined in packages/core/src/types/mapping.ts.

Configuration​

PropertyTypeDescriptionMore
namestringCustom event name override (e.g., "view_item" for "product view")
dataany | anyData transformation rules for event
settingsanyDestination-specific settings for this event mapping
conditionstringCondition function as string: return true to process event
consentWalkerOS.ConsentNot enforced yet. To require consent for a destination, set consent in its config.
policyMapping.PolicyEvent-level policy overrides (applied after config-level policy)
batchnumber | objectBatch scheduling: bare number is the debounce wait window (legacy); object form supports wait/size/age.
includeArray<string>Event sections (e.g. ["context", "globals"]) flattened into context.data
ignorebooleanSkip the event entirely. No push, no side effects. Use for suppression.
silentbooleanRun side effects (settings.identify, ...) but suppress the destination default push call.
extendMapping.RulePatchMerge mode: a partial rule deep-merged onto the package-shipped default at this key (instead of replacing it). A null value clears an inherited field.
removeArray<string>Dotted paths stripped from the produced data payload after evaluation (applied last).

Every value-shaped field (data, entries in policy, entries in settings that take values) accepts any form documented in Mapping.Value.

How destinations consume a rule​

When an event reaches a destination, the collector looks up mapping[event.entity][event.action]. An array means "try each in order, first matching rule wins". A wildcard '*' on either entity or action acts as a fallback.

Mapping wildcards differ from contract wildcards

Mapping wildcards are first-match: once entity.action (or its entity.* / *.action / *.* fallback) hits, no other rule is consulted. Contract wildcards are additive: every matching level merges in. The two systems intentionally pick different defaults: mapping selects a single transformation target, contract accumulates requirements.

Configuration
Loading...
Result
Loading...

A rule's condition is checked while matching: the first rule whose condition passes is used; if none passes, the * → * rule applies, if there is one. A condition may be async: conditions are awaited one by one, in rule order. A condition that throws is logged and counts as no match. Once a rule matches, the collector runs policy (event-level), resolves data, then hands the result to the destination's push (or buffers it when batch is set). A rule-level consent is not checked: gate a destination with config.consent, or a single value with a consent entry.

A policy entry that resolves to nothing removes the field. That is how a consent-gated entry redacts a value when consent is denied.

Batch scheduling​

batch accepts a number (debounce wait window in ms, legacy form) or an object { wait?, size?, age? } for finer control:

  • wait (ms): debounce window. The timer resets on every push.
  • size: hard count cap. Flushes immediately at this many events.
  • age (ms): hard age cap since the first entry of the current window.

A rule-level batch splits this entity-action into its own buffer. When the destination also sets config.batch, the rule's values override destination-level per field (rule ?? config ?? default); fields the rule omits fall back to config.batch. Setting config.batch alone batches all of the destination's events into one shared buffer with no rule needed. Defaults when batching is enabled: size: 1000, age: 30000. wait is a hint, not a hard delay: when events keep arriving faster than wait, the size and age caps are what force the flush.

Conditional rules​

Use an array with conditions to branch by event contents; the first matching rule wins.

Configuration
Loading...
Result
Loading...

Ignore vs skip​

Two flags control destination behaviour and they are not interchangeable:

  • ignore: true: the rule matched but nothing happens. No data transform, no destination call, no side effects. Use for suppression.
  • skip: true: the rule matched and the destination's push() is called. settings.identify, settings.revenue, settings.group, and other side effects still run. Only the destination's default forwarding call (e.g. track(), capture(), event()) is suppressed. Use for "identify without an event" style flows.

If both are set on the same rule, ignore wins.

ignore suppresses one destination. To drop an event before any destination sees it, use a route stop in collector.next; to drop it for one destination by condition, use a stop in that destination's before. See Dropping an event with stop.

Configuration
Loading...
Result
Loading...
{
user: {
login: {
skip: true,
settings: { identify: { map: { user: 'data.id' } } },
},
},
}

The example runs the destination's identify() side effect on user login events but skips the default forwarding call.

API reference​

The two functions below power mapping resolution. Destinations and sources call them through the collector, but they are exported for direct use in custom transformers or tests.

getMappingEvent​

getMappingEvent(
event: WalkerOS.PartialEvent,
mapping?: Mapping.Rules,
): Promise<Mapping.Result>

Resolves the matching rule for an event based on event.name (the "entity action" string). Returns { eventMapping, mappingKey } where eventMapping is the matched Mapping.Rule and mappingKey is the lookup key that matched (including wildcards).

getMappingValue​

getMappingValue(
value: unknown,
mapping: Mapping.Data,
context: Mapping.ValueContext, // { collector, event?, consent? }
): Promise<WalkerOS.Property | undefined>

Resolves a Mapping.Value against an input. Used internally for every data, policy, and set resolution. The companion reference for each value form lives in Mapping.Value.

The context requires the running collector: inside a step, take it from the step's context; elsewhere, use the one startFlow returns.

const { collector } = await startFlow();
await getMappingValue(event, 'data.id', { collector });

Rule-level examples​

Source mapping​

Normalize events before they reach the collector:

await startFlow({
  sources: {
    browser: {
      code: sourceBrowser,
      config: {
        mapping: {
          product: {
            click: {
              name: 'product view',
              policy: {
                'data.id': { fn: (e) => e.data?.id?.toUpperCase() },
              },
            },
          },
          test: { '*': { ignore: true } },
        },
        policy: {
          'user.email': { fn: (e) => e.user?.email?.toLowerCase() },
        },
      },
    },
  },
});

Destination mapping​

Transform for a specific destination API:

await startFlow({
  destinations: {
    gtag: {
      code: destinationGtag,
      config: {
        mapping: {
          product: {
            view: {
              name: 'view_item',
              data: {
                map: {
                  item_id: 'data.id',
                  value: 'data.price',
                  currency: { value: 'USD' },
                },
              },
            },
          },
        },
      },
    },
  },
});

Combined flow​

The same event can be transformed at both stations:

1. Browser sends: "product click"
2. Source mapping: "product click" → "product view"
3. Destination mapping: "product view" → "view_item"
4. GA4 receives: "view_item"

Patching package-shipped rules​

Some packages (such as @walkeros/transformer-ga4) ship their own default mapping rules. Normally, a user rule at the same key replaces the default in full. Two keywords change that:

extend (config layer)​

extend holds a partial rule that is deep-merged onto the package-shipped default at the same key instead of replacing it. The merge happens once at init, before any event is evaluated. A null value clears an inherited field.

Use extend when you want to add or override one field of a default rule while inheriting the rest unchanged.

remove (output layer)​

remove is a list of dotted paths stripped from the produced data payload after evaluation, regardless of how each field was produced (map, fn, include, etc.). Applied last, so it always wins.

Use remove when you must not emit a field (PII, a vendor-reserved parameter) but do not want to rewrite the full rule.

Mode switching​

A rule that has neither extend nor remove keeps today's replace behavior. Presence of either keyword activates merge mode for that rule.

Example: patch a GA4 decoder rule​

The transformer-ga4 purchase default ships id, currency, total, tax, shipping, and coupon. The config below adds affiliation from a GA4 event parameter and drops currency from the output, keeping all other default fields:

{
"settings": {
"mapping": {
"purchase": {
"extend": {
"data": { "map": { "affiliation": "params.ep.affiliation" } }
},
"remove": ["currency"]
}
}
}
}

extend.data.map is deep-merged onto the default data.map, so affiliation is added alongside the existing keys. remove: ["currency"] then strips that field from the final payload.

See also​

💡 Need implementation support?
elbwalker offers hands-on support: setup review, measurement planning, destination mapping, and live troubleshooting. Book a 2-hour session (€399)