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. It does not enforce anything at runtime: tools and humans read it for governance, documentation, and schema-driven workflows. Without a contract, schema rules can live inline (validate: on each step), which duplicates across flows.
- 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. Contracts describe what events should look like, step-level
validate:references them, consumers decide whether to enforce. - 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 |
{
"version": 4,
"contract": {
"default": {
"tagging": 1,
"schema": {
"type": "object",
"properties": {
"globals": {
"required": ["country"],
"properties": {
"country": { "type": "string" }
}
},
"consent": {
"required": ["analytics"],
"properties": {
"analytics": { "type": "boolean", "const": true }
}
}
}
}
},
"web": {
"extend": "default",
"events": {
"product": {
"add": {
"properties": {
"data": { "required": ["id", "quantity"] }
}
}
}
}
}
}
}
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.
Event schemas
Inside events, entity-action keyed entries define JSON Schemas for a partial WalkerOS.Event. The * key matches anything (see Wildcard inheritance).
"events": {
"product": {
"*": {
"description": "A product in the catalog",
"properties": {
"data": {
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string", "description": "Product SKU" },
"name": { "type": "string", "description": "Display name" }
}
}
}
},
"add": {
"description": "Product added to cart",
"properties": {
"data": {
"type": "object",
"required": ["quantity"],
"properties": {
"quantity": { "type": "integer", "minimum": 1 }
}
}
}
}
}
}
Inheritance 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.
Wildcard inheritance
Contracts support four wildcard levels in events that all 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)
Contract wildcards use additive merging: 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 | Deep merge |
Scalar keywords (minimum, pattern, ...) | Child overrides parent |
Annotations (description, examples, title, $comment) | Stripped from resolved event schemas |
Additive deep-merge applies to properties (deep-merge) and required arrays (union/dedup). Other JSON Schema keywords (oneOf, enum, type arrays, allOf) follow child-wins. 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), but the runtime schema seen by validators is the stripped form.
$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.
"destinations": {
"ga4": {
"validate": {
"schema": "$contract.web.schema",
"events": "$contract.web.events"
}
}
}
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:
taggingis a non-negative integer (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
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.
A shorter end-to-end illustration:
{
"version": 4,
"contract": {
"default": {
"tagging": 1,
"description": "Web shop tracking contract",
"schema": {
"type": "object",
"properties": {
"globals": {
"required": ["country", "currency"],
"properties": {
"country": { "type": "string", "pattern": "^[A-Z]{2}$" },
"currency": { "type": "string", "pattern": "^[A-Z]{3}$" }
}
},
"consent": {
"required": ["analytics"],
"properties": {
"analytics": { "type": "boolean", "const": true }
}
}
}
},
"events": {
"product": {
"*": { "properties": { "data": { "required": ["id", "name"] } } },
"add": { "properties": { "data": { "required": ["quantity"], "properties": { "quantity": { "type": "integer", "minimum": 1 } } } } }
},
"order": {
"complete": { "properties": { "data": { "required": ["total"], "properties": { "total": { "type": "number", "minimum": 0 } } } } }
}
}
},
"web_loggedin": {
"extend": "default",
"schema": {
"properties": {
"user": {
"required": ["id", "email"],
"properties": {
"id": { "type": "string" },
"email": { "type": "string", "format": "email" }
}
}
}
}
}
},
"flows": {
"web-shop": {
"config": { "platform": "web" },
"destinations": {
"ga4": {
"validate": {
"schema": "$contract.web_loggedin.schema",
"events": "$contract.web_loggedin.events"
}
}
}
}
}
}
For product add, a validate: block that references $contract.web_loggedin sees these rules (all from the merged shape):
| Source | Rule |
|---|---|
default.schema.properties.globals | country, currency required, both match pattern |
default.schema.properties.consent | analytics required and true |
default.events.product.* | data.id, data.name required |
default.events.product.add | data.quantity required, integer, minimum 1 |
web_loggedin.schema.properties.user | user.id, user.email required |
Next steps
- Validate: step-level
validate:primitive that consumes contracts - Mapping: transform events between steps
- Step examples: pair every step with input/output fixtures
- Reference syntax: all
$contract,$var,$flow,$store,$secret,$code:,$envreferences