Date Macros
Date Macros protocol schemas
Date Macro Tokens — the declarative placeholders the UI substitutes into filter values before sending a query to the data engine.
Why this lives in spec
Filter values in dashboards, views, reports and pages travel as JSON.
Because JSON cannot evaluate code, callers cannot write daysAgo(30)
inline; they use a tiny placeholder grammar instead:
{ published_at: { $gte: '{last_quarter_start}' } }
{ signal_at: { $gte: '{30_days_ago}' } }The placeholders are expanded on both sides of the wire, so a filter behaves the same wherever it is executed (framework#3582):
- Client —
resolveDateMacros()in@object-ui/core, just before the filter is handed to the data source. - Server —
resolveFilterTokens()in@objectstack/core, wired into the ObjectQL read AND write paths (find/findOne/count/aggregate/update/delete) and the analytics dataset executor. Filters that reach the database WITHOUT passing through a renderer — dashboard widgets, dataset definitions, REST query params, flow node filters — need this: before it, the token compared as a literal string and matched nothing. The write verbs are covered for the same reason (#3810): one filter must select one row set regardless of which verb consumes it, or a flow'sfindpreview and itsupdateact on different rows.
Either way the DRIVER only ever sees ISO date / timestamp strings,
never {tokens}. Translating an ISO comparand into a column's on-disk
form — canonical UTC text on SQLite, a native timestamptz on
Postgres, a DATETIME(3) literal on MySQL, and YYYY-MM-DD text for
a calendar day on every dialect — is the driver's job; see
SqlDriver.temporalFilterValue.
A token OUTSIDE this vocabulary is rejected rather than passed
through: @objectstack/lint's validate-filter-tokens fails the
build, and the runtime resolver throws. Silently matching nothing is
the failure mode the vocabulary exists to prevent.
AI agents and template authors author these placeholders directly, so the set of recognised tokens is part of the platform contract and must live here next to the rest of the JSON-DSL schemas, not inside any single UI implementation.
Two flavours of token
-
Fixed tokens — small, finite list (
{today},{current_quarter_start},{last_year_end}, …). Enumerated byDATE_MACRO_TOKENSbelow. -
Parameterised tokens —
{N_days_ago},{N_weeks_from_now}, etc., whereNis any non-negative integer. Matched byDATE_MACRO_PARAM_RE. Units:minute(s),hour(s),day(s),week(s),month(s),year(s). Directions:ago,from_now.
Out of scope
- CEL expressions (
cel\daysAgo(30)`) run **server-side** in the formula engine. They are unrelated to these placeholders; see@objectstack/formula`. - Token resolution semantics (week-start day, timezone, fiscal
calendars) are defined by the resolver implementation; spec only
freezes the vocabulary. One property is worth stating here
because it is authored against: a
*_endtoken is the period's last calendar DAY ({current_year_end}→2026-12-31), so on adatetimecolumn<= {current_year_end}stops at midnight on the 31st. Filter a timestamp with the half-open< {next_year_start}.
Source: packages/spec/src/data/date-macros.zod.ts
TypeScript Usage
import { DateMacroPlaceholderSchema, DateMacroTokenSchema } from '@objectstack/spec/data';
import type { DateMacroPlaceholder, DateMacroToken } from '@objectstack/spec/data';
// Validate data
const result = DateMacroPlaceholderSchema.parse(data);DateMacroPlaceholder
Type: string
DateMacroToken
Type: string