Validation
Validation protocol schemas
ObjectStack Validation Protocol
This module defines the validation schema protocol for ObjectStack, providing a comprehensive type-safe validation system similar to Salesforce's validation rules but with enhanced capabilities.
Overview
Validation rules are applied at the data layer to ensure data integrity and enforce business logic.
A validation rule is a deterministic, synchronous, side-effect-free predicate over a single
record — it must be decidable from the incoming write (and, on update, the prior record) with
no I/O. Everything advertised here runs on the write path (see
objectql/src/validation/rule-validator.ts) — insert, single-id update, and multi-row
(multi: true) update, where the evaluator runs once per matched row (#3106); nothing is a
silent no-op. The events enum admits only insert/update for this reason — see the
delete note under "Deliberately NOT validation rules" below.
The system supports these validation types:
- Script Validation: Formula-based validation using a CEL predicate
- State Machine Validation: Control allowed state transitions
- Format Validation: Validate a field's value (email, URL, phone, JSON, regex)
- Cross-Field Validation: Validate relationships between multiple fields
- JSON Schema Validation: Validate a JSON field against a JSON Schema
- Conditional Validation: Apply a nested rule based on a CEL condition
Deliberately NOT validation rules
These were once declared here but never enforced. Because the contract above rules them out (they need I/O or are client-side concerns), they were removed rather than left as silent no-ops. Use the layer that already does each one correctly:
- Uniqueness → a unique index whose scope is stated (
ObjectSchema.indexes, withunique: 'organization'for one holder per organization orunique: 'global'for one across the whole installation — ADR-0120;partialfor a scoped/conditional constraint), or field-levelunique. A SELECT-then-INSERT "rule" is inherently racy (TOCTOU); a DB unique constraint is not. - Async / remote validation → a client-form concern (
debounce/validatorUrlonly mean anything against keystrokes) and an SSRF/latency hazard on the server write path. Keep it in the form layer, or enforce the underlying invariant with auniqueindex / lifecycle hook. - Custom handler → a
beforeInsert/beforeUpdatelifecycle hook, the typed, supported extension point for arbitrary validation code. - Delete-time guards (
events: ['delete']) → abeforeDeletelifecycle hook. The evaluator only runs on the insert/update write path (a delete carries no record payload to validate), so adeleteevent was a proven silent no-op — the enum value was removed rather than left advertised-but-unenforced (#3184; see docs/audits/2026-06-validationschema-property-liveness.md).
Salesforce Comparison
ObjectStack validation rules are inspired by Salesforce validation rules but enhanced:
- Salesforce: Formula-based validation with
Error Condition Formula - ObjectStack: Multiple validation types with composable rules
Example Salesforce validation rule:
Rule Name: Discount_Cannot_Exceed_40_Percent
Error Condition Formula: Discount_Percent__c > 0.40
Error Message: Discount cannot exceed 40%.Equivalent ObjectStack rule:
{
type: 'script',
name: 'discount_cannot_exceed_40_percent',
condition: 'discount_percent > 0.40',
message: 'Discount cannot exceed 40%',
severity: 'error'
}Source: packages/spec/src/data/validation.zod.ts
TypeScript Usage
import { ConditionalValidationSchema, CrossFieldValidationSchema, FormatValidationSchema, JSONValidationSchema, ScriptValidationSchema, StateMachineValidationSchema, ValidationRuleSchema } from '@objectstack/spec/data';
import type { ConditionalValidation, CrossFieldValidation, FormatValidation, JSONValidation, ScriptValidation, StateMachineValidation, ValidationRule } from '@objectstack/spec/data';
// Validate data
const result = ConditionalValidationSchema.parse(data);ConditionalValidation
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Unique rule name (snake_case) |
| label | string | optional | Human-readable label for the rule listing |
| description | string | optional | Administrative notes explaining the business reason |
| active | boolean | optional | |
| events | Enum<'insert' | 'update'>[] | optional | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer | optional | Execution priority (lower runs first, default: 100) |
| tags | string[] | optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> | optional | |
| message | string | ✅ | Error message to display to the user |
| _lock | Enum<'none' | 'no-overlay' | 'no-delete' | 'full'> | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| _lockReason | string | optional | Human-readable reason shown when a write is refused by _lock. |
| _lockSource | Enum<'artifact' | 'package' | 'env-forced'> | optional | Layer that set _lock (artifact | package | env-forced). |
| _provenance | Enum<'package' | 'org' | 'env-forced'> | optional | Origin of the item (package | org | env-forced). |
| _packageId | string | optional | Owning package machine id. |
| _packageVersion | string | optional | Owning package version. |
| _lockDocsUrl | string | optional | Optional documentation link surfaced next to _lockReason. |
| type | 'conditional' | ✅ | |
| when | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object } | ✅ | Predicate (CEL). e.g. Precord.type == 'enterprise' |
| then | { name: string; label?: string; description?: string; active?: boolean; … } | [ConditionalValidation](#conditionalvalidation) | … +4 more | ✅ | Validation rule to apply when condition is true |
| otherwise | { name: string; label?: string; description?: string; active?: boolean; … } | [ConditionalValidation](#conditionalvalidation) | … +4 more | optional | Validation rule to apply when condition is false |
CrossFieldValidation
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Unique rule name (snake_case) |
| label | string | optional | Human-readable label for the rule listing |
| description | string | optional | Administrative notes explaining the business reason |
| active | boolean | optional | |
| events | Enum<'insert' | 'update'>[] | optional | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer | optional | Execution priority (lower runs first, default: 100) |
| tags | string[] | optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> | optional | |
| message | string | ✅ | Error message to display to the user |
| _lock | Enum<'none' | 'no-overlay' | 'no-delete' | 'full'> | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| _lockReason | string | optional | Human-readable reason shown when a write is refused by _lock. |
| _lockSource | Enum<'artifact' | 'package' | 'env-forced'> | optional | Layer that set _lock (artifact | package | env-forced). |
| _provenance | Enum<'package' | 'org' | 'env-forced'> | optional | Origin of the item (package | org | env-forced). |
| _packageId | string | optional | Owning package machine id. |
| _packageVersion | string | optional | Owning package version. |
| _lockDocsUrl | string | optional | Optional documentation link surfaced next to _lockReason. |
| type | 'cross_field' | ✅ | |
| condition | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object } | ✅ | Predicate (CEL) comparing fields. e.g. Precord.end_date > record.start_date |
| fields | string[] | ✅ | Fields involved. Only fields[0] is read (labels which field the violation attaches to); the rest are advisory. Shares script’s evaluation path. |
FormatValidation
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Unique rule name (snake_case) |
| label | string | optional | Human-readable label for the rule listing |
| description | string | optional | Administrative notes explaining the business reason |
| active | boolean | ✅ | |
| events | Enum<'insert' | 'update'>[] | ✅ | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer | ✅ | Execution priority (lower runs first, default: 100) |
| tags | string[] | optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> | ✅ | |
| message | string | ✅ | Error message to display to the user |
| _lock | Enum<'none' | 'no-overlay' | 'no-delete' | 'full'> | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| _lockReason | string | optional | Human-readable reason shown when a write is refused by _lock. |
| _lockSource | Enum<'artifact' | 'package' | 'env-forced'> | optional | Layer that set _lock (artifact | package | env-forced). |
| _provenance | Enum<'package' | 'org' | 'env-forced'> | optional | Origin of the item (package | org | env-forced). |
| _packageId | string | optional | Owning package machine id. |
| _packageVersion | string | optional | Owning package version. |
| _lockDocsUrl | string | optional | Optional documentation link surfaced next to _lockReason. |
| type | 'format' | ✅ | |
| field | string | ✅ | |
| regex | string | optional | |
| format | Enum<'email' | 'url' | 'phone' | 'json'> | optional |
JSONValidation
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Unique rule name (snake_case) |
| label | string | optional | Human-readable label for the rule listing |
| description | string | optional | Administrative notes explaining the business reason |
| active | boolean | ✅ | |
| events | Enum<'insert' | 'update'>[] | ✅ | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer | ✅ | Execution priority (lower runs first, default: 100) |
| tags | string[] | optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> | ✅ | |
| message | string | ✅ | Error message to display to the user |
| _lock | Enum<'none' | 'no-overlay' | 'no-delete' | 'full'> | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| _lockReason | string | optional | Human-readable reason shown when a write is refused by _lock. |
| _lockSource | Enum<'artifact' | 'package' | 'env-forced'> | optional | Layer that set _lock (artifact | package | env-forced). |
| _provenance | Enum<'package' | 'org' | 'env-forced'> | optional | Origin of the item (package | org | env-forced). |
| _packageId | string | optional | Owning package machine id. |
| _packageVersion | string | optional | Owning package version. |
| _lockDocsUrl | string | optional | Optional documentation link surfaced next to _lockReason. |
| type | 'json_schema' | ✅ | |
| field | string | ✅ | JSON field to validate |
| schema | Record<string, any> | ✅ | JSON Schema object definition |
ScriptValidation
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Unique rule name (snake_case) |
| label | string | optional | Human-readable label for the rule listing |
| description | string | optional | Administrative notes explaining the business reason |
| active | boolean | optional | |
| events | Enum<'insert' | 'update'>[] | optional | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer | optional | Execution priority (lower runs first, default: 100) |
| tags | string[] | optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> | optional | |
| message | string | ✅ | Error message to display to the user |
| _lock | Enum<'none' | 'no-overlay' | 'no-delete' | 'full'> | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| _lockReason | string | optional | Human-readable reason shown when a write is refused by _lock. |
| _lockSource | Enum<'artifact' | 'package' | 'env-forced'> | optional | Layer that set _lock (artifact | package | env-forced). |
| _provenance | Enum<'package' | 'org' | 'env-forced'> | optional | Origin of the item (package | org | env-forced). |
| _packageId | string | optional | Owning package machine id. |
| _packageVersion | string | optional | Owning package version. |
| _lockDocsUrl | string | optional | Optional documentation link surfaced next to _lockReason. |
| type | 'script' | ✅ | |
| condition | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object } | ✅ | Predicate (CEL). If TRUE, validation fails. e.g. Precord.amount < 0 |
StateMachineValidation
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Unique rule name (snake_case) |
| label | string | optional | Human-readable label for the rule listing |
| description | string | optional | Administrative notes explaining the business reason |
| active | boolean | ✅ | |
| events | Enum<'insert' | 'update'>[] | ✅ | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer | ✅ | Execution priority (lower runs first, default: 100) |
| tags | string[] | optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> | ✅ | |
| message | string | ✅ | Error message to display to the user |
| _lock | Enum<'none' | 'no-overlay' | 'no-delete' | 'full'> | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| _lockReason | string | optional | Human-readable reason shown when a write is refused by _lock. |
| _lockSource | Enum<'artifact' | 'package' | 'env-forced'> | optional | Layer that set _lock (artifact | package | env-forced). |
| _provenance | Enum<'package' | 'org' | 'env-forced'> | optional | Origin of the item (package | org | env-forced). |
| _packageId | string | optional | Owning package machine id. |
| _packageVersion | string | optional | Owning package version. |
| _lockDocsUrl | string | optional | Optional documentation link surfaced next to _lockReason. |
| type | 'state_machine' | ✅ | |
| field | string | ✅ | State field (e.g. status) |
| transitions | Record<string, string[]> | ✅ | Map of { OldState: [AllowedNewStates] } |
| initialStates | string[] | optional | States a record may be CREATED in. When set, an INSERT whose state field carries a value outside this list is rejected (server-enforced) — the FSM entry point. transitions only governs UPDATE, and a select field permits ANY declared option as an initial value, so without this a record could be born mid-flow (e.g. created already approved). Omit to keep the legacy behavior (no initial-state check on insert). #3165. |
ValidationRule
Union Options
This schema accepts one of the following structures:
Option 1
Type: script
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Unique rule name (snake_case) |
| label | string | optional | Human-readable label for the rule listing |
| description | string | optional | Administrative notes explaining the business reason |
| active | boolean | optional | |
| events | Enum<'insert' | 'update'>[] | optional | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer | optional | Execution priority (lower runs first, default: 100) |
| tags | string[] | optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> | optional | |
| message | string | ✅ | Error message to display to the user |
| _lock | Enum<'none' | 'no-overlay' | 'no-delete' | 'full'> | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| _lockReason | string | optional | Human-readable reason shown when a write is refused by _lock. |
| _lockSource | Enum<'artifact' | 'package' | 'env-forced'> | optional | Layer that set _lock (artifact | package | env-forced). |
| _provenance | Enum<'package' | 'org' | 'env-forced'> | optional | Origin of the item (package | org | env-forced). |
| _packageId | string | optional | Owning package machine id. |
| _packageVersion | string | optional | Owning package version. |
| _lockDocsUrl | string | optional | Optional documentation link surfaced next to _lockReason. |
| type | 'script' | ✅ | |
| condition | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object } | ✅ | Predicate (CEL). If TRUE, validation fails. e.g. Precord.amount < 0 |
Option 2
Type: state_machine
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Unique rule name (snake_case) |
| label | string | optional | Human-readable label for the rule listing |
| description | string | optional | Administrative notes explaining the business reason |
| active | boolean | optional | |
| events | Enum<'insert' | 'update'>[] | optional | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer | optional | Execution priority (lower runs first, default: 100) |
| tags | string[] | optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> | optional | |
| message | string | ✅ | Error message to display to the user |
| _lock | Enum<'none' | 'no-overlay' | 'no-delete' | 'full'> | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| _lockReason | string | optional | Human-readable reason shown when a write is refused by _lock. |
| _lockSource | Enum<'artifact' | 'package' | 'env-forced'> | optional | Layer that set _lock (artifact | package | env-forced). |
| _provenance | Enum<'package' | 'org' | 'env-forced'> | optional | Origin of the item (package | org | env-forced). |
| _packageId | string | optional | Owning package machine id. |
| _packageVersion | string | optional | Owning package version. |
| _lockDocsUrl | string | optional | Optional documentation link surfaced next to _lockReason. |
| type | 'state_machine' | ✅ | |
| field | string | ✅ | State field (e.g. status) |
| transitions | Record<string, string[]> | ✅ | Map of { OldState: [AllowedNewStates] } |
| initialStates | string[] | optional | States a record may be CREATED in. When set, an INSERT whose state field carries a value outside this list is rejected (server-enforced) — the FSM entry point. transitions only governs UPDATE, and a select field permits ANY declared option as an initial value, so without this a record could be born mid-flow (e.g. created already approved). Omit to keep the legacy behavior (no initial-state check on insert). #3165. |
Option 3
Type: format
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Unique rule name (snake_case) |
| label | string | optional | Human-readable label for the rule listing |
| description | string | optional | Administrative notes explaining the business reason |
| active | boolean | optional | |
| events | Enum<'insert' | 'update'>[] | optional | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer | optional | Execution priority (lower runs first, default: 100) |
| tags | string[] | optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> | optional | |
| message | string | ✅ | Error message to display to the user |
| _lock | Enum<'none' | 'no-overlay' | 'no-delete' | 'full'> | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| _lockReason | string | optional | Human-readable reason shown when a write is refused by _lock. |
| _lockSource | Enum<'artifact' | 'package' | 'env-forced'> | optional | Layer that set _lock (artifact | package | env-forced). |
| _provenance | Enum<'package' | 'org' | 'env-forced'> | optional | Origin of the item (package | org | env-forced). |
| _packageId | string | optional | Owning package machine id. |
| _packageVersion | string | optional | Owning package version. |
| _lockDocsUrl | string | optional | Optional documentation link surfaced next to _lockReason. |
| type | 'format' | ✅ | |
| field | string | ✅ | |
| regex | string | optional | |
| format | Enum<'email' | 'url' | 'phone' | 'json'> | optional |
Option 4
Type: cross_field
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Unique rule name (snake_case) |
| label | string | optional | Human-readable label for the rule listing |
| description | string | optional | Administrative notes explaining the business reason |
| active | boolean | optional | |
| events | Enum<'insert' | 'update'>[] | optional | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer | optional | Execution priority (lower runs first, default: 100) |
| tags | string[] | optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> | optional | |
| message | string | ✅ | Error message to display to the user |
| _lock | Enum<'none' | 'no-overlay' | 'no-delete' | 'full'> | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| _lockReason | string | optional | Human-readable reason shown when a write is refused by _lock. |
| _lockSource | Enum<'artifact' | 'package' | 'env-forced'> | optional | Layer that set _lock (artifact | package | env-forced). |
| _provenance | Enum<'package' | 'org' | 'env-forced'> | optional | Origin of the item (package | org | env-forced). |
| _packageId | string | optional | Owning package machine id. |
| _packageVersion | string | optional | Owning package version. |
| _lockDocsUrl | string | optional | Optional documentation link surfaced next to _lockReason. |
| type | 'cross_field' | ✅ | |
| condition | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object } | ✅ | Predicate (CEL) comparing fields. e.g. Precord.end_date > record.start_date |
| fields | string[] | ✅ | Fields involved. Only fields[0] is read (labels which field the violation attaches to); the rest are advisory. Shares script’s evaluation path. |
Option 5
Type: json_schema
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Unique rule name (snake_case) |
| label | string | optional | Human-readable label for the rule listing |
| description | string | optional | Administrative notes explaining the business reason |
| active | boolean | optional | |
| events | Enum<'insert' | 'update'>[] | optional | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer | optional | Execution priority (lower runs first, default: 100) |
| tags | string[] | optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> | optional | |
| message | string | ✅ | Error message to display to the user |
| _lock | Enum<'none' | 'no-overlay' | 'no-delete' | 'full'> | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| _lockReason | string | optional | Human-readable reason shown when a write is refused by _lock. |
| _lockSource | Enum<'artifact' | 'package' | 'env-forced'> | optional | Layer that set _lock (artifact | package | env-forced). |
| _provenance | Enum<'package' | 'org' | 'env-forced'> | optional | Origin of the item (package | org | env-forced). |
| _packageId | string | optional | Owning package machine id. |
| _packageVersion | string | optional | Owning package version. |
| _lockDocsUrl | string | optional | Optional documentation link surfaced next to _lockReason. |
| type | 'json_schema' | ✅ | |
| field | string | ✅ | JSON field to validate |
| schema | Record<string, any> | ✅ | JSON Schema object definition |
Option 6
Type: conditional
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Unique rule name (snake_case) |
| label | string | optional | Human-readable label for the rule listing |
| description | string | optional | Administrative notes explaining the business reason |
| active | boolean | optional | |
| events | Enum<'insert' | 'update'>[] | optional | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer | optional | Execution priority (lower runs first, default: 100) |
| tags | string[] | optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> | optional | |
| message | string | ✅ | Error message to display to the user |
| _lock | Enum<'none' | 'no-overlay' | 'no-delete' | 'full'> | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| _lockReason | string | optional | Human-readable reason shown when a write is refused by _lock. |
| _lockSource | Enum<'artifact' | 'package' | 'env-forced'> | optional | Layer that set _lock (artifact | package | env-forced). |
| _provenance | Enum<'package' | 'org' | 'env-forced'> | optional | Origin of the item (package | org | env-forced). |
| _packageId | string | optional | Owning package machine id. |
| _packageVersion | string | optional | Owning package version. |
| _lockDocsUrl | string | optional | Optional documentation link surfaced next to _lockReason. |
| type | 'conditional' | ✅ | |
| when | string | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object } | ✅ | Predicate (CEL). e.g. Precord.type == 'enterprise' |
| then | [ValidationRule](#validationrule) | ✅ | Validation rule to apply when condition is true |
| otherwise | [ValidationRule](#validationrule) | optional | Validation rule to apply when condition is false |