Who this is for: AL developers building custom tables in Business Central who want users to search by vendor name instead of memorizing vendor codes.
Most tables in Business Central use "Vendor No." as a foreign key because it is the primary key of the Vendor table — stable, unique, and fast to look up. The problem? Users rarely know vendor codes by heart. They know names.
This guide shows you how to build a name-to-number bridge: users type or pick a vendor name, and your code resolves it to "Vendor No." automatically. The pattern is borrowed directly from the base app’s Purchase Header table.
What You Will Build
A custom table where:
- Users can enter a vendor name to find a vendor.
- The table stores the vendor number as the real foreign key.
- Both fields stay in sync automatically.
- Ambiguous or missing names are handled safely.
How the Base App Solves This
Before writing any code, it helps to understand what Microsoft already built and why.
In Purchase Header, the field "Pay-to Name" lets users type a vendor name on a purchase document. Here is the relevant base app code:
field(5; "Pay-to Name"; Text[100]){ Caption = 'Pay-to Name'; TableRelation = Vendor.Name; ValidateTableRelation = false; trigger OnValidate() var Vendor: Record Vendor; begin if Rec."Pay-to Name" <> xRec."Pay-to Name" then if ShouldSearchForVendorByName("Pay-to Vendor No.") then Validate("Pay-to Vendor No.", Vendor.GetVendorNo("Pay-to Name")); end;}
Three things are happening here:
| Property / Trigger | What it does |
|---|---|
TableRelation = Vendor.Name | Gives the field a lookup into the Vendor table filtered by name |
ValidateTableRelation = false | Prevents BC from enforcing the name as a unique key (it is not) |
OnValidate + GetVendorNo | Converts whatever the user typed into a vendor number |
The helper Vendor.GetVendorNo(VendorText) does the heavy lifting. It handles partial matches, case-insensitive search, ambiguity, and opens a selection list when needed.
Key insight: The name field is only for user convenience. The vendor number is still the real stored key and the source of truth.
Step 1: Store the Vendor Number as the Real Key
Always start by adding the actual foreign key field. This is what gets stored, validated by posting routines, and used in reports.
field(10; "My Vendor No."; Code[20]){ Caption = 'Vendor No.'; TableRelation = Vendor."No.";}
This is a standard foreign key relationship. TableRelation = Vendor."No." means BC will validate that any value entered here actually exists in the Vendor table.
Why this matters: Every standard posting, reporting, and lookup flow in Business Central works with vendor numbers, not names. Skipping this field means you cannot integrate with standard functionality.
Step 2: Add a User-Facing Vendor Name Field
Now add the friendly name field that users will see and interact with.
field(20; "My Vendor Name"; Text[100]){ Caption = 'Vendor Name'; TableRelation = Vendor.Name; ValidateTableRelation = false;}
Two important properties here:
TableRelation = Vendor.Name — This gives the field a lookup dropdown that shows vendor names. Users can type and filter. Without this, the field is just a plain text box with no vendor connection.
ValidateTableRelation = false — Vendor names are not primary keys. They can be duplicated across vendors, and they may not exactly match what the user types mid-entry. Setting this to false tells BC: “do not enforce this as a strict key — I will handle validation myself in OnValidate.”
Step 3: Resolve Name to Vendor Number on Validate
This is the core step. When the user changes the name field, convert whatever they typed into a vendor number.
field(20; "My Vendor Name"; Text[100]){ Caption = 'Vendor Name'; TableRelation = Vendor.Name; ValidateTableRelation = false; trigger OnValidate() var Vendor: Record Vendor; begin if Rec."My Vendor Name" <> xRec."My Vendor Name" then Validate("My Vendor No.", Vendor.GetVendorNo("My Vendor Name")); end;}
What each line does:
if Rec."My Vendor Name" <> xRec."My Vendor Name"— only act when the value actually changed.xRecholds the previous value before editing started.Vendor.GetVendorNo("My Vendor Name")— looks up the vendor number for whatever name text was entered. Returns aCode[20]or blank if not found.Validate("My Vendor No.", ...)— sets the vendor number field and fires its ownOnValidatetrigger, so any downstream logic runs properly.
Always use
Validate(...)instead of direct assignment. Direct assignment like"My Vendor No." := ...skips the field’s trigger and any business logic attached to it.
Step 4: Keep Both Fields in Sync
There is a second scenario: the user selects or enters a vendor number directly (through lookup on the "My Vendor No." field). In that case, the name field needs to update too.
field(10; "My Vendor No."; Code[20]){ Caption = 'Vendor No.'; TableRelation = Vendor."No."; trigger OnValidate() var Vendor: Record Vendor; begin if Vendor.Get("My Vendor No.") then "My Vendor Name" := Vendor.Name else Clear("My Vendor Name"); end;}
This creates two-way synchronization:
User enters name → OnValidate resolves to vendor number → vendor number field updatedUser enters number → OnValidate reads vendor name → name field updated
Both directions end up with both fields matching the same vendor. If the vendor number does not exist (was cleared), the name is cleared too.
Step 5: Add a Guard Condition (Recommended)
The base app checks ShouldSearchForVendorByName(...) before attempting name resolution. You may not need something that complex, but adding a simple guard prevents unnecessary lookups.
A common and practical pattern: only resolve the name if the vendor number is currently blank.
trigger OnValidate()var Vendor: Record Vendor;begin if ("My Vendor Name" <> xRec."My Vendor Name") and ("My Vendor No." = '') then Validate("My Vendor No.", Vendor.GetVendorNo("My Vendor Name"));end;
This avoids overwriting an already-set vendor number just because the name field was touched. You can also add conditions based on document status or other business rules.
Full Copy-Ready Example
Here is everything together in a minimal table:
table 50100 "My Custom Header"{ DataClassification = CustomerContent; fields { field(1; "No."; Code[20]) { Caption = 'No.'; } field(10; "My Vendor No."; Code[20]) { Caption = 'Vendor No.'; TableRelation = Vendor."No."; trigger OnValidate() var Vendor: Record Vendor; begin if Vendor.Get("My Vendor No.") then "My Vendor Name" := Vendor.Name else Clear("My Vendor Name"); end; } field(20; "My Vendor Name"; Text[100]) { Caption = 'Vendor Name'; TableRelation = Vendor.Name; ValidateTableRelation = false; trigger OnValidate() var Vendor: Record Vendor; begin if Rec."My Vendor Name" <> xRec."My Vendor Name" then Validate("My Vendor No.", Vendor.GetVendorNo("My Vendor Name")); end; } }}
Copy this into your extension, change the table number and field numbers to fit your object range, and you are ready to go.
How GetVendorNo Finds the Right Vendor
Understanding the matching logic helps you predict what happens when users type partial names, codes, or ambiguous text.
GetVendorNo is defined on the Vendor table and calls the internal procedure GetVendorNoOpenCard:
procedure GetVendorNo(VendorText: Text[100]): Code[20]begin exit(GetVendorNoOpenCard(VendorText, true));end;
The matching follows a progressive strategy, trying the most specific match first and widening only if needed:
Matching Order
- Blank input — returns blank immediately. No lookup attempted.
- Exact vendor number match — if the input fits within
Code[20]and matches a vendor’s"No."exactly, returns it immediately. - Exact name match — filters unblocked vendors where
Name = VendorText. If exactly one is found, returns it. - Starts-with search (OR across No. and Name) — uses
FilterGroup := -1for an OR-style filter with patternVendorText*. If exactly one match, returns it. - Contains search (OR across six fields) — broadens to
*VendorText*across"No.",Name,City,Contact,"Phone No.", and"Post Code". If exactly one result, returns it.
What Happens Based on Result Count
| Result | Behavior |
|---|---|
| Exactly 1 match | Returns the vendor number directly |
| 0 matches | Tries a “similar name” fallback; if still none, prompts to create or errors in non-UI contexts |
| More than 1 match | Opens the vendor list for user selection; errors in non-UI (background) contexts |
The OR Filter Explained
When FilterGroup is set to -1 in AL, filters applied across different fields behave as OR conditions rather than the default AND. This is why a vendor can be found even when only one of those six fields contains the search text.
This is a built-in AL filter group feature that is rarely documented but widely used in the base app.
Common Pitfalls
Not storing the vendor number at all — some developers store only the name. This breaks every standard BC flow that expects a vendor number: posting, payment suggestions, aged payables, and more.
Using direct assignment instead of Validate(...) — "My Vendor No." := Vendor.GetVendorNo(...) skips the OnValidate trigger on the number field. The name field will not sync. Use Validate("My Vendor No.", ...) every time.
Ignoring duplicate vendor names — if two vendors share the same name, GetVendorNo will open a selection dialog. That is intentional and correct behavior, but you should be aware of it and test for it in your environment.
Only syncing in one direction — if you only handle name → number, then selecting a vendor by number lookup will leave the name field blank or stale. Implement both triggers.
Forgetting ValidateTableRelation = false — without this, BC validates the typed name as if it were a primary key. This causes errors when users type partial names or names with minor differences in spacing or casing.
Reusing This Pattern for Other Master Tables
The same design works for any master table — standard or custom. The principle is always the same:
- Keep the primary key field as the real foreign key.
- Add a display name field with
TableRelationpointing to the name field of the master table. - In the name field’s
OnValidate, resolve text to key. - In the key field’s
OnValidate, sync the display name back.
Built-in Helpers for Standard Tables
| Master table | Helper procedure |
|---|---|
| Vendor | Vendor.GetVendorNo(Text) |
| Customer | Customer.GetCustNo(Text) |
| Item | No direct equivalent — use SetFilter + FindFirst pattern |
Building Your Own Resolver for a Custom Master
If you have a custom master table, implement your own resolution procedure directly on that table:
table 50150 "My Master"{ fields { field(1; "Code"; Code[20]) { } field(2; "Name"; Text[100]) { } field(3; City; Text[50]) { } field(4; Contact; Text[100]) { } } procedure GetCodeBySearchText(SearchText: Text[100]): Code[20] var MyMaster: Record "My Master"; FilterFromStart: Text; FilterContains: Text; begin if SearchText = '' then exit(''); if StrLen(SearchText) <= MaxStrLen(MyMaster.Code) then if MyMaster.Get(SearchText) then exit(MyMaster.Code); MyMaster.SetRange(Name, SearchText); if MyMaster.FindFirst() and (MyMaster.Count() = 1) then exit(MyMaster.Code); FilterFromStart := '@' + SearchText + '*'; MyMaster.SetRange(Name); MyMaster.FilterGroup := -1; MyMaster.SetFilter(Code, FilterFromStart); MyMaster.SetFilter(Name, FilterFromStart); if MyMaster.Count() = 1 then begin MyMaster.FindFirst(); exit(MyMaster.Code); end; FilterContains := '@*' + SearchText + '*'; MyMaster.SetFilter(Code, FilterContains); MyMaster.SetFilter(Name, FilterContains); MyMaster.SetFilter(City, FilterContains); MyMaster.SetFilter(Contact, FilterContains); if MyMaster.Count() = 1 then begin MyMaster.FindFirst(); exit(MyMaster.Code); end; exit(''); end;}
Then use it in your document table the same way:
field(20; "My Master Name"; Text[100]){ Caption = 'Master Name'; TableRelation = "My Master".Name; ValidateTableRelation = false; trigger OnValidate() var MyMaster: Record "My Master"; begin if "My Master Name" <> xRec."My Master Name" then Validate("My Master Code", MyMaster.GetCodeBySearchText("My Master Name")); end;}
Practical advice: Start with exact match and starts-with only. Add the broader contains filters only when users genuinely need fuzzy search — wider filters increase the chance of ambiguous matches.
Summary
| What | How |
|---|---|
| Store the real key | TableRelation = Vendor."No." on the number field |
| Enable name lookup | TableRelation = Vendor.Name + ValidateTableRelation = false on the name field |
| Resolve name → number | Validate("Vendor No.", Vendor.GetVendorNo("Vendor Name")) in name field OnValidate |
| Sync number → name | Vendor.Get("Vendor No.") then assign Name in number field OnValidate |
| Handle edge cases | Guard conditions, handle blank, and trust GetVendorNo for UI disambiguation |
This is a clean, maintainable pattern that mirrors exactly what Microsoft does in the base app. If it ever changes in a future BC version, you will have a clear model to update against.

Leave a comment