Skip to main content

Advanced Setup Table Patterns

This file contains advanced patterns and variations for setup tables not included in the main SKILL.md to keep it concise.

Multi-Company Setup Patterns

Pattern 1: Shared Setup Across Companies

For settings that should be shared across all companies (tenant-wide):

table [ID] "[Prefix] Tenant Setup"
{
Caption = '[Module] Tenant Setup';
DataPerCompany = false; // ← Key difference
DataClassification = CustomerContent;

fields
{
field(1; "Primary Key"; Code[10])
{
Caption = 'Primary Key';
DataClassification = SystemMetadata;
}

field(10; "Global Setting"; Text[100])
{
Caption = 'Global Setting';
DataClassification = CustomerContent;
}
}

keys
{
key(PK; "Primary Key")
{
Clustered = true;
}
}
}

Use Case: License keys, global integration endpoints, shared credentials.

Caution: DataPerCompany = false means changes affect ALL companies. Use sparingly.

Pattern 2: Per-User Setup

For user-specific preferences:

table [ID] "[Prefix] User Setup"
{
Caption = '[Module] User Setup';
DataClassification = EndUserIdentifiableInformation;

fields
{
field(1; "User ID"; Code[50])
{
Caption = 'User ID';
DataClassification = EndUserIdentifiableInformation;
TableRelation = User."User Name";
NotBlank = true;
}

field(10; "Default View"; Option)
{
Caption = 'Default View';
OptionMembers = List,Card,Timeline;
OptionCaption = 'List,Card,Timeline';
DataClassification = EndUserIdentifiableInformation;
}
}

keys
{
key(PK; "User ID")
{
Clustered = true;
}
}

procedure GetOrCreate()
begin
if not Get(UserId) then begin
Init();
"User ID" := CopyStr(UserId, 1, MaxStrLen("User ID"));
Insert();
end;
end;
}

Key Difference: Primary key is User ID instead of fixed "Primary Key" field. NOT a singleton table.

Advanced Field Patterns

Option Fields with Custom Values

field(20; "Processing Mode"; Option)
{
Caption = 'Processing Mode';
OptionMembers = Synchronous,Asynchronous,Batch;
OptionCaption = 'Synchronous,Asynchronous,Batch';
DataClassification = CustomerContent;

trigger OnValidate()
begin
case "Processing Mode" of
"Processing Mode"::Asynchronous:
TestField("Background Job Queue Category");
"Processing Mode"::Batch:
begin
TestField("Batch Size");
TestField("Background Job Queue Category");
end;
end;
end;
}

Pattern: Validate dependent fields when option changes.

Enum Fields (Modern BC)

// Define enum
enum [ID] "[Prefix] Integration Mode"
{
Extensible = true;

value(0; Disabled) { Caption = 'Disabled'; }
value(1; ReadOnly) { Caption = 'Read Only'; }
value(2; ReadWrite) { Caption = 'Read/Write'; }
}

// Use in setup table
field(30; "Integration Mode"; Enum "[Prefix] Integration Mode")
{
Caption = 'Integration Mode';
DataClassification = CustomerContent;
}

Advantage: Extensible enums allow other extensions to add values.

Date Formula Fields

field(40; "Retention Period"; DateFormula)
{
Caption = 'Retention Period';
DataClassification = CustomerContent;
ToolTip = 'Specifies how long records are kept. Example: 1Y for one year.';

trigger OnValidate()
var
TestDate: Date;
begin
if Format("Retention Period") = '' then
exit;

TestDate := CalcDate("Retention Period", Today);
if TestDate < Today then
Error('Retention period must be positive.');
end;
}

Decimal Fields with Rounding

field(50; "Default Discount %"; Decimal)
{
Caption = 'Default Discount %';
DataClassification = CustomerContent;
DecimalPlaces = 2 : 5;
MinValue = 0;
MaxValue = 100;

trigger OnValidate()
begin
if "Default Discount %" < 0 then
Error('Discount cannot be negative.');
if "Default Discount %" > 100 then
Error('Discount cannot exceed 100%%.');
end;
}

Setup Page Advanced Patterns

Multi-Tab Setup with FastTabs

page [ID] "[Prefix] Setup"
{
PageType = Card;
SourceTable = "[Prefix] Setup";

layout
{
area(Content)
{
group(GeneralFastTab)
{
Caption = 'General';
ShowCaption = true;

field("Enable Feature"; Rec."Enable Feature")
{
ApplicationArea = All;
}
}

group(NumberSeriesFastTab)
{
Caption = 'Number Series';
ShowCaption = true;

field("Document Nos."; Rec."Document Nos.")
{
ApplicationArea = All;
}
}

group(IntegrationFastTab)
{
Caption = 'Integration';
ShowCaption = true;

field("API Base URL"; Rec."API Base URL")
{
ApplicationArea = All;
}
}
}
}
}

