Expressions

Expressions are used within attributes and other constructs to define conditions, default values, etc.

Expressions support logical and comparison operations:

  • Operators: ==, !=, >, <, >=, <=, in, !, &&, ||, =, +, -, *, /
  • Aggregate functions: SUM, COUNT, AVG, MIN, MAX, MEDIAN and conditional variants (see list below)
  • Scalar functions: IF, date extraction, date arithmetic, and DIFF (see list below)
  • Values: Literals (true, false, null, numbers, strings), identifiers, arrays.
  • Parentheses: Used for grouping, e.g., (a == b) && (c > d)

All operators, aggregate functions, and scalar functions can be used in @where, @set, @permission, @computed, and @validate expressions.

Arithmetic Operators

Arithmetic operators (+, -, *, /) can be used for calculations in any expression context.

Using in @set

update calculateTotal(id) {
    @set(order.total = order.subtotal + order.shipping)
}
 
update applyDiscount(id) {
    @set(order.finalPrice = order.total * 0.9)
}

Using in @where

list listHighValueProducts() {
    @where(product.price * product.quantity > 1000)
}
 
list listDiscountedProducts() {
    @where(product.basePrice - product.discount < 50)
}

Using in @permission

list listBudgetsWithRemainingBalance() {
    @permission(expression: budget.limit - budget.spent > 100)
}

Using in @computed

model Order {
  fields {
    subtotal Decimal
    shipping Decimal
    total Decimal @computed(order.subtotal + order.shipping)
    vat Decimal @computed(order.total * 0.2)
  }
}

Division and zero divisors

Division guards its divisor. When the divisor evaluates to zero, the division yields null rather than raising an error, and the null then propagates through the rest of the expression. This holds wherever an expression is used: @computed, @set, @where, @permission, and @validate.

The consequence to keep in mind is that null is not true. In a @where or @permission expression a predicate that evaluates to null does not match, so the row is left out of the results rather than the read failing. In a model-level @validate rule only a value the database can judge to be false counts as a violation, so a rule that evaluates to null does not block the write. On a non-nullable computed field, the null cannot be stored and the write is refused by the field's not-null constraint.

Use IF when you want a zero divisor to produce a particular value instead:

model Order {
  fields {
    total Decimal
    itemCount Number
    averageItemPrice Decimal @computed(IF(order.itemCount > 0, order.total / order.itemCount, 0))
  }
}

Aggregate Functions

Aggregate functions perform calculations across a collection of related records. They are commonly used with one-to-many relationships and can be used in @computed, @set, @where, @permission, and @validate expressions.

SUM

Adds up all numeric values in a collection.

Syntax: SUM(items)

Example usages:

// In @computed
totalValue Decimal @computed(SUM(customer.orders.total))
 
// In @set
update calculateTotalOrderValue(id) {
    @set(customer.totalOrderValue = SUM(customer.orders.total))
}
 
// In @where
list listHighValueCustomers() {
    @where(SUM(customer.orders.total) > 500)
}

COUNT

Counts the number of items in a collection.

Syntax: COUNT(items)

Example usages:

// In @computed
orderCount Number @computed(COUNT(customer.orders.id))
 
// In @set
update calculateOrderCount(id) {
    @set(customer.orderCount = COUNT(customer.orders.id))
}
 
// In @where
list listCustomersWithMultipleOrders() {
    @where(COUNT(customer.orders.id) >= 2)
}
 
// In @permission
list listProjectsWithTasks() {
    @permission(expression: COUNT(project.tasks.id) > 0)
}

AVG

Computes the arithmetic average of numeric values in a collection.

Syntax: AVG(items)

Example usages:

// In @computed
averageOrderValue Decimal @computed(AVG(customer.orders.total))
 
// In @set
update calculateAverageOrderValue(id) {
    @set(customer.averageOrderValue = AVG(customer.orders.total))
}
 
// In @where
list listCustomersWithHighAverageOrders() {
    @where(AVG(customer.orders.total) > 100)
}

MEDIAN

Computes the arithmetic median of the numeric values in a collection.

Syntax: MEDIAN(items)

Example usages:

// In @computed
medianOrderValue Decimal @computed(MEDIAN(customer.orders.total))
 
// In @where
list listCustomersWithHighMedianOrders() {
    @where(MEDIAN(customer.orders.total) > 75)
}

MIN

