It’s the third part of the Copilot Studio and F&O Extensibility Series, and we’re going to learn about client plugins!

Remember that to follow along with the steps of this post, there are some prerequisites that you can check here: What do you need?

Client Plugins: don't mess with them!
Client Plugins: don’t mess with them!

If you missed any of the first two posts of the series, you can read them now:

What’s a client plugin?

With client plugins, you can take action in a specific form. This form will be the context in which the action is available, and then you can call functionality, operations, or the business logic in there.

But hey, wait! Isn’t that EXACTLY THE SAME as the AI tools we learned about last week? That’s what I said too when AI tools were released: “Heh, then I can just use AI tools for everything”.

In next week’s post, we’ll learn in detail the differences between using client plugins and AI tools.

Client plugins extend the SysCopilotChatAction class using a data contract for the input and response parameters, similar to AI tools.

Action types

For client plugins, we can define three types of actions depending on the scope.

Global actions

These are client plugins that have no specific context and can run on any form in the application.

To define a class as a global action, you must decorate it with the SysCopilotChatGlobalAction attribute.

Form actions

Only applicable on the specified form (or forms). Copilot Studio will only run your client plugin of this type if you’re on one of the forms that your class specifies.

To define the forms, you must decorate the class with the Export and ExportMetadata attributes.

To use the Export and ExportMetadata attributes you need to add a using System.ComponentModel.Composition; to your class.

For example:

[ExportMetadata(formStr(BatchGroup), identifierStr(FormName))]
[ExportMetadata(formStr(Batch), identifierStr(FormName))]
[Export(identifierStr(Microsoft.Dynamics.AX.Application.SysCopilotChatAction))]

Runtime actions

Available only where YOU define! That’s real power! I’m joking. 😛

To enable this kind of action, you must use SysCopilotChatAction::addToCopilotRuntimeActions with the following parameters:

  • runTimeAction: the class name that contains your code.
  • removeOnFormChange: a boolean indicating whether the action is still registered when you go to another form.
  • context: an object containing the context to pass to the action.

To disable the action, just call the SysCopilotChatAction::removeFromCopilotRuntimeActions with the runtimeAction parameter.

Input and output parameters

For input parameters, we must decorate the parm method with the DataMember and SysCopilotChatActionInputParameter attributes.

The SysCopilotChatActionInputParameter one takes two parameters, like in AI tools:

  • description: a description of the parameter.
  • isRequired: a boolean indicating whether the parameter is mandatory.

For output parameters, decorate the parm method with the DataMember and SysCopilotChatActionOutputParameter attributes.

The SysCopilotChatActionOutputParameter only takes one parameter:

  • description: a description of the parameter.

You can have as many input and output parameters as you need, not just one.

Implement the executeAction method

And finally, to implement the X++ logic, we need to do it in the void executeAction(SysCopilotChatActionDefinitionAttribute _actionDefinition, Object _context) method.

Here you can use the input parameters that you need in your logic and the output ones to return the values, which can be used later in Copilot Studio.

Creating a client plugin

Now let’s get to work and code a bit. I’m sure you’re wondering which incredible use case I’ve come up with.

A man in a suit giving a thumbs up with a dumb face
Yes, sarcasm.

This time we will be using the Copilot sidecar chat to confirm a purchase order without clicking the form button, and that only works in the PurchTable form.

Create the class

As I said earlier, we need to create a class that extends the SysCopilotChatAction class. I’ll start by showing the top of the class:

using System.ComponentModel.Composition;

[DataContract]
[ExportMetadata(formStr(PurchTable), identifierStr(FormName))]
[Export(identifierStr(Microsoft.Dynamics.AX.Application.SysCopilotChatAction))]
[
    SysCopilotChatActionDefinition(identifierStr(MS.PA.AAS.AP.AASPurchTableConfirm),
        'Confirm a purchase order',
        'Confirms an open purchase order',
        menuItemActionStr(AASPurchTableConfirmAction), MenuItemType::Action)
]

public class AASPurchTableConfirmAction extends SysCopilotChatAction

We decorate the class with the DataContract and note the using on top; otherwise, the Export and ExportMetadata attributes won’t be available.

