Shipping a Copilot feature in Business Central that survives real users
- 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
The PromptDialog demo is genuinely easy. A page of type PromptDialog, a prompt, a call to the
model, results on screen, an afternoon and it looks like magic in the sprint review. Then real
users touch it and it gets switched off within a week. The demo was never the feature; it was
the 10% that happens to be visible. My thesis: a Copilot feature survives real users only when
the model never silently touches data and everything around it is provisioned, not improvised
and that "everything around it" is the other 90% nobody demos.
Scope the task narrowly, on purpose
The strongest thing you can do for quality is to make the model's job small. "Draft this specific field from this specific context" beats "be an assistant." A narrow task is one you can ground, validate and reason about; a broad one is a surface for the model to be confidently wrong on.
In BC terms: a Copilot action that suggests a single, structured thing, a description, a set of line items, a category, from records the user is already looking at will outperform an open-ended chat and your users will trust it more because it does less.
Ground it in the user's actual data
A model with no grounding invents plausibly. The fix is not a cleverer prompt; it is feeding the relevant records into the context so the model is summarizing reality instead of guessing. Pull the customer, the document, the history the user can see and put it in the prompt. The difference between a feature that gets adopted and one that gets disabled is usually grounding, not model quality.
The non-negotiable: generate, review, accept
The single most important design decision: the model proposes; the user disposes. Output lands in an editable, reviewable state, never written silently to the database. PromptDialog is built for exactly this rhythm:
- Generate, the model produces a proposal.
- Review, the user sees it, edits it, regenerates if it's wrong.
- Accept, only an explicit user action commits anything.
This is not a quirk of small features, it is the pattern Microsoft shipped its own GA agents on. The Sales Order and Payables agents generate, surface their work for review and gate the commit on confidence and human approval; BC28 even adds "Created/Modified by AI" indicators so you can see where a proposal became a write. The honeymoon is over and that design is now the baseline, not a nicety.
This is not just UX politeness. It is your safety net for non-determinism: the same input can produce different output and the review step is what makes that acceptable instead of dangerous. If your feature writes model output straight to records without an accept gate, you have not built a Copilot feature, you have built an unpredictable batch job.
Treat the output as untrusted before it touches data
Even after the user accepts, the values came from a model. If any of them flow into a filter, a lookup, or a write, run them through the same hygiene you would apply to any external input, validate, constrain to known domains, sanitize before the sink. An AI-suggested status that does not exist, or a string with filter metacharacters, should fail closed, not corrupt a query. Grounding reduces this; it does not remove it.
What the AL actually looks like
The shape is a page of type PromptDialog. It gives you the generate, review, accept rhythm for
free: a prompt area, a results area and the Generate/OK/Cancel system actions. The skeleton:
page 50180 "Draft Description"
{
PageType = PromptDialog;
PromptMode = Generate;
Extensible = false;
layout
{
area(Prompt)
{
field(Instruction; InstructionTxt) { ShowCaption = false; MultiLine = true; }
}
area(Content)
{
field(Result; ResultTxt) { ShowCaption = false; MultiLine = true; Editable = true; }
}
}
actions
{
area(SystemActions)
{
systemaction(Generate) { trigger OnAction() begin GenerateProposal(); end; }
systemaction(OK) { Caption = 'Keep'; }
systemaction(Cancel) { Caption = 'Discard'; }
}
}
local procedure GenerateProposal()
begin
// ground in the caller's record, call the model, put the draft in ResultTxt.
// Note: ResultTxt is Editable, the user owns it before they press Keep.
ResultTxt := AICaller.Draft(SourceRec);
end;
}
Two details that matter more than they look. Result is Editable = true, the proposal is
the user's to change before they commit it. And nothing in GenerateProposal writes to the
database; the caller only persists ResultTxt after the page returns with OK (the "Keep"
action). The page enforces the discipline; you just have to not subvert it by writing early.
The operational layer nobody demos
The parts that decide whether it survives in production, none of which appear in the prototype:
- Terms of use and consent. AI features carry usage terms that the user must have accepted. Handle the acceptance state explicitly, a feature that throws on first use because consent was never recorded reads as "broken," not "compliant."
- Permissions that fail open to off. Default to the feature being available and opt-out, not buried behind a setup step the customer never finds. The most common reason a shipped AI feature gets "no usage" is that nobody could turn it on.
- A sane default model. Do not hardcode a model that may not be enabled on the customer's tenant. The default belongs in configuration and it has to be one that actually resolves.
- Telemetry. Log prompt, response, latency and acceptance rate. "Did anyone use it and did they accept or discard?" is the only question that tells you if the feature is working and you cannot answer it after the fact if you did not instrument it.
How do you know it is good? A tiny eval set
"It looked right when I tried it" is not a quality bar; it is a sample size of one on inputs you chose. Before you ship, assemble a small set of real-ish cases, a dozen is enough to start, each with the input context and a note on what a good output looks like. Run the feature across all of them whenever you change the prompt, the grounding, or the model.
You are not building a research benchmark. You are building a regression test for a non-deterministic function. The payoff is concrete: when you swap the model to cut cost (which the gateway makes a config change), the eval set tells you in minutes whether quality held, instead of finding out from users a week later. A Copilot feature without an eval set is one you can never safely change.
It has to work on a clean tenant
The final gate and the one most prototypes fail: the feature must work on a fresh environment with no developer setup, no manual key insertion, no SQL nudges, no "first run this script." If it only works on your dev box, it does not work. That deserves its own discussion and gets one.
The shape of a real one
Putting it together, a Copilot feature that lasts looks like this:
- A narrow task, grounded in records the user can see.
- A PromptDialog generate, review, accept loop; nothing commits without an explicit accept.
- Output validated and sanitized before it touches data, failing closed.
- Consent, opt-out permissions, a resolvable default model and telemetry, all provisioned, not manual.
- Verified on a clean tenant, not just your machine.
The model is maybe 10% of that list. Everyone spends their time on the 10% because it is the part that demos. The features that survive are the ones where someone did the other 90%.
This is part three of the production-AI series, the synthesis of the guardrails (part one) and the payload lessons (part two).