Wednesday, 5 February 2025

Evaluate Dynamic Formulas in Apex GA

Image created by ChatGPT4o based on a prompt by Bob Buzzard

Introduction

Like zip handling in Apex, the ability to evaluate dynamic formulas in Apex is Generally Available in the Spring '25 release of Salesforce. Unlike zip handling, which we've all been wanting/battling with for years, this might not be such an obvious win. It's definitely something I could have used a few times in my Salesforce career, mostly around rule engines. I've written several of these in my time, to do things like apply a discount to a price based on a number of attributes of a customer, their spend, and whether they have a contract with us. 

In order to avoid everyone having to become an Apex or Flow expert, rules are configured through custom settings or custom metadata types, but defining the criteria to execute rules is a bit more tricky to surface for Administrator configuration, especially if you need to look at the values contained by a specific record. Then it's a toss up between express it in pro/low code and make it less configurable, or write something to validate and parse boolean style expressions including records and fields. Neither are great.

With the ability to evaluate formula fields in Apex, Admins can express criteria in a familiar format and this can easily be evaluated by the engine to decide whether to apply the rule.

The Sample

The examples I'd seen to date were mostly querying a record from the database and evaluating a formula to concatenate some fields against it, but I didn't find that overly exciting. Instead I went a different route - as I'm envisaging an Administrator creating the formula as plain text in a configuration record, so it would be useful to allow them to check it works before settling on it. I guess it could also be used to test someone's ability to create valid formulas without needing to go into Setup and create a field. So many possibilities!

The page itself is a pretty simple:


There's a picklist to choose the sobject type (limited to Account, Opportunity and Contact - this is a demo after all), another picklist to choose the formula return type, and a lookup to select the record to use when evaluating the formula. In this case my test record is in the Technology business and doing pretty well with annual revenue of ten million:


My rule should fire if the candidate account is in the technology business and making less than fifty million a year, so lets try that out:



And it works. But this is hardly an exhaustive test, so lets check what happens if I change the revenue target to less than nine million a year:


Which also works. But why is the result displayed in green regardless of whether it is true or false? Because the formula evaluated successfully of course - if I introduce an error into my formula, and try to compare the website to a numeric value, the result is red :


Show Me The Code!

You can find the code in my Spring '25 samples Github repository. The method to evaluate the formula is as follows:

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

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


        String fieldNameList = String.join(formulaInstance.getReferencedFields(),',');
        String queryStr = 'select ' + fieldNameList + ' from ' + sobjectType +
                          ' where id=:recordId LIMIT 1'; 
        SObject s = Database.query(queryStr);
        
        Object formulaResult=formulaInstance.evaluate(s);
        return formulaResult.toString();
    }

The information populated by the user through the various inputs is passed through verbatim and uses to build the formula instance. One aspect that has changed since I used the original beta is the ability to have the formula instance tell me which fields I need to query from the record via the getReferencedFields method, so I can drop them into my dynamic query with minimal effort:

    String fieldNameList = String.join(formulaInstance.getReferencedFields(),',');
    String queryStr = 'select ' + fieldNameList + ' from ' + sobjectType +
                      ' where id=:recordId LIMIT 1'; 
    SObject s = Database.query(queryStr);

More Information




Sunday, 2 February 2025

Zip Handling in Apex GA


Image generated by ChatGPT 4o based on a prompt from Bob Buzzard

Introduction

Anyone who's developed custom solutions in Salesforce world is likely to have come up against the challenge of processing zip files. You could use a pure Apex solution, such as Zippex, leverage the Metadata API or push it all to the front end and use JavaScript. What these options all had in common was leaving you dissatisfied. Apex isn't the best language for CPU intensive activities like compression, so the maximum file size was relatively small. The metadata API isn't officially supported and introduces asynchronicity, while forcing the user to the front end simply to extract the contents of a file isn't the greatest experience and might introduce security risks.

One year ago, in the Spring '24 release of Salesforce, we got the developer preview of Zip Handling in Apex which looked very cool. I encountered a few teething issues, but the preview was more than enough to convince me this was the route forward once Generally Available. Fast forward a week or so from now (31st Jan) and it will be GA in the Spring '25 release. That being the case, I felt I should revisit.

The Sample

I built the sample in a scratch org before the release preview window opened, and all I had to specify was the ZipSupportInApex feature. The sample is based on my earlier attempts with the developer preview - Lightning Web Component front end which allows you to select a zip file from Salesforce Files, an Apex controller to extract the entries from the zip file and more Lightning Web Component to display the details to the user:


The key differences are (a) I could get the zip entries back successfully this time and (b) I captured how much CPU and heap were consumed processing the zip file. The heap was slightly larger than the size of the zip file in this case, but the CPU was a really nice surprise - just 206 milliseconds consumed.

One expected fun side effect was the ability to breach the heap size limit quite dramatically. As we all know, this limit is enforced through periodic checking rather than a finite heap size, so you can go over as long as it's only for a short while. Processing the entries in a zip file clearly satisfies the "short while" requirement, as I was able to extract the contents of a 24Mb zip file, pushing the heap close to 25Mb:


It was gratifying to see that once again the CPU wasn't too onerous - even when handling a zip file that is theoretically far too large, only 1,375 milliseconds of CPU were consumed. 

Show Me The Code!

You can find the code in my Spring '25 Github repository. The key Apex code is as follows:

   @AuraEnabled(cacheable=true)
    public static ZipResponse GetZipEntries(Id contentVersionId)
    {
        ZipResponse response=new ZipResponse();
        List<Entry> entries=new List<Entry>();
        try 
        {
            ContentVersion contentVer=[select Id, ContentSize, VersionData, Title, Description, PathOnClient
                                        from ContentVersion
                                        where Id=:contentVersionId];

            Blob zipBody = contentVer.VersionData;
            Compression.ZipReader reader = new Compression.ZipReader(zipBody);
            for (Compression.ZipEntry zipEntry : reader.getEntries())
            {
                System.debug('Entry = ' + zipEntry.getName());
                Entry entry=new Entry();
                entry.name=zipEntry.getName();
                entry.method=zipEntry.getMethod().name();
                entry.compressedSize=zipEntry.getCompressedSize();
                entry.uncompressedSize=zipEntry.getUncompressedSize();
                entries.add(entry);
            }

            response.entries=entries;
            response.cpu=Limits.getCpuTime();
            response.size=contentver.ContentSize;
            response.heap=Limits.getHeapSize();
        }
        catch (Exception e)
        {
            System.debug(e);
        }

        return response;
    }

ZipResponse is a custom class to return the information in a friendly format to the front end, including the CPU and Heap consumption, while Entry is another custom class that wraps the information I want to show the user in an AuraEnabled form. The actual amount code required to process a Zip file is very small - once you have the zip file body as a Blob, simple create a new ZipReader on it and iterate the entries:

   Blob zipBody = contentVer.VersionData;
    Compression.ZipReader reader = new Compression.ZipReader(zipBody);
    for (Compression.ZipEntry zipEntry : reader.getEntries())
    {
        // your code here!
    }


Performance

Regular readers of this blog will know that I'm always keen to compare performance, so here's the CPU and heap consumption for the new on platform solution versus Zippex for my test zip files

Zip Size On Platform Zippex
CPU Heap CPU Heap
66,408 30 90,306 19 142,149
123,746 57 143,425 17 253,086
3,992,746 48 4,022,537 208 8,000,354
8,237,397 58 8,256,426 387 16,480,442
24,014,156 973 24,431,186 Heap size exception at 48,028,743

Zippex uses less CPU at the start, but that looks to be falling behind as the zip file size increases. Heap is where the on platform solution really scores though, needing 60-100% less than Zippex and able to process larger files.

More Information




Wednesday, 29 January 2025

Book Review - Salesforce Anti-Patterns


 Disclaimer - I didn't purchase this book, I was given a pre-release copy by Packt Publishing to review

I've been interested in anti-patterns since I read a short article, more years ago than I care to remember, that covered 3-4 of them. When I joined the Salesforce ecosystem, the anti-pattern I saw most of the time was the big ball of mud, which I still reference in talks to this day, "These systems show unmistakable signs of unregulated growth, and repeated expedient repair"- something all of us who have been here for a while are familiar with. I was therefore curious when Packt Publishing reached out and asked me to review the second edition of Lars Malmqvist's book on Salesforce Anti-Patterns - which of my old favourite would I see in there, and what was new in the world of bad solutions to problems that look good at first glance. It's fair to say I wasn't disappointed.

This is a great book for any aspiring architect, as it will teach you the red flags to look out for, which aren't always obvious. Sometimes the problems they indicate take months or even years to appear, but appear they surely will. Recognising this at the outset will save you a lot of angst.

