Using Invocable Methods with Skuid

Now that invocable methods are available I wanted to see if I could use them from inside of Skuid. Ideally, it would be great if Skuid could make invocable methods available from the Skuid builder interface similar to the way Process Builder does. It would allow us to write custom Apex code that could be executed directly from the Skuid interface. One particular use-case that comes to mind is the ability to provide users with a one-click lead conversion option rather than redirecting the user to the native lead convert page.

Currently, there is a couple of ways to accomplish this. First, would be to use the Salesforce Ajax API to initiate a lead conversion. However, that is a lot of client side code to write, debug and maintain. Plus all of the logic would be client-side javascript with little chance of re-use or to utilize unit testing to make sure future updates to the system has not affected my existing lead conversion logic. The second option would be to wrap my Skuid page inside of a Visualforce page and implement a controller with RemoteActions that I can call client-side. This option satisfies my requirement to utilize code that has been unit-tested and can be re-usable. But I now have to maintain an additional Visualforce page and controller for each page that I want to expose this functionality. For example, our team utilized this technique to transition a highly customized Visualforce application over to Skuid while keeping the bulk of the already proven and well-tested business logic in Apex.

When Salesforce released invocable methods, I thought this might be a good third option. Using this technique I could expose certain features of my business logic as an invocable method and make that logic available to Process Builder, Rest API, and to Skuid. Fortunately, I was able to utilize some sample code provided by Salesforce that used lead conversion as a use-case for how to implement and utilize invocable methods and invocable variables. You can find that documentation by visiting the InvocableVariable Annotation help page.

So let's just jump right into the code I wrote to call the invocable method using a javascript snippet:

var params = arguments[0],  
    $ = skuid.$;

try {  
    console.log(arguments);

    var model = skuid.model.getModel('LeadData');
    var row = model.getFirstRow();

    var request = '{"inputs":[{"leadId": "' + row.Id + '", "convertedStatus": "Closed - Converted"}]}';
    console.log(request);

    $.ajax('/services/data/v33.0/actions/custom/apex/LeadConvertAction', {
        data: request,
        type: 'POST',
        crossDomain: true,
        dataType: "json",
        beforeSend: function(xhr) {
            xhr.setRequestHeader('Authorization', 'Bearer ' + sforce.connection.sessionId);
            xhr.setRequestHeader('Content-Type', 'application/json');
        },
        success: function(response) {
            console.log(response);
        },
        error: function(jqXHR, textStatus, errorThrown) {
            console.log([jqXHR, textStatus, errorThrown]);
        }
    });
}
catch(e) {  
    console.log(e);
    alert(e);
}

There you have it in all of its simplicity. I was up until 2AM trying to figure this out with a bit of help from the Salesforce StackExchange and a whole lot of trial and error. But the satisfaction of getting a successful result was priceless.

Note: This snippet doesn't represent best-practices and honestly is what I ended up with at the end of a long night when I finally got a successful response. So take it for what it is. There is plenty of room for improvement here.

So let's delve in and talk about what is going on here. The invocable method that I setup is called LeadConvertAction and is pretty much a direct copy of the code utilized in the InvocableVariable Annotation help page. I did modify the response a bit so that I could return any lead conversion errors as part of the response rather than as a thrown exception.

The snippet is pretty much a standard Skuid snippet, and you will notice that I am doing the work of accessing the "LeadData" model and getting access to the current row. As a side note, the arguments object in the snippet already has a reference to the model and row since the snippet is being called from a button on the Skuid page. I could have easily just used params.model or params.row instead.

It was late, and I wasn't thinking straight :)

The request variable is the message body that I will send to my invocable method in JSON format. The parameters that you pass will need to use the object/property name "inputs", which is just part of the contract that invocable methods will expect to see when you make the call. The object values inside the array are what you will need to modify to match the fields that the invocable method is expecting to see. In this case, we have a custom request Apex object where we defined the required properties leadId and convertedStatus. All other fields are optional.

