Rule Cascade
LearnSafety

Fail closed

A rule that cannot be evaluated never passes silently. It blocks, and the engine says so.

Sometimes a rule cannot be evaluated: it divides by zero, compares a string with gt, or calls a custom operator the host did not register. The engine never treats that as a pass. The rule produces a finding with the code RULE-EVALUATION-ERROR, severity error, blocking: true, and the message "This rule could not be evaluated." The decision is deny.

An error in an action rule also withdraws every command of the request. Nothing is announced for a change that will not be saved.

Syntax

what a failed rule reports
- rule: <rule id>
  code: RULE-EVALUATION-ERROR
  severity: error
  blocking: true
  message: This rule could not be evaluated.

Example

The command's idempotency key divides the total by the quantity. With 0 items the action rule fails, so the request is denied and no command comes back.

fail-closed.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.fail-closed, version: 1.0.0, title: Fail closed }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
  - id: order.total.positive
    kind: validation
    target: { entity: Order, field: /total }
    operations: [create]
    assert: { op: gt, args: [{ var: data.total }, 0] }
    severity: error
    finding: { code: LRN-FCL-001, message: order.totalNotPositive }
  - id: order.created.event
    kind: action
    target: { entity: Order }
    operations: [create]
    enforcement: server
    commands:
      - name: order.created
        type: event
        idempotencyKey: [{ var: data.id }, { op: div, args: [{ var: data.total }, { var: data.quantity }] }]
messages:
  en:
    order.totalNotPositive: "The total must be more than 0."
tests:
  - name: an action that divides by zero denies and withdraws every command
    entity: Order
    operation: create
    given:
      data: { id: "o-1", quantity: 0, total: 20 }
    expect:
      decision: deny
      findings:
        - { rule: order.created.event, code: RULE-EVALUATION-ERROR, severity: error, blocking: true, message: This rule could not be evaluated. }
      commands: []
  - name: a text total is a type error, never a silent pass
    entity: Order
    operation: create
    given:
      data: { id: "o-2", quantity: 2, total: "20" }
    expect:
      decision: deny
      findings:
        - { rule: order.total.positive, code: RULE-EVALUATION-ERROR }
      commands: []
  - name: a normal order is allowed and announced
    entity: Order
    operation: create
    given:
      data: { id: "o-3", quantity: 4, total: 20 }
    expect:
      decision: allow
      findings: []
      commands: [order.created]
request.json
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "id": "o-1",
    "quantity": 0,
    "total": 20
  }
}

Result, from the engine

Decisiondeny1 finding, server channel

  • RULE-EVALUATION-ERRORerrorblockingThis rule could not be evaluated.(no field)
Try it YourselfOpens this ruleset and request in the playground. Nothing to install.

The second golden test sends "20" (a string) as the total. gt takes two numbers and there is no implicit coercion, so that rule fails closed too.

Common mistakes

  • Ignoring the code. RULE-EVALUATION-ERROR in production means a rule meets data it was not written for. Alert on it, and fix the rule or the caller.
  • Guarding too late. Use when to skip a rule that cannot apply, such as a division when the divisor is 0. A skipped rule produces nothing.
  • Hoping for a default. A missing value is null, and null is not a number. Check it with exists, or give it a value with coalesce.

Exercise

Make the action rule skip orders without items. An order of 0 items must then be allowed with no command, and an order of 4 items still announced with order.created.

Hint

Add a when to the action rule: quantity greater than 0.

Show answer
fail-closed.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.fail-closed, version: 1.0.0, title: Fail closed }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
  - id: order.total.positive
    kind: validation
    target: { entity: Order, field: /total }
    operations: [create]
    assert: { op: gt, args: [{ var: data.total }, 0] }
    severity: error
    finding: { code: LRN-FCL-001, message: order.totalNotPositive }
  - id: order.created.event
    kind: action
    target: { entity: Order }
    operations: [create]
    enforcement: server
    when: { op: gt, args: [{ var: data.quantity }, 0] }
    commands:
      - name: order.created
        type: event
        idempotencyKey: [{ var: data.id }, { op: div, args: [{ var: data.total }, { var: data.quantity }] }]
messages:
  en:
    order.totalNotPositive: "The total must be more than 0."
tests:
  - name: an order without items is allowed and not announced
    entity: Order
    operation: create
    given:
      data: { id: "o-1", quantity: 0, total: 20 }
    expect:
      decision: allow
      findings: []
      commands: []
  - name: a normal order is still announced
    entity: Order
    operation: create
    given:
      data: { id: "o-3", quantity: 4, total: 20 }
    expect:
      decision: allow
      findings: []
      commands: [order.created]
request.json
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "id": "o-1",
    "quantity": 0,
    "total": 20
  }
}

Result, from the engine

Decisionallow0 findings, server channel

Course overview

On this page