It's also a great book for experienced architects, as it gives you facts rather than feelings with which to argue against a particular approach. Instead of saying that you've seen this kind of thing before and it didn't end well, you can point out the planned (or current) strategy exhibits the classic symptoms of a named anti-pattern, talk knowledgeably about the likely consequences, and advise on alternatives that will lead to better outcomes. I also like that this book isn't entirely focused on programming or technical anti-patterns - it covers the human side of things too, such as communication, collaboration, allocation of work. 

While there are plenty of the old favourites in there, that apply equally to any technology project rather than just Salesforce, there are also a number that you'll only see in a Salesforce project. These are related to specific characteristics of Salesforce like sharing, licensing, the data model, so you are unlikely to encounter them elsewhere.

Each anti-pattern is explained through a scenario, followed by a description of the problem, how it is presented as the solution to a problem, and the likely outcome of using it (rarely good), followed by a description of a better solution. Each chapter concludes with key takeaways and, in a nice touch, advice around the topic area for the CTA review board. A second nice touch is the covering how the introduction of/reliance on AI can bring you into an anti-pattern that you might otherwise have avoided.

One thing I will call out though - the likely real-world experience is that the worst of the anti-pattern is rowed back, but there still isn't the appetite to accept the better solution - life is rarely that neat and tidy. And that's okay - as architects and consultants our job is to advise and make the customer aware of the risks involved. Once that is done, the final decision rests with the customer, and it will be an informed decision, aware of the potential outcome. As long as all stakeholders agree and accept the risk, nobody can point fingers later. Striving for perfection and coming up a bit short is far better than accepting failure at the outset!

The second edition is available at https://packt.link/U7I6R - you won't regret buying it.



Sunday, 1 December 2024

Guest User Access Comes to BrightSIGN

Over the last couple of years I've received a number of requests to allow Unauthenticated/Guest Users to be able to sign records like internal users. Unfortunately this isn't something that isn't possible for a security reviewed package. In order to be able to save a File/Attachment against a record, the user must have Edit access to the record, and since the Spring '21 release of Salesforce Guest Users can't have Edit access. The Experience Cloud Developer Guide has a handy workaround of executing in system context and without sharing. but if I change the BrightSIGN code to work this way it will fail the security review, and rightly so - I'd be ignoring the security settings of the org and allowing an unauthenticated user to carry out actions that should be blocked. 

While I can't publish a package that allows a Guest User to execute code in system context without sharing, there's nothing to stop the owner of the org adding this capability after installing the package. So in version 4.1 of BrightSIGN, Guest Users can capture a signature as a File. There's a caveat to this though - as I can't associate the File with a record, it will be "orphaned". 

Full details of how to configure BrightSIGN to allow Guest User access are available in the Implementation Guide for V4, but the upshot is rather than the file detail having the following sharing :


It just has the sharing for the Owner:




The admin can then create an Apex trigger on ContentVersion and take appropriate action. This is a bit tricky though, as they'll need to find a way to tie the ContentVersion back to the specific record. The other option is a second component to handle the Signature Captured Event - there's an example of using this to update a record when a signature is captured, and this can easily be tweaked to insert a ContentDocumentLink record to associate the File with the target record.

Related Posts




Saturday, 26 October 2024

Einstein Copilot vs Complex Queries

Image created by GPT-4o based in a prompt by Bob Buzzard

Introduction

Now that Agentforce (for Service at least) is GA we have access to the latest Atlas Reasoning Engine. That's the theory at least - I haven't seen a way to find out what version is in use, which models it has access to etc, but that doesn't worry me too much as I can't change or influence it anyway. Anecdotally I do feel that the ability to handle requests with more than one step has improved over the last few months, but this sounded like a step change - Chain of Thought reasoning and an iterative approach to finding the best response! 

