Skip to content

Validation - Guide

Framework: KernDX Package Type: Managed Package

> Note for Client Implementations: When using KernDX in a subscriber org, prefix framework class references with your organization's namespace (e.g., AcmeLib.UTIL_ValidationRule). See the AI Agent Instructions for details.

Target Audience:

  • Developers - Building validation rules with cross-object queries, custom context classes, and programmatic bypass logic
  • Architects - Designing validation strategies with proper bypass hierarchy, execution strategies, and bulk patterns
  • Admins - Configuring validation rules via custom metadata, Flow integration, and shadow mode testing

Table of Contents

Expand
  1. Quick Navigation
  2. Overview
  3. Quick Start
  4. Architecture
  5. Custom Metadata Configuration
  6. Context Classes
  7. Formula Syntax
  8. Flow Integration
  9. Programmatic Usage
  10. Testing
  11. Advanced Features
  12. Anti-Patterns
  13. Best Practices
  14. Troubleshooting
  15. Related Documentation

Quick Navigation

I am a...I need to...Go to...
ArchitectUnderstand validation architectureArchitecture
ArchitectPlan bypass strategiesBypass Hierarchy
DeveloperCreate a validation ruleQuick Start
DeveloperWrite custom context classesContext Classes
DeveloperTest validationsTesting
AnalystConfigure validation rulesCustom Metadata Configuration
AnalystIntegrate validations in FlowsFlow Integration

Overview

What is the Validation Framework?

The Validation Framework provides formula-driven declarative validation for advanced scenarios that standard Salesforce validation rules cannot handle. It leverages Salesforce's FormulaEval namespace (same technology used by Flow Entry Criteria) to evaluate validation formulas at runtime.

Key capabilities include:

  • Cross-object validation requiring queries (e.g., "Account must have at least one Contact")
  • Aggregate validation (e.g., "Total of child records must not exceed parent limit")
  • Conditional bypass logic based on user permissions or feature flags
  • Shadow mode deployment for testing rules in production without blocking saves
  • Warning-level validations that log but don't block
  • Centralized validation management across multiple objects

> Responsibilities: The Validation Framework evaluates rules and surfaces errors. It does not perform DML, modify field values, or > contain business logic beyond pass/fail evaluation. Data enrichment for validation (cross-object lookups) belongs in context classes.

> When NOT to use this pattern: > - Simple field-level validations (required, format, range) that standard validation rules handle natively > - Single-object checks with formulas under 5,000 characters and no bypass requirements > - Duplicate detection -- use Salesforce Duplicate Rules instead

Key Benefits

BenefitDescription
Declarative ConfigurationRules defined via custom metadata - no code deployment for rule changes
Cross-Object QueriesBulk context pattern enables efficient validation requiring related data
Three-Level BypassObject --> Group --> Rule hierarchy with permission/feature flag integration
Shadow ModeTest rules in production without blocking saves - violations logged for monitoring
Flow IntegrationInvocable actions + validationErrors component for Screen Flows
Execution StrategiesAccumulate (collect all errors) or Fail Fast (stop on first error)
Warning SeverityLog data quality issues without blocking saves
Test UtilitiesUTIL_ValidationTestHelper for subscriber org testing

> Validation Framework Scope: Formula-driven rules using Salesforce's FormulaEval namespace, managed via ValidationRule__mdt and > ValidationRuleGroup__mdt custom metadata. Includes Flow integration, shadow mode, and three-level bypass hierarchy.


KernDX vs OOTB: Validation Comparison

Salesforce Out-of-the-Box Alternatives

Salesforce provides several native validation capabilities:

  1. Validation Rules - Formula-based rules on single objects (Setup --> Object --> Validation Rules)
  2. Flow Decision Elements - Validate in Flows using Decision elements and Fault paths
  3. Apex Trigger Validation - Custom addError() calls in trigger handlers
  4. Duplicate Rules - Prevent duplicate records based on matching rules

Pros & Cons Comparison

FeatureKernDX Validation FrameworkStandard Validation RulesApex Trigger Validation
Code RequiredContext class for custom objectsNo code (formula only)Full Apex implementation
Cross-Object QueriesVia bulk context patternNot supportedManual SOQL
Aggregate ValidationVia context propertiesNot supportedManual implementation
Conditional BypassPermission/Feature Flag basedManual formula conditionsManual implementation
Shadow ModeBuilt-in logging without blockingNot availableManual implementation
Warning SeverityLog-only optionAlways blocksManual implementation
Flow IntegrationInvocable actions + LWCTriggers on DML onlyInvocables required
Centralized ManagementAll rules in custom metadataScattered per objectScattered in code
Test UtilitiesUTIL_ValidationTestHelperTest via DML onlyManual test setup
Execution ControlAccumulate or Fail FastAlways accumulatesManual implementation
Error DisplayField-level or record-levelField-levelField-level
Formula EngineFormulaEval with globalsStandard formulaN/A
Setup ComplexityMetadata + trigger actionUI-based onlyCode deployment

