In this video, I find up to 14 different ways of validating a single field. I show the different OnValidate* triggers are related. did I forget some?

In this video, Erik explores all the different ways you can validate a field in AL for Business Central. Starting from the most fundamental approach — the table-level OnValidate trigger — he progressively layers on page triggers, event subscribers, table extensions, and page extensions across multiple apps to demonstrate the full range of validation options and their execution order. By the end, he reaches 18 validation triggers firing on a single field change.
The Golden Rule: Validate on the Table
If you want the short version of this topic — under two minutes — here it is: always do validation on the table. End of discussion. If you’re not validating from the table, you’re probably doing something wrong. Every time you decide not to validate on the table, it should be a deliberate exception that you can justify.
The reason is simple: the table-level OnValidate trigger fires every time anything decides to validate that field — whether it’s a page, a codeunit, an API, or any other entry point. It’s the universal catch-all.
Setting Up the Base Example
Erik starts with a simple extension containing a table with a couple of fields and a page to display them.
The Table: OnValidate Trigger
table 56100 "Validate Table"
{
Caption = 'Validate Table';
DataClassification = ToBeClassified;
fields
{
field(1; PK; Code[10])
{
Caption = 'PK';
DataClassification = ToBeClassified;
}
field(2; "Validate This"; Text[100])
{
Caption = 'Validate This';
DataClassification = ToBeClassified;
trigger OnValidate()
begin
Message('OnValidate Table Trigger');
Rec."Validate This" := 'OnValidate Table Trigger';
end;
}
}
keys
{
key(PK; PK)
{
Clustered = true;
}
}
}
This trigger fires every time anything calls Rec.Validate("Validate This", ...) — whether from the UI, from code, or from an API. This is the foundation of all validation logic.
The Page: OnValidate Trigger
page 56100 "Validate Test"
{
Caption = 'Validate Test';
PageType = Card;
SourceTable = "Validate Table";
layout
{
area(content)
{
group(General)
{
field(PK; Rec.PK)
{
ToolTip = 'Specifies the value of the PK field.';
ApplicationArea = All;
}
field("Validate This"; Rec."Validate This")
{
ToolTip = 'Specifies the value of the Validate This field.';
ApplicationArea = All;
trigger OnValidate()
begin
Message('OnValidate Page Trigger');
Rec."Validate This" := 'OnValidate Page Trigger';
end;
}
}
}
}
}
When should you validate on the page? The easy answer is: when your field is not part of Rec. If you have a global variable or a flow field that doesn’t map to a table field, then the page OnValidate is the place to handle it because there is no table behind it. You can also use it for UI-specific logic that’s particular to this page.
With both triggers in place, typing a value into the field produces two messages: first the table trigger fires, then the page trigger fires.
Event Subscribers on the Table
Beyond the built-in triggers, you can subscribe to validation events. Business Central exposes OnBeforeValidateEvent and OnAfterValidateEvent for both tables and pages.
codeunit 56100 "Validate Events"
{
[EventSubscriber(ObjectType::Table, Database::"Validate Table",
'OnBeforeValidateEvent', 'Validate This', true, true)]
local procedure MyProcedure(var Rec: Record "Validate Table";
var xRec: Record "Validate Table"; CurrFieldNo: Integer)
begin
Message('OnBeforeValidateEvent Table Trigger');
Rec."Validate This" := 'OnBeforeValidateEvent Table Trigger';
end;
[EventSubscriber(ObjectType::Table, Database::"Validate Table",
'OnAfterValidateEvent', 'Validate This', true, true)]
local procedure MyProcedure2(var Rec: Record "Validate Table";
var xRec: Record "Validate Table"; CurrFieldNo: Integer)
begin
Message('OnAfterValidateEvent Table Trigger');
Rec."Validate This" := 'OnAfterValidateEvent Table Trigger';
end;
[EventSubscriber(ObjectType::Page, Page::"Validate Test",
'OnBeforeValidateEvent', 'Validate This', true, true)]
local procedure MyProcedure3(var Rec: Record "Validate Table";
var xRec: Record "Validate Table")
begin
Message('OnBeforeValidateEvent Page Trigger');
Rec."Validate This" := 'OnBeforeValidateEvent Page Trigger';
end;
[EventSubscriber(ObjectType::Page, Page::"Validate Test",
'OnAfterValidateEvent', 'Validate This', true, true)]
local procedure MyProcedure4(var Rec: Record "Validate Table";
var xRec: Record "Validate Table")
begin
Message('OnAfterValidateEvent Page Trigger');
Rec."Validate This" := 'OnAfterValidateEvent Page Trigger';
end;
}
Note a subtle difference in the event subscriber signatures: the table-level events include a CurrFieldNo parameter, while the page-level events do not. The CurrFieldNo parameter is useful for detecting cascading validations — when validating one field triggers validation on another, you can check the original field number to avoid circular validation loops.
Execution Order with Events
With four event subscribers plus two triggers, the execution order when changing the field value becomes:
- OnBeforeValidateEvent (Table)
- OnValidate Table Trigger
- OnAfterValidateEvent (Table)
- OnBeforeValidateEvent (Page)
- OnValidate Page Trigger
- OnAfterValidateEvent (Page)
The pattern is clear: events encapsulate the triggers. The “before” event fires first, then the actual trigger, then the “after” event. And the table-level chain completes entirely before the page-level chain begins.
Adding a Second App with Event Subscribers
Erik then creates a second app (Validate2) that depends on the first app and subscribes to the exact same events. This simulates a common real-world scenario where multiple extensions react to the same field validation.
With two apps subscribing to the same events, the order becomes:
- OnBeforeValidateEvent Table — App 1
- OnBeforeValidateEvent Table — App 2
- OnValidate Table Trigger
- OnAfterValidateEvent Table — App 1
- OnAfterValidateEvent Table — App 2
- OnBeforeValidateEvent Page — App 1
- OnBeforeValidateEvent Page — App 2
- OnValidate Page Trigger
- OnAfterValidateEvent Page — App 1
- OnAfterValidateEvent Page — App 2
Why does App 1’s subscriber always fire before App 2’s? Microsoft officially states that the order of event subscriber execution is undefined. However, in practice, there’s clearly some deterministic behavior — likely related to the app IDs or some internal sorting mechanism. Erik notes that while it’s “undefined,” undefined doesn’t mean random; somewhere there are entries going into a table that force some sort of ordering.
Table Extensions and Page Extensions
The second app also adds validation through a table extension, which provides its own OnBeforeValidate and OnAfterValidate triggers on the modified field:
tableextension 56150 "Validate Table Ext" extends "Validate Table"
{
fields
{
modify("Validate This")
{
trigger OnBeforeValidate()
begin
Message('OnBeforeValidate Table Extension Trigger');
Rec."Validate This" := 'OnBeforeValidate Table Extension Trigger';
end;
trigger OnAfterValidate()
begin
Message('OnAfterValidate Table Extension Trigger');
Rec."Validate This" := 'OnAfterValidate Table Extension Trigger';
end;
}
}
}
Similarly, a page extension can add OnBeforeValidate and OnAfterValidate triggers:
pageextension 56150 "Validate Test Ext" extends "Validate Test"
{
layout
{
modify("Validate This")
{
trigger OnBeforeValidate()
begin
Message('OnBeforeValidate Page Extension Trigger');
Rec."Validate This" := 'OnBeforeValidate Page Extension Trigger';
end;
trigger OnAfterValidate()
begin
Message('OnAfterValidate Page Extension Trigger');
Rec."Validate This" := 'OnAfterValidate Page Extension Trigger';
end;
}
}
}
An important observation: extension object triggers are “tighter” to the core trigger than events are. They sit closer to the original OnValidate, whereas events wrap around the outside.
The Full Execution Order (14 Triggers from Two Apps)
With all mechanisms in place from two apps, the complete execution order for a single field validation is:
- OnBeforeValidateEvent Table — App 1 (event)
- OnBeforeValidateEvent Table — App 2 (event)
- OnBeforeValidate Table Extension Trigger (table extension)
- OnValidate Table Trigger (original table)
- OnAfterValidate Table Extension Trigger (table extension)
- OnAfterValidateEvent Table — App 1 (event)
- OnAfterValidateEvent Table — App 2 (event)
- OnBeforeValidateEvent Page — App 1 (event)
- OnBeforeValidateEvent Page — App 2 (event)
- OnBeforeValidate Page Extension Trigger (page extension)
- OnValidate Page Trigger (original page)
- OnAfterValidate Page Extension Trigger (page extension)
- OnAfterValidateEvent Page — App 1 (event)
- OnAfterValidateEvent Page — App 2 (event)
Adding a Third App: 18 Triggers
For bonus content, Erik creates a third app with its own table extension and page extension triggers. This pushes the total to 18 validation triggers firing on a single field change. The third app’s extension triggers slot in alongside the second app’s extension triggers, with the ordering again appearing to follow some internal sorting — possibly by app ID or name.
Which Approach Should You Use?
With so many options, here are the practical guidelines:
- Table OnValidate trigger — This is your default. All core business logic validation belongs here. It fires regardless of the entry point (page, API, code).
- Table extension (modify field) — Use this when you’re extending someone else’s table from your app and need to add validation logic to their field.
- Page OnValidate trigger — Use this for UI-specific logic or when the field isn’t backed by a table field (e.g., global variables, computed values).
- Page extension (modify field) — Use this when extending someone else’s page and need to add UI-specific validation.
- Event subscribers (OnBefore/OnAfterValidateEvent) — Use these when you’re coming from outside the object and need to hook into validation without modifying the original object. These wrap around the triggers and extensions.
Conclusion
Business Central provides a rich set of validation mechanisms — table triggers, page triggers, table extensions, page extensions, and event subscribers — giving you up to 18 (or more) validation points on a single field across multiple apps. The key takeaway remains simple: validate on the table first. Everything else is an extension of that principle for specific scenarios. While Microsoft states that the execution order of event subscribers across apps is “undefined,” in practice there is a deterministic order — likely influenced by app IDs or internal sorting. Understanding the full validation chain helps you place your logic in the right spot and avoid surprises when multiple apps interact with the same fields.