Back to Blog
Published:
Last Updated:
Fresh Content
Copilot BoundaryChapter 8

How do I expose my own X++ business logic as an agent tool?

10 min read
2,273 words
high priority
Muhammad Mudassir

Muhammad Mudassir

Founder & CEO, Cognilium AI

TL;DR

Write an X++ class implementing ICustomAPI with the right attributes, wire an action menu item into a security role, and flush the cache. The descriptions you write are not comments — they are the interface the model reasons over.

How do I expose my own X++ business logic as an agent tool?

You write an X++ class that implements a specific interface, decorate it so the orchestrator can understand it, wire an action menu item into a security role, and flush the cache. Four pieces, and in practice most of the lost hours go on the last one.

8 minute read.

One status note first, because Microsoft's two sources disagree. The release plan records AI actions for finance and operations business logic with a released check mark against general availability, 26 January 2026.

The page you actually build from says the opposite. It is headed "This article is prerelease documentation and is subject to change" and carries the notice: "This feature is a preview feature… Preview features aren't meant for production use and might have restricted functionality." (Create AI plugins for copilots with finance and operations business logic (preview)) The Feature management flag is still named (Preview) Custom API Generation.

Cite both, and carry the conservative label into a design document: [PP] on the strength of the documentation and the feature flag, with the release plan's GA date named alongside it. Do not claim clean GA on the strength of one page.

When this is the right answer

Work down the decision tree before you write any X++, because this is the most expensive of the options and it is only correct at the bottom of it.

  • Does the agent need to change something? If not, this is not your route — read through the data tools.
  • Is the operation expressible as entity CRUD (create, read, update, delete)? If yes, use the data tools. Microsoft's own starter agent instructions are blunt about it: "For create/read/update/delete operations - you MUST prefer using data tools before using form tools" (Build an agent with Dynamics 365 ERP MCP). Fewer calls, less code, no deployment.
  • Is it a button on a form with the logic behind it? If yes, the form tools already reach it.
  • Only if the answer to all three is no do you write a class.

That last case is real and common enough to matter: an operation that spans several entities in one transaction, a calculation the application performs at runtime, or a guarded operation you want to expose deliberately rather than let an agent assemble from primitives.

Prerequisites, which are stricter than usual

  • **Unified developer environment** — The only place these can be built. Microsoft: "You can develop AI tools that use finance and operations business logic only in the unified developer experience"
  • **Copilot for finance and operations package** — Three solutions in the Power Platform environment: Copilot for finance and operations apps, Copilot for finance and operations generation solution, Copilot for finance and operations anchor solution
  • **Finance and Operations Virtual Entity** — Installed in the same Power Platform environment
  • **(Preview) Custom API Generation** — Enabled in Feature management. That is the feature's exact name, parentheses included

The environment constraint is the one that catches teams. The AI tools can only be built in a unified developer environment (Create AI plugins for copilots with finance and operations business logic (preview)), and the MCP server "isn't supported on Cloud Hosted Environments (CHE)" (Use Model Context Protocol for finance and operations apps).

If your developers work in a cloud-hosted environment, finding that out mid-sprint is an avoidable delay.

The class contract

The shape is a small set of attributes doing specific jobs. Every name below is Microsoft's, spelled as Microsoft spells it — including the interface, which that one page writes both ICustomAPI and ICustomApi (Create AI plugins for copilots with finance and operations business logic (preview)).

  • `AIPluginOperationAttribute` on the class — Marks it as an AI operation and binds it to the Dataverse Custom API
  • `ICustomAPI` interface — The class must implement it
  • `CustomAPIAttribute` on the class — Declares it callable. Carries CustomAPIName — a natural-language action name — and CustomAPIDescription, a natural-language definition
  • `DataContract` on the method — Lets complex types pass without hand-written serialisation
  • `CustomAPIRequestParameter` — Declares an input, with a description and an isOptional flag
  • `CustomAPIResponseProperty` — Declares an output, with a description
  • `run()` — Where the logic executes and response values are set

In code, a parameter and a response property look like this:

The descriptions are not comments — they are the interface

This is the part that decides whether your tool ever gets used, and it is easy to treat as documentation.

