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
descriptionandexamplesannotate your events - Versioned —
taggingtracks 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:
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_loggedinextendswebextendsdefault - 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:
| Section | Event field | Purpose |
|---|---|---|
globals | event.globals | Cross-event key-value pairs (country, currency) |
context | event.context | Timing/context properties |
custom | event.custom | Custom properties |
user | event.user | User identity and attributes |
consent | event.consent | Consent state |
Event schemas
Inside events, entity-action keyed entries define JSON Schema objects
describing a partial WalkerOS.Event:
Wildcard inheritance
Contracts support four wildcard levels that merge additively:
| Level | Pattern | Matches |
|---|---|---|
| 1 | * → * | All events (global rules) |
| 2 | * → action | A specific action across all entities |
| 3 | entity → * | All actions of a specific entity |
| 4 | entity → action | Exact match |
All matching levels combine. For product add, levels 1, 3, and 4 all apply:
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 keyword | Merge behavior |
|---|---|
required | Union (deduplicated) — can only add requirements, never remove |
properties | Deep 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:
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:
Advanced: $def inside contracts
Definitions can be used inside contracts for shared schema fragments:
Versioning
The tagging field tracks contract versions as an incrementing integer:
Use $contract.default.tagging to inject the version into collector config:
This lets you:
- Know which contract version validated an event
- Compare
event.version.taggingagainst 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:
CLI validation
Validate contracts with the CLI or MCP server:
The contract validator checks:
taggingis a non-negative integer (if present)extendsreferences 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
In the web-shop flow, product add merges these levels:
| Level | Source | Rules added |
|---|---|---|
Top-level globals | default (inherited) | globals.country, globals.currency required |
Top-level consent | default (inherited) | consent.analytics required |
product.* | default events (inherited) | data.id, data.name required |
product.add | default events (inherited) | data.quantity required |
Next steps
- Validator transformer — Enforce contracts at runtime
- Mapping — Transform events for destinations
- Flow configuration — Full flow config reference