Rule Cascade
LearnOperators

Date operators

daysBetween and yearsBetween count whole days and completed years between two dates.

Dates are strings in RFC 3339 form: a date "2026-10-03" or a date-time with an offset "2026-10-03T09:00:00Z". daysBetween counts calendar days from the first date to the second. yearsBetween counts completed years, the way ages are counted. The engine never reads a clock: pass today's date in ctx.

Syntax

date operators
{ op: daysBetween, args: [<from date>, <to date>] }
{ op: yearsBetween, args: [<from date>, { var: ctx.today }] }
OperatorArgumentsResult
daysBetweendate, dateWhole calendar days from the first to the second, in UTC
yearsBetweendate, dateCompleted calendar years; negative when the second is earlier

Example

The request orders on 3 October and wants delivery on 4 October. The other golden tests check a date-time and an age.

date.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.date, version: 1.0.0, title: Date operators }
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.window
    kind: validation
    target: { entity: Order, field: /deliveryDate }
    operations: [create]
    assert:
      op: between
      args:
        - { op: daysBetween, args: [{ var: data.orderDate }, { var: data.deliveryDate }] }
        - 2
        - 30
    severity: error
    finding:
      code: DAT-001
      message: delivery.window
      args:
        days: { op: daysBetween, args: [{ var: data.orderDate }, { var: data.deliveryDate }] }
  - 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: DAT-002, message: customer.adult }
messages:
  en:
    delivery.window: "Delivery is 2 to 30 days after the order, not {days}."
    customer.adult: "You must be 18 or older."
tests:
  - name: delivery the next day is too soon
    entity: Order
    operation: create
    given:
      data: { orderDate: "2026-10-03", deliveryDate: "2026-10-04" }
    expect:
      decision: deny
      findings:
        - { rule: order.delivery.window, message: "Delivery is 2 to 30 days after the order, not 1." }
  - name: delivery a week later is fine
    entity: Order
    operation: create
    given:
      data: { orderDate: "2026-10-03", deliveryDate: "2026-10-10" }
    expect: { decision: allow, findings: [] }
  - name: a date-time is converted to UTC first
    entity: Order
    operation: create
    given:
      data: { orderDate: "2026-10-03T22:30:00-04:00", deliveryDate: "2026-10-05" }
    expect:
      decision: deny
      findings:
        - { rule: order.delivery.window }
  - name: a customer one day short of 18
    entity: Customer
    operation: create
    given:
      data: { dateOfBirth: "2008-10-04" }
      ctx: { today: "2026-10-03" }
    expect:
      decision: deny
      findings:
        - { rule: customer.adult }
  - name: a customer on the 18th birthday
    entity: Customer
    operation: create
    given:
      data: { dateOfBirth: "2008-10-03" }
      ctx: { today: "2026-10-03" }
    expect: { decision: allow, findings: [] }
request.json
{
  "entity": "Order",
  "operation": "create",
  "data": {
    "orderDate": "2026-10-03",
    "deliveryDate": "2026-10-04"
  }
}

Result, from the engine

Decisiondeny1 finding, server channel

  • DAT-001errorblockingDelivery is 2 to 30 days after the order, not 1./deliveryDate
Try it YourselfOpens this ruleset and request in the playground. Nothing to install.

Common mistakes

  • Quote dates in YAML: "2026-10-03". Unquoted, some YAML parsers read a date object.
  • A date-time is converted to UTC before its date is taken: 2026-10-03T22:30:00-04:00 is 4 October.
  • An impossible date such as "2026-02-30" is an evaluation error, not null.
  • Never compute "now" in a rule. The host passes it, for example as ctx.today.

Exercise

A ticket is due within 14 days of its creation, and never before it. Write the rule on Ticket with createdAt and dueDate.

Hint

Use the Ticket entity. between with daysBetween from createdAt to dueDate, from 0 to 14.

Show answer
date.ruleset.yaml
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.date, version: 1.0.0, title: Date operators }
scope:
  - { level: organization, id: learn }
entities:
  Ticket:
    schema: { $ref: "./learn.openapi.yaml#/components/schemas/Ticket" }
rules:
  - id: ticket.due.window
    kind: validation
    target: { entity: Ticket, field: /dueDate }
    operations: [create]
    assert:
      op: between
      args:
        - { op: daysBetween, args: [{ var: data.createdAt }, { var: data.dueDate }] }
        - 0
        - 14
    severity: error
    finding: { code: DAT-001, message: due.window }
messages:
  en:
    due.window: "A ticket is due within 14 days of its creation."
tests:
  - name: due in 15 days is denied
    entity: Ticket
    operation: create
    given:
      data: { createdAt: "2026-10-01", dueDate: "2026-10-16" }
    expect:
      decision: deny
      findings:
        - { rule: ticket.due.window, fields: [/dueDate] }
  - name: due before creation is denied
    entity: Ticket
    operation: create
    given:
      data: { createdAt: "2026-10-01", dueDate: "2026-09-30" }
    expect:
      decision: deny
      findings:
        - { rule: ticket.due.window }
  - name: due in 14 days is allowed
    entity: Ticket
    operation: create
    given:
      data: { createdAt: "2026-10-01", dueDate: "2026-10-15" }
    expect: { decision: allow, findings: [] }
request.json
{
  "entity": "Ticket",
  "operation": "create",
  "data": {
    "createdAt": "2026-10-01",
    "dueDate": "2026-10-16"
  }
}

Result, from the engine

Decisiondeny1 finding, server channel

  • DAT-001errorblockingA ticket is due within 14 days of its creation./dueDate
Course overview

On this page