Expressions
Literals, variables and operator calls — and the roots a variable can start from.
An expression is plain JSON, so every language reads it the same way. It is a literal (a string,
number, boolean or null), a variable ({ var: <root>.<path> }), an operator call
({ op, args }) or a function call ({ fn, args }). Operator calls nest. A variable starts at a root:
data is the proposed entity, actor is who acts, and ctx holds facts the host passes in, such as
today's date.
Syntax
10 # literal
manager # literal string
{ var: data.discount } # variable
{ op: gt, args: [{ var: data.discount }, 10] } # operator call; args are expressions| Root | Bound to |
|---|---|
data | The proposed state of the entity |
original | The stored state; null on create |
actor | Who acts. The host sets it from its own authentication |
ctx | Facts the host passes in, such as ctx.today |
params | Parameter values |
The engine never reads a clock. If a rule needs today's date, the request carries it in ctx.
Example
A discount above 10 needs the role manager, read from actor.roles. The delivery date must be
after ctx.today. The request comes from a clerk.
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.expressions, version: 1.0.0, title: "Expressions" }
scope:
- { level: organization, id: learn }
entities:
Order:
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
- id: order.discount.manager
kind: validation
target: { entity: Order, field: /discount }
operations: [create]
when: { op: gt, args: [{ var: data.discount }, 10] }
assert: { op: in, args: [manager, { var: actor.roles }] }
severity: error
finding: { code: LRN-EXP-001, message: order.discountNeedsManager }
- id: order.delivery.future
kind: validation
target: { entity: Order, field: /deliveryDate }
operations: [create]
when: { op: exists, args: [{ var: data.deliveryDate }] }
assert: { op: gte, args: [{ op: daysBetween, args: [{ var: ctx.today }, { var: data.deliveryDate }] }, 1] }
severity: error
finding: { code: LRN-EXP-002, message: order.deliveryInPast }
messages:
en:
order.discountNeedsManager: "Only a manager may give a discount above 10."
order.deliveryInPast: "Choose a delivery date after today."
tests:
- name: a clerk cannot give a discount of 15
entity: Order
operation: create
given:
data: { discount: 15, deliveryDate: "2026-10-10" }
actor: { id: u-7, roles: [clerk] }
ctx: { today: "2026-10-03" }
expect:
decision: deny
findings:
- { rule: order.discount.manager, fields: [/discount] }
- name: a manager can
entity: Order
operation: create
given:
data: { discount: 15, deliveryDate: "2026-10-10" }
actor: { id: u-1, roles: [manager] }
ctx: { today: "2026-10-03" }
expect: { decision: allow, findings: [] }
- name: delivery today is too soon
entity: Order
operation: create
given:
data: { discount: 0, deliveryDate: "2026-10-03" }
actor: { id: u-7, roles: [clerk] }
ctx: { today: "2026-10-03" }
expect:
decision: deny
findings:
- { rule: order.delivery.future, fields: [/deliveryDate] }{
"entity": "Order",
"operation": "create",
"data": {
"discount": 15,
"deliveryDate": "2026-10-10"
},
"actor": {
"id": "u-7",
"roles": [
"clerk"
]
},
"ctx": {
"today": "2026-10-03"
}
}Result, from the engine
Decisiondeny1 finding, server channel
LRN-EXP-001errorblockingOnly a manager may give a discount above 10./discount
Common mistakes
- Taking
actorfrom the browser. The server setsactorfrom its own login session, never from the request body. - Reading the time inside a rule. Pass it in
ctxso the result is the same in every engine and every replay. - Writing
data.discount > 10. There is no text syntax: write{ op: gt, args: [...] }.
Exercise
Raise the limit: a clerk may give a discount up to 20. Above 20 still needs a manager.
Hint
Change the number in the when guard and in the message. A discount of 15 should then be allowed for a clerk.
Show answer
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.expressions, version: 1.0.0, title: "Expressions" }
scope:
- { level: organization, id: learn }
entities:
Order:
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
- id: order.discount.manager
kind: validation
target: { entity: Order, field: /discount }
operations: [create]
when: { op: gt, args: [{ var: data.discount }, 20] }
assert: { op: in, args: [manager, { var: actor.roles }] }
severity: error
finding: { code: LRN-EXP-001, message: order.discountNeedsManager }
- id: order.delivery.future
kind: validation
target: { entity: Order, field: /deliveryDate }
operations: [create]
when: { op: exists, args: [{ var: data.deliveryDate }] }
assert: { op: gte, args: [{ op: daysBetween, args: [{ var: ctx.today }, { var: data.deliveryDate }] }, 1] }
severity: error
finding: { code: LRN-EXP-002, message: order.deliveryInPast }
messages:
en:
order.discountNeedsManager: "Only a manager may give a discount above 20."
order.deliveryInPast: "Choose a delivery date after today."
tests:
- name: a clerk can give a discount of 15
entity: Order
operation: create
given:
data: { discount: 15, deliveryDate: "2026-10-10" }
actor: { id: u-7, roles: [clerk] }
ctx: { today: "2026-10-03" }
expect: { decision: allow, findings: [] }
- name: a clerk cannot give a discount of 25
entity: Order
operation: create
given:
data: { discount: 25, deliveryDate: "2026-10-10" }
actor: { id: u-7, roles: [clerk] }
ctx: { today: "2026-10-03" }
expect:
decision: deny
findings:
- { rule: order.discount.manager, fields: [/discount] }
- name: a manager can
entity: Order
operation: create
given:
data: { discount: 25, deliveryDate: "2026-10-10" }
actor: { id: u-1, roles: [manager] }
ctx: { today: "2026-10-03" }
expect: { decision: allow, findings: [] }
- name: delivery today is too soon
entity: Order
operation: create
given:
data: { discount: 0, deliveryDate: "2026-10-03" }
actor: { id: u-7, roles: [clerk] }
ctx: { today: "2026-10-03" }
expect:
decision: deny
findings:
- { rule: order.delivery.future, fields: [/deliveryDate] }{
"entity": "Order",
"operation": "create",
"data": {
"discount": 15,
"deliveryDate": "2026-10-10"
},
"actor": {
"id": "u-7",
"roles": [
"clerk"
]
},
"ctx": {
"today": "2026-10-03"
}
}Result, from the engine
Decisionallow0 findings, server channel