When to Use KernDX Validation Framework

  • Cross-object validation requiring queries to related records
  • Aggregate validation checking totals, counts, or sums
  • Conditional bypass based on Feature Flags
  • Shadow mode testing for new rules in production
  • Warning-level validations for data quality monitoring
  • Flow-based validation with error display in Screen Flows
  • Centralized rule management across multiple objects
  • Execution strategy control (fail fast vs accumulate)

When to Use Standard Validation Rules

  • Simple field validations (required, format, range)
  • Single-object validation without cross-object queries
  • Formulas under 5000 characters without complex logic
  • No bypass requirements beyond standard profile/permission controls
  • Quick implementation without custom metadata setup

Quick Start

Define validation rules declaratively via ValidationRule__mdt custom metadata -- no Apex required.

> Step-by-step walkthrough: Fast Start - Custom Validations covers implementation, > testing, and common pitfalls.

text
ValidationRule__mdt key fields:
  RuleFormula__c     = newRecord.Type = 'Enterprise' && ISBLANK(newRecord.Industry)
  ErrorMessage__c    = Industry is required for Enterprise accounts
  ErrorDisplayField__c = Industry
  Severity__c        = Error
  Order__c           = 10

For deeper coverage, continue reading the sections below.


Architecture

Architecture Diagram

text
+-------------------------------------------------------------------------+
|                    VALIDATION FRAMEWORK ARCHITECTURE                      |
+-------------------------------------------------------------------------+
|                                                                           |
|  CONFIGURATION LAYER (Custom Metadata)                                    |
|  =====================================                                    |
|                                                                           |
|  +---------------------------+     +---------------------------+          |
|  |  ValidationRuleGroup__mdt |     |    ValidationRule__mdt    |          |
|  +---------------------------+     +---------------------------+          |
|  | - TriggerSetting__c       |<----| - ValidationRuleGroup__c  |          |
|  | - TriggerTiming__c        |     | - RuleFormula__c          |          |
|  | - TriggerOperations__c    |     | - ErrorMessage__c         |          |
|  | - ExecutionStrategy__c    |     | - Severity__c             |          |
|  | - ContextClassName__c     |     | - ShadowMode__c           |          |
|  | - BypassFeatureFlag__c    |     | - Order__c                |          |
|  +---------------------------+     +---------------------------+          |
|                                                                           |
+-------------------------------------------------------------------------+
|                                                                           |
|  FRAMEWORK LAYER (Apex)                                                   |
|  ======================                                                   |
|                                                                           |
|  +---------------------------+     +---------------------------+          |
|  |    UTIL_ValidationRule    |<----|    SEL_ValidationRules    |          |
|  +---------------------------+     +---------------------------+          |
|  | - validate()              |     | - Caching layer           |          |
|  | - applyErrors()           |     | - Metadata queries        |          |
|  | - bypassObject/Group/Rule |     +---------------------------+          |
|  | - clearAllBypasses()      |                                            |
|  +---------------------------+                                            |
|              |                                                            |
|              v                                                            |
|  +---------------------------+     +---------------------------+          |
|  |    UTIL_FormulaFilter     |     |    UTIL_FormulaContext    |          |
|  +---------------------------+     +---------------------------+          |
|  | - FormulaEval engine      |     | - Built-in contexts       |          |
|  | - evaluate()              |     | - Standard object support |          |
|  +---------------------------+     +---------------------------+          |
|                                                                           |
+-------------------------------------------------------------------------+
|                                                                           |
|  INTEGRATION LAYER                                                        |
|  =================                                                        |
|                                                                           |
|  +------------------+  +----------------------+  +--------------------+   |
|  | TRG_Execute      |  | FLOW_Execute         |  | validationErrors   |   |
|  | ValidationRules  |  | ValidationRules      |  | (LWC)              |   |
|  +------------------+  +----------------------+  +--------------------+   |
|  | Trigger Action   |  | Flow Invocable       |  | Screen Flow LWC    |   |
|  +------------------+  +----------------------+  +--------------------+   |
|                                                                           |
|  +------------------+  +----------------------+                           |
|  | FLOW_Bypass      |  | FLOW_ClearValidation |                           |
|  | Validation       |  | Bypass               |                           |
|  +------------------+  +----------------------+                           |
|  | Flow Bypass      |  | Flow Clear Bypass    |                           |
|  +------------------+  +----------------------+                           |
|                                                                           |
+-------------------------------------------------------------------------+
|                                                                           |
|  BYPASS HIERARCHY                                                         |
|  ================                                                         |
|                                                                           |
|     Level 1: Object     -->  Bypasses ALL rules for an object            |
|         |                                                                 |
|         v                                                                 |
|     Level 2: Group      -->  Bypasses all rules in a specific group      |
|         |                                                                 |
|         v                                                                 |
|     Level 3: Rule       -->  Bypasses a single rule                      |
|                                                                           |
+-------------------------------------------------------------------------+

Component Overview