Finds the smallest numeric value in a collection.

Syntax: MIN(items)

Example usages:

// In @computed
minOrderValue Decimal @computed(MIN(customer.orders.total))
 
// In @set
update calculateMinOrderValue(id) {
    @set(customer.minOrderValue = MIN(customer.orders.total))
}

MAX

Finds the largest numeric value in a collection.

Syntax: MAX(items)

Example usages:

// In @computed
maxOrderValue Decimal @computed(MAX(customer.orders.total))
 
// In @set
update calculateMaxOrderValue(id) {
    @set(customer.maxOrderValue = MAX(customer.orders.total))
}

SUMIF

Adds up numeric values that satisfy a given condition.

Syntax: SUMIF(items, condition)

Example usages:

// In @computed
activeOrderTotal Decimal @computed(SUMIF(customer.orders.total, customer.orders.isActive))
 
// In @set
update calculateActiveOrderTotal(id) {
    @set(customer.activeOrderTotal = SUMIF(customer.orders.total, customer.orders.isActive))
}
 
// In @where
list listCustomersWithHighActiveOrderValue() {
    @where(SUMIF(customer.orders.total, customer.orders.isActive) > 200)
}

COUNTIF

Counts items that satisfy a given condition.

Syntax: COUNTIF(items, condition)

Example usages:

// In @computed
activeOrderCount Number @computed(COUNTIF(customer.orders.id, customer.orders.isActive))
 
// In @set
update calculateActiveOrderCount(id) {
    @set(customer.activeOrderCount = COUNTIF(customer.orders.id, customer.orders.isActive))
}
 
// In @where
list listCustomersWithActiveOrders() {
    @where(COUNTIF(customer.orders.id, customer.orders.isActive) >= 1)
}

AVGIF

Computes the average of numeric values that satisfy a given condition.

Syntax: AVGIF(items, condition)

Example usages:

// In @computed
activeOrderAvg Decimal @computed(AVGIF(customer.orders.total, customer.orders.isActive))
 
// In @set
update calculateActiveOrderAvg(id) {
    @set(customer.activeOrderAvg = AVGIF(customer.orders.total, customer.orders.isActive))
}

MEDIANIF

Finds the median of numeric values that satisfy a given condition.

Syntax: MEDIANIF(items, condition)

Example usages:

// In @computed
medianActiveOrder Decimal @computed(MEDIANIF(customer.orders.total, customer.orders.isActive))
 
// In @where
list listCustomersWithHighActiveMedian() {
    @where(MEDIANIF(customer.orders.total, customer.orders.isActive) > 100)
}

MINIF

Finds the smallest numeric value that satisfies a given condition.

Syntax: MINIF(items, condition)

Example usages:

// In @computed
minActiveOrder Decimal @computed(MINIF(customer.orders.total, customer.orders.isActive))
 
// In @set
update calculateActiveOrderMin(id) {
    @set(customer.activeOrderMin = MINIF(customer.orders.total, customer.orders.isActive))
}

MAXIF

Finds the largest numeric value that satisfies a given condition.

Syntax: MAXIF(items, condition)

Example usages:

// In @computed
maxActiveOrder Decimal @computed(MAXIF(customer.orders.total, customer.orders.isActive))
 
// In @set
update calculateActiveOrderMax(id) {
    @set(customer.activeOrderMax = MAXIF(customer.orders.total, customer.orders.isActive))
}

Quantifier Functions

Quantifier functions test conditions across collections in has-many relationships. They return boolean values and can be used in @computed, @set, @where, @permission, and @validate expressions.

The condition must read a to-many relationship, and every field reference inside one quantifier must share the same relationship path. That rules out four shapes, each of which is a build error.

A path that is not to-many reports which path is at fault. This covers a repeated scalar field, such as ANY(invoice.tags == "priority") where tags is Text[], and a to-one relationship, such as ANY(order.customer.name == "Ada"):

ANY requires a to-many relationship, but 'invoice.tags' is not to-many
ANY requires a to-many relationship, but 'order.customer' is not to-many

A predicate with no single shared path reports that instead. This covers two different relationship paths inside one quantifier, such as ALL(customer.orders.total > customer.reviews.rating), and a path rooted at ctx rather than at a model, such as ANY(ctx.identity.email != ""):

ALL predicate field references must share a to-many relationship path

