ObjectStackObjectStack

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:

  1. Script Validation: Formula-based validation using a CEL predicate
  2. State Machine Validation: Control allowed state transitions
  3. Format Validation: Validate a field's value (email, URL, phone, JSON, regex)
  4. Cross-Field Validation: Validate relationships between multiple fields
  5. JSON Schema Validation: Validate a JSON field against a JSON Schema
  6. 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, with unique: 'organization' for one holder per organization or unique: 'global' for one across the whole installation — ADR-0120; partial for a scoped/conditional constraint), or field-level unique. A SELECT-then-INSERT "rule" is inherently racy (TOCTOU); a DB unique constraint is not.
  • Async / remote validation → a client-form concern (debounce/validatorUrl only 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 a unique index / lifecycle hook.
  • Custom handler → a beforeInsert / beforeUpdate lifecycle hook, the typed, supported extension point for arbitrary validation code.
  • Delete-time guards (events: ['delete']) → a beforeDelete lifecycle hook. The evaluator only runs on the insert/update write path (a delete carries no record payload to validate), so a delete event 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

PropertyTypeRequiredDescription
namestringUnique rule name (snake_case)
labelstringoptionalHuman-readable label for the rule listing
descriptionstringoptionalAdministrative notes explaining the business reason
activebooleanoptional
eventsEnum<'insert' | 'update'>[]optionalWrite 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
priorityintegeroptionalExecution priority (lower runs first, default: 100)
tagsstring[]optionalCategorization tags (e.g., "compliance", "billing")
severityEnum<'error' | 'warning' | 'info'>optional
messagestringError message to display to the user
_lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>optionalItem-level lock — controls overlay & delete (ADR-0010).
_lockReasonstringoptionalHuman-readable reason shown when a write is refused by _lock.
_lockSourceEnum<'artifact' | 'package' | 'env-forced'>optionalLayer that set _lock (artifact | package | env-forced).
_provenanceEnum<'package' | 'org' | 'env-forced'>optionalOrigin of the item (package | org | env-forced).
_packageIdstringoptionalOwning package machine id.
_packageVersionstringoptionalOwning package version.
_lockDocsUrlstringoptionalOptional documentation link surfaced next to _lockReason.
type'conditional'
whenstring | { 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 moreValidation rule to apply when condition is true
otherwise{ name: string; label?: string; description?: string; active?: boolean; … } | [ConditionalValidation](#conditionalvalidation) | … +4 moreoptionalValidation rule to apply when condition is false

CrossFieldValidation

Properties

PropertyTypeRequiredDescription
namestringUnique rule name (snake_case)
labelstringoptionalHuman-readable label for the rule listing
descriptionstringoptionalAdministrative notes explaining the business reason
activebooleanoptional
eventsEnum<'insert' | 'update'>[]optionalWrite 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
priorityintegeroptionalExecution priority (lower runs first, default: 100)
tagsstring[]optionalCategorization tags (e.g., "compliance", "billing")
severityEnum<'error' | 'warning' | 'info'>optional
messagestringError message to display to the user
_lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>optionalItem-level lock — controls overlay & delete (ADR-0010).
_lockReasonstringoptionalHuman-readable reason shown when a write is refused by _lock.
_lockSourceEnum<'artifact' | 'package' | 'env-forced'>optionalLayer that set _lock (artifact | package | env-forced).
_provenanceEnum<'package' | 'org' | 'env-forced'>optionalOrigin of the item (package | org | env-forced).
_packageIdstringoptionalOwning package machine id.
_packageVersionstringoptionalOwning package version.
_lockDocsUrlstringoptionalOptional documentation link surfaced next to _lockReason.
type'cross_field'
conditionstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object }Predicate (CEL) comparing fields. e.g. Precord.end_date > record.start_date
fieldsstring[]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

PropertyTypeRequiredDescription
namestringUnique rule name (snake_case)
labelstringoptionalHuman-readable label for the rule listing
descriptionstringoptionalAdministrative notes explaining the business reason
activeboolean
eventsEnum<'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
priorityintegerExecution priority (lower runs first, default: 100)
tagsstring[]optionalCategorization tags (e.g., "compliance", "billing")
severityEnum<'error' | 'warning' | 'info'>
messagestringError message to display to the user
_lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>optionalItem-level lock — controls overlay & delete (ADR-0010).
_lockReasonstringoptionalHuman-readable reason shown when a write is refused by _lock.
_lockSourceEnum<'artifact' | 'package' | 'env-forced'>optionalLayer that set _lock (artifact | package | env-forced).
_provenanceEnum<'package' | 'org' | 'env-forced'>optionalOrigin of the item (package | org | env-forced).
_packageIdstringoptionalOwning package machine id.
_packageVersionstringoptionalOwning package version.
_lockDocsUrlstringoptionalOptional documentation link surfaced next to _lockReason.
type'format'
fieldstring
regexstringoptional
formatEnum<'email' | 'url' | 'phone' | 'json'>optional

JSONValidation

Properties

PropertyTypeRequiredDescription
namestringUnique rule name (snake_case)
labelstringoptionalHuman-readable label for the rule listing
descriptionstringoptionalAdministrative notes explaining the business reason
activeboolean
eventsEnum<'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
priorityintegerExecution priority (lower runs first, default: 100)
tagsstring[]optionalCategorization tags (e.g., "compliance", "billing")
severityEnum<'error' | 'warning' | 'info'>
messagestringError message to display to the user
_lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>optionalItem-level lock — controls overlay & delete (ADR-0010).
_lockReasonstringoptionalHuman-readable reason shown when a write is refused by _lock.
_lockSourceEnum<'artifact' | 'package' | 'env-forced'>optionalLayer that set _lock (artifact | package | env-forced).
_provenanceEnum<'package' | 'org' | 'env-forced'>optionalOrigin of the item (package | org | env-forced).
_packageIdstringoptionalOwning package machine id.
_packageVersionstringoptionalOwning package version.
_lockDocsUrlstringoptionalOptional documentation link surfaced next to _lockReason.
type'json_schema'
fieldstringJSON field to validate
schemaRecord<string, any>JSON Schema object definition

ScriptValidation

Properties

PropertyTypeRequiredDescription
namestringUnique rule name (snake_case)
labelstringoptionalHuman-readable label for the rule listing
descriptionstringoptionalAdministrative notes explaining the business reason
activebooleanoptional
eventsEnum<'insert' | 'update'>[]optionalWrite 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
priorityintegeroptionalExecution priority (lower runs first, default: 100)
tagsstring[]optionalCategorization tags (e.g., "compliance", "billing")
severityEnum<'error' | 'warning' | 'info'>optional
messagestringError message to display to the user
_lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>optionalItem-level lock — controls overlay & delete (ADR-0010).
_lockReasonstringoptionalHuman-readable reason shown when a write is refused by _lock.
_lockSourceEnum<'artifact' | 'package' | 'env-forced'>optionalLayer that set _lock (artifact | package | env-forced).
_provenanceEnum<'package' | 'org' | 'env-forced'>optionalOrigin of the item (package | org | env-forced).
_packageIdstringoptionalOwning package machine id.
_packageVersionstringoptionalOwning package version.
_lockDocsUrlstringoptionalOptional documentation link surfaced next to _lockReason.
type'script'
conditionstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object }Predicate (CEL). If TRUE, validation fails. e.g. Precord.amount < 0

StateMachineValidation

Properties

PropertyTypeRequiredDescription
namestringUnique rule name (snake_case)
labelstringoptionalHuman-readable label for the rule listing
descriptionstringoptionalAdministrative notes explaining the business reason
activeboolean
eventsEnum<'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
priorityintegerExecution priority (lower runs first, default: 100)
tagsstring[]optionalCategorization tags (e.g., "compliance", "billing")
severityEnum<'error' | 'warning' | 'info'>
messagestringError message to display to the user
_lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>optionalItem-level lock — controls overlay & delete (ADR-0010).
_lockReasonstringoptionalHuman-readable reason shown when a write is refused by _lock.
_lockSourceEnum<'artifact' | 'package' | 'env-forced'>optionalLayer that set _lock (artifact | package | env-forced).
_provenanceEnum<'package' | 'org' | 'env-forced'>optionalOrigin of the item (package | org | env-forced).
_packageIdstringoptionalOwning package machine id.
_packageVersionstringoptionalOwning package version.
_lockDocsUrlstringoptionalOptional documentation link surfaced next to _lockReason.
type'state_machine'
fieldstringState field (e.g. status)
transitionsRecord<string, string[]>Map of { OldState: [AllowedNewStates] }
initialStatesstring[]optionalStates 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

PropertyTypeRequiredDescription
namestringUnique rule name (snake_case)
labelstringoptionalHuman-readable label for the rule listing
descriptionstringoptionalAdministrative notes explaining the business reason
activebooleanoptional
eventsEnum<'insert' | 'update'>[]optionalWrite 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
priorityintegeroptionalExecution priority (lower runs first, default: 100)
tagsstring[]optionalCategorization tags (e.g., "compliance", "billing")
severityEnum<'error' | 'warning' | 'info'>optional
messagestringError message to display to the user
_lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>optionalItem-level lock — controls overlay & delete (ADR-0010).
_lockReasonstringoptionalHuman-readable reason shown when a write is refused by _lock.
_lockSourceEnum<'artifact' | 'package' | 'env-forced'>optionalLayer that set _lock (artifact | package | env-forced).
_provenanceEnum<'package' | 'org' | 'env-forced'>optionalOrigin of the item (package | org | env-forced).
_packageIdstringoptionalOwning package machine id.
_packageVersionstringoptionalOwning package version.
_lockDocsUrlstringoptionalOptional documentation link surfaced next to _lockReason.
type'script'
conditionstring | { 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

PropertyTypeRequiredDescription
namestringUnique rule name (snake_case)
labelstringoptionalHuman-readable label for the rule listing
descriptionstringoptionalAdministrative notes explaining the business reason
activebooleanoptional
eventsEnum<'insert' | 'update'>[]optionalWrite 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
priorityintegeroptionalExecution priority (lower runs first, default: 100)
tagsstring[]optionalCategorization tags (e.g., "compliance", "billing")
severityEnum<'error' | 'warning' | 'info'>optional
messagestringError message to display to the user
_lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>optionalItem-level lock — controls overlay & delete (ADR-0010).
_lockReasonstringoptionalHuman-readable reason shown when a write is refused by _lock.
_lockSourceEnum<'artifact' | 'package' | 'env-forced'>optionalLayer that set _lock (artifact | package | env-forced).
_provenanceEnum<'package' | 'org' | 'env-forced'>optionalOrigin of the item (package | org | env-forced).
_packageIdstringoptionalOwning package machine id.
_packageVersionstringoptionalOwning package version.
_lockDocsUrlstringoptionalOptional documentation link surfaced next to _lockReason.
type'state_machine'
fieldstringState field (e.g. status)
transitionsRecord<string, string[]>Map of { OldState: [AllowedNewStates] }
initialStatesstring[]optionalStates 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

PropertyTypeRequiredDescription
namestringUnique rule name (snake_case)
labelstringoptionalHuman-readable label for the rule listing
descriptionstringoptionalAdministrative notes explaining the business reason
activebooleanoptional
eventsEnum<'insert' | 'update'>[]optionalWrite 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
priorityintegeroptionalExecution priority (lower runs first, default: 100)
tagsstring[]optionalCategorization tags (e.g., "compliance", "billing")
severityEnum<'error' | 'warning' | 'info'>optional
messagestringError message to display to the user
_lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>optionalItem-level lock — controls overlay & delete (ADR-0010).
_lockReasonstringoptionalHuman-readable reason shown when a write is refused by _lock.
_lockSourceEnum<'artifact' | 'package' | 'env-forced'>optionalLayer that set _lock (artifact | package | env-forced).
_provenanceEnum<'package' | 'org' | 'env-forced'>optionalOrigin of the item (package | org | env-forced).
_packageIdstringoptionalOwning package machine id.
_packageVersionstringoptionalOwning package version.
_lockDocsUrlstringoptionalOptional documentation link surfaced next to _lockReason.
type'format'
fieldstring
regexstringoptional
formatEnum<'email' | 'url' | 'phone' | 'json'>optional

Option 4

Type: cross_field

Properties

PropertyTypeRequiredDescription
namestringUnique rule name (snake_case)
labelstringoptionalHuman-readable label for the rule listing
descriptionstringoptionalAdministrative notes explaining the business reason
activebooleanoptional
eventsEnum<'insert' | 'update'>[]optionalWrite 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
priorityintegeroptionalExecution priority (lower runs first, default: 100)
tagsstring[]optionalCategorization tags (e.g., "compliance", "billing")
severityEnum<'error' | 'warning' | 'info'>optional
messagestringError message to display to the user
_lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>optionalItem-level lock — controls overlay & delete (ADR-0010).
_lockReasonstringoptionalHuman-readable reason shown when a write is refused by _lock.
_lockSourceEnum<'artifact' | 'package' | 'env-forced'>optionalLayer that set _lock (artifact | package | env-forced).
_provenanceEnum<'package' | 'org' | 'env-forced'>optionalOrigin of the item (package | org | env-forced).
_packageIdstringoptionalOwning package machine id.
_packageVersionstringoptionalOwning package version.
_lockDocsUrlstringoptionalOptional documentation link surfaced next to _lockReason.
type'cross_field'
conditionstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object }Predicate (CEL) comparing fields. e.g. Precord.end_date > record.start_date
fieldsstring[]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

PropertyTypeRequiredDescription
namestringUnique rule name (snake_case)
labelstringoptionalHuman-readable label for the rule listing
descriptionstringoptionalAdministrative notes explaining the business reason
activebooleanoptional
eventsEnum<'insert' | 'update'>[]optionalWrite 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
priorityintegeroptionalExecution priority (lower runs first, default: 100)
tagsstring[]optionalCategorization tags (e.g., "compliance", "billing")
severityEnum<'error' | 'warning' | 'info'>optional
messagestringError message to display to the user
_lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>optionalItem-level lock — controls overlay & delete (ADR-0010).
_lockReasonstringoptionalHuman-readable reason shown when a write is refused by _lock.
_lockSourceEnum<'artifact' | 'package' | 'env-forced'>optionalLayer that set _lock (artifact | package | env-forced).
_provenanceEnum<'package' | 'org' | 'env-forced'>optionalOrigin of the item (package | org | env-forced).
_packageIdstringoptionalOwning package machine id.
_packageVersionstringoptionalOwning package version.
_lockDocsUrlstringoptionalOptional documentation link surfaced next to _lockReason.
type'json_schema'
fieldstringJSON field to validate
schemaRecord<string, any>JSON Schema object definition

Option 6

Type: conditional

Properties

PropertyTypeRequiredDescription
namestringUnique rule name (snake_case)
labelstringoptionalHuman-readable label for the rule listing
descriptionstringoptionalAdministrative notes explaining the business reason
activebooleanoptional
eventsEnum<'insert' | 'update'>[]optionalWrite 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
priorityintegeroptionalExecution priority (lower runs first, default: 100)
tagsstring[]optionalCategorization tags (e.g., "compliance", "billing")
severityEnum<'error' | 'warning' | 'info'>optional
messagestringError message to display to the user
_lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>optionalItem-level lock — controls overlay & delete (ADR-0010).
_lockReasonstringoptionalHuman-readable reason shown when a write is refused by _lock.
_lockSourceEnum<'artifact' | 'package' | 'env-forced'>optionalLayer that set _lock (artifact | package | env-forced).
_provenanceEnum<'package' | 'org' | 'env-forced'>optionalOrigin of the item (package | org | env-forced).
_packageIdstringoptionalOwning package machine id.
_packageVersionstringoptionalOwning package version.
_lockDocsUrlstringoptionalOptional documentation link surfaced next to _lockReason.
type'conditional'
whenstring | { 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)optionalValidation rule to apply when condition is false


On this page