8.4 Installation & Upgrade Codeunits, Data Transfer & Archive

Key Takeaways

  • Installation codeunits (Subtype = Install) run during extension installation or synchronization, exposing OnInstallAppPerCompany and OnInstallAppPerDatabase triggers for initial configuration.
  • Upgrade codeunits (Subtype = Upgrade) manage data transformations across versions via a strict, sequential six-stage trigger execution pipeline.
  • The ModuleInfo record retrieved via NavApp.GetCurrentModuleInfo provides DataVersion and AppVersion properties to conditionally gate upgrade procedures based on previous database state.
  • The DataTransfer object executes set-based, bulk SQL operations (CopyFields, CopyRows) directly on the database engine, bypassing slow row-by-row AL loops and preventing SaaS timeouts.
  • Archive management methods (NavApp.RestoreArchiveData and NavApp.DeleteArchiveData) govern data recovery and cleanup when refactoring or deprecating obsolete tables.
Last updated: August 2026

8.4 Installation & Upgrade Codeunits, Data Transfer & Archive

When deploying, reinstalling, or updating extensions in Business Central environments, developers must ensure seamless database initialization and schema migration. AL provides specialized codeunit subtypes to handle these lifecycles: Install Codeunits (Subtype = Install) for first-time tenant setup, and Upgrade Codeunits (Subtype = Upgrade) for managing multi-version data transformations. Understanding the exact execution order, version evaluation APIs, and the modern DataTransfer set-based migration API is heavily tested on the MB-820 exam.


1. Installation Codeunits (Subtype = Install)

An Install codeunit executes automatically whenever an extension is installed or synchronized in a tenant. It contains system triggers that initialize configuration tables, generate number series, and register assisted setup wizards.

codeunit 50140 "App Installation Handler"
{
    Subtype = Install;

    trigger OnInstallAppPerCompany()
    var
        AppSetup: Record "Custom App Setup";
    begin
        // Executed for every individual company in the tenant
        if not AppSetup.Get() then begin
            AppSetup.Init();
            AppSetup."Default Batch Name" := 'GENERAL';
            AppSetup."Enable Telemetry" := true;
            AppSetup.Insert(true);
        end;
        
        InitializeDefaultNumberSeries();
    end;

    trigger OnInstallAppPerDatabase()
    begin
        // Executed once across the entire tenant database
        // Used for cross-company tables (DataPerCompany = false) or global database setup
    end;

    local procedure InitializeDefaultNumberSeries()
    var
        NoSeries: Record "No. Series";
        NoSeriesLine: Record "No. Series Line";
    begin
        if not NoSeries.Get('CUST-PROMO') then begin
            NoSeries.Init();
            NoSeries.Code := 'CUST-PROMO';
            NoSeries.Description := 'Customer Promotion Series';
            NoSeries."Default Nos." := true;
            NoSeries.Insert();

            NoSeriesLine.Init();
            NoSeriesLine."Series Code" := NoSeries.Code;
            NoSeriesLine."Line No." := 10000;
            NoSeriesLine."Starting No." := 'PRM0001';
            NoSeriesLine."Ending No." := 'PRM9999';
            NoSeriesLine.Insert();
        end;
    end;
}

Triggers and Idempotency Best Practices

  • OnInstallAppPerCompany(): Runs once for each individual company in the Business Central tenant database. It should populate company-specific setup tables and master records.
  • OnInstallAppPerDatabase(): Runs once across the entire tenant database, regardless of how many companies exist. Ideal for tables configured with DataPerCompany = false.
  • Idempotency Rule: Install triggers can execute multiple times (e.g., if the extension is uninstalled and reinstalled, or when a new company is created). Code must be strictly idempotent—always verify if records exist (if not Setup.Get() then ...) before inserting to prevent primary key collisions.
Loading diagram...
Complete Upgrade Codeunit Execution Pipeline (6 Sequential Stages)

2. Upgrade Codeunits (Subtype = Upgrade)

When a newer version of an extension is deployed over an existing version, the platform invokes Upgrade codeunits. Upgrade codeunits run through a strict six-stage execution pipeline across database and company contexts.

The Six Upgrade Triggers in Execution Sequence

OrderTrigger NameContextPurpose
1OnCheckPreconditionsPerDatabase()DatabaseVerify global database prerequisites before any upgrade step begins.
2OnCheckPreconditionsPerCompany()CompanyVerify company-specific prerequisites; fail fast if required historical data is invalid.
3OnUpgradePerDatabase()DatabaseExecute data migrations on shared cross-company tables (DataPerCompany = false).
4OnUpgradePerCompany()CompanyExecute primary data migrations, table transformations, and field mappings per company.
5OnValidateUpgradePerDatabase()DatabaseValidate data integrity and record counts across the global database post-upgrade.
6OnValidateUpgradePerCompany()CompanyValidate that company-level records and migrated fields meet business rules.

Failure Semantics & Rollback

If an unhandled error occurs during any trigger in the six-step sequence:

  • The entire upgrade process terminates immediately.
  • All database modifications made across the upgrade transaction are completely rolled back.
  • The tenant remains on the existing extension version without data corruption.

Version-Checking with NavApp.GetCurrentModuleInfo

To ensure upgrade routines only run when updating from specific prior builds, developers inspect DataVersion (the version of the data currently stored in the database) against AppVersion (the version of the code package being installed).

codeunit 50145 "App Upgrade Handler"
{
    Subtype = Upgrade;

    trigger OnUpgradePerCompany()
    var
        AppInfo: ModuleInfo;
    begin
        NavApp.GetCurrentModuleInfo(AppInfo);
        
        // Gating upgrade logic: Run only if upgrading from Version 1.x to 2.x
        if AppInfo.DataVersion.Major < 2 then
            MigrateCustomerLegacyCodes();
    end;

    local procedure MigrateCustomerLegacyCodes()
    begin
        // Migration logic
    end;
}

