Rule Cascade
LearnValues and types

Dates and times

Dates are RFC 3339 strings. A date-time is converted to UTC before its calendar date is taken.

There is no date type: a date is a string in RFC 3339 form. A date is "2026-10-03". A date-time has seconds and an offset: "2026-10-03T09:00:00Z" or "2026-10-03T22:00:00-05:00". The date operators convert a date-time to UTC first, then take its calendar date. daysBetween counts whole calendar days; yearsBetween counts completed years, the way ages are counted.

Syntax

date operators
{ op: daysBetween,  args: [{ var: data.orderDate }, { var: data.deliveryDate }] }   # whole days, first to second
{ op: yearsBetween, args: [{ var: data.dateOfBirth }, { var: ctx.today }] }          # completed years
AcceptedRefused (an evaluation error)
2026-10-032026-02-30 (no such date)
2026-10-03T09:00:00Z2026-10-03T09:00Z (no seconds)
2026-10-03T09:00:00.5-04:002026-10-03 09:00:00 (space, no offset)

Example

Delivery must be at least one day after the order. At 22:00 in Chicago (UTC−5) it is already 4 October in UTC, so a delivery on the 4th is the same day.

dates.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.dates, version: 1.0.0, title: "Dates" }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
  - id: order.delivery.next-day
    kind: validation
    target: { entity: Order, field: /deliveryDate }
    operations: [create]
    assert: { op: gte, args: [{ op: daysBetween, args: [{ var: data.orderDate }, { var: data.deliveryDate }] }, 1] }
    severity: error
    finding: { code: LRN-DATE-001, message: order.deliveryTooSoon }
messages:
  en:
    order.deliveryTooSoon: "Delivery is at least one day after the order."
tests:
  - name: 22:00 in Chicago is already the next day in UTC
    entity: Order
    operation: create
    given:
      data: { orderDate: "2026-10-03T22:00:00-05:00", deliveryDate: "2026-10-04" }
    expect:
      decision: deny
      findings:
        - { rule: order.delivery.next-day, fields: [/deliveryDate] }
  - name: ordered on the 3rd in UTC, delivered on the 4th
    entity: Order
    operation: create
    given:
      data: { orderDate: "2026-10-03T10:00:00Z", deliveryDate: "2026-10-04" }
    expect: { decision: allow, findings: [] }
request.json
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "orderDate": "2026-10-03T22:00:00-05:00",
    "deliveryDate": "2026-10-04"
  }
}

Result, from the engine

Decisiondeny1 finding, server channel

  • LRN-DATE-001errorblockingDelivery is at least one day after the order./deliveryDate
Try it YourselfOpens this ruleset and request in the playground. Nothing to install.

Common mistakes

  • Leaving dates unquoted in YAML. 2026-10-03 without quotes is a date object to some parsers. Write "2026-10-03".
  • Reading today's date inside the rule. The engine has no clock: pass it as ctx.today.
  • Sending a date that does not exist. The rule cannot be evaluated, so it fails closed with RULE-EVALUATION-ERROR and the request is denied.

Exercise

Add a rule for the Customer entity: a customer must be at least 18 on ctx.today. Test a customer born on "2010-05-01" on "2026-10-03".

Hint

Declare the Customer entity, then add a validation rule with yearsBetween from data.dateOfBirth to ctx.today, at least 18.

Show answer
dates.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.dates, version: 1.0.0, title: "Dates" }
scope:
  - { level: organization, id: learn }
entities:
  Order:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
  Customer:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Customer" }
rules:
  - id: order.delivery.next-day
    kind: validation
    target: { entity: Order, field: /deliveryDate }
    operations: [create]
    assert: { op: gte, args: [{ op: daysBetween, args: [{ var: data.orderDate }, { var: data.deliveryDate }] }, 1] }
    severity: error
    finding: { code: LRN-DATE-001, message: order.deliveryTooSoon }
  - id: customer.adult
    kind: validation
    target: { entity: Customer, field: /dateOfBirth }
    operations: [create]
    assert: { op: gte, args: [{ op: yearsBetween, args: [{ var: data.dateOfBirth }, { var: ctx.today }] }, 18] }
    severity: error
    finding: { code: LRN-DATE-002, message: customer.tooYoung }
messages:
  en:
    order.deliveryTooSoon: "Delivery is at least one day after the order."
    customer.tooYoung: "You must be 18 or older."
tests:
  - name: born in 2010, too young on 2026-10-03
    entity: Customer
    operation: create
    given:
      data: { dateOfBirth: "2010-05-01" }
      ctx: { today: "2026-10-03" }
    expect:
      decision: deny
      findings:
        - { rule: customer.adult, fields: [/dateOfBirth] }
  - name: eighteen on the day itself
    entity: Customer
    operation: create
    given:
      data: { dateOfBirth: "2008-10-03" }
      ctx: { today: "2026-10-03" }
    expect: { decision: allow, findings: [] }
  - name: 22:00 in Chicago is already the next day in UTC
    entity: Order
    operation: create
    given:
      data: { orderDate: "2026-10-03T22:00:00-05:00", deliveryDate: "2026-10-04" }
    expect:
      decision: deny
      findings:
        - { rule: order.delivery.next-day, fields: [/deliveryDate] }
  - name: ordered on the 3rd in UTC, delivered on the 4th
    entity: Order
    operation: create
    given:
      data: { orderDate: "2026-10-03T10:00:00Z", deliveryDate: "2026-10-04" }
    expect: { decision: allow, findings: [] }
request.json
{
  "entity": "Customer",
  "operation": "create",
  "data": {
    "dateOfBirth": "2010-05-01"
  },
  "ctx": {
    "today": "2026-10-03"
  }
}

Result, from the engine

Decisiondeny1 finding, server channel

  • LRN-DATE-002errorblockingYou must be 18 or older./dateOfBirth
Course overview

On this page