Rule Cascade
LearnRule basics

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

the forms of an expression
10                                        # literal
manager                                   # literal string
{ var: data.discount }                    # variable
{ op: gt, args: [{ var: data.discount }, 10] }   # operator call; args are expressions
RootBound to
dataThe proposed state of the entity
originalThe stored state; null on create
actorWho acts. The host sets it from its own authentication
ctxFacts the host passes in, such as ctx.today
paramsParameter 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.

expressions.ruleset.yaml
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] }
request.json
{
  "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
Try it YourselfOpens this ruleset and request in the playground. Nothing to install.

Common mistakes

  • Taking actor from the browser. The server sets actor from its own login session, never from the request body.
  • Reading the time inside a rule. Pass it in ctx so 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
expressions.ruleset.yaml
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] }
request.json
{
  "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

Course overview

On this page