Pages

Friday, 25 April 2025

Evaluate Dynamic Formulas in Apex in Summer '25


Image generated by ChatGPT o3 in response to a prompt by Bob Buzzard
Proving yet again AI isn't good at putting text in images!

Introduction

The ability to evaluate dynamic formulas in Apex, which went GA in the Spring '25 Salesforce release, gets a minor but very useful improvement in the Summer '25 release - template mode. In this mode you can use merge syntax to refer to record fields, making formulas that concatenate strings much easier to specify.  Using the example from the release notes,  rather than writing the formula to combine an account name and website as a clunky concatenation :

    name & " (" & website & ")"

we can write:

    {!name} ({!website})

and tell the formula builder to evaluate it as a template. This felt like a good addition to my formula tester page, that I created to demonstrate the Spring '25 functionality, and it also uncovered some unexpected behaviour.

The Sample

My formula tester page is updated to allow the user to specify whether the formula should be evaluated as a template or not via a simple checkbox under the text area to input the formula:


I've also tried to make the page helpful and added an onchange handler to the text area that toggles the Formula is template checkbox based on whether the entered text contains the patter {! - note that the user can override this if they need that string literal for some other reason:



Note also that there's a timeout of 1 second in the onchange handler so it will only fire when the user has (hopefully) finished typing. 

There will be code


The revised Apex method to build and evaluate the formula is as follows:

@AuraEnabled
public static String CheckFormula(String formulaStr, String sobjectType, String returnTypeStr,
                                  Id recordId, Boolean formulaIsTemplate)
{
    FormulaEval.FormulaReturnType returnType = 
                   FormulaEval.FormulaReturnType.valueof(returnTypeStr);

    FormulaEval.FormulaInstance formulaInstance = Formula.builder()
                                    .withType(Type.forName(sobjectType))
                                    .withReturnType(returnType)
                                    .withFormula(formulaStr)
                                    .parseAsTemplate(formulaIsTemplate)
                                    .build();


    //Use the list of field names returned by the getReferenced method to generate dynamic soql
    Set<String> fieldNames = formulaInstance.getReferencedFields();
    Set<String> lcFieldNames=new Set<String>();
    for (String fieldName : fieldNames)
    {
        lcFieldNames.add(fieldName.toLowerCase());
    }
    if (lcFieldNames.isEmpty())
    {
        lcFieldNames.add('id');
    }

    String fieldNameList = String.join(lcFieldNames,',');
    String queryStr = 'select ' + fieldNameList + ' from ' + sobjectType + 
                      ' where id=:recordId LIMIT 1'; //select name, website from Account
    SObject s = Database.query(queryStr);
    Object formulaResult=formulaInstance.evaluate(s);
    system.debug(formulaResult);

    return formulaResult.toString();
}
The bit I expected to change was the additional formulaIsTemplate parameter and invoking parseAsTemplate on the builder. While testing however, I found that if I built a string literal without using any fields, my fieldNameList parameter was empty and I got an error running the SOQL query of select from Account where id='<blah>'.

My first attempt at a fix was to skip the query if the field list was empty, but the evaluate method errors when passed a null sObject parameter. No problem, I'll just add the Id field if the Set of field names is empty. 

Turns out there was a problem. I checked if the field named Id was in the Set, which it wasn't, then added it. Executing the query duly errored as I had a duplicate field in my query. It turns out that the getReferencedFields method isn't case agnostic and it considers Id as a different field to id and returns them both in the Set.

To confirm this I ran the following Apex:

FormulaEval.FormulaInstance formulaInstance = Formula.builder()
		.withType(Schema.Account.class)
		.withReturnType(FormulaEval.FormulaReturnType.STRING)
		.withFormula('{!name} {!Name} {!NaMe} {!NAME}')
		.parseAsTemplate(true)
		.build();
        
String fieldNameList = String.join(formulaInstance.getReferencedFields(),',');
System.debug('Field name list = ' + fieldNameList);
and got the output:

    09:55:32:043 USER_DEBUG [9]|DEBUG|Field name list = NAME,NaMe,Name,name

So I had to add some extra code to iterate the field names, lower case them and add them to a new Set to remove any duplicates. Then I could check if it was empty and if it was add the id field.

You can find the updated code in my Summer 25 samples repository - note that this needs a Salesforce instance on the Summer '25 which, at the time of writing (April 25th), is pre-release orgs. Sandboxes are available on May 9th and scratch orgs on May 11th, assuming Salesforce hit their dates.

More Information





No comments:

Post a Comment