Conditional Field Visibility

layout
{
area(Content)
{
group(General)
{
field("Enable Integration"; Rec."Enable Integration")
{
ApplicationArea = All;
}
}

group(IntegrationSettings)
{
Caption = 'Integration Settings';
Visible = Rec."Enable Integration"; // ← Conditional visibility

field("API Base URL"; Rec."API Base URL")
{
ApplicationArea = All;
}

field("API Key"; Rec."API Key")
{
ApplicationArea = All;
}
}
}
}

Editable Field Based on Permission

field("Critical Setting"; Rec."Critical Setting")
{
ApplicationArea = All;
Editable = IsAdmin;
}

var
IsAdmin: Boolean;

trigger OnOpenPage()
var
UserPermissions: Codeunit "User Permissions";
begin
Rec.Reset();
if not Rec.Get() then begin
Rec.Init();
Rec.Insert();
end;

IsAdmin := UserPermissions.IsSuper(UserSecurityId());
end;

Setup Table Extension Patterns

Extending Base Setup (General Ledger Setup)

tableextension [ID] "[Prefix] G/L Setup" extends "General Ledger Setup"
{
fields
{
field([ID]; "[Prefix] Enable Feature"; Boolean)
{
Caption = 'Enable [Feature Name]';
DataClassification = CustomerContent;
}

field([ID+1]; "[Prefix] Default Account"; Code[20])
{
Caption = 'Default [Feature] Account';
DataClassification = CustomerContent;
TableRelation = "G/L Account";
}
}
}

pageextension [ID] "[Prefix] G/L Setup" extends "General Ledger Setup"
{
layout
{
addlast(Content)
{
group("[Prefix] CustomGroup")
{
Caption = '[Module Name]';

field("[Prefix] Enable Feature"; Rec."[Prefix] Enable Feature")
{
ApplicationArea = All;
ToolTip = 'Specifies whether [feature] is enabled.';
}

field("[Prefix] Default Account"; Rec."[Prefix] Default Account")
{
ApplicationArea = All;
ToolTip = 'Specifies the default G/L account for [feature].';
Enabled = Rec."[Prefix] Enable Feature";
}
}
}
}
}

Use Case: Add module-specific settings to existing BC setup pages.

Extending Sales & Receivables Setup

tableextension [ID] "[Prefix] Sales Setup" extends "Sales & Receivables Setup"
{
fields
{
field([ID]; "[Prefix] Auto Assign Salesperson"; Boolean)
{
Caption = 'Auto Assign Salesperson';
DataClassification = CustomerContent;
}

field([ID+1]; "[Prefix] Default Salesperson"; Code[20])
{
Caption = 'Default Salesperson';
DataClassification = CustomerContent;
TableRelation = "Salesperson/Purchaser";
}
}
}

pageextension [ID] "[Prefix] Sales Setup" extends "Sales & Receivables Setup"
{
layout
{
addafter("Number Series")
{
group("[Prefix] SalespersonGroup")
{
Caption = 'Salesperson Assignment';

field("[Prefix] Auto Assign"; Rec."[Prefix] Auto Assign Salesperson")
{
ApplicationArea = All;
}

field("[Prefix] Default Salesperson"; Rec."[Prefix] Default Salesperson")
{
ApplicationArea = All;
Enabled = Rec."[Prefix] Auto Assign Salesperson";
}
}
}
}
}

Caching Patterns

Advanced GetRecordOnce with Refresh

table [ID] "[Prefix] Setup"
{
// ... fields ...

procedure GetRecordOnce()
begin
if IsInitialized then
exit;

Get();
IsInitialized := true;
end;

procedure GetRecordOnce(ForceRefresh: Boolean)
begin
if ForceRefresh or not IsInitialized then begin
Get();
IsInitialized := true;
end;
end;

procedure ClearCache()
begin
Clear(Rec);
IsInitialized := false;
end;

var
IsInitialized: Boolean;
}

Usage:

// First call
Setup.GetRecordOnce(); // Loads from DB

// Subsequent calls
Setup.GetRecordOnce(); // Uses cache

// Force refresh after modify
Setup.ClearCache();
Setup.GetRecordOnce(); // Reloads from DB

Global Setup Instance Pattern

For extremely high-performance scenarios:

