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’ve utilised the power of Apex code to develop an adaptable solution that can keep up with their customer’s frequently changing requirements.
Approval workflows often stand as gatekeepers, ensuring decisions align with organisational standards and the standard approval process often satisfies the reqirements of many businesses scenarios. However, a more tailored approach is sometimes required when handling complex and less common approval scenarios. This blog post discusses a solution we developed for a client with specific requirements that exceeded the capabilities of Salesforce’s standard approval processes and available AppExchange solutions.
.
The Challenge
Our client required an approval process with a high level of flexibility especially at the time of execution. Here are some of their requirements:
-
Dynamic Approval Levels: Based on the context of the record details, the process needed to decide when entering an approval step whether it would require two or three approvals.
-
Percentage-Based Approvals: The process had to accommodate approvals based on a percentage of the approvers instead of a fixed number.
-
Dynamic Assignees: The approval request details, including the assignees, had to be dynamically generated.
After evaluating the standard approval process and several other solutions, it became clear that none could meet these needs comprehensively. We decided to develop a custom approval process to offer the required flexibility and configurability while keeping it easily extendable for future requirements.
.
Our Approach
ERD
Once we determined that a custom solution was necessary, we aimed to build it in a configurable and generic manner so that the client could reuse it for any other objects in the future. This led us to the development of an approval framework.
The first step in designing the framework involved identifying the entities to be utilised and determining the relationships between them. We took inspiration from the entities and objects of the Salesforce standard approval process.
In the standard approval process:
-
The header information configured in the UI is saved as a
Process Definition
record. -
Each configured step is represented by a
Process Node
record, which holds the definition of a step. -
Upon activation, when records enter the approval process –
Process Instances
of these definitions are created. So for a particular approval process, there will always be one process definition, but multiple records can go through that approval, each having their own process instances.
Similarly, a record entering a step creates a step instance, represented by aProcess Instance Node
. -
Each step can have one or multiple approvers, represented by
Process Instance Work Items
(approval requests).
A few more objects are used in the standard approval process, but we focused mainly on these.
Finally, we needed an object to save the approval history – it would act like a read-only object that can hold and report on all steps and approval requests associated with an approval process. We can build the records of this object using triggers of Process Instance
,Process Instance Node
and Process Instance Work Items
. Being a custom object it would give us the option to control the retention period and archiving strategy for the records based on the business requirements.
Here’s a visual representation of the approval framework entities and their relationships:
Configuring Submission, Approval, and Rejection using Apex
After completing the Entity-Relationship Diagram (ERD), our attention shifted to the second building block – devising the most adaptable method to store configurations. This was crucial because this is where the client needed the greatest flexibility… and what could be more flexible than Apex? But how could we make the Apex code configurable?
When we were looking at our options, we got the idea of using an eval-
like function in Apex, similar to what exists in other languages like JavaScript. This would allow us to pass a configurable string of code to eval
, which would then execute the code stored in the string.
We started exploring this idea and came across two insightful blogs by Kevin Poorman and Daniel Ballinger. These blogs discussed the possibility of programmatically evaluating Apex strings and extracting the result using the executeAnonymous
API call.
While the executeAnonymous
call is available through both the Apex API and the Tooling API, we chose to use the Apex API, which utilizes the SOAP protocol, allowing us to add a debugging header (<apex:DebuggingHeader>
) to the request to receive custom log messages in the response. We could then parse that response to extract the result. The Tooling API, on the other hand, generates the Apex debug log but requires a separate query to retrieve it from the ApexLog.
It should be noted that using executeAnonymous
basically works the same way as if we were executing the code inside of the Developer Console’s Execute Anonymous window. There are no preexisting local variables in the execution context and any inputs need to be explicitly included in the Apex string to be executed. Also, any return values must be returned via the log and then parsed to bring them into the context of the calling Apex.
To summarise the steps:
-
Build up the anonymous Apex string including any required inputs.
Use a System.debug(LoggingLevel.Error, ‘output here’) to send back the output data. -
Call the Apex API executeAnonymous web method
-
Capture the DebuggingInfo SOAP header in the response and Parse the USER_DEBUG Error message out of the Apex Log.
-
Convert the resulting string to the target data type if required.
Let us revisit the above steps – this time with a simple example of the addition of two numbers
-
Build up the string and add the System.debug statement to print the result, so we can fetch it later
String toEval = 'Integer a = 10; Integer b = 20; Integer result = a + b;'; toEval += 'System.debug(LoggingLevelError, result);';
-
Call the Apex API executeAnonymous web method to evaluate this string
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.getBody());
The request body will then look like this:<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> </soapenv:Header> <soapenv:Body> <apex:executeAnonymous> <apex:String>Integer a = 10; Integer b = 20; Integer result = a + b; System.debug(LoggingLevel.Error, result);</apex:String> </apex:executeAnonymous> </soapenv:Body> </soapenv:Envelope>
-
And the response body that we need to parse:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/..." <soapenv:Header> <DebuggingInfo> <debugLog>31.0 APEX_CODE,ERROR Execute Anonymous: Final string of code 13:24:24.027 (27564504)|EXECUTION_STARTED 13:24:24.027 (27573409)|CODE_UNIT_STARTED|[EXTERNAL]|execute_anonymous_apex 13:24:24.028 (28065096)|USER_DEBUG|[1]|ERROR| 30 <!--Output printed here--> 13:24:24.028 (28098385)|CODE_UNIT_FINISHED|execute_anonymous_apex 13:24:24.029 (29024086)|EXECUTION_FINISHED</debugLog> </DebuggingInfo> </soapenv:Header> <soapenv:Body> <executeAnonymousResponse> <result> ... <success>true</success> </result> </executeAnonymousResponse> </soapenv:Body> </soapenv:Envelope>
-
We will capture the DebuggingInfo SOAP header in the response and Parse the USER_DEBUG Error message
Our parser should look something like this:// Helper method to parse the debug log from the SOAP response private static String extractDebugLog(String responseBody) { Dom.Document doc = new Dom.Document(); doc.load(responseBody); Dom.XmlNode debugLogNode = doc.getRootElement() .getChildElement('Header', 'http://schemas.xmlsoap.org/soap/envelope/') .getChildElement('DebuggingInfo', null) .getChildElement('debugLog', null); String debugLog = debugLogNode.getText(); return parseUserDebugMessage(debugLog); } // Helper method to parse the USER_DEBUG message // This method takes the debug log string, splits it into lines, and iterates through each line to find the USER_DEBUG message containing |ERROR|. // Once found, it extracts and returns the value after |ERROR|. private static String parseUserDebugMessage(String debugLog) { List<String> logLines = debugLog.split('\n'); for (String line : logLines) { if (line.contains('|USER_DEBUG|') && line.contains('|ERROR|')) { Integer startIndex = line.lastIndexOf('|ERROR|') + '|ERROR|'.length(); return line.substring(startIndex).trim(); } } return null; }
-
Finally, convert to the required data type
Integer result = Integer.valueOf(output.trim());
It may look like a lot of work but steps 2 to 4 would be implemented as a reusable library and effectively hidden inside methods like:
ApexEval.evalString() or ApexEval.evalNumber().
In the former example after preparing the string of code in step 1, we would call ApexEval.evalNumber(toEval)
. This method would handle the execution and parsing, returning the desired output (in our example it would be the number 30).
So, basically we build the library once and can use it for various use cases. And one such use case is a custom approval process.
And that’s all for now. In the next part, we will discuss key implementation details and share a few code examples with you. Till then, happy evaluating!
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.