ComponentTypePurpose
ValidationRuleGroup__mdtCustom MetadataGroups rules by object and trigger context
ValidationRule__mdtCustom MetadataIndividual validation rule configuration
UTIL_ValidationRuleApex ClassCore validation engine with bypass methods
UTIL_ValidationRule.ValidationResultInner ClassResult object containing errors and validity status
UTIL_ValidationRule.ValidationErrorInner ClassError details with @AuraEnabled for LWC
UTIL_ValidationRule.INT_BulkValidationContextInterfaceInterface for bulk query optimization
UTIL_ValidationTestHelperApex ClassGlobal test utility for subscriber orgs
SEL_ValidationRulesApex ClassSelector with caching (package-internal, public not global)
TRG_ExecuteValidationRulesApex ClassPre-built trigger action
FLOW_ExecuteValidationRulesApex ClassFlow invocable action
FLOW_BypassValidationApex ClassFlow bypass action
FLOW_ClearValidationBypassApex ClassFlow clear bypass action
DTO_FlowValidationErrorApex ClassValidation error DTO for Flow integration
validationErrorsLWCError display component for Screen Flows

Bypass Hierarchy

The framework supports three levels of bypass, evaluated in order:

text
1. Object Level    -> Bypasses ALL rules for an object
2. Group Level     -> Bypasses all rules in a specific group
3. Rule Level      -> Bypasses a single rule

Each level can be bypassed via:

  • Programmatic bypass - UTIL_ValidationRule.bypassObject/Group/Rule()
  • Feature Flag bypass - BypassFeatureFlag__c / RequiredFeatureFlag__c on group or rule metadata
  • Metadata bypass - BypassExecution__c checkbox

Execution Strategies

StrategyBehaviorUse Case
Accumulate (default)Collects all validation errors before returningUser-friendly - shows all issues at once
Fail FastStops after the first error per recordPerformance - quick rejection of invalid data

Configure via ExecutionStrategy__c picklist on ValidationRuleGroup__mdt.

Rule Ordering Across Groups

Important: Groups do not have an Order__c field. Instead, rule-level Order__c provides global ordering across all groups for a given object and trigger context.

This design enables architects to interleave lightweight and expensive validations regardless of group membership:

text
+-------------------------------------------------------------+
|  Execution Order (all rules sorted by Order__c globally)     |
+-------------------------------------------------------------+
|  Order 10:  Account_Require_Name      (Group: Field_Checks)  |
|  Order 20:  Account_Require_Industry  (Group: Field_Checks)  |
|  Order 100: Account_Has_Contacts      (Group: Cross_Object)  |
|  Order 110: Account_Revenue_Threshold (Group: Cross_Object)  |
+-------------------------------------------------------------+

Best Practice for Fail Fast:

  • Assign low Order values (10-50) to lightweight field checks
  • Assign high Order values (100+) to expensive cross-object queries
  • With Fail Fast, if a lightweight rule fails first, expensive queries are skipped

Example Configuration:

RuleGroupOrderType
Account_Require_NameField_Validations10Lightweight
Account_Require_IndustryField_Validations20Lightweight
Account_Has_Primary_ContactCross_Object_Checks100Expensive
Account_Credit_Limit_CheckCross_Object_Checks110Expensive

Custom Metadata Configuration

ValidationRuleGroup__mdt

Groups validation rules for a specific object and trigger context.

FieldTypeRequiredDescription
TriggerSetting__cMetadataRelationshipYesLinks to the object's TriggerSetting
TriggerTiming__cTextYesSemicolon-separated: Before, After
TriggerOperations__cTextYesSemicolon-separated: Insert, Update, Delete, Undelete
Description__cLong TextYesBusiness purpose of this group
ContextClassName__cTextNoDefault context class for rules in this group
ExecutionStrategy__cPicklistNoAccumulate (default) or Fail Fast
BypassExecution__cCheckboxNoBypass all rules in this group
BypassFeatureFlag__cMetadataRelationship(FeatureFlag__mdt)NoFeature Flag that bypasses all rules in this group when enabled
RequiredFeatureFlag__cMetadataRelationship(FeatureFlag__mdt)NoFeature Flag required for rules to execute

Timing + Operations Examples

TimingOperationsResult
BeforeInsert;UpdateBEFORE_INSERT, BEFORE_UPDATE
AfterInsert;Update;DeleteAFTER_INSERT, AFTER_UPDATE, AFTER_DELETE
Before;AfterInsertBEFORE_INSERT, AFTER_INSERT

ValidationRule__mdt

Individual validation rule configuration.

FieldTypeRequiredDescription
ValidationRuleGroup__cMetadataRelationshipYesParent group
RuleFormula__cLong TextYesFormula that returns true when validation fails
ErrorMessage__cLong TextYesMessage shown when validation fails
ErrorDisplayField__cTextNoAPI name of field to attach error to
Severity__cPicklistYesError (blocks save) or Warning (logs only)
Order__cNumberNoGlobal execution order across all groups (lower = first)
Description__cLong TextYesBusiness purpose of this rule
ContextClassName__cTextNoOverride context class for this rule
ShadowMode__cCheckboxNoLog but don't block (testing mode)
BypassExecution__cCheckboxNoBypass this rule
BypassFeatureFlag__cMetadataRelationship(FeatureFlag__mdt)NoFeature Flag that bypasses this rule when enabled
RequiredFeatureFlag__cMetadataRelationship(FeatureFlag__mdt)NoFeature Flag required for this rule to execute