My focus continues to be on Copilot (aka Agentforce for CRM, or whatever name it's going by this week), but I'm writing rather more custom actions than I'd like. Each of these introduces a maintenance overhead and as Robert Galanakis wisely wrote "The code easiest to maintain is the code that was never written", so if there's a chance to switch to standard functionality I'm all over it.

The Tests

Where I've found standard Copilot actions less than satisfactory in the past is around requests that require following a relationship between two objects and applying filters to each object. Show me my accounts created in the last 200 days with contacts that I've got open tasks against, that kind of thing. Typically it would satisfy the account ask correctly but miss the contact requirement. Now I can easily create an Apex action to handle this request, but the idea of an AI assistant is that it handles requests for me, rather than sending me the requirements so I can build a solution!

I've created 4 products with similar names:

  • Cordless Drill 6Ah
  • Cordless Drill 4Ah
  • Cordless Drill 2Ah
  • Travel Cordless Drill
and created a single Opportunity for the Cordless Drill 8Ah. I then ask some questions about this.

Test #1


Ask Copilot to retrieve Opportunities based on the generic product name 'Cordless Drill'

When I've tested this in the past, Copilot has refined 'Cordless Drill' to one of the four available products and then search for Opportunities containing that product. Sometimes it gets lucky and picks the right one, but more often than not it picks the wrong one (3 -1 odds) and tells me I don't have any.

The latest Copilot gets this right first time.



and checking the Query Records output shows the steps involved:



  • First it looked for products matching 'Cordless Drill'
    A limit of 10,000 seems a bit large, but I guess this isn't being passed on to an LLM and consuming tokens.
  • Then it finds the opportunities which have line items matching any of the 'Cordless Drill' products.
  • Then it pulls the information about the Opportunity.
    Interesting that it only narrows it to my Opportunities at this point - it feels like the line item query could get a lot of false positives.
So I can pick the odd hole, but all in all a good effort.

Test #2


The next test was to add some filtering to the opportunity aspect of the request - I'm only interested in opportunities that are open. Once again, Copilot has no problem handling this request:


Test #3


The final test was using a prompt that had failed in earlier tests. This introduced a time component - the opportunities had to be open and created in the last 300 days, and used slightly different working, asking for opportunities that include "the cordless drill product".

This time Copilot was wrong-footed:


My assumption here was that the date component had tripped it up - maybe it didn't include today - or the "the cordless drill product" had resulted in the wrong product being chosen. Inspecting the queries showed something else though:


  • The products matching 'cordless drill' had been identified correctly
    This was through a separate query, presumably because I'd mentioned 'product'
  • The opportunity line items are queried for the products, but this time the query is retrieving the line item Id rather than the related Opportunity Id
  • An attempt is then made to query opportunity records that are open, created in the last 300 days and whose Id matches the line item Id, which will clearly never be successful. 
Rewording the request to "Show my open opportunities for the last 300 days that include cordless drills" gave me the correct results, so it appears use of the keyword 'product' changed the approach and caused it to lose track of what it was supposed to be doing.

Conclusion


The latest reasoning engine is definitely an improvement on previous versions, but it still gets tripped up with requests that reference multiple sObject types with specific criteria. 

While rewording the request did give a successful response, that isn't something I can see going down well with users - they just want to ask what is on their mind rather than figure out how to frame the request so that Copilot will answer it.

So I can't retire my custom actions just yet, but to be fair to Salesforce they have said that not all aspects of the Atlas Reasoning Engine will be available until February 2025. That said, I'm not sure I'd be happy if I'd been charged $2 to see it get confused!

Related Posts




Saturday, 5 October 2024

Agentforce - The End of Salesforce Human Capital?

Image generated by GPT-4o based on a prompt by Bob Buzzard

It's been a couple of weeks since Dreamforce and according to Salesforce there were 10,000 Agents built by the end of the conference, which means there's probably more than 20,000 now. One of those is mine, which I built to earn the Trailhead badge while waiting for the start of a Data Cloud theatre session and it did only take a few minutes.

So does this mean we need to sit back and prepare a the life of leisure while the Agents cater to our every whim?

The End of Humans + Salesforce?

Is this the end of human's working on/in Salesforce? Well not for some time in my opinion - these are very entry level tasks right now, and highly reliant on existing automation to do the actual work of retrieving or changing data. I suppose it's possible that eventually we'll end up with a perfect set of Agent actions (and supporting flows/Apex), but given we haven't achieved anything like that kind of reusability in the automation work that we've carried out over the last 20 odd years in Salesforce, it seems hopeful that we'll suddenly crack it. Even with the assistance of unlimited Agents. Any reports of our demise will be greatly exaggerated.

Fewer Humans + Salesforce?

This seems more plausible - not because of the current Agent capabilities displacing humans, but the licensing approach that Salesforce are taking. By moving away from per-seat licensing to per-conversation, Salesforce are clearly signalling that they expect to sell fewer licenses once Agentforce is available. 

Again, I don't think we're anywhere close to this happening, but it's the direction of travel. What it probably will do is given organisations pause for thought when creating their hiring plans next year. Will quite so many entry level recruits be required compared to previous years? 

Without Juniors, Where are the Seniors?

Something that often seems to be glossed over when talking about how generative AI will replace great swathes of the workforce is succession planning. Everyone who is now working as a Senior <anything> started out as a Junior <anything>, learned a bunch of stuff, gained experience and progressed. Today's Seniors won't last forever, there's a few years certainly, but eventually they'll run out of steam. And if there are no Juniors being hired, where do our crop of Seniors come from by 2034 and beyond?

It's likely to be one of two ways:

  • We grow them differently. Using AI we can fast-track their experience and reduce the amount they have to learn and retain themselves. Rather than people organically encountering teachable moments in customer interactions, we'll engineer those scenarios using an "<insert role here> Coach". The coach will present as a specific type of customer in a specific scenario, receive assistance and then critique the performance.
  • We don't need them, as the AI is so incredibly powerful that it exceeds the sum of all human knowledge and experience and can perform any job at any level.
I'm expecting the first way, but if I'm wrong I'm ready to welcome our Agent overlords.

Conclusion

No, I don't think Agentforce means the end of Human Capital in the Salesforce ecosystem. It does mean we need to do things differently though, and we shouldn't get so excited about the technology that we forget about the people. 

Related Posts

Sunday, 11 August 2024

The Evil Co-Worker presents Evil Copilot - Your Untrustworthy AI Assistant

Image generated by gpt4o based on a prompt from Bob Buzzard

Introduction

Regular readers of this blog will be familiar with the work of my Evil Co-Worker to worsen the Salesforce user experience wherever possible. The release of Einstein Copilot has opened up a whole raft of possibilities, where the power of Generative AI can be leveraged to make the world a better place .... but only for them! This week we saw the results of their initial experiments when the Evil Copilot was launched on an unsuspecting Sales Team - your untrustworthy AI assistant.

The Evil Actions

Evil Copilot only has a couple of actions, but these are enough to unsettle a Sales team and help the Evil Co-Worker gain control of important opportunities.

What Should I Work On

The first action is What Should I Work On. An unsuspecting Sales rep asks for advice about which accounts and deals they should focus on today, expecting to be pointed at their biggest deals that will close soon, and the high value accounts that should be closely monitored. Instead they are directed to their low value/low probability opportunities and accounts that they haven't dealt with for ages. They are also informed that it doesn't look like Sales is really for them, and advised to look for a different role! Quite the demotivating start to the day:


Opportunity Guidance


Note also that the rep is advised that they have an opportunity that is a bit tricky and they should seek help from E. Coworker. Before they do this, they use Copilot to look up the opportunity:


It turns out this is their biggest opportunity, so the user seeks the sage advice of Copilot, only to hit another evil action and another knock to their confidence - Copilot flat our says that they aren't up to the job and the best bet is to hand it over to, you guessed it, E. Coworker!


With just a couple of actions, Evil Copilot has most of the Sales team focused on trivia, while all the top opportunities end up in the hands of the Evil Co-Worker - not a bad return for a few hours work!

But Seriously Folks!

Now this is all good fun, but in practice my Evil Co-Worker would require administrator access to my Salesforce instance and very little oversight to be able to unleash Evil Copilot on my users. And I've no doubt there are more than a few companies out there where it would be entirely possible to do this, at least until an angry Sales rep called up to ask what was going on!

But a Copilot implementation doesn't have to be intentionally Evil to cause issues. The Large Language Models will follow the instructions given to them in Prompt Templates, even if that wouldn't be reasonable course of action for a human member of staff - if the instruction is to tell a user they don't appear to be suited to their job, they'll do it without question. While us humans can tell that this isn't appropriate, the models will see it as perfectly reasonable. It won't be picked up as toxic either - simple constructive criticism won't raise any flags. 

That's why you always need to test your Prompt Templates, and your Copilot actions, with a variety of data and users - to ensure that your intended request doesn't turn into something completely different in the wild. We've all sent emails that we were convinced had 100% clarity, only to see someone take them entirely the wrong way, and then we we look at them again we realise how ambiguous or subjective they were. And always have a second pair of eyes reviewing the content of a Prompt Template before making it available to users - Evil Co-Workers are constantly on the lookout for weak points in a process.

Related Posts