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.
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
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).
Administrative notes explaining the business reason
active
boolean
optional (default: true)
events
Enum<'insert' | 'update'>[]
optional (default: ["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
Administrative notes explaining the business reason
active
boolean
optional (default: true)
events
Enum<'insert' | 'update'>[]
optional (default: ["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
Administrative notes explaining the business reason
active
boolean
optional (default: true)
events
Enum<'insert' | 'update'>[]
optional (default: ["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
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).
Administrative notes explaining the business reason
active
boolean
optional (default: true)
events
Enum<'insert' | 'update'>[]
optional (default: ["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
Administrative notes explaining the business reason
active
boolean
optional (default: true)
events
Enum<'insert' | 'update'>[]
optional (default: ["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
Administrative notes explaining the business reason
active
boolean
optional (default: true)
events
Enum<'insert' | 'update'>[]
optional (default: ["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
Administrative notes explaining the business reason
active
boolean
optional (default: true)
events
Enum<'insert' | 'update'>[]
optional (default: ["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
Administrative notes explaining the business reason
active
boolean
optional (default: true)
events
Enum<'insert' | 'update'>[]
optional (default: ["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
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).
Administrative notes explaining the business reason
active
boolean
optional (default: true)
events
Enum<'insert' | 'update'>[]
optional (default: ["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
Administrative notes explaining the business reason
active
boolean
optional (default: true)
events
Enum<'insert' | 'update'>[]
optional (default: ["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
Administrative notes explaining the business reason
active
boolean
optional (default: true)
events
Enum<'insert' | 'update'>[]
optional (default: ["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
Administrative notes explaining the business reason
active
boolean
optional (default: true)
events
Enum<'insert' | 'update'>[]
optional (default: ["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
Administrative notes explaining the business reason
active
boolean
optional (default: true)
events
Enum<'insert' | 'update'>[]
optional (default: ["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
Administrative notes explaining the business reason
active
boolean
optional (default: true)
events
Enum<'insert' | 'update'>[]
optional (default: ["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
Administrative notes explaining the business reason
active
boolean
optional (default: true)
events
Enum<'insert' | 'update'>[]
optional (default: ["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
Administrative notes explaining the business reason
active
boolean
optional (default: true)
events
Enum<'insert' | 'update'>[]
optional (default: ["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
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).
Administrative notes explaining the business reason
active
boolean
optional (default: true)
events
Enum<'insert' | 'update'>[]
optional (default: ["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
Administrative notes explaining the business reason
active
boolean
optional (default: true)
events
Enum<'insert' | 'update'>[]
optional (default: ["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
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).
Administrative notes explaining the business reason
active
boolean
optional (default: true)
events
Enum<'insert' | 'update'>[]
optional (default: ["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
Administrative notes explaining the business reason
active
boolean
optional (default: true)
events
Enum<'insert' | 'update'>[]
optional (default: ["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
Administrative notes explaining the business reason
active
boolean
optional (default: true)
events
Enum<'insert' | 'update'>[]
optional (default: ["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
Administrative notes explaining the business reason
active
boolean
optional (default: true)
events
Enum<'insert' | 'update'>[]
optional (default: ["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