Morten Laske AI × Business Central
← Writing

Calling AI from Business Central: the platform realities nobody warns you about

Calling AI from Business Central: the platform realities nobody warns you about

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.

AL extensionHttpClient.Send
gate 1HttpClient flag
closed
"service not enabled"names the service, not the flag
open
gate 2anti-SSRF list
closed
Send = falseno error at all
open
gate 3<host>-<tenant>
closed
401 Unauthorizedlooks like an auth bug
open
AI endpointAzure · gateway
The call falls through three gates and a closed gate never names itself. Only a call that clears all three reaches the model; a closed gate ejects it sideways with an error that points anywhere but at the gate.

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 401 is an auth verdict, not a guardrail. Do not confuse it with the silent send=false failures above. If Send returned 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:

  1. send=false or "not enabled" → is Allow HttpClient Requests on for this extension?
  2. send=false against a private/internal address → is the target on the anti-SSRF allowlist (and did you restart the service tier)?
  3. 401 calling your own tier → are you using <host>-<tenant>, not localhost?
  4. 401/403 with 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.

Found this useful? Share on LinkedIn · email me a correction or follow-up.

Related