Create your own actions with Apex Classes

Last published at: 2023-03-31 21:03:01 UTC
Delete

The Run An Apex Class action is available for the Premium and PDM licenses.

With Salesforce's Apex Classes you can execute apex code for almost any purpose you want. By running an Apex Class as a DAP Action, your end users can execute Apex Classes as easy as any other DAP action, letting them run complex tasks that go beyond what standard actions can accomplish.

Use cases

Use cases for the Run an Apex Class action could be for example:
- Update multiple records with data from external sources
Write an Apex Class that integrates with an external API or database. Use it in the Run an Apex Class action to fetch and update a selection of records in Salesforce with the latest data from that source.
- Execute complex calculations
The Run an Apex Class action can automate performing complex calculations on records, such as aggerating data from multiple fields, or comparing values across multiple records.
- Initiating custom business processes or logic that goes beyond the capabilities of standard DAP actions, or of Salesforce tools like Process Builder or Workflow.

Provide your Apex Classes as an action in DAP job, or in list views via the Action Launcher. This way your users can apply an apex class to multiple records at once in batch, schedule the class execution, filter records to apply it to, and much more. If you provide classes as a Macro‍, users cannot change the configuration and will always use the correct settings.

Apex Classes need to adhere to certain requirements in order to be compatible with DAP actions. This article explains how to set up an Apex Class that is suitable for use in a DAP action. Some knowledge of writing Apex Classes is required, but examples will be provided.
Once the class is written, you can run it in the Run An Apex Class action‍.

Best practices

When writing the class, there are a few things to keep in mind. The following guidelines will produce effective code that will make sure you do not run into limits unnecessarily. 

  • Query all the data you need in one go, instead of running a query for each record.
  • Query and prepare before your processing loop, and clean up after your processing loop, instead of doing all this inside the loop.
  • Do not throw exceptions in the loop, but catch them and add them to the dapCustomApexActionOutputV1 error map.

Writing the apex code

The methods to implement are described more extensively further below the format example. At the end of this article you can find more examples.

1. In Salesforce Setup, open Apex Classes and click New.

2. Create the custom action by implementing the dapInterfaceCustomApexActionV1 interface. Make sure that the action class is global.

global class MyCustomApexAction implements plauti.dapInterfaceCustomApexActionV1 {
    ... public methods declare here ... }

3. In the processRecords method, define what should happen with the records that are passed in via the dapCustomApexActionInputV1 object. It then needs to return a dapCustomApexActionOutputV1 object.
Note that instantiating a new dapCustomApexActionInputV1 will not work. Please use the one that is provided as a parameter.
Please see the extensive method explanation below for more information, e.g. about the correct way to handle exceptions.

4. In the generateAuditLogSummary method, generate a label-value pair to display on the Summary tab of the Action Audit Log info modal. This will be in addition to the label-value pairs already set. When this does not need to be used, please make sure it returns null.
Note that it will use the key string as label and the value part as the value.

5. In the generateActionSummary method, generate a label-value pair to display on the Confirmation screen that is show when configuring the action in the Action Launcher. This will be in addition to the label-value pairs already set. When this does not need to be used, please make sure it returns null.
Note that it will use the key string as label and the value part as the value.

6. Use the canUserInvokeAction if a specific access permission check for executing the Custom Action Class is required.
The DAP action where the Apex Class will be used does not perform access checks, except for the check to see whether the user is allowed to use the Run an Apex Class action on the Data Action Platform.

global interface plauti.dapInterfaceCustomApexActionV1 {
    /**
     * Define what needs to be done with the records that are passed
     * in via the dapCustomApexActionInputV1 object.
     */
    plauti.dapCustomApexActionOutputV1 processRecords(plauti.dapCustomApexActionInputV1 input);

    /**
     * Generate a label-value pair to display on the Summary tab
     * of the Action Audit Log info modal. This will be
     * addition to the label-value pairs already set.
     *
     * When this does not need to be used, please make sure it returns null
     *
     * Please note that it will use the key string as label and the value part
     * as the value.
     */
    Map<String, String> generateAuditLogSummary();

    /**
     * Generate a label-value pair to display on the Confirmation screen when
     * configuring the action in the Action Launcher (either from a list view
     * or from a create job context). This will be in addition to the label-value
     * pairs already set.
     *
     * When this does not need to be used, please make sure it returns null
     *
     * Please note that it will use the key string as label and the value part
     * as the value.
     */
    Map<String, String> generateActionSummary();

    /**
     * When a specific access permission check for executing the Custom Action
     * Class is required, this method can be implemented. The Plauti action itself
     * does not perform access checks, except for the check to see whether the user
     * is allowed to use the Run an Apex Class action on the Data Action Platform.
     */
    Boolean canUserInvokeAction(Id userId);
}