Context Classes

Context classes provide data to validation formulas. They expose properties that formulas can reference.

Built-in Context Classes

The framework auto-detects context classes for standard objects via UTIL_FormulaContext:

ObjectContext ClassProperties
AccountUTIL_FormulaContext.AccountContextoldRecord, newRecord
ContactUTIL_FormulaContext.ContactContextoldRecord, newRecord
LeadUTIL_FormulaContext.LeadContextoldRecord, newRecord
OpportunityUTIL_FormulaContext.OpportunityContextoldRecord, newRecord
CaseUTIL_FormulaContext.CaseContextoldRecord, newRecord
CampaignUTIL_FormulaContext.CampaignContextoldRecord, newRecord
TaskUTIL_FormulaContext.TaskContextoldRecord, newRecord
EventUTIL_FormulaContext.EventContextoldRecord, newRecord
UserUTIL_FormulaContext.UserContextoldRecord, newRecord

Important: Do NOT extend the built-in UTIL_FormulaContext classes. If you need additional properties (e.g., for cross-object queries), create a fresh context class that implements the interface directly. Extending framework classes creates fragile dependencies on internal code that may change between versions.

Creating Custom Context Classes

For custom objects or when you need additional context data, create a custom context class:

apex
/**
 * @description Context class for CustomObject__c validation rules.
 */
global inherited sharing class VAL_CustomObjectContext
	implements UTIL_FormulaFilter.INT_SObjectFormulaEvaluationContext
{
	/**
	 * @description The record state BEFORE the DML operation.
	 */
	global CustomObject__c oldRecord;

	/**
	 * @description The record state AFTER the DML operation.
	 */
	global CustomObject__c newRecord;

	/**
	 * @description Sets the context for formula evaluation.
	 *
	 * @param oldSObject The record before DML
	 * @param newSObject The record after DML
	 */
	public void setContext(SObject oldSObject, SObject newSObject)
	{
		this.oldRecord = (CustomObject__c)oldSObject;
		this.newRecord = (CustomObject__c)newSObject;
	}
}

Register the context class in ContextClassName__c on the ValidationRuleGroup or ValidationRule.

Bulk Context Pattern

For validation rules that need to query related data (cross-object validation), implement the INT_BulkValidationContext bulk context pattern to avoid SOQL in loops:

apex
/**
 * @description Bulk context for Account validation with Contact data.
 * Demonstrates efficient cross-object validation.
 */
global inherited sharing class VAL_AccountWithContactsContext
	implements UTIL_FormulaFilter.INT_SObjectFormulaEvaluationContext,
	UTIL_ValidationRule.INT_BulkValidationContext
{
	global Account oldRecord;
	global Account newRecord;

	/**
	 * @description Number of contacts for this account.
	 * Populated by preLoad(), used in formulas.
	 */
	global Integer ContactCount;

	// Cache populated by preLoad()
	private Map<Id, Integer> contactCountByAccountId = new Map<Id, Integer>();

	/**
	 * @description Called ONCE before processing all records.
	 * Query all related data here to avoid SOQL in loops.
	 */
	global void preLoad(List<SObject> newRecords, List<SObject> oldRecords)
	{
		Set<Id> accountIds = new Set<Id>();
		for(SObject record : newRecords)
		{
			if(record.Id != null)
			{
				accountIds.add(record.Id);
			}
		}

		if(accountIds.isEmpty())
		{
			return;
		}

		// Single bulk query for all records using QRY_Builder
		List<QRY_Builder.AggregateRow> results = QRY_Builder.selectFrom(Contact.SObjectType)
			.count('Id')
			.groupBy(Contact.AccountId)
			.condition(Contact.AccountId).isIn(new List<Id>(accountIds))
			.toAggregateList();

		for(QRY_Builder.AggregateRow result : results)
		{
			contactCountByAccountId.put(
				(Id)result.get('AccountId'),
				(Integer)result.get('count_Id')
			);
		}
	}

	/**
	 * @description Called for EACH record to set current context.
	 */
	public void setContext(SObject oldSObject, SObject newSObject)
	{
		this.oldRecord = (Account)oldSObject;
		this.newRecord = (Account)newSObject;

		// Retrieve pre-loaded data for this record
		Id accountId = this.newRecord?.Id ?? this.oldRecord?.Id;
		this.ContactCount = contactCountByAccountId.get(accountId) ?? 0;
	}
}

Usage in Formula

text
newRecord.Type = 'Customer' && ContactCount = 0

Formula Syntax

Supported Functions

The framework uses Salesforce's FormulaEval namespace for dynamic formula evaluation. See Formula Evaluation in Apex for details on the underlying engine.

Common supported functions include:

CategoryFunctions
LogicalAND(), OR(), NOT(), IF(), CASE(), ISBLANK(), ISNULL()
TextTEXT(), LEFT(), RIGHT(), MID(), LEN(), CONTAINS(), BEGINS()
MathABS(), CEILING(), FLOOR(), ROUND(), MAX(), MIN()
DateTODAY(), NOW(), DATE(), YEAR(), MONTH(), DAY()
Comparison=, <>, <, >, <=, >=

For the complete list of supported functions, see Formula Operators and Functions in Salesforce Help.

Global Variables:

Global variables available in FormulaEval:

VariableDescriptionExample
$UserCurrent user fields$User.Id, $User.ProfileId
$ProfileCurrent user's profile$Profile.Name
$PermissionCustom permissions$Permission.Bypass_Validation
$OrganizationOrg-level info$Organization.Id
$LabelCustom labels$Label.MyCustomLabel

Accessing Context Properties

Formulas access context class properties directly:

text
// Access new record fields
newRecord.Name
newRecord.Industry
newRecord.AnnualRevenue

// Access old record fields (for update context)
oldRecord.Status
oldRecord.Amount

// Access custom context properties
ContactCount
HasActiveContract
TotalLineItemAmount

Change Detection

ISCHANGED alternative patterns:

text
// Field changed from any value to specific value
newRecord.Status__c = 'Closed' && oldRecord.Status__c <> 'Closed'

// Any change to field
newRecord.OwnerId <> oldRecord.OwnerId

// Insert detection (ISNEW equivalent)
ISBLANK(oldRecord)

Error Message Merge Fields

Error messages support merge fields using {!PropertyName} syntax:

text
Account {!newRecord.Name} requires at least one contact before converting to Customer status.

Supported merge patterns:

  • {!newRecord.FieldName} - New record field value
  • {!oldRecord.FieldName} - Old record field value
  • {!CustomProperty} - Custom context property value

Flow Integration

Validating Records in Flow

Use the Execute Validation Rules invocable action (FLOW_ExecuteValidationRules):

Input Variables:

VariableTypeRequiredDescription
recordsSObject CollectionYesRecords to validate
oldRecordsSObject CollectionNoOld versions (for update context)
triggerContextTextNoOverride context: BEFORE_INSERT, BEFORE_UPDATE, etc.

Output Variables:

VariableTypeDescription
hasErrorsBooleanTrue if blocking errors exist
hasWarningsBooleanTrue if warnings exist
errorMessageTextCombined error message
errorsDTO_FlowValidationError[]List of error details
warningsDTO_FlowValidationError[]List of warning details

Bypassing Validation in Flow

Use the Bypass Validation invocable action (FLOW_BypassValidation):

Input Variables:

VariableTypeRequiredDescription
bypassTypeTextNoOBJECT_NAME (default), GROUP_NAME, or RULE_NAME
nameTextYesAPI name to bypass

Use the Clear Validation Bypass invocable action (FLOW_ClearValidationBypass) to clear bypasses:

Input Variables:

VariableTypeRequiredDescription
clearAllBooleanNoWhen true, clears all active bypasses (name is ignored)
nameTextConditionalAPI name to clear (required when clearAll is false or omitted)

Example Flow:

  1. Bypass Validation (bypassType: OBJECT_NAME, name: Account)
  2. Update Records
  3. Clear Validation Bypass (clearAll: true)

Displaying Errors in Screen Flows

Add the validationErrors component to a Screen element:

Component Properties:

PropertyTypeDescription
errorsObject[]Errors from validation action
warningsObject[]Warnings from validation action
errorTitleStringCustom title for errors section
warningTitleStringCustom title for warnings section
showFieldNamesBooleanShow field names with messages
showRuleNamesBooleanShow rule names (debugging)

Programmatic Usage

Bypass Methods

apex
// Bypass all rules for an object
UTIL_ValidationRule.bypassObject('Account');

// Bypass a specific group
UTIL_ValidationRule.bypassGroup('Account_BeforeInsert');

// Bypass a specific rule
UTIL_ValidationRule.bypassRule('Account_Require_Industry_Enterprise');

// Clear a specific bypass
UTIL_ValidationRule.clearBypass('Account');

// Clear all bypasses
UTIL_ValidationRule.clearAllBypasses();

// Check if bypassed
Boolean isBypassed = UTIL_ValidationRule.isBypassed('Account');

Bypass Audit Trail

Every bypassObject / bypassGroup / bypassRule / clearBypass / clearAllBypasses call emits a LogEntryEvent__e with category BypassEvent via UTIL_BypassAudit.emit. Audit entries record surface = 'validation', the action (BYPASS / CLEAR / CLEAR_ALL), the target with a scope prefix (object: / group: / rule:), and the optional reason latched via UTIL_BypassAudit.setBypassReason(String). The same audit channel covers trigger / query / DML bypasses so subscribers get a single forensic-query shape across the framework.

