Saturday 11 May 2024

Formatted Output from Copilot Custom Actions

Image generated by DALL-E 3 based on a prompt from Bob Buzzard


If you've seen any of the demos of Einstein Copilot, you'll notice that sometimes the responses are nicely formatted using the Lightning Design System, while other times they are simply text on a darker background - e.g. from the TrailblazerDX keynote, the pipeline information is text:

While the contacts are shown as a formatted list, with fields specific to the solution:

This isn't particularly well documented, so it's a matter of trial and error to figure out what works and what doesn't.

Text Responses

Text responses came out the same regardless of what I tried - the text that I return appears on a dark background. If I format it as JSON or CSV, it comes out in JSON or CSV format, even if I included instructions to display as a list of label/value pairs. 

The same goes for HTML markup - it's taken as text and shown to the user. Using \n for a line break works, but aside from that what you return from your custom action is what you see on the screen.

Custom Class

Returning custom class instances introduce a little more formatting. You can only return a single "value" from your custom action, but this can contain a list of custom class instances and Copilot will render each of the properties in it's own "text box". In the example below I return a single instance of my custom class Output, but this contains a list of instances another custom class. These in turn which contain the fields from Task records (as using the Tasks directly throws errors) :

public class Output
    public List<CustomRec> recs;

public class CustomRec
    public String ident;
    public String subject;
    public String description;
    public String activityDate;

    public CustomRec(Task task)
        this.ident=(null!'ID : ' + task.Id:null);
        this.subject=(null!=task.Subject?'Subject: ' + task.Subject:null);
        this.description=(null!=task.Description?'Description :\n' + task.Description:null);
        this.activityDate=(null!=task.activityDate?'Due Date : ' + task.activityDate:null);

Copilot displays the properties from each record, although there isn't any separation between records. There's also a wrinkle in there I wasn't expecting - the properties are displayed in alphabetical order : activityDate, description, ident, subject. While unexpected, this does give me a way to order the properties if I need to:

sObject Records

As before, I can only return a single item from a custom action, so if I want to send back a list of records I have to wrap them in an containing class:

public class Output
    public List<Opportunity> opportunities;

    public Output()
        opportunities=new List<Opportunity>();

And this comes out very nicely, with a card for each record:

sObject Records and more!

This is the one that I'm most pleased about - it allows me to display records and some commentary about each record, while still retaining the Lightning Design System formatting for the record itself.

Once again I'm returning a custom class containing a list of other custom classes, but this time it's a wrapper class containing an sObject record and some additional information:

public class Output
    public List<OpportunityWrapper> opportunities;

    public Output()
        opportunities=new List<OpportunityWrapper>();

public class OpportunityWrapper
    public Opportunity opp;

    public String message;

    public String trailer;
    public String zzzSeparator='-----------------------------';

    public OpportunityWrapper(Opportunity opp, String message, String trailer)

I really didn't expect this to work, but it did!

I've taken advantage of the fact that the properties are displayed in alphabetical order to display a message about how near the close date is, then the record, then some commission information, followed by a rather ugly separator. I think if I was going to use this in production I'd have all the message above or below the record so that I didn't have to put a clunky separator in, but I wanted to see if I above and below would work.


I don't think this is too bad, given that Copilot is at its heart a text based tool. Hopefully in time we'll be able to apply more formatting to text output, but at least sObject records are styled for the Lightning Experience. Worst case, we all end up polluting our Salesforce org with a bunch of fake records that are just used as carriers for Copilot responses! 

Related Posts

Sunday 5 May 2024

Einstein Copilot - AI + (most of your) Data + CRM?

Image created by DALL-E 3 based on a prompt from Bob Buzzard


Einstein Copilot from Salesforce went GA around a week ago (25th April 2024, for future readers), which changes my attitude to it. When things are in preview/pilot/beta, I'll take a bit of additional effort tracking down issues if it means I get my hands on things earlier. Once a feature goes GA though, the gloves are off - real money is changing hands to use these features so we hold them to a higher standard.