The next hurdle was figuring out how to format the URL. I am a bit uncomfortable with hard coding the invocable method's URL and maybe there is a better way to determine the URL without hard coding, but that is something to explore in the near future. For now just know that you can use a relative URL syntax and the last pathname should be the name of the class that contains your invocable method. Salesforce doesn't need to know what your invocable method is named because you can only have one invocable method per class.

Now, the only thing that was left to do was construct the REST API message, making sure to pass the current session id as part of the request header. Also, I spent a good half-hour trying to debug some "invalid message type" error until I realized I wasn't setting the content-type of the request header to application/json. So be cautious of that.

Also, if you are interested, below is a screenshot of the output from my console window showing the results of the lead conversion.
Invocable Method Output Screenshot


I hope you find this information useful and that you start to explore how to use invocable methods in your Skuid applications. I already have plans to start implementing this in some real-world situations, and as I come across any issues or successes I will be sure to write. Happy coding!

Below is a full copy of the Apex class with the invocable method that I used in this article.

global class LeadConvertAction {  
  @InvocableMethod(label='Convert Leads')
  global static List<ConvertLeadActionResult> convertLeads(List<ConvertLeadActionRequest> requests) {
    List<ConvertLeadActionResult> results = new List<ConvertLeadActionResult>();

    try {
      for (ConvertLeadActionRequest request : requests) {
        results.add(convertLead(request));
      }
    }
    catch(Exception ex) {
      System.debug(ex);
    }

    System.debug(results);
    return results;
  }

  public static ConvertLeadActionResult convertLead(ConvertLeadActionRequest request) {
    Database.LeadConvert lc = new Database.LeadConvert();
    lc.setLeadId(request.leadId);
    lc.setConvertedStatus(request.convertedStatus);

    if (request.accountId != null) {
        lc.setAccountId(request.accountId);
    }

    if (request.contactId != null) {
      lc.setContactId(request.contactId);
    }

    if (request.overWriteLeadSource != null && request.overWriteLeadSource) {
      lc.setOverwriteLeadSource(request.overWriteLeadSource);
    }

    if (request.createOpportunity != null && !request.createOpportunity) {
      lc.setDoNotCreateOpportunity(!request.createOpportunity);
    }

    if (request.opportunityName != null) {
      lc.setOpportunityName(request.opportunityName);
    }

    if (request.ownerId != null) {
      lc.setOwnerId(request.ownerId);
    }

    if (request.sendEmailToOwner != null && request.sendEmailToOwner) {
      lc.setSendNotificationEmail(request.sendEmailToOwner);
    }

    try {
      Database.LeadConvertResult lcr = Database.convertLead(lc, true);
      if (lcr.isSuccess()) {
        ConvertLeadActionResult result = new ConvertLeadActionResult();
        result.accountId = lcr.getAccountId();
        result.contactId = lcr.getContactId();
        result.opportunityId = lcr.getOpportunityId();
        return result;
      } else {
        throw new ConvertLeadActionException(lcr.getErrors()[0].getMessage());
      }
    }
    catch(Exception ex) {
      ConvertLeadActionResult result = new ConvertLeadActionResult();
      result.error = ex.getMessage();
      return result;
    }
  }

  global class ConvertLeadActionRequest {
    @InvocableVariable(required=true)
    public ID leadId;

    @InvocableVariable(required=true)
    public String convertedStatus;

    @InvocableVariable
    public ID accountId;

    @InvocableVariable
    public ID contactId;

    @InvocableVariable
    public Boolean overWriteLeadSource;

    @InvocableVariable
    public Boolean createOpportunity;

    @InvocableVariable
    public String opportunityName;

    @InvocableVariable
    public ID ownerId;

    @InvocableVariable
    public Boolean sendEmailToOwner;
  }

  global class ConvertLeadActionResult {
    @InvocableVariable
    public ID accountId;

    @InvocableVariable
    public ID contactId;

    @InvocableVariable
    public ID opportunityId;

    @InvocableVariable
    public String error;
  }

  class ConvertLeadActionException extends Exception {}
}