Anindya Halder – Senior Salesforce Developer and Pavel Hrbáček – Delivery Consultant and Senior Salesforce Developer at Bluewave recently presented at CzechDreamin, offering a new perspective on the custom approval process. Find out more about how they utilised the power of Apex code to develop an adaptable solution that can keep up with their customer’s frequently changing requirements.
In our previous blog, we discussed about the idea of eval in apex and how it can be used to dynamically evaluate code. In today’s blog, we’ll dive deeper into the topic and explore the idea of using it in our custom approval process..
Framework Class Structure
Utilising the eval
function, the class structure of our approval framework becomes organised into distinct layers:
-
Eval: At the heart of the framework.
-
Core Methods: Workflow methods that call
eval
. -
Public Methods: Methods like submit, approve, reject, etc., that utilise the core methods.
Here is a simple example of using the framework’s eval
for a submission of accounts for approval where the entry criteria are to check if the industry of the account is finance.
We can configure the entry criteria directly in the associated Process Definition
record. Let us say it has a field EntryCriteria__c
which is a text area. Its value will be a string like this:
result = record.Industry == 'Finance' ? '':'Criteria doesn't match';
The public method submit
would call a core method WF_Helper.checkEntryCriteria
, and if the entry criteria match, it would proceed and start the Process Instance
, compute the first step ID and then activate the Process Instance Node
with that step ID, and finally create approval requests (Process Instance Work Items
). If the criteria do not match, it should receive the error message as a string and surface that in the UI.
//Public methods submit(processDefinition,contextRecord) { String errorMessage = WF_helper.checkEntryCriteria(processDefinition, contextRecord); If(String.isBlank(errorMessage)) { //start process instance, compute first stepid, activate node with that stepid and create workitems/approval requests } }
The core method checkEntryCriteria
would prepare the string of code to be passed to eval
. It would first write a string of query to access the Industry of the context account record. Then it would fetch the config saved in the EntryCriteria__c
field of process definition. Then it will concatenate all these strings ensuring all input parameters and variables are declared and included in the string. It will also concatenate a System.Debug
statement in the final string.
//Workflow Core methods String contextRecordString = 'Account record = [SELECT Id, Industry FROM Account WHERE Id =\' '+ contextRecord.Id + '\'];'; String configScript = processDefinition.EntryCriteria__c; String toEval = contextRecordString; toEval += 'String result = \'\';'; toEval += configScript; toEval += 'System.debug(LoggingLevelError, result);'; String finalResult = WF_ExecuteAnonymousApex.eval(toEval); return finalResult;
So the final string would contain the below code – for better visibility here is the code but not in string format:
Account record = [SELECT Id, Industry FROM Account WHERE Id ='<accountId>']; String result = record.Industry == 'Finance' ? '' : 'Criteria doesn\'t match'; System.debug(LoggingLevel.Error, result);
And the eval
call can be elaborated as follows:
- HTTP Request to make the ExecuteAnonymous API Call:
//ExecuteAnonymous API call String endpoint = URL.getSalesforceBaseUrl().toExternalForm() + '/services/Soap/s/56.0'; HttpRequest req = new HttpRequest(); req.setEndpoint(endpoint_x); req.setMethod('POST'); req.setHeader('Content-Type', 'text/xml; charset=UTF-8'); req.setHeader('SOAPAction', 'blank'); req.setBodyDocument(doc); Http http = new Http(); HTTPResponse res = http.send(req); return extractDebugLog(res.getBodyDocument());
- Request Body
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:apex="http://soap.sforce.com/2006/08/apex"> <Soapenv:Header> <apex:DebuggingHeader> <apex:categories> <apex:category>Apex_code</apex:category> <apex:level>ERROR</apex:level> </apex:categories> <apex:debugLevel>NONE</apex:debugLevel> </apex:DebuggingHeader> <apex:SessionHeader> <apex:sessionId>Session Id</apex:sessionId> </apex:SessionHeader> </soapeny:Header> <Soapenv:Body> <apex:executeAnonymous> <apex:String>Final string of code*</apex:String> </apex:executeAnonymous> </soapenv:Body> </soapenv:Envelope>
// *Final String of code Account record = [SELECT Id, Industry FROM Account WHERE Id = '<accountId>']; String result = record.Industry == 'Finance' ? '' : 'Criteria doesn\'t match'; System.debug(LoggingLevel.Error, result);
-
Response:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/e..."> <soapenv:Header> <DebuggingInfo> <debugLog>31.0 APEX_ CODE, ERROR Execute Anonymous:Final string of code 13:24:24.027 (27573409) CODE LUNT STARTED EXTERNAL execute_anonymous._ape 13:24:24.028 (28065096) |USER_DEBUG| [1] |ERROR| Actual Output ("" or Criteria doesn\'t match) 13:24:24.028 28098385) |CODE_UNIT_FINISHED| execute_anonymous_apex 13:24:24.029 29024086) |EXECUTION_FINISHED</debugLog> </Debugging.Info> </soapenv:Header> <soapenv:Body> <executeAnonymous Response> <result> <column>-1</column> <compileProblem xsi:nil="true" /> <compiled>true</compiled> <exceptionMessage xsi:nil="true" /> <exceptionstackTrace xsi:nil="true" /> <line>-1</line> <success>true</success> </result> </executeAnonymousResponse> </soapenv:Body> </soapenv:Envelope>
Imagine the above scenario. The client wants to change the entry criteria and wants to add the industry ‘Banking’ too. All we need to do is open the right process definition record and change the text in its EntryCriteria__c
field from
result = record.Industry == 'Finance' ? '' : 'Criteria doesn\'t match';
to
result = record.Industry == 'Finance' || record.Industry == 'Banking' ? '' : 'Criteria doesn\'t match';
Next, we will explore a sample scenario and show how the framework can be used to configure things quite flexibly.
.
Sample Approval Process Scenario
Consider the following scenario – “Developers who are employees of an IT company have been contracted to work on a project for a client. When they want to go on annual leave, they need to get approval from both their own direct managers and the client’s project manager.”
Let’s summarise the rules:
-
50% Approval from Direct Managers: The leave request requires approval from 50% of the direct managers (which could be 2 or 3 managers).
-
Project Manager’s Approval: After receiving approval from the direct managers, the client’s project manager must approve the request, provided the developer has an active project assigned.
.
Execution Flow of the Submission Process
-
Submission
-
The developer submits their leave request.
-
Our public framework method
submit()
is called, which starts executing. -
It evaluates the entry criteria and, if all conditions are met, calls a method to create the Process Instance record and sets some defaults.
-
-
Initial Process Step
-
The
computeStepId
method is executed, which returns the first step ID based on the initial Process Step ID. -
The
activateNode
method uses the step configuration and creates a new ProcessInstanceNode record, setting its defaults. -
The Process Instance status is updated to “Started”.
-
-
Creating Approval Requests
-
The first set of approval requests is created for the direct managers using the After Submission Action (see the below section) configuration.
-
The Process Instance status is set to “In Progress,” and the Leave Request record is flagged as part of an ongoing approval process.
-
.
After Submission Configuration
Now, let’s have a look at a sample After Submission configuration, which illustrates the process we just described.
Before we dive in, it’s important to note that our Process Definition object includes several text and text area fields used for the Approvals configuration. The content of these fields is evaluated and executed at various stages of the approval process.
One of these fields is the ‘After Submission Action’. Here’s a simplified example of its configuration:
-
Fetching User Names of Direct Managers
We first retrieve the usernames of the direct managers.
-
Creating Request Parameters for Each Manager
Instances of Request Parameters are created for each manager. These objects store a few details about the request, such as a prompt or an assignee.
-
Creating Approval Request Records
Finally, we call a method that creates the Approval Request records.
Here is the code that would be stored inside a text field on the process configuration record:
Map<String, List<User>> roleToUsersMap = WF_Actors.getRoleUsers(contextRecordId, new List<String> {'Line Manager', 'Delivery Manager', 'Division Manager'}); List<WF_App_Actions.RequestParams> requestParamsList = new List<WF_App_Actions.RequestParams>(); for (String role : roleToUsersMap.keySet()) { for (User assignee : roleToUsersMap.get(role)) { requestParamsList.add(new WF_App_Actions.RequestParams( 'Approve - ' + role, 'Select \'Approve\' if you are happy to approve the leave.', assignee, role, null)); } } } WF_App_Actions.createApproverRequest(processInstanceId, processInstanceNodeId, requestParamsList);
This code snippet accomplishes the following:
-
It uses the
WF_Actors.getRoleUsers
method to fetch the usernames of the managers based on their roles. -
It creates a list of
WF_App_Actions.RequestParams
objects, each representing a request parameter for a manager. -
Finally, it calls the
WF_App_Actions.createApproverRequest
method to create the approval request records in the system.
By configuring the After Submission Action in this way, we ensure that the appropriate approval requests are generated and assigned to the relevant managers as soon as a leave request is submitted. This setup demonstrates the flexibility and power of our custom approval framework in handling complex approval scenarios.
.
After Approval Configuration
Now, let’s examine what happens after one of the managers approves the request.
When one of the managers approves the request, the framework needs to check if the 50% approval threshold is reached. This is done by computing the percentage of received approvals relative to the total number of requests generated for this step (node).
Here is how it works:
- Check Approval Percentage
-
We calculate the percentage of approvals received so far.
-
If the 50% threshold is met, we can move to the next step.
// Is Final Approval - Framework code public static Boolean checkIfFinalApproval(Id precessInstanceNodeld, Integer percentageRequired) { ... completedApprovals = Integer.valueof(processInstanceNode.NumberQfAppravals__c); Integer allApprovals = [SELECT Count () FROM WF_RrocessInstanceWorkItem__c WHERE processInstanceNode =: processInstanceNodeId]; return (completedApprovals/allApproxals*100) >= percentageRequired; }
// Is Final Approval - Framework configuration result = WF_App_Actions.checkIfFinalApproval(processInstanceNodeId, 50); // When at least 50% percentage is required
-
- Compute Next Step Id
- Once the approval threshold is achieved, we compute the Step Id of the next node where the process will move.
// Approved Next Step Id - Framework configuration result = ourHR.hasActiveProject(contextRecord.RequestedBy__c ? '002' : null);
- Once the approval threshold is achieved, we compute the Step Id of the next node where the process will move.
- Complete Current Step and Start Next Step
-
The framework marks the current step as complete and initiates the next step.
-
The process then uses the
After Final Approval Action
, another configurable endpoint/hook, to generate the new approval request for the Project Manager.
-
- Handle Absence of Project Manager
-
If the developer has no active project or project manager, the approval steps are considered complete.
-
The process reverts to the Process Definition and executes the
AFTER APPROVAL ACTION
, which can be configured to send an email notification to the developer or requester, informing them that their leave has been approved.Here is a simplified example of how the
After Final Approval Action
configuration might look:// After Final Approval Action - Framework code Map<String, List<User>> roleToUsersMap = LeaveApproval.getRoleUsers(contextRecordId, new List<String> {'Project Manager'}); If (roleToUsersMap.size() == 0) return; // Finish the step when user has no Project Manager List<WF_App_Actions.RequestParams> requestParamsList = new List<WF_App_Actions.RequestParams>(); for (String role : roleToUsersMap.keySet()) { for (User assignee : roleToUsersMap.get(role)) { requestParamsList.add(new WF_App_Actions.RequestParams( 'Approve a leave request', 'Select \'Approve\' if you are happy to approve', assignee, role, null, null, false )) } } WF_App_Actions.createApRceverRequest(processInstanceId, nextProcessInstanceNodeId, requestParamsList);
-
By configuring the After Approval Action in this way, we ensure that the process correctly handles multiple approval steps and dynamically adapts based on the developer’s current project status. This showcases the flexibility and robustness of our custom approval framework in managing complex approval workflows.
.
Implementation Highlights
Implementing our custom approval framework came with some challenges. Here are some key issues we faced and how we tackled them.
Topic | Solution Approach |
---|---|
Executing anonymous apex script runs as the running user, not in system mode, and if the script needs to refer to any Apex classes to invoke methods, we will need to grant the users permissions that we wouldn’t want to, such as Author Apex or access to Setup. | Using Named Credential to authenticate and login as an admin user.
With Named Credentials, we can provide the callout with admin credentials so the dynamic code can be executed with as much flexibility as our written code. The Named Credential takes in the URL of the callout we want to make and the necessary authentication credentials for an admin user. When the callout is being made, those credentials are used to authenticate and perform the callout. |
Executing anonymous Apex script relies on making a callout, and we need to avoid the error of ‘uncommitted changes in transaction…’ whenever trying to make a callout after any DMLs within the same transaction | We execute the Apex script in a future call. |
Configuration details – All configs are saved in custom object records like ProcessDef, Process Node etc.
We need a way to migrate these records across orgs and as well make them available for unit tests |
Fetch the records and save them as CSVs and upload them as static resources. The static resources can be migrated from one org to another using source control system.
We can then have a command line data loader or an Apex class to read static resources and load records. |
.
If you have any further questions about our Custom Approval Process, feel free to contact us – here for Anindya and here for Pavel. For more information about Bluewave and the bespoke Salesforce-powered digital transformations we are known for, feel free to get in touch with a member of our dedicated team today.