Most things can and should be built in AL. But AL in Business Central online has limits: no .NET libraries, no file system access, no long calculations without locking the user. When we hit them, we put the logic in an Azure Function and call it from AL.
This guide shows when it makes sense, what a minimal setup looks like, and which mistakes we see again and again.
Understanding Azure Functions
In short: a small piece of code in Azure that runs when something calls it, and that you only pay for while it runs.
What are Azure Functions?
An Azure Function is a serverless function started by a trigger: an HTTP call, a timer, a message on a queue or a new file in Blob Storage. You write the function in C#, JavaScript, Python or PowerShell, and Azure takes care of running it. On the Consumption or Flex Consumption plan you pay per execution, and for most BC integrations the bill ends up at a few kroner a month.
Key benefits for Business Central integration
Why not just write it all in AL? Because a Function can do things AL online cannot:
- Full .NET with NuGet packages — for example for PDF processing, SFTP, encryption or a vendor SDK
- Runs outside BC's session time, so heavy jobs do not block the user or the job queue
- Can receive webhooks from webshops and other systems and write to BC through the API
- Timer triggers for scheduled runs, independent of BC's job queue
- Secrets in Azure Key Vault instead of in a setup table
- Logging in Application Insights, the same place as the BC telemetry
Our customers who have implemented Azure Functions with Business Central have seen the development time for custom extensions reduced by up to 60% compared with traditional extension methods, while also improving scalability and reducing running costs.
Common use cases
These are the scenarios where we typically choose a Function over pure AL:
Custom API integrations
Freight carriers, industry systems and older solutions that only speak SOAP with a client certificate, SFTP or a proprietary file format. The Function translates to a clean JSON call that AL can handle. Typical examples: importing bank statements from an SFTP server, or sending orders to a 3PL warehouse that only accepts CSV on an FTP.
Background processing
If a webshop has to send 2,000 orders at once, or a price sheet with 50,000 lines has to be imported, that is not something a user should wait for. The Function accepts the request, puts the task on a queue and writes to BC in chunks through the API. BC's job queue can do a lot too, but it shares capacity with everything else in the environment.
Complex calculations and business logic
Freight calculation with zone tables, excise duty calculation across many item groups, or generating and signing PDFs. It can be written in AL, but it is often quicker and easier to test in C# with a ready-made library. Keep the business rules in BC and the calculation itself in the Function, so the consultant can still see what is happening.
Scheduled tasks and monitoring
Timer triggers are good for nightly syncs, exchange rates from an external source, or a daily check that the integrations have actually run. They are also the obvious place to renew webhook subscriptions in BC, which expire after three days if nobody renews them.
Setting up your development environment
It is quick to set up if you already develop AL in VS Code.
Prerequisites
You will need:
- An Azure subscription — preferably in the same Entra tenant as Business Central
- Visual Studio Code with the Azure Functions extension and C# Dev Kit
- Azure Functions Core Tools v4 and the .NET 8 SDK
- A Business Central sandbox for testing — never directly against production
- An app registration in Entra ID if the Function has to call back to BC's API
Creating your first Azure Function project
This is how you create a project in VS Code:
- Press F1 and choose 'Azure Functions: Create New Project'.
- Choose an empty folder for the project.
- Choose C# and the runtime '.NET 8.0 Isolated'. Do not choose in-process — that model loses support on 10 November 2026.
- Choose the 'HTTP trigger' template.
- Give the function a name and choose the authorisation level 'Function', so calls require a key.
- Run the project locally with F5 and test it with a POST from, for example, REST Client before you deploy.
A basic HTTP trigger function example
A minimal example in the isolated worker model that receives a document number from BC and responds with JSON:
using System.Net;
using System.Text.Json;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
namespace Invice.BCFunctions;
// Isolated worker model (.NET 8). The in-process model loses support on 10 November 2026.
public class ProcessBusinessCentralData(ILogger<ProcessBusinessCentralData> logger)
{
[Function("ProcessBusinessCentralData")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
{
using var doc = await JsonDocument.ParseAsync(req.Body);
var documentNo = doc.RootElement.GetProperty("documentNo").GetString();
logger.LogInformation("Processing {DocumentNo} from Business Central", documentNo);
// The business logic goes here. Keep it short: the user in BC is waiting for the response.
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new { documentNo, status = "processed" });
return response;
}
}Integration patterns with Business Central
There are three patterns we use: BC calls the Function directly (synchronous), BC puts a message that the Function processes later (asynchronous), or an external system calls the Function, which then writes to BC through the API. The first is the most common, so that is the one we show here.
HTTP-triggered functions
BC calls the Function with HttpClient and waits for the response. It suits lookups and calculations that take less than a few seconds: a freight price, an address validation, a generated PDF. If it takes longer, do it asynchronously.
Example of AL code that calls the Function above:
procedure CallAzureFunction(Payload: JsonObject) Result: JsonObject
var
Client: HttpClient;
Request: HttpRequestMessage;
Response: HttpResponseMessage;
Content: HttpContent;
ContentHeaders: HttpHeaders;
RequestHeaders: HttpHeaders;
PayloadText: Text;
ResponseText: Text;
begin
Payload.WriteTo(PayloadText);
Content.WriteFrom(PayloadText);
Content.GetHeaders(ContentHeaders);
ContentHeaders.Remove('Content-Type');
ContentHeaders.Add('Content-Type', 'application/json');
Request.Method := 'POST';
Request.SetRequestUri(GetFunctionUrl()); // from a setup table, not hard-coded
Request.Content := Content;
Request.GetHeaders(RequestHeaders);
RequestHeaders.Add('x-functions-key', GetFunctionKey()); // stored in IsolatedStorage, not in the URL
Client.Timeout := 30000; // the default is 100 seconds, and the user is waiting meanwhile
if not Client.Send(Request, Response) then
Error('The Azure Function could not be called: %1', GetLastErrorText());
if not Response.IsSuccessStatusCode() then
Error('The Azure Function responded %1: %2', Response.HttpStatusCode(), Response.ReasonPhrase());
Response.Content.ReadAs(ResponseText);
Result.ReadFrom(ResponseText);
end;The mistakes we see most often:
- The function key is in the URL as ?code=… and ends up in logs and telemetry. Send it as the x-functions-key header.
- The return value from Client.Send is ignored, so a network error turns into an incomprehensible runtime error for the user.
- It works in production but not in the sandbox: outgoing HTTP calls are blocked by default in sandboxes and copied environments. Switch on 'Allow HttpClient Requests' under Extension Management → Configure.
- No timeout set. The default is 100 seconds, and that is a long time to stare at an hourglass.
- URL and key hard-coded in AL, so the sandbox and production hit the same Function.
Do you have an integration AL cannot solve on its own?
We build both the AL extension and the Azure Function behind it, and we run it afterwards. You talk to the developer who writes the code.