Microsoft's parameter table is precise about what these strings do. CustomAPIDescription is "a natural language definition of what the action does", and "Copilot Studio uses this definition for plugin orchestration, to determine when the plugin should be invoked based on the user's prompt."

For a request parameter: "Copilot Studio uses this description to match text from the user's prompt to the parameter." (Create AI plugins for copilots with finance and operations business logic (preview))

So those strings are production code with a natural-language syntax. Write them the way you would write an API contract for a competent reader who does not know your domain: say what the operation does, when it applies, and what each input means in business terms rather than in field names.

The failure mode when you get this wrong is not an error. It is silence — a correctly deployed, correctly secured tool that the orchestrator never selects, because nothing in its description told the model when it would be the right choice.

One condition on all of that: it assumes generative orchestration. Microsoft is explicit that "when you enable classic orchestration in an agent, you must create a separate topic to invoke the action that you add to the agent" — on a classic agent a trigger phrase selects the tool, and your description is not doing that work (Create AI plugins for copilots with finance and operations business logic (preview)).

Security wiring, in three steps

Then verify it, because the wiring is what determines whether the class is visible at all.

  1. In Visual Studio, create an action menu item named similarly to your class.
  2. Set ObjectType = Class and Object to your class name.
  3. Add that menu item to a security role as a privileged item.

Verify at System administration > Setup > Synchronize Dataverse Custom APIs in your finance and operations environment. Microsoft's wording is that the grid "includes every class that meets the following criteria": it implements the ICustomApi interface, it contains the [CustomApi] attribute, and it has an associated action menu item that is included in a security privilege that is assigned to a duty/role (Create AI plugins for copilots with finance and operations business logic (preview)).

That grid is a genuine diagnostic. Absent from it means the wiring is wrong, and the three conditions tell you which of them to check.

The cache flush that explains most lost afternoons

Microsoft's note is one sentence and it is the whole problem: "After deploying the new classes to your environment, ensure the extension cache is flushed before the new classes can be invoked."

Flushing happens as part of database synchronisation, or by running the SysFlushAOD class directly (Create AI plugins for copilots with finance and operations business logic (preview)):

"Deployed, secured correctly, and the agent still cannot see the tool" is almost always this. It is the single most useful troubleshooting artefact in this area and it is not where anyone looks first.

Two routes to reach the class once it exists

Which one you pick turns mostly on whether you already run the Dynamics 365 ERP (enterprise resource planning) MCP (Model Context Protocol — the open standard an agent uses to call tools in another system) server.

  • **Through the ERP MCP server — What you build: No extra Dataverse objects. Deploy the class with the security wiring above and the actions "become automatically accessible through the MCP server" — `api_find_actions` finds them, `api_invoke_action` calls them · When to choose it: The default, and least work by a distance. The server has its own floor: version 10.0.47, 10.0.46 PQU-2 or 10.0.45 PQU-7, a Tier 2 or unified developer environment, and your agent platform listed in Allowed MCP clients**
  • **Through a Dataverse Custom API** — What you build: A Custom API object with request-parameter and response-property records, plus a Dataverse connector tool on the agent · When to choose it: When you are not using the MCP server, or you deliberately want one narrow tool rather than the whole server

For the second route, the Custom API is created in Power Apps under New > More > Other > Custom API, with Plugin Type set to Microsoft.Dynamics.Fno.Copilot.Plugins.InvokeFnoCustomAPI. That exact string, namespace and all.

Unique names take the form <your solution's prefix>_<name of the X++ class for the action>, and each request parameter and response property is its own record whose unique name appends the data member name. You then add it to the agent with the Microsoft Dataverse connector's Perform an unbound action in selected environment action (Create AI plugins for copilots with finance and operations business logic (preview)).

One limit to design around, and it is preview [PP]. api_invoke_action accepts a returnAsResource parameter. Set it to true and the response comes back as an embedded resource instead of inline text, "raising the allowed response size to 5 MB."

Inline responses are "capped at 160,000 characters (~160 KB). Exceeding this limit returns a ResponseTooLarge error." Microsoft's limitations table adds that there is "no workaround for files larger than 5 MB today. Raise the limit through ECS if necessary" — it names ECS and does not expand it, so design to 5 MB rather than to a raised ceiling (Files and attachments with Dynamics 365 ERP MCP (preview)).

