Rule Cascade
Playbooks

Author and ship a rule change

From an edited ruleset to production - pull request, CI checks and compile, version bump, immutable bundle, rollout and rollback.

The running example makes the transfer memo mandatory in retail: the organisation rule org.transfer.memo-recommended is raised from warning to error in examples/contracts/payments-transfer.ruleset.yaml. Substitute your own ruleset and rule.

Before you start

  • The command rule-cascade (build it as in the quickstart), or Python 3.10 with pip install -r tools/requirements.txt and python tools/rulecheck.py in its place.
  • Write access to the repository that holds your rulesets, and CI that runs on pull requests.

Steps

Branch from main

git checkout -b feat/transfer-memo-required origin/main

Branch names are <type>/<short-description>; commits follow Conventional Commits (contributing).

Change the rule

The parent marks the memo rule tighten-only, so the feature ruleset may raise its severity in overrides:

payments-transfer.ruleset.yaml
overrides:
  rules:
    - rule: org.transfer.memo-recommended
      set: { severity: error }
      reason: Retail operations needs a memo on every transfer for dispute handling.

For a new rule, give it an id, a finding code and a message key in the style of the naming conventions, a message under messages.<locale>, and an enforcement. Anything a user must not read is server (guideline section 8).

Raise the version

Choose the bump by what callers can observe (guideline 1.5):

ChangeVersion
Adds an info rule, a message or a testpatch
Adds a warning without acknowledgement, a state or compute rule, or a parameterminor
Adds an error rule or a warning that must be acknowledged, raises a severity, tightens a parameter, removes or renames anythingmajor

Raising a severity is major: metadata.version: 2.0.0.

Add golden tests

One test that the change triggers, under tests::

  - name: a transfer without a memo is denied
    entity: Transfer
    operation: create
    given:
      data: { id: t-20, type: domestic, amount: 50, currency: USD,
              beneficiary: { name: Jo Lee, country: US } }
    expect:
      decision: deny
      findings:
        - { rule: org.transfer.memo-recommended, blocking: true }

Check, and fix what it reports

rule-cascade check examples/contracts/*.ruleset.yaml

With only the steps above done, check refuses the change twice, and both are right:

acme.payments.transfer@2.0.0  sha256:b1faf3a8db6c...  14 rules (9 client-safe), 4 params
  BINDING_MISMATCH: ./payments.openapi.yaml wants version ^1.0.0
  FAIL  valid domestic transfer is allowed, memo missing is a non-blocking warning
        decision "deny" != "allow"
        no finding matches {"rule":"org.transfer.memo-recommended","blocking":false}
  15 golden tests, 1 failed
  • The API description binds the ruleset with x-rule-cascade: { version: ^1.0.0 }. A major version is a contract change for the API too: update the binding to ^2.0.0 in payments.openapi.yaml.
  • An existing golden test asserted the old behaviour. Update it (add a memo, expect no findings) rather than deleting it.

Run check again until every ruleset ends with 0 failed and the exit status is 0:

acme.payments.transfer@2.0.0  sha256:b1faf3a8db6c...  14 rules (9 client-safe), 4 params
  15 golden tests, 0 failed

Other problems check reports: LOAD FAILED with a code from specification section 5, YAML_NOT_PORTABLE (quote no, 012, 2026-10-03), NUMBER_NOT_PORTABLE. Check every ruleset that extends the one you changed: their checksums change with it.

Open the pull request; CI checks and compiles

CI runs check on every ruleset, fails on a non-zero status, compiles the bundle and keeps it as an artefact:

- name: Check the rulesets and run their golden tests
  run: rule-cascade check rules/*.ruleset.yaml
- name: Compile
  run: rule-cascade compile rules/payments-transfer.ruleset.yaml -o build/acme.payments.transfer.bundle.json
- uses: actions/upload-artifact@v4
  with:
    name: rule-bundles
    path: build/*.bundle.json

This repository's own job is contract in .github/workflows/ci.yml. .github/CODEOWNERS routes rulesets to their owners for review.

Store the bundle immutably

After the merge, store the bundle CI compiled under its id, version and checksum, and never overwrite one. The checksum identifies the resolved rules, parents included.

Roll out

Where the rules runHow the new bundle arrives
A service that embeds a runtimeA rolling deployment with the new bundle. A broken bundle stops start-up, so the rollout stops at the first instance
The rule serverA new ConfigMap and a rolling restart (Kubernetes playbook), or SIGHUP to reload; a failed reload keeps the previous rules
BrowsersNothing to deploy: the next manifest fetch revalidates the ETag and receives the new client manifest

A browser can hold an older manifest than the server enforces for a while. That is safe: the server decides, and its responses name the new checksum so the UI refetches (enforcement guide step 8).

Verify, or roll back

Every decision log line carries the ruleset id, version and checksum. Confirm the new checksum appears in production. To roll back, deploy the previous bundle from the store.

Done when check exits 0 in CI, the deployed bundle is the one CI compiled, and production decision logs show acme.payments.transfer 2.0.0 with its new checksum, with no change to UI or API code.

Source: site/content/docs/playbooks/ship-a-rule-change.mdx

On this page