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.
The stages
| Stage | What you do | Done when |
|---|---|---|
| Author a rule | Write one validation rule and one action rule in YAML | The playground evaluates it |
| Write golden tests | Write the requests and the answers you expect, in the ruleset | Every test passes, and a wrong one fails |
| Check | Run rcas check | It exits with status 0 |
| Review in a pull request | Let CI run the same check on every change | The required checks are green |
| Compile to a bundle | Run rcas compile | You have a bundle and its checksum |
| Publish and version | Store the bundle immutably; serve manifests with an ETag | A second request answers 304 |
| Load in applications | Load the bundle in your service, or ask the rule server | The service logs the checksum it loaded |
| Enforce in the UI and the API | Evaluate in the browser for feedback, on the server for the decision | A denied request gets 422 and nothing is saved |
| Roll out and roll back | Swap the rules without a restart; put the old bundle back when needed | The served checksum is the one you expect |
| Monitor | Log the checksum; alert on RULE-EVALUATION-ERROR | You can tell which rules made each decision |
| Change a rule across levels | Override an inherited limit in a child ruleset | A 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.
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.