As the title of this post suggests, my first disappointment post-GA was that I can't use all my data across Generative AI - specifically Tasks and Events, part of the Salesforce Activities feature. This won't come as a surprise to old hands in the Salesforce ecosystem - hands up those who remember using text fields to capture IDs because custom lookups weren't available! More recently, they still aren't supported by the User Interface API, which means some of the standard LWC functionality won't work for them, and it's slightly concerning that an idea to add this capability has been open for 3+ years without even a comment from Salesforce. Yes, Activities are really Tasks, Events and Calendars on each others shoulders in a long overcoat, but this was the choice Salesforce made and they need to own it

There are workarounds for most areas of the platform where support for Activities is not as complete as it could be, but that isn't the case for the first issue that I encountered.

Prompt Template Parameters

Activities can't be specified as parameters to Prompt Builder. The docs suggest they should be available, as they are standard objects:

My use case was I wanted to send an Event record to an LLM, which contained details of a meeting that had taken place with a customer. The record includes the intended outcome, notes from the meeting, and any comments from company attendees after they've had a short period of time to reflect.  The LLM would then recommend the best course of action based on the meeting.This would then be used to create a Copilot action, so my users didn't could focus on doing the next step rather than figuring it out.

Searching for Event in the resource picker gave me pause for thought:

Maybe it's under Activity/Activities? Sadly not:

For the sake of completeness I also checked for Task - same result:

Now this doesn't mean that Activities can't be used with Prompt Builder - I can access the Open Activities and Activity History if I specify another object like Opportunity as the resource:

As this is the only way to specify a record to a Prompt Template, there aren't any workarounds to handle this, or none that don't suck anyway.

I could pass the Who and What information to the template and try to figure out the Event - for example, if the Event was with a Contact to talk about an Opportunity, I could pass the IDs for those in and use Apex/Flow grounding to retrieve the Events that match these IDs and hope I can figure out which one the user is interested in. If there are a number of them, or the user doesn't want something simple like the last one, I'm highly likely to pick the wrong one and give them unwanted advice about it.

I could make the users populate a lookup to a custom object and then pass the ID of that into the Prompt Template. The downside to this is my users have to do extra work in order to make the AI assistant useful - the exact opposite of how it's supposed to work. I'd also have to do a data fix to create custom object records for the recent history of Events.

Rendering Lists of Activities

One of the nice features of Copilot is if my custom action returns a list of sobjects in the correct format, it will apply Lightning Design System formatting:

So I decided to create a custom action that retrieves the Tasks that I should be focusing on - this has a bunch of rules in the action to determine which Tasks are actually important, as anyone who creates a Task for me will naturally mark it as Urgent and High Priority.

I'm able to retrieve the list of Tasks, but Copilot can't render them:

and check out that error message - Copilot fails to render the information provided and suggests you talk to your admin about it! I predict frustration all round!

Undeterred, I changed my code to return a list of custom Apex class instances that mirrored a task, so a property for Id, a property for Subject etc. Slightly better, in that it briefly rendered some text and then errored again.

After quite a bit of trial and error I was able to get a list of custom Apex class instances to render - I had to either remove the Id of the task, or put it into a String property that had any name other than Id. It looked awful by comparison, and I had no way to generate a link to any of the Tasks, as I'd had to put a false nose and dark glasses on the Id. The user could copy and paste the Id, but it seemed like the introduction of an AI assistant was once again leading to more work for the User, not less. So here there is a workaround, but it's not a great user experience.

My suspicion regarding this problem is that if Copilot can detect that there is either a Task or the Id of a Task in the data, it tries to pull some information about the Task sObject and then errors. Maybe via the User Interface API, given this is rendering on the screen, which doesn't support Tasks.


I'm really surprised at this gap, as Tasks and Events are pretty fundamental to Customer Relationship Management. It's either quite the oversight or, left out because the APIs don't support them in. the hope that nobody notices. It's all well and good having every web and commerce interaction with your customer in Data Cloud, but if you can't ask questions of your standard CRM objects, then we really aren't getting the AI + Data + CRM that we've been promised will change our lives.

Related Posts

Thursday 18 April 2024

Chaining Einstein Copilot Custom Actions

Image created by DALL-E 3 based in a prompt by Bob Buzzard


I've been testing out Salesforce's Einstein Copilot assistant for a few weeks now, but typically I've been tacking a single custom action onto the end of one or more standard actions, and usually a Prompt Template where I can easily ground it with some data from a specific record. 

