Deploy BC Extensions (AL / Business Central)
How PSI builds and publishes per-tenant AL extensions to Business Central SaaS environments — reproducibly, via CI, sandbox-first. Reference implementation: bc-metadata-extension (
DEPLOYMENT.md).
Applies to all PSI BC extension repos (bc-metadata-extension, PSI-Shipping’s bc-extension, psi-workflow, psi-vendor-scorecard, psi-related-docs).
Build — standalone alc.exe, not AL-Go
Compile with the standalone .NET AL compiler shipped in the Microsoft.Dynamics.BusinessCentral.Development.Tools NuGet package, on a windows-latest runner. We deliberately avoid AL-Go for GitHub so production BC builds don’t depend on unpinned third-party actions.
- Pull platform symbols in CI from the public BC artifact CDN (BcContainerHelper, e.g. v28 sandbox/US) — nothing is committed, no reachable sandbox required for a PR build.
- Gotcha: extract the compiler NuGet outside the repo workspace. It ships template
.alfiles (ALGo Template/HelloWorld.al, object 50100) thatalc /project:.will otherwise sweep into the build and fail on.
& $alc /project:. /packagecachepath:.alpackages /out:"<publisher>_<name>_<version>.app"Object ID ranges
Per-tenant extensions use the free 50000–99999 range (the compiler enforces app.json idRanges). Object IDs must be globally unique across all installed extensions in an environment, so partition the range across PSI’s BC repos to avoid “won’t install” collisions. AppSource apps (Cosmo, Insight Works, Cavallo, ExFlow, Avalara) use Microsoft-assigned ranges and won’t collide with 50000–99999.
Deploy — Automation API, sandbox-first
Publish via the BC Automation API with a client-credentials token. The extension upload is a 3-step flow (easy to get wrong):
POST companies({id})/extensionUpload— body{ "schedule": "Current version", "schemaSyncMode": "Add" }.schedulegoes in the body, not the query string.schemaSyncMode: "Add"is additive (safe for additive-only extensions; use"Force Sync"only when schema must change).PATCH companies({id})/extensionUpload({id})/extensionContent— the.appbytes,If-Match: *.POST companies({id})/extensionUpload({id})/Microsoft.NAV.upload— install.- Poll
extensionDeploymentStatusuntilCompleted.
extensionUpload is a singleton with no DELETE — a partial run leaves a staging record, so get-or-reuse it rather than POSTing a new one (else Internal_EntityWithSameKeyExists).
Always sandbox → DEV → Production, with Production guarded behind an explicit confirmation input.
⚠️ Per-environment registration is REQUIRED (the big gotcha)
In this tenant, app-only identities must be registered inside BC in every environment — Automation.ReadWrite.All / Graph admin consent alone does not grant BC access (you get Authentication_InvalidCredentials). For each environment:
- BC → Microsoft Entra Applications → + New → enter the app’s Client ID.
- State = Enabled → this provisions the app’s BC user. The permission-set grid is locked until enabled.
- Assign the required permission set(s).
- Grant Consent.
BC UI automation note: the Entra-app grid and dropdowns reject programmatically-typed text. Card fields (Client ID) accept typing; dropdowns need keyboard (open → ↑/↓ → Enter, which also fires confirm dialogs). Permission-set rows are easiest to assign via the Automation API (POST users({sid})/userPermissions {"roleId":"..."}) with an admin token, once the app is registered + enabled.
Roles by app purpose
| App role | Permission set(s) | Why |
|---|---|---|
| Deploy SP (publishes extensions) | D365 AUTOMATION and EXTEN. MGT. - ADMIN | Automation API access + extension install (Microsoft.NAV.upload fails “insufficient permissions” without EXTEN. MGT.) |
| Client/consumer app (calls custom APIs) | The extension’s own permission set (e.g. PSI API Access) | Access to the custom API pages |
Deploy service principal
Create a dedicated SP (don’t reuse the runtime/client app). Grant Dynamics 365 Business Central → Automation.ReadWrite.All (Application) + admin consent; store its secret/config in repo secrets/variables. Then register it per-environment as above.
For bc-metadata-extension the deploy SP is bc-extension-deploy (dc2fdf7c-…) — see Azure Resource Map.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
Authentication_InvalidCredentials | App not registered + Enabled in that BC environment |
You do not have sufficient permissions on install | Deploy SP missing EXTEN. MGT. - ADMIN |
Error in query syntax | schedule passed in query string instead of POST body |
Internal_EntityWithSameKeyExists | Stale extensionUpload record — reuse it, don’t recreate |
Entity does not support delete | extensionUpload can’t be deleted — reuse it |
AL compile fails on object 50100 | Compiler NuGet extracted inside the repo; extract to $RUNNER_TEMP |
| Permission set / API entity missing after install | Extension not actually installed in that environment — verify via extensionDeploymentStatus / a real entity-set probe (the API group root returns 200 even when nothing publishes it) |
Related
- Deploy to Azure · Deploy ProApps · Deploy Android
- ERP Migration Tool — consumes the BC Metadata extension’s import APIs
- Azure Resource Map — app registrations / client IDs / secrets