Contract
A contract is the schema for your event data: which fields are required, what types they have, what values are allowed. It lives alongside flows in flow.json as a named, inheritable block that any flow can reference via $contract.<name>.<path>.
{
"contract": {
"default": {
"tagging": 1,
"schema": {
"type": "object",
"properties": {
"globals": { "required": ["country"] }
}
},
"events": {
"product": {
"*": { "properties": { "data": { "required": ["id", "name"] } } },
"add": { "properties": { "data": { "required": ["quantity"] } } }
}
}
}
}
}
Why use a contract
A contract is a single, inheritable description of what your events should look like. The contract itself describes; enforcement is an explicit @walkeros/transformer-validate step that references it. Tools and humans also read the contract for governance, documentation, and schema-driven workflows.
- Single source of truth. Define
product addrequirements once. Every flow that ships these events references the same definition. - Inheritance. Layer additional rules on top with
extend(for example,web_loggedinextendwebextenddefault) rather than copying. - Versioned.
taggingtracks contract revisions alongside the events they govern. - Self-documenting. Each schema is JSON Schema, so
descriptionandexamplesannotate fields the same way humans and tools read them. - Decoupled from enforcement. The contract describes what events should look like; the validate transformer enforces it where you place it in the pipeline.
- Composable with the rest of the config. Reference fragments anywhere via
$contract.<name>.<path>(see Reference syntax).
If your flow has a single throwaway event, you do not need a contract. Reach for one as soon as the same shape needs to hold across more than one flow.
The shape
contract is a top-level key on flow.json (parallel to flows, not nested inside a flow). Each entry is a named contract. The full shape:
| Field | Purpose |
|---|---|
extend? | Inherit from another named contract |
tagging? | Integer revision marker |
description? | Human-readable note |
events? | Entity-action keyed JSON Schemas, applied per event |
schema? | A single JSON Schema applied to every event |
The default contract of
flow-complete.json,
the tested example flow, uses every field but extend:
{
"description": "Every event from the shop: explicit consent, a page language, and complete ecommerce data.",
"tagging": 1,
"schema": { … },
"events": { … }
}contract/contract/default/description: "Every event from the shop: explicit consent, a page language, and complete ecommerce data.". description says what the contract is for. Guide chapter contract
/contract/default/tagging: 1. tagging is the contract version the tagging follows. Guide chapter contract
Schema
schema is a JSON Schema for the full event. Standard event field names (globals, context, consent, user, custom, source, data) live inside schema.properties. The schema runs on every event the contract governs, in addition to any per-event rules under events.
{
"type": "object",
"required": [
"consent",
"globals"
],
"properties": {
"consent": {
"type": "object",
"required": [
"functional"
],
"properties": {
"functional": {
"const": true
},
"marketing": {
"const": true
}
}
},
"globals": {
"type": "object",
"required": [
"language"
],
"properties": {
"language": {
"type": "string",
"pattern": "^[a-z]{2}$"
}
}
},
"user": {
"type": "object",
"properties": {
"device": {
"type": "string"
},
"session": {
"type": "string"
}
}
}
}
}contractEvent schemas
Inside events, entity-action keyed entries define JSON Schemas for a partial WalkerOS.Event. The * key matches anything (see Wildcard inheritance).
{
"product": {
"*": { … },
"impression": { … },
"add": { … }
},
"order": {
"complete": { … }
},
"session": {
"start": { … }
}
}contract{
"properties": {
"data": {
"type": "object",
"required": [
"id",
"name",
"price"
],
"properties": {
"id": {
"type": "string",
"minLength": 1
},
"name": {
"type": "string",
"minLength": 1
},
"price": {
"type": "number",
"minimum": 0
}
}
}
}
}contractInheritance with extend
Use extend to inherit from another named contract. Inheritance is additive:
schemamerges additively across the chain (deep merge ofproperties, union ofrequired)eventsmerge at the entity-action level- Scalars (
tagging,description): child overrides if set, otherwise inherited from parent - Chains supported:
web_loggedinextendwebextenddefault - Circular references are detected and throw
Extend chains are resolved first, then wildcards expand on the merged result. Schema merging follows the same additive rules as wildcard merging (see Merge rules), so the same limitations on JSON Schema composition keywords apply.
"contract": {
"default": {
"tagging": 1,
"schema": {
"properties": {
"globals": { "required": ["country"] }
}
}
},
"web": {
"extend": "default",
"events": {
"product": {
"add": { "properties": { "data": { "required": ["id", "quantity"] } } }
}
}
},
"web_loggedin": {
"extend": "web",
"schema": {
"properties": {
"user": {
"required": ["id"],
"properties": { "id": { "type": "string" } }
}
}
}
}
}
web_loggedin resolves to: tagging: 1 (from default), globals.country required (from default), events.product.add (from web), and user.id required (added by web_loggedin), all within a single merged schema.
In the complete example, the server contract extends default with what the
server adds before any vendor sees an event, the cookieless visitor hash:
/contract/server/extend: "default". server extends default with what the server adds: the visitor hash. Guide chapter contract
{
"type": "string",
"description": "Cookieless visitor id: HMAC-SHA256 with a secret salt over the anonymized IP, browser with major version, OS and site. Rotates daily (UTC) and is per site, so count visitors per day."
}contractWildcard inheritance
Contracts support four wildcard levels in events. For entity-action pairs listed under events, all matching levels 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 |
For product add, levels 1, 3, and 4 all apply and combine:
"events": {
"*": { "*": { "properties": { "consent": { "required": ["analytics"] } } } },
"product": { "*": { "properties": { "data": { "required": ["id", "name"] } } },
"add": { "properties": { "data": { "required": ["quantity"] } } } }
}
// Resolved "product add":
// consent.analytics required (from * -> *)
// data.id, data.name required (from product -> *)
// data.quantity required (from product -> add)
Wildcards expand into the entity-action pairs listed under events, and only there do all matching levels merge. An event whose pair is not listed gets a single entry instead, the first that exists of: entity → * (which already carries the * → * rules), * → action (without the * → * rules), * → *. In the example above, product view gets the product → * rules plus the * → * rules. List a pair explicitly when several levels must apply to it.
Contract wildcards use additive merging for pairs listed under events: all matching levels combine.
Mapping wildcards use fallback matching: the first match wins.
This difference is intentional. Contracts express cumulative requirements (entity-level rules apply to every action); 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 rules
When multiple wildcard levels (or extend chains) match, the JSON Schemas merge with these rules:
| JSON Schema keyword | Merge behavior |
|---|---|
required | Union (deduplicated) |
properties and other object-valued keywords (items, not, ...) | Deep merge |
Scalar and array keywords (minimum, pattern, enum, oneOf, ...) | Child overrides parent |
Annotations (description, examples, title, $comment) | Stripped from resolved event schemas |
required arrays are united and object values merge key by key. Every other array (oneOf, enum, type arrays, allOf) and every scalar is replaced by the child's value. Use allOf composition at the schema level if full JSON Schema composition is needed.
Annotations stay on the source contract for tooling (CLI hints, IDE descriptions). Resolved event schemas are stripped; the schema block keeps its annotations. Validators ignore annotations either way.
$contract references
Reference any part of a resolved contract with $contract.<name>.<path>. The contract is fully resolved (extend + wildcards) before path access, so the returned value is the merged shape, not the raw entry. A validate transformer references a contract via its contract setting:
"transformers": {
"validate": {
"package": "@walkeros/transformer-validate",
"config": { "settings": { "contract": ["$contract.web"], "mode": "strict" } }
}
}
Deep paths
Walk into the resolved shape with dot notation:
| Path | Returns |
|---|---|
$contract.web | The fully resolved contract entry |
$contract.web.schema | The merged event-level JSON Schema |
$contract.web.schema.properties.globals | Just the resolved globals sub-schema |
$contract.web.schema.properties.consent | Just the resolved consent sub-schema |
$contract.web.events | All resolved event schemas for the web contract |
$contract.web.events.product.add | Resolved schema for product add, with all matching wildcard levels merged in |
$contract.web.tagging | The contract revision number |
CLI validation
Validate contracts standalone or as part of a flow:
# Validate a contract file
walkeros validate contract.json --type contract
# Validate inline
echo '{"default":{"events":{"product":{"add":{"properties":{}}}}}}' | walkeros validate --type contract
# Validate a full flow (covers contract structure + example compliance)
walkeros validate flow.json
The contract validator checks:
- The root is an object of named contract entries; a flat entity-action map or a
$-prefixed root key is rejected as the flat shape taggingis a number (if present)extendreferences exist and are not circular- Entity and action keys are non-empty
schema, if present, is a valid JSON Schema object- Each event entry is a valid JSON Schema object
- A key outside
extend,tagging,description,events,schemais a warning, not an error. Exception: top-levelglobals,context,custom,userandconsentkeys pass without a warning, but no validator reads them. Put these rules underschema.properties.
Advanced: shared schema fragments via variables
Top-level variables holds reusable values that any part of the config can pull in with $var.<name>. Whole-string references preserve native type; deep paths walk into the value:
{
"variables": {
"idSchema": {
"required": ["id"],
"properties": { "id": { "type": "string" } }
}
},
"contract": {
"web": {
"events": {
"product": {
"*": { "properties": { "data": "$var.idSchema" } }
}
}
}
}
}
Use variables when the same JSON Schema fragment shows up in many events. For contract-shaped reuse across flows, prefer extend.
Complete example
A canonical, tested flow with a multi-entry contract lives at
packages/cli/examples/flow-complete.json.
Its server flow's validate step references $contract.server (see
Validate). For
product add, that reference resolves to these rules, all from the merged
shape:
| Source | Rule |
|---|---|
default.schema.properties.consent | functional required and true, marketing true when present |
default.schema.properties.globals | language required, two lower-case letters |
default.events.product.* | data.id, data.name non-empty, data.price a number of at least 0 |
default.events.product.add | data.quantity required, integer, minimum 1 |
server.schema.properties.user | user.hash required |
Next steps
- Validate: enforce a contract at runtime with the validate transformer
- Mapping: transform events between steps
- Step examples: pair every step with input/output fixtures
- Reference syntax: all
$contract,$var,$flow,$store,$secret,$code:,$envreferences