AL extension loop¶
Goal: develop and ship a Business Central AL extension end-to-end from a terminal — no VS Code, no global alc install, no curl calls against /dev/*. Four CLI verbs do it: env download-symbols, al compile, env publish, env launch-json.
The wire spec is extracted from Microsoft.Dynamics.Nav.Deployment.dll (the .NET assembly that ships with every BC container). The CLI's verbs are thin wrappers over the same HTTPS endpoints the AL VS Code extension calls — /dev/packages for symbols, /dev/apps for publish, /ALLanguage.vsix (port 8080) for the compiler bundle. Documented under Wire URLs below for the "reverse-engineer this when bcdock breaks" path.
Prerequisites¶
- A running BC environment. If you don't have one:
bcdock env create --name my-dev --version 27 --country au --wait. - An AL project with
app.jsondeclaring the symbol packages your code references. bcdockonPATHand authenticated (bcdock auth whoamireturns your identity).
The four phases¶
ENV=my-dev # whichever env name you used
# 1. Pull the symbol packages your app.json declares
bcdock env download-symbols "$ENV" --out-dir .alpackages
# 2. Compile with the env's bundled alc (matched to the env's BC platform version)
bcdock al compile --env "$ENV" --out build/MyApp.app
# 3. Publish — install/upgrade codeunits run synchronously
bcdock env publish "$ENV" build/MyApp.app
# 4. Generate VS Code launch.json (parity surface for IDE users)
bcdock env launch-json "$ENV" --out .vscode/launch.json
On a warm pool with a cached vsix, the full loop is ~90 seconds for a small extension.
Phase 1 — download-symbols¶
Pulls the symbol packages your app.json declares from the env's BC /dev/packages endpoint. Synthesises the modern split bundle (System Application + Base Application + Business Foundation + Application) from the single application: <ver> declaration. 404s on names absent from the env's BC version are silent skips — the package may simply not exist in older / newer BC versions.
Flags worth knowing:
| Flag | Default | Notes |
|---|---|---|
--app-json |
app.json |
Path to the project manifest |
--out-dir |
.alpackages |
Symbol cache directory |
--tenant |
default |
BC tenant for multi-tenant envs |
--force |
false |
Re-download even if cached |
The cache is content-addressed by version, so a no-op run is cheap (~200ms).
Phase 2 — al compile¶
Compiles your AL project using the alc that ships in the env's bundled ALLanguage.vsix. The vsix is fetched once per BC platform version, cached under ~/.cache/bcdock/al-vsix/{platformVersion}/, and reused across envs on the same platform.
First compile per platform version: ~5s (vsix fetch + extract + first alc run). Subsequent compiles: ~2s.
Flags worth knowing:
| Flag | Default | Notes |
|---|---|---|
--env |
(required) | Whose BC platform's alc to use |
--project |
. |
AL project dir (forwarded as /project:) |
--package-cache |
.alpackages |
Forwarded as /packagecachepath: |
--out |
(alc default) | Output .app path |
--refresh |
false |
Re-download vsix even if cached |
-- /… |
— | Anything after -- is forwarded verbatim to alc |
For agent loops: pin --env to a hibernated env; resume only when you need a publish step. Compile is local — no env round-trip.
Phase 3 — env publish¶
POSTs the .app file to BC's /dev/apps endpoint as multipart, with Basic auth from the env's admin password. Synchronous — returns when BC's install/upgrade codeunits finish. Default --timeout is 10 minutes.
Flags worth knowing:
| Flag | Default | Notes |
|---|---|---|
--schema-update-mode |
synchronize |
synchronize | forcesync | recreate (recreate drops table data) |
--tenant |
default |
|
--dependency-publishing |
(server default) | Default | Ignore | Strict |
--timeout |
10m |
Increase for large extensions with heavy install codeunits |
Publish failures surface BC's error message verbatim on stderr — usually enough to diagnose without opening BC's event log.
Phase 4 — env launch-json¶
Generates a .vscode/launch.json aimed at the env. Useful even if you mostly use the CLI — VS Code's debugger remains the best step-through experience, and the F5 publish path uses the same /dev/apps endpoint.
serverInstance is derived from the env's devEndpointUrl first path segment: BC-dev (subdomain mode) or {containerName}-dev (path mode). Credentials are not written — VS Code prompts on the first publish, the same UX as a hand-configured launch.json.
Verifying a publish¶
Two options. Option A (/dev/packages round-trip) is the strongest signal — same auth as publish, byte-for-byte comparison:
DEV=$(bcdock env get "$ENV" -o json | jq -r .devEndpointUrl)
PASS=$(bcdock env get "$ENV" -o json | jq -r .password)
curl -s -o /tmp/installed.app -w "HTTP %{http_code} bytes=%{size_download}\n" \
-u "admin:$PASS" \
"${DEV}dev/packages?publisher=BCDock&appName=MyApp&versionText=1.0.0.0&tenant=default"
diff -q /tmp/installed.app build/MyApp.app # silent → BC has the same bytes
Option B (OData extensions entity-set) uses the Web Service Access Key surfaced as webServiceAccessKey on the env DTO since 2026-05-04. BC SOAP/OData reject the admin password under NavUserPassword auth — only the access key works:
ODATA=$(bcdock env get "$ENV" -o json | jq -r .oDataUrl)
WSKEY=$(bcdock env get "$ENV" -o json | jq -r .webServiceAccessKey)
# Cheapest auth+routing probe:
curl -sS -u "admin:$WSKEY" -o /dev/null -w "metadata=%{http_code}\n" \
"${ODATA}\$metadata?tenant=default"
# Extensions for the first company (URL-encode the company name):
COMPANY=$(curl -sS -u "admin:$WSKEY" "${ODATA}Company?tenant=default" \
| jq -r '.value[0].Name' | jq -sRr @uri)
curl -sS -u "admin:$WSKEY" \
"${ODATA}Company('${COMPANY}')/extensions?tenant=default" \
| jq '.value[] | {DisplayName, Publisher, Version}'
Envs created before 2026-05-04 have webServiceAccessKey: null — recreate to backfill, or fall back to Option A.
Wire URLs (informational)¶
The CLI handles these — documented for the "reverse-engineer this when bcdock breaks" path:
| Verb | URL constructed |
|---|---|
download-symbols |
GET {devEndpointUrl}dev/packages?publisher=…&appName=…&versionText=…&tenant=… |
env publish |
POST {devEndpointUrl}dev/apps?SchemaUpdateMode=…&tenant=… (multipart) |
al compile (vsix fetch) |
GET {downloadsUrl}ALLanguage.vsix (anonymous, served by every BC container's port 8080) |
devEndpointUrl is {server}/{serverInstance}/ — first path segment is BC-dev (subdomain mode) or {containerName}-dev (path mode).
What this replaces¶
| Old way | New way |
|---|---|
| Install AL VS Code extension globally; keep matched to BC version manually | bcdock al compile --env <name> — vsix from the env, version-matched |
Publish-NavApp PowerShell or BcContainerHelper wrapper |
bcdock env publish — direct multipart to /dev/apps |
Hand-edit launch.json per env, paste in URLs and credentials |
bcdock env launch-json --env <name> |
| Open BC, navigate to "Extension Management" to verify install | curl /dev/packages (Option A) or OData extensions (Option B) |
The CLI is the API surface — there's no VS Code-only path. The same flow works in CI, in agent loops, and on a developer laptop.
Next steps¶
- GitHub Actions guide — same loop, one ephemeral env per PR
- Claude Code guide — agent driving this loop with reasoning between steps
- CLI reference: env publish — every flag, auto-generated from the binary