apex
List<LogEntry__c> validationBypasses = QRY_Builder.selectFrom(LogEntry__c.SObjectType)
	.condition(LogEntry__c.ContextData__c).contains('"category":"BypassEvent"')
	.andCondition(LogEntry__c.ContextData__c).contains('"surface":"validation"')
	.orderBy(LogEntry__c.CreatedDate).descending()
	.toList();

The BypassAudit_Enabled FeatureFlag__mdt is a master kill-switch (default-on). Subscribers disable runtime emission via a FeatureFlagStrategy__mdt override when audit volume is too high for an environment.

Direct Validation

apex
// Validate records
List<Account> accounts = new List<Account>{account1, account2};
List<UTIL_ValidationRule.ValidationResult> results =
	UTIL_ValidationRule.validate(accounts, null, TriggerOperation.BEFORE_INSERT);

for(UTIL_ValidationRule.ValidationResult result : results)
{
	if(!result.isValid)
	{
		// Handle errors
		for(UTIL_ValidationRule.ValidationError error : result.errors)
		{
			LOG_Builder.build().error(error.message).emitAt('MyClass.myMethod');
		}
	}
}

// Apply errors to records (adds to SObject.addError())
UTIL_ValidationRule.applyErrors(accounts, results);

Testing

Using UTIL_ValidationTestHelper

The UTIL_ValidationTestHelper class provides assertion methods for testing validation rules without boilerplate setup:

> Cross-namespace surface. UTIL_ValidationTestHelper is the only validation testing surface callable from > subscriber tests — kern-internal @TestVisible private factories are not visible across namespaces. Drive > every subscriber-side validation test through UTIL_ValidationTestHelper.assertRuleFails / assertRulePasses, > or fall back to full DML inside Test.startTest() / Test.stopTest().

apex
@IsTest
private class AccountValidation_TEST
{
	/**
	 * @description Tests that Enterprise accounts require Industry.
	 */
	@IsTest
	private static void shouldRequireIndustryForEnterpriseAccounts()
	{
		Account account = (Account)TST_Builder.of(Account.SObjectType)
			.withOverrides(new Map<SObjectField, Object>{ Account.Name => 'Test', Account.Type => 'Enterprise' })
			.withoutInsertion()
			.build();

		// Assert the rule fails
		UTIL_ValidationTestHelper.assertRuleFails(account, 'Account_Require_Industry_Enterprise');
	}

	/**
	 * @description Tests that rule passes when Industry is provided.
	 */
	@IsTest
	private static void shouldPassWhenIndustryProvided()
	{
		Account account = (Account)TST_Builder.of(Account.SObjectType)
			.withOverrides(new Map<SObjectField, Object>{ Account.Name => 'Test', Account.Type => 'Enterprise', Account.Industry => 'Technology' })
			.withoutInsertion()
			.build();

		// Assert the rule passes
		UTIL_ValidationTestHelper.assertRulePasses(account, 'Account_Require_Industry_Enterprise');
	}

	/**
	 * @description Tests update context with old/new record comparison.
	 *
	 * Assumes you have a rule named `Account_Cannot_Change_Type_Without_Approval` that
	 * rejects `ISCHANGED(oldRecord, newRecord, Type)` unless an approval flag is set.
	 * Replace `Account.Type` with any standard or custom field your rule inspects.
	 */
	@IsTest
	private static void shouldValidateTypeChange()
	{
		Account oldAccount = (Account)TST_Builder.of(Account.SObjectType)
			.withOverrides(new Map<SObjectField, Object>{ Account.Name => 'Test', Account.Type => 'Prospect' })
			.withoutInsertion()
			.build();
		Account newAccount = oldAccount.clone();
		newAccount.Type = 'Customer - Direct';

		// Assert rule fails on type change
		UTIL_ValidationTestHelper.assertRuleFails(
			newAccount,
			oldAccount,
			'Account_Cannot_Change_Type_Without_Approval',
			TriggerOperation.BEFORE_UPDATE
		);
	}

	/**
	 * @description Tests advanced assertions using ValidationResult.
	 */
	@IsTest
	private static void shouldReturnMultipleErrors()
	{
		Account account = (Account)TST_Builder.of(Account.SObjectType)
			.withOverride(Account.Name, 'Test')
			.withoutInsertion()
			.build();

		UTIL_ValidationRule.ValidationResult result =
			UTIL_ValidationTestHelper.validate(account);

		Assert.areEqual(2, result.errors.size(), 'Expected 2 validation errors');
		Assert.isFalse(result.isValid, 'Record should be invalid');
	}
}

Available Methods:

MethodDescription
assertRuleFails(record, ruleName)Assert a specific rule fails (insert context)
assertRuleFails(record, oldRecord, ruleName, operation)Assert a specific rule fails (any context)
assertRulePasses(record, ruleName)Assert a specific rule passes (insert context)
assertRulePasses(record, oldRecord, ruleName, operation)Assert a specific rule passes (any context)
validate(record)Get full ValidationResult for custom assertions
validate(record, oldRecord, operation)Get full ValidationResult with context