Two conditions come with it. That page lists version 10.0.48 or later and the Enable output menu items for MCP feature under its output-scenario prerequisites. And the release plan shows the attachments feature at public preview 24 April 2026 with general availability Jul 2026 not yet marked released (2026 release wave 1).

The counter-argument

"Skip all of this. Let the agent drive the form the way a person would, and you write no code at all."

Sometimes right, and for a genuinely one-off operation it is the pragmatic answer. Form tools reach the button, and the button already contains the logic.

It stops being right at three points. When the operation must be atomic across several entities, a form sequence is a series of steps that can half-complete. When correctness matters more than convenience, an explicit contract with typed inputs beats a sequence of control manipulations that an orchestrator assembled.

And when the logic sits behind a custom control, the form route is closed — Microsoft's third known limitation says "Custom controls aren't supported" (Use Model Context Protocol for finance and operations apps). Unless the same operation is expressible as entity CRUD, the class is what is left.

There is a maintenance argument too. A form-tool sequence is coupled to the form layout, so a personalisation or an update can change agent behaviour. A class with a declared contract changes when you change it.

What to do this week

  1. Take the operation you most want an agent to perform and walk the decision tree honestly. Most candidates stop at "this is entity CRUD" and never need a class.
  2. If you have already built one and the agent cannot see it, open Synchronize Dataverse Custom APIs and then flush the cache. In that order.
  3. Read your CustomAPIDescription strings aloud to someone who does not work on your team. If they cannot tell you when the operation should be used, neither can the orchestrator.

Where we would draw the line

We would not write a class for an operation the data tools already cover, because every one is a deployment, an application-lifecycle artefact and a thing to maintain across updates. We would not expose a broad, general-purpose operation as a single tool — narrow, well-described operations get selected correctly and broad ones get selected wrongly. And we would not ship an operation whose description we could not defend to a colleague. The wiring and the cache decide whether a tool can be called at all; the description decides whether it ever is.

About Cognilium Cognilium is the AI optimization layer for Dynamics 365 — complementary apps that optimize the pricing, inventory, warehouse and planning decisions your ERP manages but can't optimize. Built on Power Platform, Dataverse and Azure. https://cognilium.ai · https://www.linkedin.com/company/37180269/

If you are deciding whether an operation deserves a class or belongs on the data tools, we will walk the decision tree with you on a call. https://cognilium.ai

Sources

Sources

Share this article

Muhammad Mudassir

Muhammad Mudassir

Founder & CEO, Cognilium AI

Mudassir Marwat's argument is that ERP systems record decisions they never optimise.

Founder & CEO of Cognilium AI; 37 AI agents in production across four products; 4 production AI products built and operated; three clouds in production (AWSGCPAzure)
Agentic AIRAG → GraphRAG retrievalVoice AIMulti-Agent Orchestration
Next in this series
What happens to my ERP agent on 1 October 2026?
Chapter 9 · 8 min
In short

Key takeaways

  • Custom X++ logic reaches an agent through a class implementing ICustomAPI, decorated so the orchestrator can discover it, with an action menu item wired into a security role.
  • The natural-language descriptions on the class and its parameters are the interface the model reasons over — not documentation. A poor description produces silence, not an error.
  • The class appears in the Synchronize Dataverse Custom APIs grid only when the interface, the attribute and the role-assigned menu item are all in place. Absence from that grid is the diagnostic.
  • New classes are not invokable until the extension cache is flushed. Most "deployed but invisible" reports end here.
  • Write a class only after the decision tree rules out data tools and form tools. It is the most expensive route and correct only at the bottom.
What goes wrong

Common mistakes to avoid

  • Writing a class for something the data tools already do. It buys a deployment and a maintenance obligation for nothing.
  • Treating the description strings as comments. They decide whether the tool is ever selected.
  • Developing AI tools outside a unified developer environment, which is the only place it is supported.
  • Debugging a missing tool by rewriting the class, when the answer is usually the security wiring or an unflushed cache.