Calling AI from Business Central: the platform realities nobody warns you about
- Calling AI from Business Central: the platform realities nobody warns you about
- A 502 in your PDF-to-LLM pipeline is the gateway, not the model
- Shipping a Copilot feature in Business Central that survives real users
- Your AI feature must run on a fresh tenant, or it doesn't run
Built-in agents are GA now, so calling an AI service from Business Central looks like a solved
problem. It isn't, not the moment you wire up your own. You want BC to call Azure OpenAI, a
gateway, your own inference endpoint. In AL that is an HttpClient. You write it, you publish,
you run it and Send returns false with a response you cannot make sense of, or nothing at
all. Your code is fine. What is fighting you is a set of platform guardrails that, by design,
fail silently and the platform will never once tell you which one. Here is the order I now
check them in, because every one of them has cost me an afternoon.
1. Outbound HTTP is off by default
A published extension cannot make outbound HTTP calls until the Allow HttpClient Requests flag is enabled for that app. Until then, calls fail with a "service not enabled"-class error that does not obviously point at a permission toggle.
In production this is a customer-facing setting on the extension. In any automated environment, set it as part of provisioning, flipping it after the fact, by hand, on every fresh environment, is exactly the kind of step that gets forgotten and then blamed on the code.
The lesson that generalizes: outbound networking from AL is opt-in, per extension. Treat it as a deployment artifact, not a dev-machine afterthought.
2. BC blocks private IPs, the anti-SSRF allowlist
This one is subtle and good. The platform refuses HttpClient requests from AL to private IP
ranges, as anti-SSRF protection. Sensible default: you do not want extensions reaching the
metadata endpoint or poking around the internal network.
But the moment your AI endpoint is inside the same network, a sidecar, a gateway on the same
host, a self-hosted model, the protection blocks the call you actually want. And it does so
silently: Send comes back false, no helpful exception.
The fix is to add the target address (IP and/or hostname) to the allowlist
(NavHttpClientAntiSSRFAllowedAddresses, a JSON array) and restart the service tier. The
discipline: if you are calling anything self-hosted, assume the anti-SSRF guard is on until
you have explicitly allowlisted the target. A silent send=false against a private address
is this guard nine times out of ten.
3. Loopback needs the tenant hostname, not localhost
The trap that looks most like a bug. If AL needs to call its own service tier, the classic
case being a bootstrap or setup routine that hits the container's own OData/SOAP, pointing at
localhost gets you a 401 Unauthorized. The request technically arrives, but authentication
resolves against the wrong host context.
The call has to go to the tenant-qualified hostname, of the form <host>-<tenant> (for example
bc-service-default), not localhost. Same endpoint, same credentials, different host string
and the difference is the entire bug.
// Wrong: resolves auth against the bare host, 401
Client.Get('http://localhost/BC/ODataV4/...', Response);
// Right: tenant-qualified host
Client.Get('http://bc-service-default/BC/ODataV4/...', Response);
4. Authentication is the part that should be loud, make it so
The three guardrails above fail quietly. Authentication, by contrast, usually fails loudly
(a 401/403 with a body) and your job is to not swallow that signal. For anything beyond a
public endpoint you are doing OAuth client-credentials: an Entra app registration, a client id
and secret (or certificate), a token endpoint and a scope. The token is bearer auth on the
request:
Headers.Add('Authorization', StrSubstNo('Bearer %1', AccessToken));
Two things that save real time here:
- Cache the token, respect its expiry. Fetching a token on every call is slow and will get
you throttled. Cache it and refresh on expiry, but also refresh on a
401, because a token can be revoked before it expires. - A
401is an auth verdict, not a guardrail. Do not confuse it with the silentsend=falsefailures above. IfSendreturned a response at all, transport worked, you are now debugging credentials or scope, a completely different search space.
Make the silence loud
The reason these guardrails cost afternoons is that AL lets you ignore the one signal that
matters: the boolean Send returns. Stop ignoring it. The smallest useful habit:
if not Client.Send(Request, Response) then
Error('HTTP send failed before reaching the endpoint: check HttpClient flag / anti-SSRF allowlist for %1', Url);
if not Response.IsSuccessStatusCode() then
Error('Endpoint returned %1 %2', Response.HttpStatusCode, Response.ReasonPhrase);
Send returning false means the request never left BC, that is always one of guardrails one
through three. A false from Send and a non-2xx status code are different failures with
different causes and code that collapses them into one generic "AI call failed" message is
code that guarantees you will debug the wrong layer. Separate them at the source and the
diagnosis is half done before you open the logs.
The checklist
When an outbound or self-directed AI call from BC misbehaves, before you touch the code:
send=falseor "not enabled" → is Allow HttpClient Requests on for this extension?send=falseagainst a private/internal address → is the target on the anti-SSRF allowlist (and did you restart the service tier)?401calling your own tier → are you using<host>-<tenant>, notlocalhost?401/403with a response body from the endpoint → not a guardrail. Transport worked; it is credentials, scope, or a revoked token.
None of these throw a message that names the real cause. That is the whole reason they waste time: the platform is protecting you correctly, just quietly. Internalize the three and the "my HttpClient just won't work" class of problem mostly disappears.
This is part one of a short series on running AI in Business Central for real. Next: what happens when the payload is the problem.