Category Archives: CRM2011

How-To: Overcome Rollup Field Limitations with Rolling Batch Processing.. Even In the Cloud

Published by:

Rollup fields are great. But their filters are limited. The most common use case I can imagine is something like year to date sales. But you can’t do that with rollup fields because the filters don’t have relative dates. Want quarter-to-date sales? Sorry folks.

Here’s how to make it happen. This works.. even in the cloud. It should also work in 2011 although I have not tested it there.

In this scenario, we are using a Connection between a Product and an Account to hold quarter to dates sales numbers. I want to regularly calculate the sum of Invoices against the Account for the given Product and save it on the Connection. I’m calling the Connection my “target entity.”

Overcoming the Depth Property and Timeouts

Normally, you could create a recursive workflow that processes something, then calls itself. But you run into a problem with the Depth property; CRM will stop the process after so many iterations and consider it a runaway. So if your workflow calls itself, the 2nd time it runs the Depth property will be 2. After so many times, if Depth = x (I think 15 by default), CRM will kill the process.

The secret comes from Gonzalo Ruiz: the Depth property is cleared after 60 minutes.

The other issue is CRM’s timeouts; you need to make sure the update process doesn’t croak because it’s chugging through too many records.

So we’re going to chunk up our data into 1000 batches and run each batch asynchronously every 61 minutes. A lot of processes doing a little bit of work each. I don’t recommend this with synchronous processing.

The Approach

Here’s the process we’re going to create.

  1. Upon create, assign your target entity a random batch number. I’m using 1000 batches.
  2. An instance of a custom entity contains a batch number (1000 batch controller records for 1000 batches). A workflow fires on this custom entity record every 61 minutes.
  3. The workflow contains a custom workflow activity that updates all target records in its batch with a random number in a “trigger” field.
  4. A plugin against your target entity will listen for the trigger and fire the recalc.

2015-10-06 15_27_28-Drawing1 - Visio Professional

First, create a custom entity. I’ve called mine rh_rollingcalculationtrigger. All you need on it is one integer field.

Now, on your Connection (or whatever entity you want to store the rolling calculated fields), create two fields: rh_systemfieldrecalculationtrigger, and rh_systemfieldrecalculationbatch.

Now create a simple plugin to set the batch number to between 0-999 when the record is created. If you have existing records, you can export to Excel and reimport them with a random batch assignment – the Excel formula randbetween() is great for this.

protected void ExecutePostConnectionCreate(LocalPluginContext localContext)
	if (localContext == null)
	throw new ArgumentNullException(localContext);
	Random rand = new Random();
	IPluginExecutionContext context = localContext.PluginExecutionContext;
	Entity postImageEntity = (context.PostEntityImages != null && context.PostEntityImages.Contains(this.postImageAlias)) ? context.PostEntityImages[this.postImageAlias] : null;
	ITracingService trace = localContext.TracingService;
	IOrganizationService service = localContext.OrganizationService;

	// super-simple update
	Entity newConnection = new Entity("connection");
	// Add a random number between 0 and 1000.
	newConnection["rh_systemfieldcalculationbatch"]= rand.Next(0, 1000);
	newConnection.Id = postImageEntity.Id;

