Skip to main content
Ask your AI

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

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

The default contract of flow-complete.json, the tested example flow, uses every field but extend:

/contract/default
{
  "description": "Every event from the shop: explicit consent, a page language, and complete ecommerce data.",
  "tagging": 1,
  "schema": { … },
  "events": { … }
}
The contract names what every event must carry; the validate step and validate --strict read it. Guide chapter 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.

/contract/default/schema
{
  "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"
        }
      }
    }
  }
}
schema applies to every event: functional consent is a hard gate, language a required global. Guide chapter contract

Event schemas​

Inside events, entity-action keyed entries define JSON Schemas for a partial WalkerOS.Event. The * key matches anything (see Wildcard inheritance).

/contract/default/events
{
  "product": {
    "*": { … },
    "impression": { … },
    "add": { … }
  },
  "order": {
    "complete": { … }
  },
  "session": {
    "start": { … }
  }
}
events hold one JSON Schema per entity and action. Guide chapter contract
/contract/default/events/product/*
{
  "properties": {
    "data": {
      "type": "object",
      "required": [
        "id",
        "name",
        "price"
      ],
      "properties": {
        "id": {
          "type": "string",
          "minLength": 1
        },
        "name": {
          "type": "string",
          "minLength": 1
        },
        "price": {
          "type": "number",
          "minimum": 0
        }
      }
    }
  }
}
product * applies to every product action. Guide chapter contract

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.

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

/contract/server/schema/properties/user/properties/hash
{
  "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."
}
The description tells analysts what user.hash is and how to count it. Guide chapter contract

Wildcard inheritance​

Contracts support four wildcard levels in events. For entity-action pairs listed under events, all matching levels merge additively:

LevelPatternMatches
1* → *All events (global rules)
2* → actionA specific action across all entities
3entity → *All actions of a specific entity
4entity → actionExact 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.

Contracts vs mapping wildcards

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 keywordMerge behavior
requiredUnion (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:

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:

  • 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
  • tagging is a number (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
  • A key outside extend, tagging, description, events, schema is a warning, not an error. Exception: top-level globals, context, custom, user and consent keys pass without a warning, but no validator reads them. Put these rules under schema.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:

SourceRule
default.schema.properties.consentfunctional required and true, marketing true when present
default.schema.properties.globalslanguage 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.adddata.quantity required, integer, minimum 1
server.schema.properties.useruser.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:, $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)