ObjectOS
Reference

CEL Expressions

The expression language used for formulas, predicates, schedules, and templated strings — surfaced via five tagged templates.

CEL Expressions

ObjectOS uses CEL (Common Expression Language) for every place where you need a small, safe, sandboxed expression: formula fields, validation rules, visibility predicates, sharing conditions, flow guards, schedules, and templated strings.

Authoring is done through five tagged templates imported from @objectstack/spec. They all produce a small JSON object the runtime parses:

{ dialect: 'cel' | 'template' | 'cron', source: string }

Schema source: packages/spec/src/shared/expression.zod.ts.

The five tagged templates

TemplateDialectUse it forExample
F`...`celFormula fields — derived values stored alongside the recordF`record.amount * 0.1`
P`...`celPredicates — booleans for validation / sharing / visibility / conditionsP`record.status == "open"`
cel`...`celGeneral CEL — when neither F nor P fits (e.g. a parameter value)cel`now() + duration("P30D")`
tmpl`...`templateString templates with {{var}} interpolationtmpl`Order from {{record.customer.name}}`
cron`...`cronSchedules — standard 5-field cron syntaxcron`0 9 * * 1-5`

There is no functional difference between F, P, and cel at evaluation time — they all run CEL. The split exists so that schemas (and AI agents) know what role the expression plays and the editor can type-check (formulas must return a value, predicates must return a bool).

Import

import { F, P, cel, tmpl, cron } from '@objectstack/spec'

Where each one is used

Field on a specTagExample location
Field.expression (formula type)F*.object.ts formula fields
Field.conditionalRequiredPobject fields
Validation.predicatePobject validations
SharingRule.conditionPsharing rules
View.conditionalFormatting[].conditionPviews
Flow.step.when / Flow.transition.whenPflows
Action.guardPactions
Notification subjects / message bodiestmplnotifications
Schedule.croncronscheduled flows / reports
Any value parametercelflow step inputs

Variable scope

CEL expressions evaluate in a context with these top-level variables:

VariableAvailable whenContents
recordalmost alwaysThe current record being evaluated
previouson update hooks / change-detectionThe pre-change state of the record (or null)
inputactions, flow stepsThe user-supplied input payload
os.useralways{ id, roles: string[], permissions: string[] }
os.orgalwaysOrganisation / tenant context
os.envalwaysEnvironment variables exposed to expressions

Legacy OLD / NEW variables were removed in M9.5. Use previous and record.

Standard library

Registered in packages/formula/src/stdlib.ts. The most-used built-ins:

Time

FunctionReturnsNotes
now()TimestampPinned to the evaluation context — stable within one query
today()TimestampStart of UTC day
daysFromNow(int)TimestampFuture date
daysAgo(int)TimestampPast date

CEL also includes the native timestamp(...), duration(...), date.getDayOfWeek(), etc. — see the CEL spec.

Utility

FunctionPurpose
isBlank(x)true if null, undefined, "", or empty list
coalesce(a, b)First non-null
trim(s)Strip whitespace
joinNonEmpty(list, sep)Concatenate non-empty entries

Native CEL string helpers (.contains(...), .startsWith(...), .matches(...), .size()) are always available.

Examples

Formula field — line-item total:

{ name: 'subtotal', type: 'formula', expression: F`record.quantity * record.unit_price` }

Validation — close date must be after today:

{ message: 'Close date must be in the future', predicate: P`record.close_date > today()` }

Visibility — show field only to managers:

{ visibleIf: P`'manager' in os.user.roles` }

Flow guard — skip step when amount is small:

{ when: P`record.amount >= 1000` }

Schedule — weekday 9am:

{ schedule: cron`0 9 * * 1-5` }

Template — notification subject:

{ subject: tmpl`[{{record.priority}}] {{record.subject}}` }

Errors

Expressions are compiled at load time. Failures show up as VALIDATION_ERROR with the source location:

{ "code": "VALIDATION_ERROR", "message": "CEL: unknown field 'amout' on Record", "details": { "field": "subtotal", "expression": "record.amout * 0.1" } }

A failed evaluation at runtime is treated as null for formula fields and false for predicates, plus an entry in the audit log.

See also

On this page