Skip to main content

Contract

A contract defines what your events should look like — which fields are required, what types they have, and what values are allowed. Contracts are named entries with optional inheritance via extends.

{
"contract": {
"default": {
"tagging": 1,
"globals": { "required": ["country"] },
"events": {
"product": {
"*": { "properties": { "data": { "required": ["id", "name"] } } },
"add": { "properties": { "data": { "required": ["quantity"] } } }
}
}
},
"web": {
"extends": "default",
"consent": { "required": ["analytics"] }
}
}
}

Why contracts?

Without contracts, validation rules live inside each transformer config, duplicated across flows. Contracts solve this:

  • Single source of truth — Define event requirements once at the config level
  • Named and composable — Multiple contracts with inheritance via extends
  • Self-documenting — JSON Schema description and examples annotate your events
  • Versionedtagging tracks contract versions alongside your events
  • Dot-path access — Reference any part with $contract.name.section

Named contracts

Contracts are always a map of named entries. Each entry can contain sections (globals, context, custom, user, consent), event schemas (events), metadata (tagging, description), and an optional extends reference:

Loading...

Inheritance with extends

Use extends to inherit from another named contract. Inheritance is additive — the child contract merges on top of the parent:

  • Sections (globals, consent, etc.) merge additively
  • Events merge at the entity-action level
  • Scalars (tagging) — child wins
  • Chains work: web_loggedin extends web extends default
  • Circular references are detected and throw an error

Resolution order: extends chains are resolved first, then wildcards are expanded on the fully merged result.

Sections

Each section is a JSON Schema for the corresponding WalkerOS.Event field:

SectionEvent fieldPurpose
globalsevent.globalsCross-event key-value pairs (country, currency)
contextevent.contextTiming/context properties
customevent.customCustom properties
userevent.userUser identity and attributes
consentevent.consentConsent state

Event schemas

Inside events, entity-action keyed entries define JSON Schema objects describing a partial WalkerOS.Event:

Loading...

Wildcard inheritance

Contracts support four wildcard levels that merge additively:

LevelPatternMatches
1**All events (global rules)
2*actionA specific action across all entities
3entity*All actions of a specific entity
4entityactionExact match

All matching levels combine. For product add, levels 1, 3, and 4 all apply:

Loading...
Contracts vs mapping wildcards

Contract wildcards use additive merging — all matching levels combine. Mapping wildcards use fallback matching — the first match wins.

This difference is intentional. Contracts define cumulative requirements (entity-level rules always apply to all actions), while mappings select a single transformation target.

Contract: product.* rules AND product.add rules both apply to product add

Mapping: product.add matches first, so product.* is never checked

Merge algorithm

When multiple levels match, schemas merge with these rules:

JSON Schema keywordMerge behavior
requiredUnion (deduplicated) — can only add requirements, never remove
propertiesDeep merge — child wins on conflict for scalar values
Scalar keywords (minimum, maximum, pattern, etc.)Child overrides parent
Annotations (description, examples)Stripped from resolved event schemas
Merge example

Given a parent schema from product.*:

{
"properties": {
"data": { "type": "object", "required": ["id", "name"] }
}
}

And a child schema from product.add:

{
"properties": {
"data": { "type": "object", "required": ["name", "quantity"] }
}
}

The merged result for product add:

{
"properties": {
"data": { "type": "object", "required": ["id", "name", "quantity"] }
}
}

The required arrays are unioned and deduplicated: ["id", "name"] + ["name", "quantity"] = ["id", "name", "quantity"].

$contract dot-path references

Use $contract.name.path to reference any part of a resolved contract. The contract is fully resolved (extends + wildcards) before path access:

Loading...

Deep paths

Access nested values with dot notation:

  • $contract.web.events — all event schemas for the "web" contract
  • $contract.web.events.product.add — single event schema
  • $contract.web.consent — consent section
  • $contract.web.tagging — tagging version number
Advanced: $def aliasing

Reduce repetition by aliasing a contract in definitions:

Loading...
Advanced: $def inside contracts

Definitions can be used inside contracts for shared schema fragments:

Loading...

Versioning

The tagging field tracks contract versions as an incrementing integer:

Loading...

Use $contract.default.tagging to inject the version into collector config:

Loading...

This lets you:

  • Know which contract version validated an event
  • Compare event.version.tagging against the current contract
  • Track contract evolution over time

Using contracts with the validator

The validator transformer enforces contracts at runtime. Wire contract sections to the validator settings:

Loading...

CLI validation

Validate contracts with the CLI or MCP server:

Loading...

The contract validator checks:

  • tagging is a non-negative integer (if present)
  • extends references exist and are not circular
  • Entity and action keys are non-empty
  • Each entry is a valid JSON Schema object
  • Sections (globals, context, etc.) are valid JSON Schema objects

Complete example

Loading...

In the web-shop flow, product add merges these levels:

LevelSourceRules added
Top-level globalsdefault (inherited)globals.country, globals.currency required
Top-level consentdefault (inherited)consent.analytics required
product.*default events (inherited)data.id, data.name required
product.adddefault events (inherited)data.quantity required

Next steps

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