I have a confession to make. In my five years working with Dynamics CRM, its successor, Dynamics 365 Customer Engagement (CE) and, more recently, the Power Platform and Microsoft Dataverse, I have never once set up, worked with or leveraged custom actions. Of course, I had a sketchy awareness of what they could do (as that’s the only way I would’ve been able to pass exams! 😏) and had heard multiple people extolling their virtues. But I had never once been tempted to look at them further or, perhaps more crucially, found a customer requirement that I thought they could be a good match for. Notwithstanding all of this, they remain an undoubtedly powerful tool for developers looking to craft their own bespoke Messages within a Dynamics 365 CE / Microsoft Dataverse environment, which can then support their own set of input or output parameters. These messages can then be called via code or by functional specialists via classic workflows or Power Automate flows. Impressive stuff, to be sure.

So with the recent hullabaloo regarding Custom API’s, I committed to embracing and learning more about them, if only to make up for my previous stubbornness against custom actions. Now, I can finally see what all the fuss is about. 😁 We can best think of Custom API’s as a new and improved variant of custom actions, and they do share a lot of the same capability. However, they have a few specific advantages that may make them preferable to utilise:

  • It is possible to define a set of privileges a user must have before calling a Custom API. Custom Actions have no limitation and, therefore, could be subject to users executing them without regard to a formal process or any restrictions on their user account.
  • Custom API’s can be marked as private, thereby preventing other users or developers from using them. Custom Actions must always be public, in comparison.
  • Unlike Custom Actions, Custom API’s support more modern features, including creating OData functions, binding an operation to table collection and providing localised labels covering different languages.

Custom actions very much remain a supported and acceptable approach for you to adopt. But I would urge developers to review the capabilities within Custom API’s first, and if they can meet your particular needs, use them over custom actions as much as possible.

I recently built out my first Custom API and had a requirement to call this via a JavaScript function tied to a Ribbon button. Straightforward stuff if you are using a custom action – call the Xrm.WebApi.online.execute method and refer to the convenient sample Microsoft provide for such a situation. However, looking at the documentation, it is unclear how you would do the same for a Custom API. As it turns out, the process is remarkably similar to custom actions, which might not be too surprising. Let’s assume we have a Custom API setup that looks a little something like this:

Which has three request parameters defined like so

  • SampleGUID: Setup with a type of Guid
  • SampleBoolean: Setup with a type of Boolean
  • SampleString: Setup with a type of String

And a single output (response) parameterEntityID – which, in this case, we assume links back to a record that our Custom API will create. We can now use code similar to the below to call the API and pass in the required parameters:

if (typeof (CRMChap) === 'undefined') 
{var CRMChap = {__namespace: true};}

var Sdk = window.Sdk || {};
/**
 * Sample custom API properties
 * @param {boolean} sampleBoolean - Sample data value of type boolean
 * @param {GUID} sampleGUID - Sample data value of type GUID
 * @param {string} sampleString - Sample data value of type string
 */
Sdk.jjg_SampleCustomAPI = function(sampleBoolean, sampleGUID, sampleString) {
    this.SampleBoolean = sampleBoolean;
	this.SampleGUID = sampleGUID;
	this.SampleString = sampleString;
};
// NOTE: The getMetadata property should be attached to the function prototype instead of the
// function object itself.
Sdk.jjg_SampleCustomAPI.prototype.getMetadata = function () {
    return {
        boundParameter: null,
        parameterTypes: {
            'SampleGUID': {
                'typeName': 'Edm.Guid',
                'structuralProperty': 5 // Entity Type
            },
            'SampleBoolean': {
                'typeName': 'Edm.Boolean',
                'structuralProperty': 1 // Primitive Type
			},
			'SampleString': {
                'typeName': 'Edm.String',
                'structuralProperty': 1 // Primitive Type
            }
        },
        operationType: 0, // This is an action. Use '1' for functions and '2' for CRUD
        operationName: 'jjg_SampleCustomAPI',
    };
};

CRMChap.RibbonFunctions = {
		
	callCustomAPISample: function (formContext) {
        'use strict';
        //Get current record ID (i.e a GUID we can pass)
        var guid = formContext.data.entity.getId();
        //Call the Custom API	
        var jjgSampleCustomAPIRequest = new Sdk.jjg_SampleCustomAPI(false, entityID, 'This is a test');       
        Xrm.WebApi.online.execute(jjgSampleCustomAPIRequest).then(
            function (result) {
                result.json().then(
                    function (response) {
                        //TODO: Add your logic here. In this case, we may want to read the output parameter defined using the following code:
                        var entityID = response.EntityID;
                    }
                );
            },
            function (error) {
                Xrm.Navigation.openErrorDialog({ details: error.message, message: 'An error occurred while calling the jjg_SampleCustomAPI Custom API.'});
            }
        );      
    },

    __namespace: true
};

As we can see, the approach is virtually indistinguishable from what we would follow when working with custom actions. Which I think makes it even more of a good reason for developers to check them out, start using them and unlock some of the benefits they can bring to the table.

Share This