An Apex Class that is compatible with DAP should follow the above format.

7. Click Save

The class is now ready to run in the Run An Apex Class action‍. Or add it to a Macro‍ based on that action, for easy execution by your end users.


Methods to implement

processRecords

The processRecords method is where the logic that you want to perform on the records resides. This method gets called with a dapCustomApexActionInputV1 object that holds a list of record IDs. The size of the list ranges from 1 to 25. The list can be retrieved by calling the getRecordIds method from an instance of the input object.

The processRecords method needs to return a dapCustomApexActionOutputV1 object.

Handling exceptions

Do not throw an exception when an action on a record fails. Doing this will result in a failure of the entire batch chunk, and will roll back any changes that were already made to records within the chunk.

When there is a single record failure, please catch the exception in your loop, and use the setError method from the dapCustomApexActionOutputV1 class. This needs to be set for every record that fails. If no error is set for a record, it is assumed that the action was successful.

/** EXAMPLE OF THROWING AN EXCEPTION THAT IS NOT RECOMMENDED
* With this code, if one record triggers an error exception, all previous records 
* that where processed correctly will be rolled back, and processing for this
* chunk will stop.
*/
processRecords(plauti.dapCustomApexActionInputV1 input) {
  
  for (String recordId : input.getRecordIds()) {
    couldPossibleThrowException(recordId);
  }
  
  return new plauti.dapCustomApexActionInputV1(input);
}

// <=============> //
/** BEST PRACTICE FOR CATCHING AN EXCEPTION
* With this code, if one record triggers an error exception, all previous records 
* that where processed correctly will remain in their new state. Only for
* the failing record an error message will be shown in the Record Audit Log.
*/

processRecords(plauti.dapCustomApexActionInputV1 input) {
  plauti.dapCustomApexActionOutputV1 output = new plauti.dapCustomApexActionOutputV1();
  for (String recordId : input.getRecordIds()) {
    try{
      couldPossibleThrowException();
    } catch (Exception e) {
      output.setError(recordId, 'Error to display');
    }
  }
  
  return output;
}

generateAuditLogSummary

When you want to return information about the action on the Summary tab of the Action Audit Log info modal, use this method to return a map object with a label-value pair. The key string of the map is used as the label, and the value part of the map as the value. 

If there is no need to append anything to the Action Audit Log summary, please return null

Please note: The DAP action will always show the executed class in the Action Audit Log summary, as well as the chunksize. The chunksize however is only shown if the Run Apex Class action was executed in a DAP Job context. 

For example, say you want to show the following information in the Action Audit Log:

Author of the action
Jane Fielding

Code lines in action
10
public Map<String, String> generateAuditLogSummary() {
return new Map<String, String>{
  'Author of the action' => 'Jane Fielding',
  'Code lines in action' => '10'
  }
}

The Run an Apex Class action already shows the executed class name and the chunk size by default. The map returned by the generateAuditLogSummary methods appends on that output.

If there is no need to append anything to the Action Audit Log summary, please return null

generateActionSummary

When configuring the DAP action before use, a confirmation window is shown that sums up the action's settings. If you want to append information there, return a map object from the generateActionSummary method, in the same way as with the abovementioned generateAuditLogSummary. 

The Run an Apex Class action already shows the selected class name and the chunk size by default. The map returned by the generateActionSummary method appends on that output.

If there is no need to append anything to the action confirmation summary, please return null

Please note: The DAP action will always show the executed class in the confirmation summary, as well as the chunksize. The chunksize however is only shown if the Run An Apex Class action is being configured to be executed in a DAP Job context, or when configuring a DAP Macro.

canUserInvokeAction

If you want to restrict access to the class, add the logic for checking permissions in the canUserInvokeAction method. The DAP action that will eventually execute the class will only check whether the user is allowed to use the Run An Apex Class action, whether the class is active, and whether the class is valid. It does not check any permission sets or profiles to see if the user has access to the custom apex class.

Alternatively, create a Macro‍ based on the Run An Apex Class action, select the class in that macro, and assign only the macro to the user or profile, not the Run An Apex Class action.


Objects

dapCustomApexActionInputV1
Method getRecordIds returns a List<String> 

dapCustomApexActionInputV2 
Method setError(Id recordId, String errorMessage) returns void


Examples

Base template

Developer of this class assumes that all users can invoke this custom action

global class TestClass implements plauti.dapInterfaceCustomApexActionV1{
    public plauti.dapCustomApexActionOutputV1 processRecords(plauti.dapCustomApexActionInputV1 input) {         return new plauti.dapCustomApexActionOutputV1(input);     }         public Map<String, String> generateAuditLogSummary() {         return null;     }         public Map<String, String> generateActionSummary() {         return null;     }         public Boolean canUserInvokeAction(Id userId) {         return true;     } }

