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
{ op: daysBetween, args: [<from date>, <to date>] }
{ op: yearsBetween, args: [<from date>, { var: ctx.today }] }| Operator | Arguments | Result |
|---|---|---|
daysBetween | date, date | Whole calendar days from the first to the second, in UTC |
yearsBetween | date, date | Completed 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.
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: [] }{
"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
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:00is 4 October. - An impossible date such as
"2026-02-30"is an evaluation error, notnull. - 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
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: [] }{
"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