Exam Watchout — DataVersion vs. AppVersion:

  • AppInfo.DataVersion: Represents the version of the data currently persisted in the tenant database. For a fresh install on a clean tenant, DataVersion = Version.Create(0, 0, 0, 0).
  • AppInfo.AppVersion: Represents the version of the incoming code extension package defined in app.json.

3. High-Performance Bulk Migration: The DataTransfer Object

Historically, migrating data between tables during an upgrade required iterating through records with FindSet() and updating them row-by-row with Modify(). In cloud tenants with millions of rows, this generated massive transaction logs and timed out. AL introduced the DataTransfer object, which compiles directly into set-based SQL statements (INSERT INTO ... SELECT and UPDATE ... FROM), executing directly in the database engine.

codeunit 50150 "Fast Data Upgrade"
{
    Subtype = Upgrade;

    trigger OnUpgradePerCompany()
    var
        AppInfo: ModuleInfo;
    begin
        NavApp.GetCurrentModuleInfo(AppInfo);
        if AppInfo.DataVersion.Major < 2 then begin
            CopyCustomerFieldsBulk();
            MigrateLegacyTableRowsBulk();
        end;
    end;

    local procedure CopyCustomerFieldsBulk()
    var
        Customer: Record Customer;
        DataXfer: DataTransfer;
    begin
        // Set-based field copy within the same table
        DataXfer.SetTables(Database::Customer, Database::Customer);
        DataXfer.AddFieldValue(Customer.FieldNo("Legacy Loyalty Code"), Customer.FieldNo("Loyalty ID"));
        DataXfer.AddSourceFilter(Customer.FieldNo("Legacy Loyalty Code"), '<>%1', '');
        DataXfer.CopyFields(); // Emits a single direct SQL UPDATE statement
    end;

    local procedure MigrateLegacyTableRowsBulk()
    var
        LegacyStaging: Record "Legacy Staging Table";
        NewArchive: Record "New Customer Archive";
        DataXfer: DataTransfer;
    begin
        // Bulk copy rows from one table into a new table
        DataXfer.SetTables(Database::"Legacy Staging Table", Database::"New Customer Archive");
        DataXfer.AddFieldValue(LegacyStaging.FieldNo("Entry No."), NewArchive.FieldNo("Entry No."));
        DataXfer.AddFieldValue(LegacyStaging.FieldNo("Customer Code"), NewArchive.FieldNo("Customer No."));
        DataXfer.AddFieldValue(LegacyStaging.FieldNo("Amount"), NewArchive.FieldNo("Total Balance"));
        DataXfer.AddConstantValue(Today(), NewArchive.FieldNo("Migrated Date"));
        DataXfer.CopyRows(); // Emits a single direct SQL INSERT INTO ... SELECT statement
    end;
}

DataTransfer Methods & Rules

  • SetTables(SourceTableId, TargetTableId): Specifies source and destination tables.
  • AddFieldValue(SourceFieldNo, TargetFieldNo): Maps source columns to target columns.
  • AddConstantValue(Value, TargetFieldNo): Populates target fields with a fixed constant value.
  • AddJoin(SourceFieldNo, TargetFieldNo): Establishes join conditions between distinct source and target tables when updating fields.
  • AddSourceFilter(SourceFieldNo, FilterString): Limits the SQL query to a filtered subset of rows.
  • CopyFields(): Executes a set-based SQL UPDATE.
  • CopyRows(): Executes a set-based SQL INSERT INTO ... SELECT.
  • Platform Restrictions: DataTransfer can only be executed within Upgrade codeunits (Subtype = Upgrade). It bypasses table triggers and validation for maximum performance and cannot target system tables.

4. Data Archive Management & Table Deprecation

When a table or table extension is marked ObsoleteState = Removed or during breaking schema changes where fields are refactored into new extensions, the platform safeguards data using table archives.

Data Archive Lifecycle APIs

  • NavApp.RestoreArchiveData(Database::TableName): Restores archived data from a previous version into the target table during an upgrade.
  • NavApp.DeleteArchiveData(Database::TableName): Permanently purges backed-up archive data once validation has succeeded, freeing database storage.

Comparison of Data Migration Approaches

Migration ApproachExecution MechanismPerformance on 1M+ RecordsTrigger ExecutionPermitted Context
DataTransfer ObjectSet-based SQL (UPDATE / INSERT SELECT)Instantaneous (seconds)No (Bypasses triggers)Subtype = Upgrade only
Record Looping (FindSet + Modify)Row-by-row AL iterationExtremely slow (minutes/hours; timeouts)Optional (Modify(true))Anywhere in AL
ModifyAll()Parameterized SQL UPDATEFastNoAnywhere in AL
XMLport Export/ImportFile-based streamingSlow (I/O intensive)OptionalNormal / Batch Jobs
Test Your Knowledge

Which trigger in an Install codeunit (Subtype = Install) executes once for each individual company in a tenant when an extension is deployed or synchronized?

A
B
C
D
Test Your Knowledge

What is the correct sequential execution order of triggers during an extension upgrade in Business Central?

A
B
C
D
Test Your Knowledge

A developer needs to migrate 5 million records from an obsolete staging table to a new table structure during an extension upgrade. To achieve optimal performance and prevent transaction timeouts in SaaS, which technology should be used?

A
B
C
D
Test Your Knowledge

When inspecting ModuleInfo via NavApp.GetCurrentModuleInfo(AppInfo) during an upgrade, what does the AppInfo.DataVersion property represent?

A
B
C
D