Testing Best Practices

  1. Test each rule individually - Use assertRuleFails and assertRulePasses for focused tests
  2. Test both positive and negative cases - Ensure rules pass when conditions are met
  3. Test update context - Use old/new record pairs for update validations
  4. Test bypass behavior - Verify rules are skipped when bypassed
  5. Use ValidationResult for complex assertions - Access error details, counts, field names

Advanced Features

Shadow Mode

Shadow mode allows testing validation rules in production without blocking saves:

  1. Set ShadowMode__c = true on the validation rule
  2. When the rule fails:
    • Error is logged to LogEntry__c with [SHADOW] prefix
    • Save is NOT blocked
    • Violation captured for monitoring

Use cases:

  • Testing new rules before enforcement
  • Monitoring data quality without disruption
  • Gradual rollout of validation rules

Querying Shadow Violations

apex
List<LogEntry__c> shadowViolations = QRY_Builder.selectFrom(LogEntry__c.SObjectType)
	.fields(new List<SObjectField>{LogEntry__c.Message__c, LogEntry__c.CreatedDate})
	.condition(LogEntry__c.LogLevel__c).equals('WARN')
	.andCondition(LogEntry__c.Message__c).contains('[SHADOW]')
	.orderBy(LogEntry__c.CreatedDate).descending()
	.withLimit(100)
	.toList();

Severity Levels

SeverityBehavior
ErrorBlocks save, adds error to record via record.addError()
WarningLogs to LogEntry__c via LOG_Builder.build().warn().emitAt(), does NOT block save

Warning use cases:

  • Data quality monitoring
  • Soft deprecation notices
  • Informational alerts

Multi-Language Support

For orgs requiring translated error messages, use Custom Labels with the {$Label.LabelName} syntax:

text
ErrorMessage__c: {$Label.VAL_Account_Email_Required}

Why Custom Labels?

  • Reusable across multiple validation rules
  • Proper translation workflow via Setup --> Translation Workbench
  • Can be packaged and versioned with your application
  • Standard Salesforce internationalization pattern

Important: The framework does not use toLabel() in metadata queries. Direct translations on the ErrorMessage__c field via Translation Workbench will not be applied at runtime. Always use Custom Labels for multi-language orgs.

Example Setup

  1. Create a Custom Label:

    • Name: VAL_Account_Email_Required
    • Value: Email address is required for all accounts.
  2. Add translations via Setup --> Translation Workbench

  3. Reference in your validation rule:

    ErrorMessage__c: {$Label.VAL_Account_Email_Required}

The framework will resolve the Custom Label at runtime using the user's language preference.


Anti-Patterns

Anti-PatternWhy It's WrongInstead
Using the framework for simple required-field checksAdds unnecessary overhead for validations that standard required fields or validation rules handle nativelyUse standard validation rules for simple ISBLANK() checks; reserve the framework for cross-object or complex logic
Embedding complex formula logic in RuleFormula__c fieldsLong formulas are hard to debug, maintain, and testUse custom context classes to pre-compute values, then reference them with simple formulas
Ignoring bulk context in custom context classesQueries inside preLoad() run per-record, causing SOQL limit exceptions in bulk operationsUse the BulkContext pattern to query once for all records, then cache results
Not providing bypass mechanisms for data loadsBulk data loads fail or run slowly due to unnecessary validationUse FLOW_BypassValidation, BypassExecution__c, or one of the UTIL_ValidationRule.bypassObject() / .bypassGroup() / .bypassRule() runtime APIs for migrations
Skipping shadow mode testing for new rulesRules go live with unexpected failures or false positivesEnable ShadowMode__c on new rules first to log violations without blocking records

Best Practices

> Performance Warning: While this framework is bulkified, Apex-based formula evaluation consumes more CPU time than native validation rules. For massive data loads (e.g., > 10,000+ records via Data Loader), bypass the framework using BypassExecution__c or a Feature Flag to ensure stability. > See Bulk Data Load Considerations below.

  1. Use standard validation rules first - Only use this framework for scenarios standard rules cannot handle

  2. Implement bulk context - Always use INT_BulkValidationContext for cross-object queries

  3. Keep formulas simple - Complex logic should be computed in context class properties

  4. Use meaningful names - DeveloperName should clearly indicate purpose

  5. Document rules - Always fill Description__c field

  6. Test with shadow mode - Enable shadow mode before enforcing new rules

  7. Order rules using Order__c - Assign low values (10-50) to lightweight checks, high values (100+) to expensive cross-object queries. With Fail Fast strategy, this ensures quick rejection before expensive operations.

  8. Use appropriate severity - Reserve "Error" for blocking issues

  9. Leverage bypass hierarchy - Use object/group bypass for bulk operations

  10. Monitor performance - Check LogEntry__c for slow validation rules

  11. Use Custom Labels for multi-language - For translated error messages, use {$Label.LabelName} syntax

  12. Use UTIL_ValidationTestHelper - Leverage the test helper for clean, focused validation tests

Subscriber-shipped demo rules

