Rule Cascade
LearnThe process, end to end

The process, end to end

From a new rule to a rule enforced in production, and back out again. One lesson per stage, each with a hands-on step and a check that tells you it is done.

A rule goes through the same stages every time: you write it, prove it with tests, check it, get it reviewed, compile it into a sealed bundle, publish that bundle, load it in your applications, enforce it, roll it out, and watch it. This track walks through every stage with one small ruleset for an order form. Every command on these pages was run, and every output is the real one.

Diagram, described in Mermaid: flowchart TD A[Author a rule] --> B[Write golden tests] B --> C[Check] C --> D[Review and CI] D --> E[Compile to a bundle] E --> F[Publish and version] F --> G[Load in apps] G --> H[Enforce in UI and API] H --> I[Roll out] I --> J[Monitor] J -->|a change is needed| K[Change across levels, then start again at Author] I -->|something is wrong| R[Roll back to the previous bundle]

The stages

StageWhat you doDone when
Author a ruleWrite one validation rule and one action rule in YAMLThe playground evaluates it
Write golden testsWrite the requests and the answers you expect, in the rulesetEvery test passes, and a wrong one fails
CheckRun rcas checkIt exits with status 0
Review in a pull requestLet CI run the same check on every changeThe required checks are green
Compile to a bundleRun rcas compileYou have a bundle and its checksum
Publish and versionStore the bundle immutably; serve manifests with an ETagA second request answers 304
Load in applicationsLoad the bundle in your service, or ask the rule serverThe service logs the checksum it loaded
Enforce in the UI and the APIEvaluate in the browser for feedback, on the server for the decisionA denied request gets 422 and nothing is saved
Roll out and roll backSwap the rules without a restart; put the old bundle back when neededThe served checksum is the one you expect
MonitorLog the checksum; alert on RULE-EVALUATION-ERRORYou can tell which rules made each decision
Change a rule across levelsOverride an inherited limit in a child rulesetA tightening loads; a loosening is refused

How to follow along

The lessons use the rcas command. Download and verify it first. The sample project is three files: an entity schema, the organisation ruleset shop.orders and a child ruleset shop.orders.eu for the EU business unit.

orders.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet

metadata:
  id: shop.orders
  version: 1.0.0
  title: Orders
  owner: shop-team
  status: active

scope:
  - { level: organization, id: shop }

entities:
  Order:
    schema: { $ref: "./orders.openapi.yaml#/components/schemas/Order" }

params:
  maxQuantity:
    type: integer
    default: 10
    overridePolicy: tighten-only
    tightenDirection: lower

rules:
  - id: order.quantity.max
    kind: validation
    title: An order has at most maxQuantity items
    target: { entity: Order, field: /quantity }
    operations: [create, update]
    triggers: [change, submit]
    when: { op: exists, args: [{ var: data.quantity }] }
    assert: { op: lte, args: [{ var: data.quantity }, { var: params.maxQuantity }] }
    severity: error
    finding:
      code: SHOP-ORD-001
      message: order.quantityTooHigh
      args: { max: { var: params.maxQuantity } }

  - id: order.placed
    kind: action
    title: Announce a new order once it is saved
    target: { entity: Order }
    operations: [create]
    enforcement: server
    commands:
      - name: order.placed
        type: event
        ref: OrderPlaced
        payload: { orderId: { var: data.id }, quantity: { var: data.quantity } }
        idempotencyKey: ["order.placed", { var: data.id }]

messages:
  en:
    order.quantityTooHigh: "You can order at most {max} items."

tests:
  - name: ten items are allowed and announced
    entity: Order
    operation: create
    given:
      data: { id: o-1, quantity: 10 }
    expect:
      decision: allow
      findings: []
      commands: [order.placed]

  - name: eleven items are denied and nothing is announced
    entity: Order
    operation: create
    given:
      data: { id: o-2, quantity: 11 }
    expect:
      decision: deny
      findings:
        - { rule: order.quantity.max, fields: [/quantity], message: You can order at most 10 items. }
      commands: []

  - name: the browser checks the quantity too
    entity: Order
    operation: create
    channel: client
    given:
      data: { id: o-3, quantity: 11 }
      trigger: change
    expect:
      decision: deny
      findings:
        - { rule: order.quantity.max }

Every output on these pages ends with [exit status N]: the status the command exited with. A check that finds a problem exits with 1, so a CI step that runs it fails.

The lessons stand alone. Each one links to a playbook that covers the same stage in more depth for a real service.

Course overview

On this page