Business Central Install Codeunit Generator
Generates production-ready install codeunits for Business Central extensions. Handles first install detection, data initialization, reinstall scenarios, and proper separation of per-company vs per-database operations.
Overview
Install codeunits (Subtype = Install) run automatically when an extension is installed or reinstalled. They do not run during upgrades — use upgrade codeunits for that. Install code runs when:
- An extension is installed for the first time
- An uninstalled extension version is reinstalled
This skill generates:
- Install codeunit with proper triggers
- Fresh install vs reinstall detection
- Data seeding procedures (setup records, defaults)
- Per-company and per-database operation separation
- Optional telemetry logging for install events
Complete examples and patterns: references/install-examples.md
Prerequisites
- AL workspace with established object ID range
- Prefix established (e.g., BCS, CUSTOM, etc.)
- Knowledge of what data/setup needs initialization
- Setup table(s) identified (if seeding defaults)
Quick Start
codeunit [ID] "[Prefix] Install"
{
Subtype = Install;
trigger OnInstallAppPerDatabase()
var
AppInfo: ModuleInfo;
begin
NavApp.GetCurrentModuleInfo(AppInfo);
if AppInfo.DataVersion = Version.Create(0, 0, 0, 0) then
HandleFreshInstallDatabase()
else
HandleReinstallDatabase();
end;
trigger OnInstallAppPerCompany()
var
AppInfo: ModuleInfo;
begin
NavApp.GetCurrentModuleInfo(AppInfo);
if AppInfo.DataVersion = Version.Create(0, 0, 0, 0) then
HandleFreshInstallCompany()
else
HandleReinstallCompany();
end;
local procedure HandleFreshInstallDatabase()
begin
// Database-wide operations for first install
end;
local procedure HandleReinstallDatabase()
begin
// Database-wide operations for reinstall
end;
local procedure HandleFreshInstallCompany()
begin
// Per-company operations for first install
end;
local procedure HandleReinstallCompany()
begin
// Per-company operations for reinstall
end;
}
Install Triggers
| Trigger | Scope | Runs | Use For |
|---|---|---|---|
OnInstallAppPerDatabase() | Once per install | One time regardless of company count | Database-level setup: permission assignments, app-wide configuration, telemetry registration |
OnInstallAppPerCompany() | Per company | Once for each company in the database | Company-specific data: setup records, default values, number series, demo data |
Key rule: Per-database triggers run first, per-company triggers run once for each company.
Fresh Install vs Reinstall Detection
Use ModuleInfo.DataVersion to distinguish between first install and reinstall:
var
AppInfo: ModuleInfo;
begin
NavApp.GetCurrentModuleInfo(AppInfo);
if AppInfo.DataVersion = Version.Create(0, 0, 0, 0) then
// First-time install — DataVersion is 0.0.0.0
else
// Reinstall — same version being installed again
end;
ModuleInfo Properties
| Property | Type | Description |
|---|---|---|
DataVersion | Version | Data version of the extension. 0.0.0.0 on first install. |
AppVersion | Version | App version from app.json |
Id | Guid | Extension App ID |
Name | Text | Extension name |
Publisher | Text | Extension publisher |
Dependencies | List of ModuleDependencyInfo | Extension dependencies |
Common Install Patterns
Pattern 1: Setup Record Initialization
Create the singleton setup record if it doesn't exist:
local procedure InitializeSetupRecord()
var
MySetup: Record "[Prefix] [Module] Setup";
begin
if not MySetup.Get() then begin
MySetup.Init();
MySetup.Insert(true);
end;
end;
Pattern 2: Default Configuration Values
Seed default values into the setup record:
local procedure SetDefaultConfiguration()
var
MySetup: Record "[Prefix] [Module] Setup";
begin
if not MySetup.Get() then begin
MySetup.Init();
MySetup."Enable Feature" := true;
MySetup."Default Language" := 'ENU';
MySetup."Max Retry Count" := 3;
MySetup.Insert(true);
end;
end;
Pattern 3: Seed Reference Data
Populate reference/lookup tables with initial data:
local procedure SeedReferenceData()
var
Category: Record "[Prefix] Category";
begin
InsertCategoryIfNotExists(Category, 'GENERAL', 'General Category');
InsertCategoryIfNotExists(Category, 'SPECIAL', 'Special Category');
end;
local procedure InsertCategoryIfNotExists(var Category: Record "[Prefix] Category"; Code: Code[20]; Description: Text[100])
begin
if not Category.Get(Code) then begin
Category.Init();
Category.Code := Code;
Category.Description := Description;
Category.Insert(true);
end;
end;
Pattern 4: Reinstall Data Patch
Check and repair data consistency on reinstall:
local procedure HandleReinstallCompany()
begin
PatchMissingDefaults();
ValidateExistingData();
end;
local procedure PatchMissingDefaults()
var
MySetup: Record "[Prefix] [Module] Setup";
begin
if MySetup.Get() then begin
if MySetup."Max Retry Count" = 0 then
MySetup."Max Retry Count" := 3;
MySetup.Modify(true);
end else
SetDefaultConfiguration();
end;
Pattern 5: Telemetry on Install
Log install events for monitoring:
local procedure LogInstallTelemetry(FreshInstall: Boolean)
var
AppInfo: ModuleInfo;
Dimensions: Dictionary of [Text, Text];
begin
NavApp.GetCurrentModuleInfo(AppInfo);
Dimensions.Add('ExtensionName', AppInfo.Name);
Dimensions.Add('ExtensionVersion', Format(AppInfo.AppVersion));
Dimensions.Add('FreshInstall', Format(FreshInstall));
Session.LogMessage(
'BCS-0001',
'Extension installed',
Verbosity::Normal,
DataClassification::SystemMetadata,
TelemetryScope::ExtensionPublisher,
Dimensions
);
end;
Design Guidelines
- Idempotent: Install code may run multiple times (reinstall). Always check before inserting records.
- No user interaction: Install code runs without UI — no
Confirm,Message, or page calls. - Error handling: Wrap risky operations in error handling. A failing install codeunit blocks the extension install.
- Multiple codeunits: You can have more than one install codeunit per extension, but execution order is not guaranteed. Each must be independent.
- Not for upgrades: Install code does NOT run when upgrading to a new version. Use upgrade codeunits (
Subtype = Upgrade) for that. - Performance: Keep install code fast. Avoid long-running operations that could time out.
Install Codeunit Design Workflow
- Identify initialization needs — What data/config must exist after install?
- Separate concerns — Database-wide vs per-company operations
- Plan fresh vs reinstall — Different logic for each scenario
- Implement idempotently — All inserts check existence first
- Add telemetry — Log install for monitoring (optional)
- Test both paths — Verify fresh install and reinstall scenarios
File Naming Convention
Follow the pattern: [Prefix]Install.Codeunit.al
Examples:
BCSInstall.Codeunit.alBCSStatAccInstall.Codeunit.al
Place in: src/Codeunit/ or feature folder src/[Feature]/Codeunit/.
Checklist
Before completing install codeunit generation:
-
Subtype = Installset on codeunit - Fresh install vs reinstall detection using
DataVersion -
OnInstallAppPerCompanyfor company-specific data -
OnInstallAppPerDatabasefor database-wide operations (if needed) - All inserts are idempotent (check before insert)
- No UI interaction (no Message, Confirm, Page.Run)
- Setup records initialized with sensible defaults
- Reference data seeded if applicable
- Telemetry logging added (optional)
- File follows naming convention:
[Prefix]Install.Codeunit.al
References
For complete examples:
- references/install-examples.md — Full working examples (basic install, data seeding, telemetry, reinstall handling, multi-table initialization)
External Resources
- Writing Extension Install Code — Microsoft Docs
- NavApp Data Type — ModuleInfo methods
- Build Your First Extension — Extension with install and upgrade code
- Upgrading Extensions — Upgrade codeunits (related)