bc-upgrade-codeunit-generator
Skill28 starsChanged 7 months agoMITView on GitHub
Generates upgrade codeunits (Subtype = Upgrade) for Business Central extensions that run data migration code when upgrading to a new version. Creates codeunits with OnUpgradePerCompany and OnUpgradePerDatabase triggers, precondition checks via OnCheckPreconditionsPerCompany and OnCheckPreconditionsPerDatabase, post-upgrade validation via OnValidateUpgradePerCompany and OnValidateUpgradePerDatabase, upgrade tag management using the System Application Upgrade Tag module (HasUpgradeTag, SetUpgradeTag, OnGetPerCompanyUpgradeTags, OnGetPerDatabaseUpgradeTags), companion upgrade tag definitions codeunit, install codeunit integration for setting tags on fresh install, version-based upgrade control using ModuleInfo.DataVersion, ExecutionContext guards for protecting sensitive code during upgrades, and NavApp.RestoreArchiveData for archived data restoration. Handles field migration, new table population, enum conversion, data patching, and broken upgrade fixes. Use when asked to create upgrade code, add data migration between versions, implement upgrade logic, create upgrade codeunit, migrate fields to new tables, handle schema changes on upgrade, add upgrade tags, or implement version upgrade handling for a Business Central extension.
In this repository38 other files it gives its agents.
- bc-agent-instructions-debuggerSkill
- bc-al-bug-fixSkill
- bc-al-code-reviewerSkill
- bc-al-project-contextSkill
- bc-api-page-generatorSkill
- bc-api-query-generatorSkill
- bc-approval-workflow-generatorSkill
- bc-attachments-generatorSkill
- skill-batch-processingSkill
- bc-business-events-generatorSkill
- bc-cds-page-generatorSkill
- bc-dataverse-entity-generatorSkill
- bc-dataverse-mapping-generatorSkill
- bc-field-propagationSkill
- bc-install-codeunit-generatorSkill
- bc-isolated-storage-generatorSkill
- bc-job-queueSkill
- bc-manifest-handoff-generatorSkill
- bc-matrix-page-generatorSkill
- bc-number-series-generatorSkill
- bc-pricing-refactorSkill
- bc-report-selection-generatorSkill
- bc-reportsSkill
- bc-setup-table-generatorSkill
- bc-setup-wizard-generatorSkill
- bc-table-dimensions-generatorSkill
- bc-telemetry-generatorSkill
- bc-test-codeunit-generatorSkill
- documentation-bc-analysis-generatorSkill
- documentation-bc-architecture-generatorSkill
- documentation-bc-ccn-generatorSkill
- documentation-bc-md-to-docx-converterSkill
- documentation-bc-phase-plan-generatorSkill
- documentation-bc-release-note-generatorSkill
- documentation-bc-technical-spec-generatorSkill
- documentation-bc-user-story-generatorSkill
- onprem-remote-deploySkill
- skill-contribution-assistantSkill
The file
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 2 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 217
When using upgrade tags, implement three companion codeunits:
- Very specific: line 416
Use
SetLoadFields: before FindSet when iterating large tables - Very specific: line 448
Place in: src/Codeunit/ or feature folder src/[Feature]/Codeunit/.
- Distinctive: line 452
Before completing upgrade codeunit generation:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Business Central Upgrade Codeunit Generator
Generates production-ready upgrade codeunits for Business Central extensions. Handles data migration between versions, precondition validation, upgrade tag management, and post-upgrade verification.
Overview
Upgrade codeunits (Subtype = Upgrade) run automatically when an extension is upgraded to a newer version (higher version number in app.json). They do not run during fresh installs or reinstalls — use install codeunits for that.
Upgrade code runs when:
- A new version of an already-installed extension is published and data-upgraded
- The new version has a higher version number than the currently installed version
This skill generates:
- Upgrade codeunit with all six triggers
- Precondition checks to block incompatible upgrades
- Data migration procedures with upgrade tag guards
- Post-upgrade validation
- Companion upgrade tag definitions codeunit
- Install codeunit integration (setting tags on fresh install)
- ExecutionContext guards for sensitive event subscribers
Complete examples and patterns: references/upgrade-examples.md
Prerequisites
- AL workspace with established object ID range
- Prefix established (e.g., BCS, ABC, etc.)
- Knowledge of what data needs migration between versions
- System Application dependency (for Upgrade Tag module)
- Existing install codeunit (to register upgrade tags on fresh install)
Quick Start — Version-Based Control
For simple extensions with few versions:
codeunit [ID] "[Prefix] Upgrade"
{
Subtype = Upgrade;
trigger OnCheckPreconditionsPerCompany()
begin
// Validate upgrade can proceed
end;
trigger OnUpgradePerCompany()
var
AppInfo: ModuleInfo;
begin
NavApp.GetCurrentModuleInfo(AppInfo);
// DataVersion = version upgrading FROM
if AppInfo.DataVersion < Version.Create(2, 0, 0, 0) then
UpgradeToV2();
end;
trigger OnValidateUpgradePerCompany()
begin
// Verify upgrade succeeded
end;
local procedure UpgradeToV2()
begin
// Data migration logic
end;
}Quick Start — Upgrade Tags (Recommended)
For larger applications or frequent version changes:
codeunit [ID] "[Prefix] Upgrade"
{
Subtype = Upgrade;
trigger OnUpgradePerCompany()
begin
MigrateFieldData();
end;
local procedure MigrateFieldData()
var
UpgradeTagDef: Codeunit "[Prefix] Upgrade Tag Def.";
UpgradeTag: Codeunit "Upgrade Tag";
begin
if UpgradeTag.HasUpgradeTag(UpgradeTagDef.GetFieldMigrationTag()) then
exit;
// Migration logic here
UpgradeTag.SetUpgradeTag(UpgradeTagDef.GetFieldMigrationTag());
end;
}
codeunit [ID+1] "[Prefix] Upgrade Tag Def."
{
Access = Internal;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Upgrade Tag", OnGetPerCompanyUpgradeTags, '', false, false)]
local procedure OnGetPerCompanyTags(var PerCompanyUpgradeTags: List of [Code[250]])
begin
PerCompanyUpgradeTags.Add(GetFieldMigrationTag());
end;
procedure GetFieldMigrationTag(): Code[250]
begin
exit('[PREFIX]-[ID]-[Description]-[YYYYMMDD]');
end;
}Upgrade Triggers
Triggers execute in the following guaranteed order:
| Order | Trigger | Scope | Purpose |
| 1 | OnCheckPreconditionsPerDatabase() | Once | Validate database-level prerequisites |
| 2 | OnCheckPreconditionsPerCompany() | Per company | Validate company-level prerequisites |
| 3 | OnUpgradePerDatabase() | Once | Execute database-wide data migration |
| 4 | OnUpgradePerCompany() | Per company | Execute company-specific data migration |
| 5 | OnValidateUpgradePerDatabase() | Once | Verify database-level upgrade success |
| 6 | OnValidateUpgradePerCompany() | Per company | Verify company-level upgrade success |
Key rules:
PerCompanytriggers run once for each company in the databasePerDatabasetriggers run once regardless of number of companies- Errors in precondition triggers abort the entire upgrade
- Errors in validation triggers mark the upgrade as failed
- Multiple upgrade codeunits may exist but execution order between them is not guaranteed
Version Data in Upgrade Context
| Property | Value During Upgrade |
AppVersion | Version upgrading to (new version) |
DataVersion | Version upgrading from (old version) |
var
AppInfo: ModuleInfo;
begin
NavApp.GetCurrentModuleInfo(AppInfo);
// AppInfo.DataVersion = old version (upgrading from)
// AppInfo.AppVersion = new version (upgrading to)
end;Controlling When Upgrade Code Runs
Method 1: Version Comparison
Best for simple extensions with manual version control:
trigger OnUpgradePerCompany()
var
AppInfo: ModuleInfo;
begin
NavApp.GetCurrentModuleInfo(AppInfo);
if AppInfo.DataVersion < Version.Create(2, 0, 0, 0) then
UpgradeToV2();
if AppInfo.DataVersion < Version.Create(2, 1, 0, 0) then
UpgradeToV2_1();
end;Method 2: Upgrade Tags (Recommended)
Best for larger applications, frequent releases, or complex upgrade chains:
local procedure MigrateData()
var
UpgradeTagDef: Codeunit "[Prefix] Upgrade Tag Def.";
UpgradeTag: Codeunit "Upgrade Tag";
begin
if UpgradeTag.HasUpgradeTag(UpgradeTagDef.GetMyMigrationTag()) then
exit;
// Perform migration
UpgradeTag.SetUpgradeTag(UpgradeTagDef.GetMyMigrationTag());
end;When to Use Which
| Criteria | Version Comparison | Upgrade Tags |
| Large application with many versions | Recommended | |
| Version changes frequently (>1x/year) | Recommended | |
| Version is set manually | Recommended | |
Checking for first-time install (0.0.0.0) | Recommended | |
| Fixing a broken upgrade | Recommended |
Upgrade Tag Convention
Format: [Prefix]-[ObjectID]-[Description]-[YYYYMMDD]
Examples:
BCS-50100-ShoeSizeUpgrade-20260101BCS-50100-FieldMigration-20260315ABC-50200-EnumConversion-20260401
Three-Codeunit Pattern (Upgrade Tags)
When using upgrade tags, implement three companion codeunits:
1. Upgrade Codeunit (Subtype = Upgrade)
Contains the actual upgrade logic, guarded by HasUpgradeTag / SetUpgradeTag.
2. Upgrade Tag Definitions Codeunit
- Defines tag values as procedures (not hard-coded strings)
- Subscribes to
OnGetPerCompanyUpgradeTags/OnGetPerDatabaseUpgradeTagsto register tags for new companies Access = Internalto prevent external dependencies on tag values
3. Install Codeunit (Subtype = Install)
- Calls
UpgradeTag.SetAllUpgradeTags()(or sets individual tags) onOnInstallAppPerCompany - Ensures upgrade code doesn't run on freshly installed extensions
Common Upgrade Patterns
Pattern 1: Field Migration (Old Field → New Field)
local procedure MigrateFieldData()
var
UpgradeTagDef: Codeunit "[Prefix] Upgrade Tag Def.";
UpgradeTag: Codeunit "Upgrade Tag";
Customer: Record Customer;
begin
if UpgradeTag.HasUpgradeTag(UpgradeTagDef.GetFieldMigrationTag()) then
exit;
Customer.SetLoadFields("[Prefix] Old Field", "[Prefix] New Field");
Customer.SetFilter("[Prefix] Old Field", '<>%1', '');
Customer.SetRange("[Prefix] New Field", '');
if Customer.FindSet() then
repeat
Customer."[Prefix] New Field" := Customer."[Prefix] Old Field";
Customer.Modify(false);
until Customer.Next() = 0;
UpgradeTag.SetUpgradeTag(UpgradeTagDef.GetFieldMigrationTag());
end;Pattern 2: Populate New Table from Existing Data
local procedure PopulateNewTable()
var
UpgradeTagDef: Codeunit "[Prefix] Upgrade Tag Def.";
UpgradeTag: Codeunit "Upgrade Tag";
Source: Record "Source Table";
Target: Record "[Prefix] New Table";
begin
if UpgradeTag.HasUpgradeTag(UpgradeTagDef.GetNewTableTag()) then
exit;
Source.SetLoadFields("No.", "Field A", "Field B");
if Source.FindSet() then
repeat
if not Target.Get(Source."No.") then begin
Target.Init();
Target."Entry No." := Source."No.";
Target."Field A" := Source."Field A";
Target."Field B" := Source."Field B";
Target.Insert(false);
end;
until Source.Next() = 0;
UpgradeTag.SetUpgradeTag(UpgradeTagDef.GetNewTableTag());
end;Pattern 3: Enum Value Migration
local procedure MigrateEnumValues()
var
UpgradeTagDef: Codeunit "[Prefix] Upgrade Tag Def.";
UpgradeTag: Codeunit "Upgrade Tag";
MyRecord: Record "[Prefix] My Record";
begin
if UpgradeTag.HasUpgradeTag(UpgradeTagDef.GetEnumMigrationTag()) then
exit;
MyRecord.SetLoadFields(Status, "Legacy Status Code");
MyRecord.SetRange(Status, MyRecord.Status::" ");
MyRecord.SetFilter("Legacy Status Code", '<>%1', 0);
if MyRecord.FindSet() then
repeat
case MyRecord."Legacy Status Code" of
1:
MyRecord.Status := MyRecord.Status::Draft;
2:
MyRecord.Status := MyRecord.Status::Active;
3:
MyRecord.Status := MyRecord.Status::Closed;
end;
MyRecord.Modify(false);
until MyRecord.Next() = 0;
UpgradeTag.SetUpgradeTag(UpgradeTagDef.GetEnumMigrationTag());
end;Pattern 4: Default Value Seeding on Upgrade
local procedure SetNewFieldDefaults()
var
UpgradeTagDef: Codeunit "[Prefix] Upgrade Tag Def.";
UpgradeTag: Codeunit "Upgrade Tag";
Setup: Record "[Prefix] Module Setup";
begin
if UpgradeTag.HasUpgradeTag(UpgradeTagDef.GetDefaultValuesTag()) then
exit;
if Setup.Get() then begin
Setup."New Feature Enabled" := false;
Setup."Max Items" := 100;
Setup.Modify(false);
end;
UpgradeTag.SetUpgradeTag(UpgradeTagDef.GetDefaultValuesTag());
end;Pattern 5: Data Archive Restoration
trigger OnUpgradePerDatabase()
begin
NavApp.RestoreArchiveData(Database::"[Prefix] Custom Table");
end;Pattern 6: Precondition — Block Incompatible Versions
trigger OnCheckPreconditionsPerCompany()
var
AppInfo: ModuleInfo;
begin
NavApp.GetCurrentModuleInfo(AppInfo);
if AppInfo.DataVersion < Version.Create(1, 0, 0, 0) then
Error(MinVersionRequiredErr, '1.0.0.0', Format(AppInfo.DataVersion));
end;
var
MinVersionRequiredErr: Label 'Upgrade requires minimum version %1. Current data version is %2.', Comment = '%1 = Required version, %2 = Current version';Pattern 7: Post-Upgrade Validation
trigger OnValidateUpgradePerCompany()
var
Setup: Record "[Prefix] Module Setup";
begin
if not Setup.Get() then
Error(SetupMissingErr);
if Setup."Required Field" = '' then
Error(RequiredFieldBlankErr);
end;
var
SetupMissingErr: Label 'Setup record is missing after upgrade.';
RequiredFieldBlankErr: Label 'Required field is blank after upgrade.';Protecting Sensitive Code During Upgrade
Event subscribers may fire during upgrades. Guard irreversible operations:
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", OnAfterPostSalesDoc, '', false, false)]
local procedure OnAfterPostSalesDoc(var SalesHeader: Record "Sales Header")
begin
if Session.GetExecutionContext() <> ExecutionContext::Normal then
exit;
// Safe to call external services here
end;ExecutionContext Values
| Value | When |
ExecutionContext::Normal | Normal user/API operation |
ExecutionContext::Install | During extension install |
ExecutionContext::Uninstall | During extension uninstall |
ExecutionContext::Upgrade | During extension upgrade |
Design Guidelines
- Use upgrade tags for anything beyond trivial upgrades — they're more robust than version checks
- Register tags for new companies via
OnGetPerCompanyUpgradeTags/OnGetPerDatabaseUpgradeTags - Set tags on fresh install via install codeunit so upgrade code doesn't run on new data
- Use
Modify(false)during bulk migration to skip trigger execution - Use
SetLoadFieldsbeforeFindSetwhen iterating large tables - Keep safety checks — verify target fields are blank/default before overwriting
- No UI interaction — upgrade code runs without user context
- Idempotent — upgrade may be retried after failure; guard with tags
- Independent codeunits — don't depend on execution order between upgrade codeunits
- Validate after upgrade — use validation triggers to confirm migration succeeded
- Log telemetry — record step names, record counts, and timing in Application Insights
- Limit tag nesting — keep tag checks to max two levels of nesting
Upgrade Codeunit Design Workflow
- Identify data changes — What changed between versions? New fields, removed fields, table restructuring?
- Choose control method — Version comparison or upgrade tags?
- Plan migration steps — List each data transformation needed, in dependency order
- Create upgrade tag definitions — One tag per migration step, registered for new companies
- Implement upgrade codeunit — Migration logic guarded by tags with
SetLoadFieldsandModify(false) - Add precondition checks — Block incompatible source versions
- Add validation checks — Verify each migration step succeeded
- Update install codeunit — Set upgrade tags on fresh install (
SetAllUpgradeTagsor individual tags) - Add telemetry — Log start/end of each step with record counts
- Test upgrade path — Verify from each supported source version to the new version
File Naming Convention
Follow the pattern:
- Upgrade codeunit:
[Prefix]Upgrade.Codeunit.al - Upgrade tag definitions:
[Prefix]UpgradeTagDef.Codeunit.al
Examples:
BCSUpgrade.Codeunit.al/BCSUpgradeTagDef.Codeunit.alBCSStatAccUpgrade.Codeunit.al/BCSStatAccUpgradeTagDef.Codeunit.al
Place in: src/Codeunit/ or feature folder src/[Feature]/Codeunit/.
Checklist
Before completing upgrade codeunit generation:
Subtype = Upgradeset on upgrade codeunit- Upgrade triggers used appropriately (Preconditions → Upgrade → Validate)
- Upgrade tags created for each migration step (if using tag pattern)
- Tag definitions codeunit created with
Access = Internal - Tags registered for new companies via
OnGetPerCompanyUpgradeTags/OnGetPerDatabaseUpgradeTags - Install codeunit updated to set tags on fresh install
SetLoadFieldsused beforeFindSeton large tablesModify(false)used for bulk data migration (skip triggers)- Safety checks before overwriting fields (verify blank/default)
- No UI interaction (no Message, Confirm, Page.Run)
- Precondition checks block incompatible source versions
- Validation checks verify migration success
- Telemetry logging for each step (optional)
- ExecutionContext guards on sensitive event subscribers
- File follows naming convention
References
For complete examples:
- references/upgrade-examples.md — Full working examples (version-based, upgrade tags, database-level, multi-step with telemetry, ExecutionContext guards, broken upgrade fixes, archive restoration, anti-patterns)
External Resources
- Upgrading Extensions — Microsoft Docs
- Writing Extension Install Code — Install codeunits (companion)
- NavApp Data Type — ModuleInfo methods
- Upgrade Tags (BCApps) — System Application source code
- Analyzing Extension Upgrade Telemetry — Upgrade telemetry signals
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