When you ship a demo or sample validation rule in your own subscriber package, set BypassExecution__c = true on the ValidationRule__mdt record. Subscribers then activate the rule by flipping the flag from their managed-package configuration UI — this prevents the rule from contaminating every record insert across unrelated tests in their org. The framework's own sample rule follows this convention: it ships with BypassExecution__c = true and is activated only when a subscriber explicitly opts in.

Bulk Data Load Considerations

When performing bulk data operations (Data Loader, Bulk API, or batch Apex processing large volumes):

VolumeRecommendation
< 1,000 recordsFramework operates normally
1,000 - 10,000 recordsMonitor CPU time; consider Fail Fast strategy
> 10,000 recordsBypass framework via BypassExecution__c or Feature Flag

Bypass Options for Bulk Loads

  1. Metadata Bypass: Set BypassExecution__c = true on the ValidationRuleGroup before the load, then uncheck after
  2. Feature Flag Bypass: Configure BypassFeatureFlag__c with a Feature Flag targeting integration users
  3. Programmatic Bypass: Call UTIL_ValidationRule.bypassObject('Account') in batch Apex before DML

Why bypass? The framework uses Apex-based FormulaEval which has higher CPU overhead than native validation rules compiled into the database engine. For massive loads, native validation rules or post-load data quality reports are more appropriate.


Troubleshooting

Common Issues

"No validation rules executed"

  • Verify TriggerAction__mdt is registered for the correct timing
  • Check that ValidationRuleGroup timing/operations match the trigger context
  • Ensure BypassExecution__c is not checked

"Formula evaluation failed"

  • Check formula syntax using Formula Builder
  • Verify context class exposes required properties as global
  • Ensure property names match formula references exactly

"Context class not found"

"SOQL limit exceeded"

  • Implement INT_BulkValidationContext for cross-object queries
  • Move queries to preLoad() method
  • Use aggregate queries where possible

Enabling Performance Monitoring

The framework includes built-in performance timing for context class processing, enabled by default via the EnableValidationPerformanceLogging__c hierarchy setting. Each context class's total processing time is measured, including:

  • preLoad() execution (bulk queries, loop processing, map building)
  • All formula evaluations for that context

Configuration via LogSetting__c:

FieldDefaultDescription
EnableValidationPerformanceLogging__ctrueEnable/disable validation context timing
ValidationPerformanceThresholdMs__c100Log context processing exceeding this threshold (ms)

To tune or disable:

  1. Create or edit a LogSetting__c record (hierarchy custom setting — org-default, profile, or user level)
  2. Set EnableValidationPerformanceLogging__c = false to turn monitoring off, or leave the default true to keep it on
  3. Set ValidationPerformanceThresholdMs__c to your threshold (e.g., 100 = log context processing >100ms)

Once enabled, each context class logs:

  • Total elapsed time (preLoad + formula evaluations)
  • Rule count and record count
  • Nested within trigger action context for tracing

Why context-level timing? Individual formula evaluations are sub-millisecond. The expensive operations happen in preLoad() (bulk queries, complex loops). Timing the entire context class processing gives you actionable data - if VAL_AccountWithContactsContext is slow, you know exactly which context class needs optimization.

Nested Tracing: Validation performance logs appear nested within trigger action logs, providing drill-down visibility:

text
TRG_ExecuteValidationRules (BEFORE_INSERT) completed in 450ms
  +-- VAL_AccountWithContactsContext validation completed in 380ms (Rules: 5, Records: 200)

Query validation performance logs:

apex
List<LogEntry__c> validationLogs = QRY_Builder.selectFrom(LogEntry__c.SObjectType)
	.fields(new List<SObjectField>{LogEntry__c.Message__c, LogEntry__c.DurationMs__c, LogEntry__c.ContextData__c})
	.condition(LogEntry__c.LogLevel__c).equals('PERFORMANCE')
	.andCondition(LogEntry__c.ClassMethod__c).contains('UTIL_ValidationRule/processRulesForContext')
	.orderBy(LogEntry__c.DurationMs__c).descending()
	.withLimit(50)
	.toList();

Debugging Tips

  1. Enable debug logging:

    apex
    LOG_Builder.ignoreTestMode = true;
  2. Check cached rules (development org only):

    apex
    // Note: SEL_ValidationRules is package-internal (public, not global)
    // This code only works in the development org, not in subscriber orgs
    List<SEL_ValidationRules.ValidationRuleWithGroup> rules =
    	SEL_ValidationRules.findByObjectAndOperation('Account', TriggerOperation.BEFORE_INSERT);
    LOG_Builder.build().info('Found ' + rules.size() + ' rules').emitAt('SEL_ValidationRules');
  3. Test formula in isolation:

    apex
    UTIL_FormulaFilter filter = new UTIL_FormulaFilter(
    	'ValidationRule:Test',
    	'UTIL_FormulaContext.AccountContext',
    	'newRecord.Type = \'Enterprise\' && ISBLANK(newRecord.Industry)'
    );
    UTIL_FormulaFilter.DTO_FilterResults results = filter.filter(
    	null, new List<Account>{ account }
    );
    Boolean formulaMatched = !results.newRecords.isEmpty();