Skip to main content

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 add requirements once. Every flow that ships these events references the same definition.
  • Inheritance. Layer additional rules on top with extend (for example, web_loggedin extend web extend default) rather than copying.
  • Versioned. tagging tracks contract revisions alongside the events they govern.
  • Self-documenting. Each schema is JSON Schema, so description and examples annotate 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:

FieldPurpose
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:

  • schema merges additively across the chain (deep merge of properties, union of required)
  • events merge at the entity-action level
  • Scalars (tagging, description): child overrides if set, otherwise inherited from parent
  • Chains supported: web_loggedin extend web extend default
  • 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:

LevelPatternMatches
1**All events (global rules)
2*actionA specific action across all entities
3entity*All actions of a specific entity
4entityactionExact 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)
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 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 keywordMerge behavior
requiredUnion (deduplicated)
propertiesDeep 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:

PathReturns
$contract.webThe fully resolved contract entry
$contract.web.schemaThe merged event-level JSON Schema
$contract.web.schema.properties.globalsJust the resolved globals sub-schema
$contract.web.schema.properties.consentJust the resolved consent sub-schema
$contract.web.eventsAll resolved event schemas for the web contract
$contract.web.events.product.addResolved schema for product add, with all matching wildcard levels merged in
$contract.web.taggingThe 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:

  • tagging is a non-negative integer (if present)
  • extend references 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):

SourceRule
default.schema.properties.globalscountry, currency required, both match pattern
default.schema.properties.consentanalytics required and true
default.events.product.*data.id, data.name required
default.events.product.adddata.quantity required, integer, minimum 1
web_loggedin.schema.properties.useruser.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:, $env references
💡 Need implementation support?
elbwalker offers hands-on support: setup review, measurement planning, destination mapping, and live troubleshooting. Book a 2-hour session (€399)