Combine two quantifiers with && rather than putting two paths inside one.

ANY

Returns true if at least one item in the collection satisfies the condition.

Syntax: ANY(condition)

Example usages:

// In @where - find customers with at least one active order
list listCustomersWithActiveOrders() {
    @where(ANY(customer.orders.status == OrderStatus.Active))
}
 
// In @where - find customers with at least one high-value order
list listHighValueCustomers() {
    @where(ANY(customer.orders.total > 1000))
}
 
// In @permission - allow access if the project has any member for this identity
list listProjects() {
    @permission(expression: ANY(project.members.identity == ctx.identity))
}
 
// Multiple conditions in ANY
list listCustomersWithRecentActiveOrders() {
    @where(ANY(customer.orders.status == OrderStatus.Active && customer.orders.createdAt > ctx.now - 30d))
}
 
// In @computed - check if customer has any active orders
model Customer {
    fields {
        orders Order[]
        hasActiveOrders Boolean @computed(ANY(customer.orders.status == OrderStatus.Active))
    }
}
 
// In @set - set flag based on order status
update updateCustomerStatus(id) {
    @set(customer.hasActiveOrders = ANY(customer.orders.status == OrderStatus.Active))
}

ALL

Returns true if every item in the collection satisfies the condition.

Syntax: ALL(condition)

Example usages:

// In @where - find customers where all orders are delivered
list listCustomersAllDelivered() {
    @where(ALL(customer.orders.status == OrderStatus.Delivered))
}
 
// In @where - find projects where all tasks are completed
list listCompletedProjects() {
    @where(ALL(project.tasks.isCompleted))
}
 
// In @permission - require every one of the record's grants to be approved
list listSensitiveData() {
    @permission(expression: ALL(report.grants.isApproved))
}
 
// Combined with other conditions
list listVerifiedProducts() {
    @where(product.isActive && ALL(product.reviews.isVerified))
}
 
// In @computed - check if all orders are delivered
model Customer {
    fields {
        orders Order[]
        allOrdersDelivered Boolean @computed(ALL(customer.orders.status == OrderStatus.Delivered))
    }
}
 
// In @set - set flag when all tasks are complete
update markProjectComplete(id) {
    @set(project.isComplete = ALL(project.tasks.isCompleted))
}

Combining ANY and ALL

You can combine ANY and ALL with other expressions for complex filtering:

// Find customers with all delivered orders AND at least one high-value order
list listPremiumCustomers() {
    @where(
        ALL(customer.orders.status == OrderStatus.Delivered) &&
        ANY(customer.orders.total > 500)
    )
}
 
// Find projects where all tasks are assigned and any task is high priority
list listActiveProjects() {
    @where(
        ALL(project.tasks.assigneeId != null) &&
        ANY(project.tasks.priority == Priority.High)
    )
}

Scalar Functions

Scalar functions operate on individual values and return a single result. They can be used in @computed, @set, @where, @permission, and @validate expressions.

IF

Returns one value if a condition is true, and another if false. Useful for conditional logic in expressions.

Syntax: IF(condition, thenValue, elseValue)

The two branches do not have to be the same type as long as the types are compatible, so IF(product.isActive, product.price, 0) is valid where price is a Decimal. The result takes the type of the then branch. Mixing types from different families, such as a Decimal with a Text, is rejected.

Example usages:

// In @computed - conditional text based on boolean field
importanceLabel Text @computed(IF(appointment.isImportant, "High Priority", "Normal"))
 
// In @computed - conditional pricing
finalPrice Decimal @computed(IF(product.isActive, product.price * 0.9, product.price))
 
// In @set - apply discount based on condition
update applyActiveDiscount(id) {
    @set(product.discountedPrice = IF(product.isActive, product.price * 0.9, product.price))
}
 
// In @set - set status text
update setStatus(id) {
    @set(product.status = IF(product.isActive, "Available", "Discontinued"))
}
 
// Nested IF for multiple conditions
update setTier(id) {
    @set(product.tier = IF(product.isPremium, "Premium", IF(product.isBulk, "Bulk", "Standard")))
}
 
// In @where
list listActiveOrExpensiveProducts(minPrice: Decimal) {
    @where(IF(product.isActive, product.price, product.price * 2) > minPrice)
}

Date Extraction Functions

These functions extract specific components from Date or Timestamp fields.

YEAR