(Side note: In C#, Random.Next() returns a value exclusive of the upper bound. So each record will get a value 0-999 inclusive.)

Now, we create a custom workflow activity. This inputs the batch number and fires a process called FireBatch. So when this workflow runs on a rh_rollingcalculationtrigger entity with Batch ID = 5, it will call FireBatch against all records with batch ID = 5.

In my case, I like to assemble the service, context and tracing service in the plugin and call my own class.

public sealed class WorkflowActivities : CodeActivity
        /// <summary>
        /// Executes the workflow activity.
        /// </summary>
        /// <param name="executionContext">The execution context.</param>
        protected override void Execute(CodeActivityContext executionContext)
            // Create the tracing service
            ITracingService tracingService = executionContext.GetExtension<ITracingService>();

            if (tracingService == null)
                throw new InvalidPluginExecutionException("Failed to retrieve tracing service.");

            tracingService.Trace("Entered Class1.Execute(), Activity Instance Id: {0}, Workflow Instance Id: {1}",

            // Create the context
            IWorkflowContext context = executionContext.GetExtension<IWorkflowContext>();

            if (context == null)
                throw new InvalidPluginExecutionException("Failed to retrieve workflow context.");

            tracingService.Trace("Class1.Execute(), Correlation Id: {0}, Initiating User: {1}",

            IOrganizationServiceFactory serviceFactory = executionContext.GetExtension<IOrganizationServiceFactory>();
            IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);

                IWorkflowContext wfContext = executionContext.GetExtension<IWorkflowContext>();
                IOrganizationServiceFactory wfServiceFactory = executionContext.GetExtension<IOrganizationServiceFactory>();
                IOrganizationService wfService = wfServiceFactory.CreateOrganizationService(context.InitiatingUserId);
                int batchId = (int)this.BatchNumber.Get(executionContext);
                Guid thisGuid = ((EntityReference)this.ThisEntity.Get(executionContext)).Id;
                RollupCalculations.FireBatch(service, tracingService, batchId, thisGuid, context.Depth);
            catch (FaultException<OrganizationServiceFault> e)
                tracingService.Trace("Exception: {0}", e.ToString());

                // Handle the exception.

            tracingService.Trace("Exiting Class1.Execute(), Correlation Id: {0}", context.CorrelationId);
        [Input("Batch Number")]
        public InArgument<int> BatchNumber { get; set; }
        [Input("This Config Entity")]
        public InArgument<EntityReference> ThisEntity { get; set; }

FireBatch: query all records with batchID = x and update them with a random number.

// In this case, the processingSequenceNumber is going to be the value from the CRM batch controller entity.
public static void FireBatch(IOrganizationService service, ITracingService trace, int processingSequenceNumber)
            // First, get all Connections with that batch ID.
            String fetch = @"
            <fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'>
              <entity name='connection'>
                <attribute name='connectionid' />
                <order attribute='description' descending='false' />
                <filter type='and'>
                  <condition attribute='rh_systemfieldcalculationbatch' operator='eq' uiname='' uitype='product' value='" + processingSequenceNumber + @"' />

            // Now do a bulk update of them. 
            EntityCollection result = service.RetrieveMultiple(new FetchExpression(fetch));
            trace.Trace("Processing " + result.Entities.Count + " records on batch " + processingSequenceNumber);

            ExecuteMultipleRequest multipleRequest = new ExecuteMultipleRequest()
                Settings = new ExecuteMultipleSettings()
                    ContinueOnError = false,
                    ReturnResponses = false
                Requests = new OrganizationRequestCollection()

            Random rand = new Random();
            int testLimit = 0;
            if (result != null && result.Entities.Count > 0)
                // In this section _entity is the returned one
                foreach (Entity _entity in result.Entities)
                    Guid thisGUID = ((Guid)_entity.Attributes["connectionid"]);
                    var newConnection = new Entity("connection");
                    newConnection.Id = thisGUID;
                    // Note here that we're just dropping a random number in the field. We don't care what the number is, since all it's doing is triggering the subsequent plugin.
                    newConnection["rh_systemfieldrecalculationtrigger"] = rand.Next(-2147483647, 2147483647);

                    UpdateRequest updateRequest = new UpdateRequest { Target = newConnection };
                    //trace.Trace("Completed record #" + testLimit);


Warning: use ExecuteMultiple to avoid timeouts.

Now, create a plugin against the Connection to do whatever it is you want. It should fire on change of the rh_systemfieldrecalculationtrigger field.

        protected void ExecutePostConnectionUpdate(LocalPluginContext localContext)
            if (localContext == null)
                throw new ArgumentNullException("localContext");

            IPluginExecutionContext context = localContext.PluginExecutionContext;
            Entity postImageEntity = (context.PostEntityImages != null && context.PostEntityImages.Contains(this.postImageAlias)) ? context.PostEntityImages[this.postImageAlias] : null;
            ITracingService trace = localContext.TracingService;
            IOrganizationService service = localContext.OrganizationService;

            // In this method I do the logic I want to do against the specific record.
            RollupCalculations.SalesToDate(service, trace, postImageEntity);

The final piece is to, well, get it rolling. First, create 1000 instances of rh_rollingcalculationtrigger, setting Batch ID’s 0-999.

Remember, we can create a recursive workflow with the 60 minute workaround. I’m setting it to 61 just to be safe.

2015-10-06 15_09_06-Document1 - Word

Manually fire the workflow once on each of your 1000 recalculation entities. Congratulations, you have perpetual recalculation.

I recommend setting up bulk deletion jobs to remove the system job records this creates. It can be a lot.

ClickDimensions: What a bug, what a bug..

Published by:

We use ClickDimensions a lot for marketing automation. It’s a pretty good tool. But today, working with their support, I came across this doozie of a bug.

I had a form that was working fine unless an option set for country was on it.

Here’s what we found – when you create a web form and map it to an option set, it will replace the substring “options” in the field name with “select”. My option set was called “new_countryoptionset” and ClickDimensions failed because internally it thought the field was called “new_countryselectet”. I recreated the field and called it “new_countrypicklist” and it worked fine.

I don’t know whether to laugh or cry. But if you are having issues with option sets in ClickDimensions keep this in mind.

Perform a Mileage Expense Calculation Using EasyPlugins

Published by:

We’ve recently deployed a few custom plugins based on the EasyPlugins tool for Microsoft Dynamics CRM 2013 and Microsoft Dynamics CRM 2015, available for free on CodePlex. In this blog we’d like to share some of the things we’ve learned, since the tool is great but not very well documented.

In this blog we’ll review how to calculate an expense mileage reimbursement amount. There are other ways to do this, but this way allows for business users to manage reimbursement rates and effective dates. I’m also making it it slightly complex because I want to demonstrate some of the things that aren’t covered in the official documentation.

In my example, I have:

  • An entity called ics_expense that contains:
    • A date (ics_date) that, by business requirement, the user must be able to leave blank. If they do, the date will default to today.
    • The per-mile rate that was applied (ics_permilerate), a Currency value.
    • A distance value (ics_distance) as a Decimal value.
    • An Amount value (ics_amount), also a Currency, that represents the amount to be paid to the employee.
    • An Option Set (picklist) (ics_type)
      • 100000000 : Itemized Expense
      • 100000001 : Mileage
  • An entity called ics_mileagereimbursementrate, which contains:
    • An effective start date (ics_effectivestartdate) as a Date
    • An effective end date (ics_effectiveenddate) as a Date
    • A reimbursement rate (ics_reimbursementrate) as Currency.

When the Expense is saved, this plugin should:

  • Identify if the user filled in the date (ics_date), if not default it to today.
  • Pull the mileage reimbursement rate that should be applied based on that date. (Design assumption is that there’s only one rate applicable for a given date.)
  • Calculate the amount to reimburse.
  • Fill in the total amount on the Expense but only if it’s of type Mileage.

Here’s the approach:

  1. Stage the data (left side of the EasyPlugins screen)
    1. Pull down the user-input date, the type, and the distance and store them in variables local to EasyPlugins.
    2. Determine if I need to use the user’s date or default it to today.
    3. Pull down the appropriate rate.
    4.  Calculate the total reimbursement.
  2. Update the Expense (right side of the EasyPlugins screen)
    1. Set the Per Mile and total expense fields on the Expense.
    2. Set the date on the Expense if the user left it blank.

Start by creating a new Plugin using EasyPlugins ( – install the package then access it under Settings -> EasyPlugins then selecting New -> Plugin. Give it a name and set it to run PostCreate, Post Update, and Synchronously.

First I need to stage up some data on the left side. First, I’m going to pull down the metadata from the entity. Add Attributes:

  • ics_date – becomes #p1
  • ics_distance – becomes #p2
  • ics_type – becomes #p3

For these, we want the State to be “after action” since we want the value the user actually input.

Now, we determine whether to use the user’s date or ours. To do this, we check if the user input one and if so use that, otherwise make a new one. To do this we use C# syntax in a Calculated field:

#p1 == null ?
DateTime.Today :

Hit Test to make sure your expression is syntactically correct. This will place your new value in #p4.
You may find this syntax unfamiliar. In a more familiar format, it might look like:

if (p1 == null)
#p4 = new DateTime();
#p4 = #p1;

Now, find the Expense reimbursement rate to use. We need to find the rate that applied as of the date of the Expense, which is now held in #p4. To do this, we need to find the rate with an effective start date in the past, an effective end date in the future. Construct an Advanced Find like this – the dates you use don’t matter at this point. Make sure the reimbursement rate is in the returned values – add it using Add Columns.

Download the FetchXML. A few edits you need to make:

  1. Delete all attributes except ics_mileagereimbursementrate. That’s the number we’re really after.
  2. In the date fields, replace the value of the date with your calculated date variable, #p4.

It’ll look like this when ready:

<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
  <entity name="ics_mileagereimbursementrate">
    <attribute name="ics_reimbursementrate" />
    <order attribute="ics_reimbursementrate" descending="false" />
    <filter type="and">
      <condition attribute="ics_effectivestartdate" operator="on-or-after" value="#p4" />
      <condition attribute="ics_effectiveenddate" operator="on-or-before" value="#p4" />

Remember, our assumption going in was that there’s only one reimbursement rate that applies for a given date. So this should only return one record.

Now drop it in a Request parameter. In this case the Aggregate Function is “First” since we are expecting only one record to come back and we want the value from the first returned record.
The results of this will be placed in an array called #p5.

The last thing to do is to use this rate to calculate the actual amount to reimburse. Enter a Calculated parameter and use this syntax:

Convert.ToDecimal(#p5[0]) * #p2

We need to be careful here.

  • #p2 is the mileage and is a Decimal value. So to make sure we’re calculating apples to apples, we need to convert the value returned from the query by using Convert.ToDecimal (C# syntax).
  • #p5 is the results of our query. Since we didn’t use an aggregate function, it comes back as an array. Rather than using the variable #p5 straight-up, we need to force it to read the first record, thus the [0] syntax.

And that’s #p6, the actual amount to pay the employee.

Phew. A lot of work but our data is now staged and ready to go. To recap:

  • #p1 is the date the user set on the entity.
  • #p2 is the distance traveled as provided by the user.
  • #p3 is the option set that indicates the type of Expense (mileage or otherwise).
  • #p4 is the date the expense occurred – either provided by the user or defaulted to today.
  • #p5 is the array of expense rates that encompass your effective date.
  • #p6 is the total reimbursable amount (distance * rate).

First, we need to update the Expense with the mileage rate and the calculated value. On the right side, Add an Update record.

  • Entity is ics_expense.
  • Where ID = #id (Current ID) – we’re updating the current record, not anything related.

But how do we filter it so that this only happens if the type is Mileage on the Expense? We need to get it to read the Option Set (picklist) value. That’s stored in #p3. Option Sets are managed by EasyPlugins with a Code and a Label attribute. To get at them, you need to use:


But since we are looking for a Condition here, you need to force the Code value to a String and then check if it’s the value you’re after.


Tricky, but that’s what worked for me. Then, just set the value of the amount and the rate.
Then, set:

  • ics_amount = #p6
  • ics_permilerate = #p5[0]

What’d we miss? We didn’t set the Date. We need to use a separate Action step because we only want to override it if the user didn’t fill it out, regardless of the type of Expense. (The last Action only applied to Mileage records.)

Last step: make another Update action.

  • Entity = ics_expense
  • Filter = #p1 == null (you can check if a variable is null with a simple == operator like this)
  • Where ID = #id

And set the date field to #p4.
There you have it, folks.

Associate A Record To One of Two Entity Types (CRM 2011)

Published by:

Here’s another old blog I’m republishing. This functionality is now native in CRM 2013 and CRM 2015 as Business rules.

Sometimes, you want to one same entity and associate it to two other entity types. For example, you might have a License that could apply to either an Account or a Contact. You could create two entities – an “Account License” and a “Contact License,” or you could use this simple technique to use one entity that appears to be linked to a Contact or an Account.

Here’s how:

  1. Create your new entity.
  2. On the Entity form, add three fields:
    1. A “Two Options” field called “Owned By,” and set option 0 to be Account and 1 to be Contact. Set the default to Account (1).
    2. A Lookup to a Contact called something like “Owner (Contact)”
    3. A Lookup to an Account called something like “Owner (Account)”.
  3. Create a new Web Resource with the following code:
    function licenseOnLoad()
            if (Xrm.Page.ui.getFormType() == 1)
                            // On form create (formType = 1), the system will map either the Contact or Account that it was created from.
                            // We defaulted the Owned By to assume it's an Account.
                            // So if it's a Contact, change the field.
                              if ("new_ownercontact").getValue() != null)
            // Disable this field in all cases, not just on create.
            // Call the below function which will display only Account or Contact to the user.
    function accountContactToggle()
        // Toggles ownership field (contact/account) based on the user input.
            var ownedBy ="new_ownedby").getValue();    // Account = 0; Contact = 1
            if (ownedBy == 1) // Account
            if (ownedBy == 0)  // Contact
  4. In the Form Properties, in the Events tab, add the new Web Resource and set the onLoad() to call the new licenseOnLoad() function.

As this is jScript, if you are deploying on mobile platforms, be careful.

One drawback to this approach is that if the user creates a License but not from Account or Contact, such as from the FILE tile in the top left, there will be no mapping to indicate Account or Contact ownership, so the License will always be associated to Account. You can enforce this with process or provide a Dialog or Workflow to allow users to fix retroactively.

How to Flip Sender and Recipient in a Phone Call Record

Published by:

There is an annoying tendency in Microsoft Dynamics CRM 2011 for the system to set the Recipient of a Phone Call incorrectly. For example, when adding a Phone Call to a Case, it assumes the Recipient is the Account of the Case it is tied to and marks the call as Incoming.

preview flip sender (1)

It doesn’t make sense that an Incoming call would be directed to an Account. Rather, it should be a User.

The business rule I needed to implement was:  if the call is Incoming, the Recipient must be a User and conversely, if the call was Outgoing, the Sender must be a User.

Here is some scripting you can implement – based on Rajeev Pentyala’s blog based here:
It’s tricky to work this out since there are Activity Parties / Party Lists involved.

Note here that I have some utility functions that contain some Xrm.Page functions. This will make it easier to update encase there is another change in Microsoft’s syntax such as the 4.0 to 2011 transition.

Here are my definitions:

· getFieldValue returns
· getAttribute returns Xrm.Page.getAttribute(field)
· getAttributeValue returns Xrm.Page.getAttribute(field).getValue()

First, the following function will return true if the field contains a record of the type you need. For example, if you want to see if Sender is a User, send it “from” and “8”. This just pulls the values from the array and if it finds one of the type you need, returns true.

function isPartyPresent(field, type)

      // Parameters :
      // Field is the field to look in
      // Type is the ObjectTypeCode of the thing you are looking for (eg 8 = User)
      var partyArray = getAttributeValue(field);
      if (partyArray == null)
      {  // If partyArray comes back null, there is nothing there, so there is no party present.
            return false;
      for (var indxParties = 0; indxParties < partyArray.length; indxParties++) {
      if (partyArray[indxParties].type == 8) {
            return true;
return false;

Next, implement a quick function that will tell you if there is an “inversion.”

function detectPartyInversion()

      var direction = getFieldValue("directioncode");
      // Direction -> Incoming = 0 (False) and Outgoing = 1 (true)
      if ((direction == false) && (isPartyPresent("to",8) == false ))
            return true;
      if ((direction == true) && (isPartyPresent("from",8) == false ))
            return true;
      return false;

This allows us to implement a pretty simple handler to detect the inversion and correct it.

function invertedPartyHandler()
      if (detectPartyInversion())
            var recipient = getAttribute("from");
            var sender = getAttribute("to");
            var recipientValues = getAttributeValue("from");
            var senderValues = getAttributeValue("to");

Finally, we can run this check onLoad.

function phoneCallOnLoad()
      if (getFormType() == 1)

All that’s left to do is to add the phoneCallOnLoad() function to the onLoad event of your form.

How to Prevent Ribbon Actions Based on User Input (CRM 2011)

Published by:

To kick things off, here’s a classic blog from a few years back.

Alright ladies and gents, this is a fairly hefty blog post but is also very informative.

I was once asked to implement a requirement where orders could not be submitted if one or more Quote Products was a write-in in Microsoft Dynamics CRM 2011.

To do this, we needed to modify the button in the Ribbon that submits the order. Here’s a simple way to do that. It looks complicated but it’s really not that bad!

Here’s a step-by-step guide:

Using the Visual Ribbon Editor on Codeplex ( , open your Organization and the Quote entity.

Notice that the Create Order button is there, but since it’s a System button, it can’t be edited. So we’re going to have to replace it with our own. Make a new button and copy the Display Rules from the native button. In this case, the “Entity Salesorder Privilege = Create Basic” rule is an EntityPrivilegeRule. Under Enable Rules, copy the JavaScript path and function names into two new rules. Hold off on the Action section for now.

Your new button’s configuration will look like this so far:




Now comes the harder part. You need to make your own jScript function that will run a quick web service call to see if there are any Quote Products that are write-ins.

First, make an Advanced Find with the query you want. In this case, we are looking for Quote Products where the Quote is the specific one we are on. So when making the Advanced Find, pick any Quote in the system for now. Later we’ll have the jScript swap out the individual Quote GUID on the fly.

Here is our Advanced Find:

Download the FetchXML into a Text file. This is going to be added to jScript so we need to wrap each line in quotes to turn it into a string (meaning the single tick at the beginning and the single tick at the end, the plus concatenating it):

'<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">'+
' <entity name="quotedetail">'+
' <attribute name="productid" />'+
' <attribute name="productdescription" />'+
' <attribute name="priceperunit" />'+
' <attribute name="quantity" />'+
'<attribute name="extendedamount" />'+
' <attribute name="quotedetailid" />'+
' <order attribute="productid" descending="false" />'+
' <filter type="and">'+
' <condition attribute="isproductoverridden" operator="eq" value="1" />'+
' </filter>'+
' <link-entity name="quote" from="quoteid" to="quoteid" alias="ad">'+
' <filter type="and">'+
' <condition attribute="quoteid" operator="eq" uiname="Interested in our newer offerings (sample)" uitype="quote" value="{6EE28A24-602A-E111-8EBC-1CC1DEE87ACD}" />'+
' </filter>'+
' </link-entity>'+
' </entity>'+

Now we are going to make a jScript function out of this that will tell us the quantity of Quote Products that match. This requires the XRMServiceToolkit from Codeplex ( You don’t have to know how it works. Download the code from Codeplex and add it to your system as a Web Resource of type Script. Then add the Web Resource to your form (Form Properties, then Form Libraries). Back in your text editor, make a jScript function out of your query.

function getQuantityWriteInOrders()


var fetchQuery =
'<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">'+
' <entity name="quotedetail">'+
' <attribute name="productid" />'+
' <attribute name="productdescription" />'+
' <attribute name="priceperunit" />'+
' <attribute name="quantity" />'+
' <attribute name="extendedamount" />'+
' <attribute name="quotedetailid" />'+
' <order attribute="productid" descending="false" />'+
' <filter type="and">'+
' <condition attribute="isproductoverridden" operator="eq" value="1" />'+
' </filter>'+
' <link-entity name="quote" from="quoteid" to="quoteid" alias="ad">'+
' <filter type="and">'+
' <condition attribute="quoteid" operator="eq" uiname="Interested in our newer offerings (sample)" uitype="quote" value="' + + '" />'+
' </filter>'+
' </link-entity>'+
' </entity>'+
var returnArray = XrmServiceToolkit.Soap.Fetch(fetchQuery);
if (returnArray == null)
{ return null; }
return +returnArray.length;

A couple of things here. Note that in the “condition attribute” clause, I replaced what was the GUID from the Fetch download with the method. This will insert the GUID of the current Quote you are on at run-time. In short, it makes the query say, get me a list of Quote Products that are write-ins that are associated to the Quote I am looking at right now.

XRMServiceToolkit will return the results of the query as an array. Since we are only interested in the quantity, we can just return the length of the array. The + operator forces the return to an integer value.

Now we create the control function. This will simply run the query, get how many Quote Products match, and if the number is greater than zero, die.

function checkOrders()
if (getQuantityWriteInOrders() == 0 )
alert ("You can not submit orders with write-in products.");
return null;

Where did acceptQuoteOrCreateOrder() come from? It’s the native function within CRM that pops the Quote Submit screen. We know this from the definition of the original Create Order button back in the Ribbon editor:


Basically, the checkOrders function runs the query and only lets you submit the order if the quantity of Write-In products is zero.

Take the two functions and add them to CRM as a Web Resource, then add it to the Form. Your form Properties will look like this:


No need to set triggers onLoad, onSave or onChange since we are going to fire this code when the user clicks the Ribbon button.

Now, we need to add the Action to our new custom button. Back in the Ribbon editor, add a JavaScript function under the Action tab.


checkOrders is the name of the function we wrote above. To get the Library URL, it’s at the bottom of the Web Resource configuration screen:


In this case, we pull off the beginning part and keep everything after WebResources; start with one slash not two (“/WebResources/new…”)
Lastly, use the toggles in the Sequence field to bump the button left or right to be next to the old one.

Once you save your Ribbon in the Editor, your new button will be published.

Once it works, you change the Label of the new one to “Create Order” and disable the original button by clicking Hide. Even though you’ve replaced the button, it looks like the original button to users.