Something that hasn't been overly clear to me is how I can link a couple of custom actions and pass information other than record ids between them. The kind of information that would be shown to the user if it was a single action, or the kind of information that the user had to provide in a request.


The other aspect I was interested in was some kind of DML. I've seen examples where a record gets created or fields get changed based on a request, but the beta standard actions don't have this capability, so it was clear I'd need to build that myself. So the scenario I came up with was : Given an opportunity, it's existing related activities, and a few rules (like if it's about to close someone should be in contact every day), Copilot suggests a follow up task and inserts it into the Salesforce database. 

This can't easily (or sensibly, really) be done in a single custom action. I guess I could create a custom Apex action that uses a Prompt Template to get a task recommendation from the LLM and then inserts it, but it seems a bit clunky and isn't overly reusable. What I want here are two custom actions:

  1. A Prompt Template custom action that gets the suggestion for the task from the LLM
  2. An Apex custom action that takes the details of the suggestion and uses it to create a Salesforce task.
I also don't want to have to expend a lot of effort processing the recommendation response to pick out the important details - if I'm writing that much code I might as well figure out the task that I need as well.


The key aspect of the Prompt Template is explaining how I want the response to be formatted, so that it can be easily processed by Apex code. I was half expecting to have to use few shot prompting to get this to work, but was pleasantly surprised to find that I could just describe it in natural language with a few rules about labelling:
Generate a recommendation for a follow up task in JSON format, including the following details:
- The subject of the task with the element label 'subject'
- A brief description with the element label 'description' - this should include the names of anyone other than the user responsible for the task who should be involved
- The date the task should be completed by, in DD/MM/YYYY format with the element label' due_date'
- The record id of the user who is responsible for the task with the element label 'user_id'
- {!$Input:Candidate_Opportunity.Id} with the element label 'what_id'

Do not include any supporting text, output JSON only.
Note that I did have to remind it not to add any text over and above the JSON output - this is something LLMs tend to suffer from a lot in my experience, always wanting to a add a "here you go" or "as requested". Nice that they try to be polite, but not helpful when you need structured data!

Trying this out in Copilot Builder showed that I was getting the output that I wanted:

Note that while there is the additional text of 'Great, I have generated ...', that's Copilot rather than the LLM, so if I can chain this to another custom action I'll just get the JSON format data.

My Apex Custom Action code is surprisingly svelte:

public with sharing class CopilotOppFollowUpTask 
    @InvocableMethod(label='Create Task' description='Creates a task')
    public static List<String> createTask(List<String> tasksJSON) {

        JSONParser parser=JSON.createParser(tasksJSON[0]);
        Map<String, String> params=new Map<String, String>();

        while (null!=parser.nextToken()) 
            if (JSONToken.FIELD_NAME==parser.getCurrentToken())
                String name=parser.getText();
                String value=parser.getText();
                System.debug('Name = ' + name + ', Value = ' + value);
                params.put(name, value);

        String dateStr=params.get('due_date');

        Date dueDate=date.newInstance(Integer.valueOf(dateStr.substring(6,10)),
                                      Integer.valueOf(dateStr.substring(3, 5)),
                                      Integer.valueOf(dateStr.substring(0, 2)));

        Task task=new Task(Subject=params.get('subject'),

        insert task;
        return new List<String>{'Task created with id ' + task.Id};

All that's needed to make it available for a Custom Action is the invocable aspect :

    @InvocableMethod(label='Create Task' description='Creates a task')

When I define the custom action, it's all about the instruction:

which hopefully is enough for the Copilot reasoning engine to figure out it can use the JSON format output from the task recommendation action. Of course I still need to give Copilot the correct instruction so it understands it needs to chain the actions:

And here's the task it came up with:

Related Posts

Sunday 7 April 2024

Einstein Copliot Custom Actions

Image created by DALL-E 3 based on a prompt by Bob Buzzard


One of the key features of Einstein Copilot from Salesforce is its extensibility - while there are a bunch of standard actions, and many more coming, there will always be something more you want to give your users. Custom actions allow you to create an AI assistant that targets your specific business challenges, adding capabilities to your copilot that generate real value for your users.


In my last post, I introduced the Sales Coach - a custom Lightning Web Component that you add to an Opportunity record page. When the Sales Coach component renders, it executes Apex code to hydrate a prompt template with details of the Opportunity and request some guidance from an LLM. 

While this works well for users that are viewing an Opportunity record, I also want to provide a way tfor users receive guidance on deals while they are in another part of the Salesforce application - their Home Page, for example.

Creating the Custom Copilot Action

Custom Copilot Actions can be created using Apex, Flow or Prompt Templates. I've gone for a Prompt Template, as I already have this in place for the Sales Coach component. Once I choose the Reference Action Type of Prompt Template, I get to select from a list of existing prompts in the Reference Action selector. 

Once I've chosen my action, I provide details of how the Copilot can use it :

The Copilot Action Instructions is pre-populated from my Prompt Template description. As I want to use it in exactly the same way, I can leave that alone. I then provide the instructions around the Candidate Opportunity input, and check the box in the Output section to Show in conversation - without this, the user won't be able to receive the coaching they so richly deserve.

Note that while the Prompt Template pull several fields from the Opportunity record - Name, Amount, Close Date, Stage Name - I don't need to specify any of this information here. I don't even need to specify that it's an Opportunity record - that is picked up automatically from the Prompt Template input. Pretty simple really.

Once my custom action is created, I need to add it to the Copilot. I do this via the Copilot Builder, selecting my new action from the list of available actions and then clicking a button to add it to the Active Copilot :

As an aside, this Copilot isn't Active yet, so the text in the button is slightly misleading, but not a big deal as I can only have one Copilot so I'm hardly likely to get confused.

Putting it Together

Remember earlier when I created the Copilot action I gave it details of how to use it? The Copilot Action Instructions field does what it says on the tin - it instructs the Copilot as to the purpose of the action and when it should be used:

I've defined this as providing advice to progress a specific opportunity, so in order to include the action in the Copilot plan, I need to ask the it to help me do that. As we're in AI world, I don't need to be as obvious as asking for advice to progress an opportunity, instead I used my own words and asked for "help to win a deal".

Interestingly, when I ran this to capture the screenshots, Copilot told me that it was following the instructions that I'd provided for the action, which I hadn't seen before:

Something to remember when using Prompt Template actions is they will lead to a slower response time, as each one involves a round trip to an LLM. There will always be one round trip, to create the plan, but if you manage to generate a request that involves multiple Prompt Template actions, there will be separate requests made for each of those, and you'll be consuming additional tokens. 

Monday 1 April 2024

Deploy Code at the Speed of AI with Ship Happens

Image from DALL-E 3 based on a prompt by Bob Buzzard


It's been quite the year for Salesforce Einstein - Copilot, Prompt Builder, lots of new Generative AI features, and it's only the beginning of April!

Einstein for Developers has been out in beta for a while now, built on CodeGen - Salesforce's in-house open-source LLM. To date this as been focused on generating code from natural language prompts, auto-completions. and unit tests, but now it's time for artificial intelligence to dip a toe into the Dev Ops space with Ship Happens.

Ship Happens

In keeping with most Generative AI launches, Einstein Dev Ops goes live with a single feature and the expectation that more will come in a few months. It's quite the feature though - with Ship Happens, your code will barely have a chance to touch down in a scratch org before it's flung to production and into the hands of users.

You've Got to be Shipping Me

Research shows that by far the biggest blocker to getting code into production is developers accepting that they have finished. While this sounds like a simple decision, developers can't help polishing - always looking to squeeze another feature in, or a percentage point of unit test coverage.  Ship Happens unblocks your development team by handing the decision off to Generative AI.

As development progresses, the code is constantly analysed by a Large Language Model trained on years of Salesforce deployments, both successful and unsuccessful, and the quality of code for your specific instance. Once the LLM decides the code is as good as it's ever going to be, it's committed to version control and on it's way to production before the developer knows what has hit them. To ensure full traceability, Ship Happens reuses the same commit message when it takes the decision to ship a feature - "You've Got to be Shipping Me".

Welcome to Ship Creek

Crunching the numbers after the pilot program showed that users are delighted when Ship Happens. An updated experience every time they login, reduced waiting time for new features, and the oh so familiar bugs they have come to expect with every release. Once they accept their role under our new AI overlords, your developers will enjoy the freedom to focus purely on writing code - with Ship Happens in charge, everyone knows that features will be released exactly when they should be, not a moment earlier and not a moment later. Before long, everyone will be happily floating along Ship Creek and forget they were ever in a different place.

Sunday 24 March 2024

Einstein Prompt Templates in Apex - the Sales Coach

Image by DALL-E 3 based on a prompt from Bob Buzzard


Thus far in my journey with Generative AI in Salesforce I've been focused on using prompt templates out of the box - sales emails, field generation, or in custom copilot actions. Salesforce isn't the boss of me though, so I'm also keen to figure out how to include prompts into my own applications.

The Use Case

I want a way for sales reps to get advice on what they should do with an opportunity - is it worth proceeding with, what does it make sense to do next, that kind of thing. I don't want this happening every time I load the page, as this will consume tokens and cost money, so I'm going to make it available as an LWC inside a tab that isn't the default on the opportunity page:

The Coach tab contains my LWC, but the user has to select the tab to receive the coaching. I could make this a button, but pressing buttons fulfils an innate desire for control and exploration in humans, so it could still end up consuming a lot of tokens. Opening tabs, for some reason, doesn't trigger the same urges.

The Implementation

The first thing to figure out was how to "execute" a prompt from Apex - how to hydrate the prompt with data about an opportunity, send it to a Large Language Model, and receive a response. This is achieved via the Apex ConnectAPI namespace. It's a bit of a multi-step process, so you should be prepared to write a little more code than you might expect.

Prepare the Input

The prompt template for coaching takes a single parameter - the id of the Opportunity the user is accessing. Prompt templates receive their inputs via a ConnectApi.​EinsteinPrompt​Template​GenerationsInput instance, which contains a map of ConnectApi.WrappedValue elements keyed by the API name of the parameter from the prompt template. The ConnectApi.WrappedValue is also a map though, in this case with a single entry of the opportunity id with the key value 'id'. As mentioned, a little more code than you might have expected to get a single id parameter passed to a prompt, but nothing too horrific:

ConnectApi.EinsteinPromptTemplateGenerationsInput promptGenerationsInput = 
                           new ConnectApi.EinsteinPromptTemplateGenerationsInput();

Map<String,ConnectApi.WrappedValue> valueMap = new Map<String,ConnectApi.WrappedValue>();

Map<String, String> opportunityRecordIdMap = new Map<String, String>();
opportunityRecordIdMap.put('id', oppId); 

ConnectApi.WrappedValue opportunityWrappedValue = new ConnectApi.WrappedValue();
opportunityWrappedValue.value = opportunityRecordIdMap;

valueMap.put('Input:Candidate_Opportunity', opportunityWrappedValue);

promptGenerationsInput.inputParams = valueMap;
promptGenerationsInput.isPreview = false;
Note that the ConnectApi.EinsteinPromptTemplateGenerationsInput handles more than just the inputs - for example, I've set the isPreview property to false, indicating that I don't just want to preview the prompt hydrated with data, I want to send it to the LLM and receive a response. There's also a ConnectApi.​EinsteinLlm​Additional​ConfigInput property to send granular configuration information such as temperature to the LLM

Make the Request

Once I have the input ready, I can send the request to the LLM.
ConnectApi.EinsteinPromptTemplateGenerationsRepresentation generationsOutput = 
Note that I've hardcoded the ID of the prompt template in the request. I really didn't want to do this, but I can't find a way to query the prompt templates in the system. The metadata type is GenAiPromptTemplate, but this doesn't appear to be queryable, not by me at least! This is the name that getSObjectType returns, but it doesn't appear in the Salesforce Object Reference and both Apex and the Tooling API error when I try to use it in a query. I'm sure this is on its way, but if I put this into production I'll probably expose a property on the LWC so that whoever is building the record page can supply the Id there. And if it's my Evil Co-Worker, they can supply a completely different prompt template Id to introduce confusion and chaos, so everybody should be happy.

Extracting the Response

The output from the LLM is in the form of a ConnectAPI.EinsteinLLMGenerationItemOutput. This contains a generations property that is a list of responses from the LLM. Right now I'm just picking the first element in the list and pulling the text property containing the actual information that I want to display to the user.  I can also access the safety score if I want.

Putting it in front of the User

The Lightning Web Component is pretty vanilla - when the user opens the tab the record id is passed via a property setting decorated with @api. This triggers the request to the server to send the prompt to the LLM:
@api get recordId() { 
    return this._recordId; 

set recordId(value) {
    GetAdvice({oppId : this._recordId})
    .then(data => {

Note that I have to do this via an imperative Apex call rather than using the wire service, as calling an Apex ConnectAPI method consumes a DML call, which means my controller method can't be cacheable, which means it can't be wired.

The user receives a holding message that the coach is thinking:

Then after a few seconds, the Coach is ready to point them in the right direction:

Related Posts

Saturday 16 March 2024

Einstein Prompt Grounding with Apex

Image generated by DALL-E 3 in response to a prompt from Bob Buzzard


A couple of weeks ago I published my initial experience and thoughts on Einstein Prompt Builder, and one of my findings was that I couldn't get grounding with Apex to work, even using the example code in the help. Thanks to some help from Claudio Moraes from the Salesforce Partner team, that is no longer the case and I can pull information into my prompt via an Apex class. 

Example Scenario

The prompt I've created is following up on a lead where we have a pretty good idea what product they are interested in. Using standard prompting I can get the user to select the product in questions, but I also want to include the standard price to give the lead an idea of what it might cost them. I can pull the PricebookEntries related list from the product into the prompt, but that is all or nothing, whereas I just want the a single entry from the standard price book.

The Apex Code

In order to be considered for use in a Sales Email Prompt Template, I need a method with the following signature:

@InvocableMethod(label='Price for Lead' 
                 description='Gets the price of a product for a lead' 
public static List<Response> getProductStandardPrices(List<Request> requests) 

The key aspects of this are:

It must be an invocable method

Which simply means it needs the @InvocableMethod annotation. Without this, the Apex class will never be considered by Prompt Builder.

It has to have the CapabilityType attribute on the @InvocableMethod annotation

This identifies the type of Prompt Template that the method can be used with. As this is for a Sales Email, I specify :


It must take a list of Request class instances

Request is an inner class that I have to define, with the format:

 public class Request {
    public User Sender;

    public Lead Recipient;

    public Product2 RelatedObject;

The variable names must be exactly as shown here - including the capitalisation (thanks Claudio!). These match the properties of the Prompt Template.

It must return a list of Response class instances

Again, Response is an inner class that I define, with the format:

 public class Response {
    public String prompt;

The body of the method itself is pretty straightforward:

@InvocableMethod(label='Price for Lead' 
                 description='Gets the price of a product for a lead' 
public static List<Response> getProductStandardPrices(List<Request> requests) 
    Request req=requests[0];
    Id productId=req.RelatedObject.Id;
    Response resp=new Response();
    PricebookEntry entry=[
            select Id, UnitPrice, CurrencyISOCode from PricebookEntry
            where Product2.Id=:productId
            and   Pricebook2.IsStandard = true];

    resp.prompt=entry.CurrencyISOCode + ' ' + entry.UnitPrice;
    return new List<Response>{resp};

Note that while the method has to take a list of requests and return a list of responses, there's only ever one element present.

Putting it Together

If I deploy my class and it doesn't show up in Prompt Builder because I've got the capitalisation wrong, it doesn't appear to be possible to recover it. Instead, I have to delete the class and try the next attempt with a new one (thanks again, Claudio!).  

Once I have the correct breed of chicken and the right colour chalk for the pentangle, Apex appears in the resource picker :

And clicking into the Apex option shows me the name of my enclosing class and the label attribute from the @InvocableMethod annotation :

And selecting this adds it to my prompt, helpfully highlighted to show me that it is a reference rather than plaintext :

The Prompt and Response

I've added this to a pretty simple prompt - I doubt it would make much difference to a potential customer, but the point of this blog is to show the Apex, not what makes a great prompt! Note that as I didn't ground the prompt with any details of the user, they are expected to add that themselves :

and the response from the LLM:


Thank You for Your Interest in Solar Panels


Dear Lauren Reynolds, 

Thank you for your interest in our solar panels. We are pleased to share the price with you. The cost for a single panel is USD 1725.00. Please note that discounts are available for larger orders, but this price already reflects an excellent value for such a high-quality product. 

We hope this information is helpful to you, and we look forward to the possibility of working with you. 

Should you have any further questions or require additional details, please do not hesitate to reach out. 

Warm regards,
[Your Name]
Credera Sales Team

More Information