JavaScript can be distributed as a Module, but a Business Central ControlAddIn does not have direct support for modules. Check the video for an easy solution to this challenge!

In this video, Erik demonstrates how to use JavaScript modules in AL for Business Central. The core problem: Business Central’s control add-in framework doesn’t natively support ES modules (i.e., <script type="module">), but many modern JavaScript libraries are packaged as modules. Erik walks through a clever workaround — declaring the module file as an image resource and dynamically importing it — using the open-source Deep Chat library as a practical example.
The Problem: JavaScript Modules in Business Central
JavaScript has two main flavors when it comes to how scripts are loaded:
- Classic scripts — everything lives in the global scope. You define something, and it’s available everywhere.
- ES Modules — a more organized approach where you explicitly
importandexportfunctionality between files.
Business Central’s control add-in infrastructure supports the first flavor just fine. You list your .js files in the Scripts property of the control add-in definition and they’re loaded as classic scripts. But if a JavaScript library uses ES module syntax — export, import, type="module" — you’ll immediately run into errors like:
“Unexpected token export” and “Failed to execute write on document: unexpected token export.”
There is no type="module" attribute you can set in the control add-in manifest. So how do you include a modern JavaScript library that’s packaged as a module?
Setting Up the Project
Erik starts with a fresh AL app and walks through building the control add-in from scratch. The example uses Deep Chat, an open-source, MIT-licensed chat UI component — perfect for building an agent-style chat experience inside Business Central.
Step 1: Download the JavaScript Bundle
Rather than referencing an external CDN URL, Erik downloads the Deep Chat JavaScript bundle (deepChat.js) directly into the project. This gives full control over the version being used and avoids external dependencies at runtime.
Step 2: Create the Control Add-in
A control add-in definition in AL has three key sections: scripts (and resources), properties, and an interface. Here’s the control add-in definition:
controladdin DeepChat
{
Scripts =
'deepchat/script.js',
'deepchat/startup.js';
Images =
'deepchat/deepChat.js';
VerticalStretch = true;
HorizontalStretch = true;
event ControlReady();
procedure Init();
}
Notice a few important things:
Scripts— includes two classic scripts:script.js(which contains theInitfunction) andstartup.js(which fires theControlReadyevent).Images— this is where the trick happens. The Deep Chat module (deepChat.js) is declared as an image. It’s obviously not an image, but theImagesproperty is really just a general-purpose resource bucket. Business Central doesn’t care about the file type — it just makes the file available as a resource.ControlReady— every control add-in should fire this event so the AL code knows when it’s safe to call into the control. If you call into the control before it’s ready, things simply won’t work.Init— a procedure the AL side can call to initialize the chat UI.
Step 3: Create the Host Page
The page uses the UserControlHost page type, which is purpose-built for hosting control add-ins:
page 50100 "Deep Chat Test"
{
PageType = UserControlHost;
Caption = 'Deep Chat Demo';
ApplicationArea = all;
layout
{
area(Content)
{
usercontrol(deepchat; DeepChat)
{
trigger ControlReady()
begin
CurrPage.deepchat.Init();
end;
}
}
}
}
The flow is straightforward: when the control signals it’s ready, the page calls Init() to set up the chat interface.
Step 4: The Startup Script
The startup script is executed when the control first loads. Its job is to signal back to AL that the control is ready:
Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('ControlReady', []);
InvokeExtensibilityMethod is the bridge from JavaScript back to AL. The first parameter is the event name, and the second is an array of parameter values (empty in this case).
Step 5: The Init Script — And the Module Import Trick
Here’s where the magic happens. In script.js, the Init function needs to place the Deep Chat component on the page. Control add-ins render inside an iframe, and Microsoft places a <div> with the ID controlAddIn inside that iframe:
function Init() {
var container = document.getElementById('controlAddIn');
container.innerHTML = `<deep-chat demo="true"></deep-chat>`;
}
But if you try to load deepChat.js as a regular script (via the Scripts property), you’ll get the “unexpected token export” error because it’s an ES module.
The Workaround: Dynamic Import via Image Resource
Since we declared deepChat.js in the Images property, we can retrieve its URL at runtime using Microsoft.Dynamics.NAV.GetImageResource. Then we use a dynamic import() — which is the standard JavaScript way to load a module at runtime — to bring it in:
import(Microsoft.Dynamics.NAV.GetImageResource('deepchat/deepChat.js'));
This line goes in the startup.js file, before the ControlReady call, so the module is loaded before the AL side tries to use its components. The key insight is:
- The
Imagesproperty makes the file available as a resource with a URL. GetImageResourcereturns that URL at runtime.import()performs a dynamic ES module import from that URL.
The browser treats the dynamic import as a proper module load — with full export/import support — bypassing the limitation that the Scripts property only loads classic scripts.
The complete startup.js looks like this:
import(Microsoft.Dynamics.NAV.GetImageResource('deepchat/deepChat.js'));
Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('ControlReady', []);
After deploying and refreshing, the Deep Chat component renders successfully inside Business Central — a fully functional chat interface loaded from a JavaScript module.
Summary
Here’s the complete recipe for using JavaScript modules in a Business Central control add-in:
- Declare your module file in the
Imagesproperty of the control add-in, not inScripts. TheImagesproperty is really just a resource bucket — it doesn’t care about file types. - Use
import(Microsoft.Dynamics.NAV.GetImageResource('path/to/module.js'))in your startup script to dynamically load the module at runtime. - Use your classic scripts normally for your own
Initfunctions, event wiring, and other non-module code.
Erik notes that ideally Microsoft should extend the control add-in definition to allow marking a script as a module directly. But until that happens, this approach is a clean, workable hack that lets you use any modern JavaScript library — modules and all — inside Business Central.