Generate Action Confirmation Summary

Generates an addition to the default information that is given when running the Run an Apex Class action

global class TestClassAuditLogSummary implements plauti.dapInterfaceCustomApexActionV1{
    public plauti.dapCustomApexActionOutputV1 processRecords(plauti.dapCustomApexActionInputV1 input) {         return new plauti.dapCustomApexActionOutputV1(input);     }         public Map<String, String> generateAuditLogSummary() {         return null;     }         public Map<String, String> generateActionSummary() {         return new Map<String, String>{             'Action summary label 1' => 'Action summary value 1',             'Action summary label 2' => 'Action summary value 2'             };     }         public Boolean canUserInvokeAction(Id userId) {         return true;     } }

 

Generate Action Audit Log Summary

Generates an addition to the default information that is logged in the Action Audit Log after the Run an Apex Class action has run

 global class TestClassAuditLogSummary implements plauti.dapInterfaceCustomApexActionV1{
    public plauti.dapCustomApexActionOutputV1 processRecords(plauti.dapCustomApexActionInputV1 input) {         return new plauti.dapCustomApexActionOutputV1(input);     }         public Map<String, String> generateAuditLogSummary() {         return new Map<String, String>{             'Action summary label 1' => 'Action summary value 1',             'Action summary label 2' => 'Action summary value 2'             };     }         public Map<String, String> generateActionSummary() {         return null;     }         public Boolean canUserInvokeAction(Id userId) {         return true;     } }

Make class only invokeable by specific users

Grants access to users whose alias starts with Test

global class TestClassGrantsAccessForSpecificUser implements plauti.dapInterfaceCustomApexActionV1{
    public plauti.dapCustomApexActionOutputV1 processRecords(plauti.dapCustomApexActionInputV1 input) {         return new plauti.dapCustomApexActionOutputV1(input);     }         public Map<String, String> generateAuditLogSummary() {         return null;     }         public Map<String, String> generateActionSummary() {         return null;     }         public Boolean canUserInvokeAction(Id userId) {         // only the users which start with Test in their alias         // are able to excute this action         Integer usersLocated = [SELECT Count() FROM User WHERE Id =: userId AND Alias LIKE 'Test%'];         return usersLocated == 1;     } }

 

Example Action that returns an error

Executes a basic update, but when the Contact has the lastname 'error' (case sensitive), it reports an error. This is the preferred way of handling errors

global class TestClassSetContactLastname implements plauti.dapInterfaceCustomApexActionV1 {
    public plauti.dapCustomApexActionOutputV1 processRecords(plauti.dapCustomApexActionInputV1 input) {         List<String> recordIds = input.getRecordIds();         plauti.dapCustomApexActionOutputV1 output = new plauti.dapCustomApexActionOutputV1(input);         List<Contact> allContacts = [SELECT Id, Lastname FROM Contact WHERE Id IN :recordIds];         for (Contact c : allContacts) {             if (c.Lastname == 'error') {                 output.setError((Id) c.Id, 'Could not update');             } else {             c.AssistantName = 'Updated Through Class';                 }         }         upsert allContacts;         return output;     }         public Map<String, String> generateAuditLogSummary() {         return new Map<String, String>{             'Expect in' => 'Action Audit log summary'                 };     }         public Map<String, String> generateActionSummary() {         return new Map<String, String>{             'Expect in' => 'Action Confirmation summary'                 };     }         public Boolean canUserInvokeAction(Id userId) {         return true;     } }

 

Example Action that returns an error (how not to use)

Executes a basic update, but when the Contact has the lastname 'error' (case sensitive), it reports an error. This is NOT the preferred way of handling errors

global class TestClassThrowsExceptionInProcess implements plauti.dapInterfaceCustomApexActionV1 {
    private class CustomException extends Exception{}         public plauti.dapCustomApexActionOutputV1 processRecords(plauti.dapCustomApexActionInputV1 input) {         List<String> recordIds = input.getRecordIds();         plauti.dapCustomApexActionOutputV1 output = new plauti.dapCustomApexActionOutputV1(input);         List<Contact> allContacts = [SELECT Id, LastName FROM Contact WHERE Id IN :recordIds];         for (Contact c : allContacts) {             if (c.LastName == 'Error') {                 throw new CustomException('Exception');             } else {                 c.LastName = 'Updated';             }                     }         upsert allContacts;         return output;     }         public Map<String, String> generateAuditLogSummary() {         return new Map<String, String>{             'Expectation' => 'Updated account',                 'Exception' => 'Lastname error'                 };     }         public Map<String, String> generateActionSummary() {         return new Map<String, String>{             'Trying' => 'Updated account',                 'Skipping' => 'Lastname error'                 };     }         public Boolean canUserInvokeAction(Id userId) {         return true;     } }