Extracts the year from a date or timestamp.

Syntax: YEAR(dateOrTimestamp)

Example usages:

// In @computed
scheduledYear Number @computed(YEAR(appointment.scheduledDate))
 
// In @set
update extractYear(id) {
    @set(event.extractedYear = YEAR(event.eventDate))
}
 
// In @where
list listEventsInYear(year: Number) {
    @where(YEAR(event.eventDate) == year)
}

MONTH

Extracts the month (1-12) from a date or timestamp.

Syntax: MONTH(dateOrTimestamp)

Example usages:

// In @computed
scheduledMonth Number @computed(MONTH(appointment.scheduledDate))
 
// In @where
list listEventsInMonth(month: Number) {
    @where(MONTH(event.eventDate) == month)
}

DAY

Extracts the day of the month (1-31) from a date or timestamp.

Syntax: DAY(dateOrTimestamp)

Example usages:

// In @computed
scheduledDay Number @computed(DAY(appointment.scheduledDate))
 
// In @where
list listEventsOnDay(day: Number) {
    @where(DAY(event.eventDate) == day)
}

HOUR

Extracts the hour (0-23) from a timestamp.

Syntax: HOUR(timestamp)

Example usages:

// In @computed
scheduledHour Number @computed(HOUR(appointment.scheduledTimestamp))
 
// In @where
list listEventsAtHour(hour: Number) {
    @where(HOUR(event.eventTimestamp) == hour)
}

MINUTE

Extracts the minute (0-59) from a timestamp.

Syntax: MINUTE(timestamp)

Example usage:

// In @computed
scheduledMinute Number @computed(MINUTE(appointment.scheduledTimestamp))

SECOND

Extracts the second (0-59) from a timestamp.

Syntax: SECOND(timestamp)

Example usage:

// In @set
update extractSecond(id) {
    @set(event.extractedSecond = SECOND(event.eventTimestamp))
}

Date Arithmetic Functions

These functions add or subtract time periods from Date or Timestamp fields.

ADDDAYS

Adds or subtracts days from a date or timestamp.

Syntax: ADDDAYS(dateOrTimestamp, numberOfDays)

Example usages:

// In @set - add days
update addDaysToDate(id) {
    @set(event.newDate = ADDDAYS(event.eventDate, 10))
}
 
// In @set - subtract days (use negative number)
update subtractDaysFromDate(id) {
    @set(event.newDate = ADDDAYS(event.eventDate, -7))
}
 
// Combined with IF
update adjustDeadlineByPriority(id) {
    @set(task.adjustedDeadline = IF(task.isHighPriority, ADDDAYS(task.deadline, -3), ADDDAYS(task.deadline, 7)))
}

ADDMONTHS

Adds or subtracts months from a date or timestamp.

Syntax: ADDMONTHS(dateOrTimestamp, numberOfMonths)

Example usages:

// In @set - add months
update addMonthsToDate(id) {
    @set(event.newDate = ADDMONTHS(event.eventDate, 3))
}
 
// In @set - subtract months
update subtractMonthsFromDate(id) {
    @set(event.newDate = ADDMONTHS(event.eventDate, -1))
}

ADDYEARS

Adds or subtracts years from a date or timestamp.

Syntax: ADDYEARS(dateOrTimestamp, numberOfYears)

Example usages:

// In @set - add years
update addYearsToDate(id) {
    @set(event.newDate = ADDYEARS(event.eventDate, 1))
}
 
// In @set - subtract years
update subtractYearsFromDate(id) {
    @set(event.newDate = ADDYEARS(event.eventDate, -5))
}

DIFF

Calculates the difference in days between two dates or timestamps. Returns a positive number if the first date is after the second, or negative if before.

Syntax: DIFF(dateOrTimestamp1, dateOrTimestamp2)

Example usages:

// In @computed
durationDays Number @computed(DIFF(appointment.endDate, appointment.scheduledDate))
 
// In @set
update calculateDaysDiff(id) {
    @set(event.daysDifference = DIFF(event.dueDate, event.eventDate))
}
 
// In @where - find tasks due within a certain number of days
list listTasksDueSoon(maxDays: Number) {
    @where(DIFF(task.deadline, task.startedAt) <= maxDays)
}
 
// In @where - find overdue events
list listOverdueEvents() {
    @where(DIFF(event.dueDate, event.eventDate) < 0)
}