In the ExportMetadata attribute, we pass two parameters:

  • The name of the form.
  • And the FormName identifier.

If you want this plugin to be called from more forms, you can add as many ExportMetadata lines here.

Then we add the Export attribute with the identifierStr(Microsoft.Dynamics.AX.Application.SysCopilotChatAction) parameter.

And for the last attribute, we add SysCopilotChatActionDefinition with the following parameters:

  • The identifier of your plugin. It must start with MS.PA. and after that you can name it however you want.
  • The name of your plugin.
  • A description for the action it does.
  • An action menu item that calls this class.

Next, we have the class members:

private PurchId purchId;
private boolean hasError;
private str errorMessage;

[   
    DataMember('purchaseOrderNumber'),
    SysCopilotChatActionInputParameter('The purchase order number of the PO you want to confirm.', true)
]
public PurchId parmPurchId(PurchId _purchId = purchId)
{
    purchId = _purchId;
    return purchId;
}

[   
    DataMember('hasError'),
    SysCopilotChatActionOutputParameter('Indicates whether there was an error during the confirmation process.')
]
public boolean parmHasError(boolean _hasError = hasError)
{
    hasError = _hasError;
    return hasError;
}

[   
    DataMember('errorMessage'),
    SysCopilotChatActionOutputParameter('The error message if there was an error during the confirmation process.')
]
public str parmErrorMessage(str _errorMessage = errorMessage)
{
    errorMessage = _errorMessage;
    return errorMessage;
}

As I said, you can have as many input and output parameters. Just make sure you decorate them with the correct attributes.

Finally, here are the executeAction method and a helper method:

public void executeAction(SysCopilotChatActionDefinitionAttribute _actionDefinition, Object _context)
{
    super(_actionDefinition, _context);

    try
    {
        PurchTable purchTable = PurchTable::find(this.parmPurchId());

        if (purchTable.DocumentState != VersioningDocumentState::Approved)
        {   
            throw error(strFmt("Purchase order %1 is not in an approved state and cannot be confirmed.", this.parmPurchId()));
        }

        PurchFormLetter purchFormLetter = PurchFormLetter::construct(DocumentStatus::PurchaseOrder);

        PurchaseOrderId purchOrderDocNum = strFmt('%1-%2', this.parmPurchId(), VendPurchOrderJour::numberOfPurchaseOrderVersions(purchTable) + 1);

        purchFormLetter.update(purchTable, purchOrderDocNum);

        FormRun formRun = _context as FormRun;

        if (formRun)
        {
            formRun.dataSource(formDataSourceStr(PurchTable, PurchTable)).research(true);
        }
    }
    catch (Exception::Error)
    {
        this.parmHasError(true);
        this.parmErrorMessage(AASPurchTableConfirmAction::getInfolog());
    }
}

private static str getInfolog()
{
    SysInfologEnumerator enumerator;
    SysInfologMessageStruct msgStruct;
    Exception exception;
    str error;

    enumerator = SysInfologEnumerator::newData(infolog.copy(1, infolog.num()));

    while(enumerator.moveNext())
    {
        msgStruct = new SysInfologMessageStruct(enumerator.currentMessage());
        exception = enumerator.currentException();
        error = strFmt("%1 %2", error, msgStruct.message());
    }

    return error;
}

This checks that the order is in the Approved status, posts the PO confirmation journal, and refreshes the form.

As a last step, create an action menu item for this class, and we’re done on the X++ side.

Create Copilot Studio topics

We now need to create two topics in Copilot Studio. If you don’t know what topics are, I recommend reading the first article in the series: Copilot Studio 101.

Trigger action topic

The first topic will be used to trigger your action. Go to the Topics tab and click the Add a topic button, selecting From blank:

Create new topic
Create new topic

Give it a short but descriptive name. I’m following kind of a naming pattern for topics, where I have an AAS prefix, a dot, and a short name. I’ve named this one AAS.ConfirmPO.

In the trigger, add some phrases you’d like to use. Like confirm the order:

Copilot Studio trigger
Copilot Studio trigger