codeunit [ID] "[Prefix] Setup Management"
{
var
[Prefix]Setup: Record "[Prefix] Setup";
IsInitialized: Boolean;

procedure GetSetup(var SetupRec: Record "[Prefix] Setup")
begin
if not IsInitialized then begin
if not [Prefix]Setup.Get() then begin
[Prefix]Setup.Init();
[Prefix]Setup.Insert();
end;
IsInitialized := true;
end;

SetupRec := [Prefix]Setup;
end;

procedure ClearCache()
begin
Clear([Prefix]Setup);
IsInitialized := false;
end;

[EventSubscriber(ObjectType::Table, Database::"[Prefix] Setup", 'OnAfterModifyEvent', '', false, false)]
local procedure OnAfterModifySetup(var Rec: Record "[Prefix] Setup")
begin
ClearCache();
end;
}

Advantage: Single global instance cached at codeunit level, cleared automatically on modify.

Validation Patterns

Cross-Field Validation

trigger OnValidate()
begin
if "Enable Integration" then begin
TestField("API Base URL");
TestField("API Key");
end;
end;

Range Validation with User Confirmation

field(60; "Batch Size"; Integer)
{
Caption = 'Batch Size';
DataClassification = CustomerContent;
MinValue = 1;
MaxValue = 10000;

trigger OnValidate()
begin
if "Batch Size" > 1000 then
if not Confirm('Batch sizes above 1000 may cause performance issues. Continue?') then
Error('');
end;
}

Email Validation

field(70; "Notification Email"; Text[250])
{
Caption = 'Notification Email';
DataClassification = EndUserIdentifiableInformation;
ExtendedDatatype = EMail;

trigger OnValidate()
var
MailManagement: Codeunit "Mail Management";
begin
if "Notification Email" <> '' then
MailManagement.CheckValidEmailAddress("Notification Email");
end;
}

Testing Patterns

Test Setup Access

codeunit [ID] "[Prefix] Setup Test"
{
Subtype = Test;

[Test]
procedure TestSetupAutoInitialization()
var
[Prefix]Setup: Record "[Prefix] Setup";
begin
// [GIVEN] Empty setup table
[Prefix]Setup.DeleteAll();

// [WHEN] GetRecordOnce is called
[Prefix]Setup.GetRecordOnce();

// [THEN] Record is created automatically
Assert.RecordCount([Prefix]Setup, 1);
end;

[Test]
procedure TestSetupSingleton()
var
[Prefix]Setup: Record "[Prefix] Setup";
begin
// [GIVEN] Existing setup record
[Prefix]Setup.DeleteAll();
[Prefix]Setup.Init();
[Prefix]Setup.Insert();

// [WHEN] Attempting to insert another record
asserterror begin
[Prefix]Setup.Init();
[Prefix]Setup.Insert();
end;

// [THEN] Error occurs (only one record allowed)
Assert.ExpectedError('');
end;
}

Cloud-Ready Patterns

Avoiding OnInsert Defaults (Cloud Compatibility)

Problem: OnInsert triggers don't fire in cloud-based upgrade scenarios.

Solution: Use GetRecordOnce with explicit defaults.

procedure GetRecordOnce()
begin
if IsInitialized then
exit;

if not Get() then begin
Init();
// Set defaults explicitly here instead of OnInsert
"Enable Feature" := true;
"Max Retry Count" := 3;
"Batch Size" := 100;
Insert();
end;

IsInitialized := true;
end;

API Integration with Secret Storage

For SaaS environments, use Isolated Storage for secrets:

procedure SetAPIKey(APIKey: Text)
begin
if EncryptionEnabled() then
IsolatedStorage.SetEncrypted('[Prefix]APIKey', APIKey, DataScope::Company)
else
IsolatedStorage.Set('[Prefix]APIKey', APIKey, DataScope::Company);
end;

procedure GetAPIKey() APIKey: Text
begin
if not IsolatedStorage.Get('[Prefix]APIKey', DataScope::Company, APIKey) then
APIKey := '';
end;

procedure HasAPIKey(): Boolean
begin
exit(IsolatedStorage.Contains('[Prefix]APIKey', DataScope::Company));
end;

Benefit: API keys stored securely, not in plain text field.

Summary

These advanced patterns cover:

  • Multi-company and per-user setup variations
  • Advanced field types (Enums, DateFormulas, Options with validation)
  • Multi-tab and conditional visibility UI patterns
  • Setup table extensions for base BC tables
  • Advanced caching and performance patterns
  • Cross-field validation and email validation
  • Test patterns for setup tables
  • Cloud-ready patterns with Isolated Storage

Use these patterns when basic setup table doesn't meet requirements.