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
| Property | Type | Description | More |
|---|---|---|---|
name | string | Custom event name override (e.g., "view_item" for "product view") | |
data | any | any | Data transformation rules for event | |
settings | any | Destination-specific settings for this event mapping | |
condition | string | Condition function as string: return true to process event | |
consent | WalkerOS.Consent | Not enforced yet. To require consent for a destination, set consent in its config. | |
policy | Mapping.Policy | Event-level policy overrides (applied after config-level policy) | |
batch | number | object | Batch scheduling: bare number is the debounce wait window (legacy); object form supports wait/size/age. | |
include | Array<string> | Event sections (e.g. ["context", "globals"]) flattened into context.data | |
ignore | boolean | Skip the event entirely. No push, no side effects. Use for suppression. | |
silent | boolean | Run side effects (settings.identify, ...) but suppress the destination default push call. | |
extend | Mapping.RulePatch | Merge 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. | |
remove | Array<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 picks one rule from
mapping[entity][action], where entity and action are the two words of the
event name. An array means "try each in order, first matching rule wins". A
'*' key on the entity or the action level is a wildcard. The lookup tries
four tiers, most specific first:
entity action, the exact key.entity *, the entity's own wildcard action.* action, a wildcard entity with the exact action.* *, the global wildcard.
The first tier that has a rule whose condition passes wins. A tier that has
rules but whose conditions all fail falls through to the next tier, so a
product * rule is still used when every product view rule fails its
condition.
In this example product click has no exact rule, so the second tier wins:
product * is used and * click is never consulted.
An event that matches no rule is still sent. The destination's push receives
it under its own name with no rule (context.rule is undefined), and
context.data holds only what the destination-level data and include
produce. What the destination sends for an unmapped event depends on the
destination; its page says. No setting sends only mapped events. To get that,
ignore everything with a catch-all rule and map the events you want:
{
'*': { '*': { ignore: true } },
order: { complete: { name: 'purchase' } },
}
With the catch-all in place, an event whose specific rules all fail their condition is ignored too (step 3).
Mapping wildcards are first-match: the lookup above picks one rule and 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.
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 fails the event: it is not treated as no match,
because falling through could send an event the rule meant to stop. The push
answers ok: false with the reason mapping, and the collector logs one ERROR
line. 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 and the destination batches). 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 (the debounce wait window in ms) 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.
Batching works only on a destination that implements pushBatch; its page
says whether it does. On any other destination batch has no effect and every
event goes to push right away. Pending batches are flushed when the collector
shuts down (walker shutdown, which the
runner sends on exit), waiting up to 5
seconds per batch. In the browser nothing sends walker shutdown, so events
still waiting in a batch when the page unloads are not sent. The
pushBatch contract
covers what the destination receives.
Conditional rules
Use an array with conditions to branch by event contents; the first matching rule wins.
Ignore vs silent
Two flags control destination behaviour and they are not interchangeable:
ignore: true: the rule matched and the destination'spush()is not called, so nothing is sent and the rule'snameanddataare not applied. What runs before the rule is applied still runs: the destination'siniton its first event, itsbeforechain, the destination-levelpolicyanddata, and the rule'spolicy. Use for suppression. An ignored event is a deliberate skip, not a delivery: the push result lists the destination neither underdonenor underfailed,status.outand the destination'scountdo not move, andstatus.destinations.<id>.skippedcounts the event. The collector writes a DEBUG line, and Observe shows a skip with the reasonignored.silent: true: the rule matched and the destination'spush()is called with the rule. Every destination honourssilent:settings.identify,settings.revenue,settings.group, and other side effects still run and only the default forwarding call (e.g.track(),capture(),event()) is suppressed. Use for "identify without an event" style flows. A destination has no side effect to run for a rule that maps none, so the rule then sends nothing and is not a failure.
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.
{
user: {
login: {
silent: true,
settings: { identify: { map: { user: 'data.id' } } },
},
},
}
On a destination such as Amplitude, the example runs the
identify() side effect on user login events but skips the default
forwarding call.
API reference
The two functions below resolve mappings. 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,
collector?: Collector.Instance,
): Promise<Mapping.Result>
Resolves the matching rule for an event based on event.name (the
"entity action" string), in the lookup order above. Returns
{ eventMapping, mappingKey } where eventMapping is the matched
Mapping.Rule and mappingKey is the lookup key that matched (including
wildcards). When no rule matches, eventMapping is undefined and mappingKey
is an empty string. When there is no mapping, or the event name is a single
word with no action, the result is {}. Pass the collector when rules have conditions: it is
the conditions' context, and without it a condition that throws is not logged
but rejects the call.
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. At a source a rule renames
the event (name), edits it (policy) or drops it (ignore); data,
include, batch, settings and silent have no effect there, because only
the edited event moves on. walkeros validate and the bundle reject those
keys (MAPPING_NO_OP), naming the path and what applies at a source; if such a
config still runs, the collector logs one ERROR line per step and key when the
source registers and runs the step. A dropped event's push result is
{ ok: true, dropped: true }.
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
@walkeros/transformer-ga4 ships its 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. Only a package that ships defaults merges
extend, so it has an effect in @walkeros/transformer-ga4's
settings.mapping only. In any other rule, a destination's own mapping
included, walkeros validate and the bundle reject it (MAPPING_NO_OP): write
the fields on the rule itself.
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.). It is 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. remove works in every rule,
including a destination's own mapping.
Mode switching
A rule that has neither extend nor remove replaces the default in
full. Presence of either keyword activates merge mode for that rule.
Example: patch a GA4 decoder rule
The transformer-ga4 purchase default ships id, currency, value, 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
- Mapping.Value: the eight value forms used in every rule field
- Consent guide: consent model and gating
- Destinations reference: destination-specific
settings mapping.tssource