Then add an event activity type action from the Advanced menu:

Add event activity
Add event activity

Set the Name field to the same value as the first parameter of the SysCopilotChatActionDefinition attribute in the class. Mine was MS.PA.AAS.AP.AASPurchTableConfirm.

In the Value field, add this expression:

Concatenate("{""purchaseOrderNumber"": """,Global.PA_Copilot_ServerForm_PageContext.titleField1Value, """}")

Here we’re creating a JSON variable with our purchaseOrderNumber input parameter, and as its value, I’m using the first title field of the PurchTable table with the variable Global.PA_Copilot_ServerForm_PageContext.titleField1Value.

If the table doesn’t have a title field, you can use the Global.PA_Copilot_ServerForm_PageContext.rootTableRecId variable and replace the input parameter for a RecId referencing the PurchTable record.

Finally, add an End current topic action from the Topic management menu:

End current topic
End current topic

Save it, and our trigger topic is ready.

Response topic

Now we need a topic to process the response. I’ve named this one AAS.ConfirmPOResponse.

Change the trigger to the type A custom client event occurs. Set its Event name property to MS.PA.AAS.AP.AASPurchTableConfirm.

Now we need to add a Parse value action from the Variable management menu:

Create X++ Client Plugins for Copilot Studio in Dynamics 365 F&O
Parse action

Here we will parse the response from our class. Set the Parse value field to the System.Activity.Text variable (in the System tab):

Activity.Text variable
Activity.Text variable

Then in the Data type field, select From sample data and click the Get schema from sample JSON button. Paste this sample:

{
    "isResponse": true,
    "purchaseOrderNumber": "PO-00001",
    "hasError": true,
    "errorMessage": "Oh, no! An error"
}

This is similar to parsing JSON on Power Automate. You’ll now have these four JSON fields available individually.

Now add an Add a condition control. We’ll show the user a different message depending on the outcome of the confirm operation.

On the left condition, we’ll manage errors. Select the ConfirmResponse.hasError variable and check if it’s true:

Error condition
Error condition

Then add a Send a message action, with the following:

  • Text: “The purchase order “
  • Variable: ConfirmResponse.purchaseOrderNumber
  • Text: ” couldn’t be confirmed, an error occurred. “
  • Variable: ConfirmResponse.errorMessage

So it becomes human-readable when it’s displayed.

In the other branch and add a Send a message action with the following:

  • Text: “Purchase order “
  • Variable: ConfirmResponse.purchaseOrderNumber
  • Text: ” confirmed!”

Save your topic, and now click the Publish button so it’s available on F&O’s sidecar chat.

Hey! What about the DataAreaId!?

Before testing the plugin, let me answer this question.

If you remember from the previous post about the AI tools, we had to explicitly specify the company in which the code was running; otherwise, it would take the default company.

With client plugins, the company that the code will run in is the active one. That’s also another benefit of the context!

Test it!

Now go to the purchase order form and select one that’s not confirmed. Open the sidecar chat and tell Copilot, “Confirm the order”. The process will run and…

Order confirmed!
Order confirmed!

We have confirmed the order! Well, not we, but Copilot Studio.

Now let’s try to run the other conditional branch, the one where we get an error. Just try confirming an order that is already confirmed.

That works too! We get the error message in the sidecar chat!

The error appears
The error appears

As expected, we get the message from our throw error in the function.

Conclusions

Client plugins give Copilot superpowers inside the form you’re on—perfect when you need context or guardrails.

We’ve learned the essentials:

  • Define the scope (Global, Form, Runtime).
  • Decorate the class (DataContract, Export/ExportMetadata, SysCopilotChatActionDefinition).
  • Create inputs and outputs.
  • Implement executeAction().
  • Create the trigger and response topics.

Reach for client plugins when the action must respect the current form/session and feel like a menu action; keep AI tools in mind for broader, cross-form logic.

In the next post, we’ll put them head-to-head so you know exactly when to use each.

See you in part 4!

Subscribe!

Receive an email when a new post is published
Author

Microsoft Dynamics 365 Finance & Operations technical architect and developer. Business Applications MVP since 2020.

Write A Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.