Use dates and datetimes
Use Date and DateTime values in your PyRel definitions when you need to compare facts based on calendar days, specific moments in time, or elapsed time.
This guide covers constructing values, parsing and formatting, extracting components for bucketing logic, writing relative-time conditions, computing elapsed time, and building calendar/time windows.
- PyRel is installed. See Set Up Your Environment for instructions.
- You are comfortable deriving facts with
Model.define()and filtering withModel.where(). See Derive facts with logic.
Construct Date and DateTime values
Section titled “Construct Date and DateTime values”There are a couple of reasons to use the std.datetime constructors instead of Python’s built-in datetime module:
- They let you express time-based logic in PyRel that is tied to the model’s execution, rather than the notebook/script execution.
A Python
datetimeis calculated in your notebook or script first, so PyRel sees it as a fixed timestamp. - They create PyRel
DateandDateTimevalues, so comparisons behave the way you expect indefine(),where(), andselect().
Use the date() and datetime() constructors to create literal values in your definitions:
from relationalai.semantics import Date, DateTime, Integer, Modelfrom relationalai.semantics.std.datetime import date, datetime
m = Model("SupportModel")
Ticket = m.Concept("Ticket", identify_by={"id": Integer})Ticket.created_at = m.Property(f"{Ticket} created at {DateTime:created_at}")Ticket.due_by = m.Property(f"{Ticket} due by {Date:due_by}")
data = m.data([ {"id": 201, "created_at": datetime(2026, 2, 1, 9, 0, 0, tz="UTC"), "due_by": date(2026, 2, 8)}, {"id": 202, "created_at": datetime(2026, 2, 10, 16, 45, 0, tz="UTC"), "due_by": date(2026, 2, 17)},])
m.define(Ticket.new(data.to_schema()))
df = m.select(Ticket.id, Ticket.created_at, Ticket.due_by).to_df()print(df)- If you omit the
tzargument, the datetime will be created in UTC by default. - You can pass a timezone string like
"America/Los_Angeles"to create the datetime in that timezone, or to have it converted from UTC to that timezone. - You can also use Python’s built-in
datetime.tzinfoobjects for more complex timezone handling, and pass them to thetzargument.
Parse dates and datetimes from strings
Section titled “Parse dates and datetimes from strings”Use the std.datetime parsing and formatting functions to convert between text and temporal types, and to produce formatted strings from Date and DateTime values values.
To parse text ISO dates, use date.fromisoformat():
from relationalai.semantics import Date, Integer, Modelfrom relationalai.semantics.std.datetime import date
m = Model("SupportModel")
Ticket = m.Concept("Ticket", identify_by={"id": Integer})Ticket.created_on = m.Property(f"{Ticket} created on {Date:created_on}")
4 collapsed lines
data = m.data([ {"id": 401, "created_on": "2026-02-01"}, {"id": 402, "created_on": "2026-02-10"},])
m.define(Ticket.new(id=Ticket.id, created_on=date.fromisoformat(data.created_on)))
df = m.select(Ticket.id, Ticket.created_on).to_df()print(df)To parse a datetime string, use datetime.strptime() with a format string:
from relationalai.semantics import DateTime, Integer, Modelfrom relationalai.semantics.std.datetime import datetime
9 collapsed lines
m = Model("SupportModel")
Ticket = m.Concept("Ticket", identify_by={"id": Integer})Ticket.created_at = m.Property(f"{Ticket} created at {DateTime:created_at}")
data = m.data([ {"id": 501, "created_at": "2026-02-01T09:00:00Z"}, {"id": 502, "created_at": "2026-02-10T16:45:00Z"},])
parsed = datetime.strptime(data.created_at, "%Y-%m-%dT%H:%M:%SZ", tz="UTC")m.define(Ticket.new(id=Ticket.id, created_at=parsed))
df = m.select(Ticket.id, Ticket.created_at).to_df()print(df)For your convenience, datetime.ISO provides common format strings as constants:
from relationalai.semantics import DateTime, Integer, Modelfrom relationalai.semantics.std.datetime import datetime
9 collapsed lines
m = Model("SupportModel")
Ticket = m.Concept("Ticket", identify_by={"id": Integer})Ticket.created_at = m.Property(f"{Ticket} created at {DateTime:created_at}")
data = m.data([ {"id": 601, "created_at": "2026-02-01T09:00:00Z"}, {"id": 602, "created_at": "2026-02-10T16:45:00Z"},])
parsed = datetime.strptime(data.created_at, datetime.ISO.DATETIME_TZ, tz="UTC")m.define(Ticket.new(id=Ticket.id, created_at=parsed))
df = m.select(Ticket.id, Ticket.created_at).to_df()print(df)datetime.strptime(...)parses the datetime string according to the provided format, and thetz="UTC"argument ensures the resultingDateTimevalue is in UTC.datetime.ISO.DATETIME_TZis a convenient constant for the common ISO format with timezone.
Format Date and DateTime values as strings
Section titled “Format Date and DateTime values as strings”Use datetime.format() to produce formatted strings from Date and DateTime values:
from relationalai.semantics import DateTime, Integer, Modelfrom relationalai.semantics.std.datetime import datetime
m = Model("SupportModel")
Ticket = m.Concept("Ticket", identify_by={"id": Integer})Ticket.created_at = m.Property(f"{Ticket} created at {DateTime:created_at}")
4 collapsed lines
m.define( Ticket.new(id=701, created_at=datetime(2026, 2, 1, 9, 0, 0, tz="UTC")), Ticket.new(id=702, created_at=datetime(2026, 2, 10, 16, 45, 0, tz="UTC")),)
formatted = datetime.format(Ticket.created_at, "Y-m-d H:M:S Z")df = m.select(Ticket.id, Ticket.created_at, formatted.alias("created_at_fmt")).to_df()print(df)datetime(..., tz="UTC")creates the datetime in UTC.datetime.format(...)formats the datetime into a string using the provided format string.- The format string
"Y-m-d H:M:S Z"produces an ISO-like format with timezone offset.
datetime.format()uses Julia-style format strings, which are different from Python’sstrftimeformat codes.
Extract date and time components
Section titled “Extract date and time components”Use part methods like datetime.to_date(), datetime.weekday(), and datetime.hour() to extract components from DateTime values for bucketing and comparisons:
from relationalai.semantics import Date, DateTime, Integer, Modelfrom relationalai.semantics.std.datetime import datetime
m = Model("SupportModel")
Ticket = m.Concept("Ticket", identify_by={"id": Integer})Ticket.created_at = m.Property(f"{Ticket} created at {DateTime:created_at}")Ticket.created_on = m.Property(f"{Ticket} created on {Date:created_on}")Ticket.weekday = m.Property(f"{Ticket} weekday {Integer:weekday}")Ticket.hour = m.Property(f"{Ticket} hour {Integer:hour}")WeekendTicket = m.Concept("WeekendTicket", extends=[Ticket])
4 collapsed lines
m.define( Ticket.new(id=702, created_at=datetime(2024, 6, 13, 14, 30, tz="UTC")), Ticket.new(id=703, created_at=datetime(2024, 6, 15, 10, 0, tz="UTC")),)
m.define( Ticket.created_on(datetime.to_date(Ticket.created_at)), Ticket.weekday(datetime.weekday(Ticket.created_at)), # 0=Mon ... 6=Sun Ticket.hour(datetime.hour(Ticket.created_at)),)
m.define(WeekendTicket(Ticket)).where((Ticket.weekday == 5) | (Ticket.weekday == 6))
df = m.select( WeekendTicket.id, WeekendTicket.created_on, WeekendTicket.weekday, WeekendTicket.hour,).to_df()print(df)datetime.to_date(...)convertsDateTimetoDatebefore date-based extraction.datetime.weekday(...)returns0for Monday through6for Sunday.datetime.hour(...)extracts the hour using the datetime’s timezone (or UTC).
Add or subtract time periods
Section titled “Add or subtract time periods”Use time period constructors like datetime.days(), datetime.hours(), and datetime.minutes() to add or subtract time from Date and DateTime values for relative time conditions:
from relationalai.semantics import DateTime, Integer, Modelfrom relationalai.semantics.std import datetime as dt
m = Model("SupportModel")Ticket = m.Concept("Ticket", identify_by={"id": Integer})Ticket.created_at = m.Property(f"{Ticket} created at {DateTime}")Ticket.due_by = m.Property(f"{Ticket} due by {DateTime}")
4 collapsed lines
m.define( Ticket.new(id=801, created_at=dt.datetime(2024, 6, 1, 12, 0, 0)), Ticket.new(id=802, created_at=dt.datetime(2024, 6, 2, 15, 30, 0)),)
due_date = dt.datetime.add(Ticket.created_at, dt.days(7))m.define(Ticket.due_at(due_date))
df = m.select(Ticket.id, Ticket.created_at, Ticket.due_at).to_df()print(df)datetime.days(7)creates a time period of 7 days that you can add to or subtract from aDateTimevalue.datetime.subtract(...)subtracts the time period from the datetime, resulting in a newDateTimecutoff boundary.
Compute elapsed time between two events
Section titled “Compute elapsed time between two events”Use datetime.datetime.period_milliseconds() to compute elapsed time in milliseconds between two DateTime values, then apply transforms to convert to other units or compare against a value:
from relationalai.semantics import DateTime, Integer, Modelfrom relationalai.semantics.std import numbers, datetime as dt
m = Model("SupportModel")
Ticket = m.Concept("Ticket", identify_by={"id": Integer})Ticket.created_at = m.Property(f"{Ticket} created at {DateTime:created_at}")Ticket.first_response_at = m.Property(f"{Ticket} first responded to at {DateTime:first_response_at}")Ticket.response_sla_breached = m.Property(f"{Ticket} breached response SLA")
17 collapsed lines
m.define( Ticket.new( id=1001, created_at=dt.datetime(2026, 2, 1, 9, 0, 0, tz="UTC"), first_response_at=dt.datetime(2026, 2, 1, 11, 0, 0, tz="UTC"), ), Ticket.new( id=1002, created_at=dt.datetime(2026, 2, 1, 9, 0, 0, tz="UTC"), first_response_at=dt.datetime(2026, 2, 1, 15, 30, 0, tz="UTC"), ),)
elapsed_ms = dt.datetime.period_milliseconds(Ticket.created_at, Ticket.first_response_at)SLA_4_HOURS_MS = numbers.integer(4 * 60 * 60 * 1000)m.define(Ticket.response_sla_breached).where(elapsed_ms > SLA_4_HOURS_MS)
df = m.select( Ticket.id, elapsed_ms.alias("response_ms"), Ticket.response_sla_breached.as_bool().alias("breached_ticket_id"),).to_df()print(df)datetime.datetime.period_milliseconds(...)computes the elapsed time in milliseconds between the two datetimes.- The SLA threshold is defined as a constant in milliseconds for the comparison.
- The resulting
Ticket.response_sla_breachedproperty is a unary property that represents whether the SLA was breached. - In the
select(),Ticket.response_sla_breached.as_bool()converts the unary property into a boolean flag for easier interpretation in the output.
Build calendar/time windows
Section titled “Build calendar/time windows”Use datetime.date.range() and datetime.datetime.range() to build calendar and time windows for bucketing and filtering:
from relationalai.semantics import Date, Integer, Modelfrom relationalai.semantics.std import aggregates, datetime as dt
m = Model("SupportModel")
Ticket = m.Concept("Ticket", identify_by={"id": Integer})Ticket.created_on = m.Property(f"{Ticket} created on {Date}")
START_DATE = dt.date(2024, 6, 1)
5 collapsed lines
m.define( Ticket.new(id=1201, created_on=dt.date.subtract(START_DATE, dt.days(1))), Ticket.new(id=1202, created_on=dt.date.subtract(START_DATE, dt.days(1))), Ticket.new(id=1203, created_on=dt.date.subtract(START_DATE, dt.days(3))),)
# Create a 4-day date range ending at START_DATE.day = dt.date.range(end=START_DATE, periods=4, freq="D")
# Count how many tickets were created on each day in the range.tickets_created = ( aggregates.count(Ticket).per(day).where(Ticket.created_on == day)) | 0 # Fill in 0 for days with no tickets created
df = m.select(day.alias("day"), tickets_created.alias("tickets_created")).to_df()print(df)datetime.date.range(...)creates a range of 4 dates ending atSTART_DATEwith a daily frequency.aggregates.count(Ticket).per(day)counts how many tickets were created on each day in the range..where(Ticket.created_on == day)filters the tickets to those created on the current day in the range.- The
| 0fills in a count of0for any days in the range where no tickets were created.
datetime.date.range()anddatetime.datetime.range()treat theendparameter as inclusive, so the range includes theSTART_DATEin this example.- The
periodsparameter specifies how many time periods to include in the range, andfreq="D"specifies that the periods are days. - Other frequency options include
"ms"for milliseconds,"s"for seconds,"m"for minutes,"H"for hours,"W"for weeks,"M"for months, and"Y"for years.
Avoid common pitfalls
Section titled “Avoid common pitfalls”Time-based definitions often fail in subtle ways. The most common causes are type mismatches, timezone assumptions, and off-by-one boundaries.
Use this checklist when results are empty, shifted, or off by one day:
- Verify types: compare
DatetoDateandDateTimetoDateTime. - Set timezone on purpose: pass
tzwhen you construct or parse datetimes, and when you extract local components. - Check range boundaries:
date.range(..., end=...)anddatetime.range(..., end=...)treatendas inclusive. - Keep token sets separate: range
frequses tokens like"m"(minutes) and"M"(months), and formatting uses datetime format strings and ISO constants likedatetime.ISO.SECONDS_TZ. - Validate in small steps:
select()raw timestamps, then your computed boundary, then the final boolean condition.