リファレンス
このページはまだ翻訳されていません。英語版を表示しています。

CEL Expressions

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

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 conditions, 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
A formula field's expressionF*.object.ts formula fields
A field's requiredWhen / visibleWhen / readonlyWhenPobject fields
A script validation rule's conditionPobject validations
SharingRule.conditionPsharing rules
A list view's conditionalFormatting[].conditionPviews
A start node's config.condition, an edge's conditionPflows
An action's visible / disabledPactions
A schedule's expressioncronscheduled flows and jobs
An object's titleFormat (deprecated) and AI prompt templatestmplobjects, AI models
A field's defaultValuecelobject fields

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, name, email, positions: string[], organizationId }
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:

subtotal: Field.formula({
  label: 'Subtotal',
  expression: F`record.quantity * record.unit_price`,
}),

Validation — close date must be in the future. A script rule's condition describes the invalid record: when it is true, the write is refused.

// One validation rule, an entry of the object's `validations`
{
  type: 'script',
  name: 'close_date_in_future',
  message: 'Close date must be in the future',
  condition: P`record.close_date <= today()`,
}

Visibility — show a field only once the deal is won:

close_reason: Field.textarea({
  label: 'Close reason',
  visibleWhen: P`record.stage == 'closed_won'`,
}),

Flow condition — start a record-change flow only for large amounts:

// The `config` of a `start` node; the rest of the flow is omitted (see Flows).
config: {
  objectName: 'opportunity',
  triggerType: 'record-after-update',
  condition: P`record.amount >= 1000`,
}

Schedule — weekday 9am:

// The `config` of a `start` node; the rest of the flow is omitted (see Flows).
config: {
  triggerType: 'schedule',
  schedule: { type: 'cron', expression: cron`0 9 * * 1-5` },
}

Notification text is not a tmpl slot. A notify node's title and message are plain strings with {record.field} placeholders:

// The `config` of a `notify` node; the node around it is omitted.
config: {
  recipients: '{record.owner}',
  title: '[{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" } }

Invalid expressions do not fail silently. A malformed or unknown-field expression fails os compile with the located message above (including a did-you-mean hint for a misspelled field). At runtime, a bad expression throws an attributed error rather than silently evaluating to null or false, so the failure is visible in logs and the audit trail instead of quietly corrupting a formula value or a guard decision.

See also