// article
Every Business Central environment that runs a custom AL extension eventually faces the same question: how do you ship a new version of that extension without losing the data the previous version wrote, breaking dependent extensions, or stalling a tenant during a Microsoft platform update? Microsoft pushes platform updates on a fixed cadence, and customers expect their customizations to survive each one. The mechanism Microsoft provides for this is the upgrade codeunit, plus a small set of conventions around versioning and data preservation.
This article walks through the upgrade strategy for production AL extensions: when to write upgrade code, how the upgrade triggers fire, how to plan version numbers, and how to keep multi-extension dependencies consistent across a tenant.
Per Microsoft Learn, an upgrade is defined as enabling an extension whose version number in app.json is greater than the version currently installed. The platform compares the four-part version string in the new .app file against what is recorded for the published version on the tenant. If the new number is higher, the publish-and-install flow runs as an upgrade and any upgrade codeunits in the extension fire. If the number is equal or lower, the flow is rejected.
That makes the version field in app.json a load-bearing piece of metadata. Treat it as part of the source of truth, not as a build artifact. Bump it deliberately every time the extension changes any persisted schema or any data that downstream code reads.
Upgrade logic lives in a codeunit whose Subtype property is set to Upgrade. That codeunit is published and synchronized along with the rest of the extension, and its triggers run when an administrator (or the platform, during a managed update) executes the data upgrade step on the new version.
Microsoft documents that data not modified by upgrade code is preserved automatically. The upgrade codeunit is only required when the new version needs to do something to existing data that the platform cannot infer on its own — typically backfilling new fields, splitting or renaming fields, populating new tables from existing records, or removing data that is no longer represented in the schema.
An example shape:
// AL
codeunit 50110 RewardUpgrade
{
Subtype = Upgrade;
trigger OnUpgradePerCompany()
var
Reward: Record Reward;
begin
if Reward.Get('BRONZE') then begin
Reward.Rename('ALUMINUM');
Reward.Description := 'Aluminum reward level';
Reward.Modify();
end;
end;
}This pattern follows the walkthrough Microsoft publishes for upgrading the sample Rewards extension, which renames an existing record and amends descriptive fields when version 2 of the extension is installed.
An upgrade codeunit supports several system triggers, and Microsoft documents a fixed sequence in which they are invoked. The two most commonly used are OnUpgradePerDatabase, which runs once for the tenant database, and OnUpgradePerCompany, which runs once per company in the tenant. Database-level changes — for example, populating a record that all companies share — belong in the per-database trigger. Company-scoped data changes belong in the per-company trigger.
An extension may contain more than one upgrade codeunit. The order in which the triggers fire across codeunits is fixed (per-database before per-company), but the order between different codeunits at the same trigger level is not guaranteed. If your upgrade splits across multiple codeunits, write each one so that it does not depend on another codeunit having already run.
The four-part version field gives you room to encode meaning. A pragmatic convention for AL extensions on Business Central:
Major — incompatible changes (a removed object, a renamed table, a breaking API contract change).
Minor — additive schema or behavior change that requires upgrade code (new fields, populated reference tables, data migrations).
Build — additive change with no data migration (new pages, new procedures, refactors that do not touch persisted data).
Revision — hotfixes, build-pipeline metadata, CI-driven increments.
Bump at least the build segment on every release that goes to a customer tenant. The platform will refuse a republish at the same version, so a deliberate bump prevents accidental shadowing and gives the admin center a clear audit trail.
Upgrade codeunits run inside the data upgrade process, which is a single atomic step from the administrator's point of view but can cover thousands of records per company. Three rules keep migrations safe:
Make the migration idempotent. Guard every write with a check so that re-running the upgrade against an already-migrated dataset produces the same end state. The example above guards on Reward.Get('BRONZE') rather than blindly renaming.
Do not read fields that the new schema removed. Upgrade codeunits run against the new object definitions. If the upgrade removes or renames a column, source the data from the obsoleted definition (using an ObsoleteState field that is still readable in the upgrade window) before the next major version drops the field entirely.
Keep heavy migrations out of business hours. Microsoft explicitly recommends scheduling extensions that require extensive upgrade logic outside business hours, because the upgrade process can affect users working against the same business data.
Most production tenants run more than one custom extension. When two extensions depend on each other — typically a customer-specific extension that consumes objects from a shared base extension — the dependent extension must declare the dependency in its app.json with a minimum version. Upgrading then becomes a coordinated operation: the base extension upgrades first, the dependent extension upgrades against the new base.
Three practical guardrails:
Pin the dependency to a minimum version, not an exact version, so that platform-driven updates of the base extension do not block the dependent extension from installing.
When you change a public object in the base extension (a table, an interface, an event publisher), bump the major or minor version of the base and update every consumer's app.json at the same time.
Test the upgrade order in a sandbox tenant before scheduling production. Publish the new base, run its upgrade, then publish and upgrade the dependent extension, and confirm that data written by version 1 of the dependent extension is still readable.
For customer engagements, Bitta Apps standardizes on a four-step release flow: build the new extension version against the matching Business Central platform version in a sandbox, run the upgrade end-to-end against a production-shaped data copy, capture the upgrade duration and any per-company log output, then schedule the production publish during a maintenance window with the customer's admin on standby. The data upgrade step is the one that touches customer data; everything else is reversible. Treating the upgrade codeunit as production code — reviewed, tested, and idempotent — is what keeps a customization from becoming a liability the next time Microsoft ships a wave update.
If your team is preparing an AL extension for an upcoming Business Central update and wants a second set of eyes on the upgrade codeunit, dependency graph, or release plan, reach out to Bitta Apps for an upgrade review.