SFDC Stop - Always the latest about Salesforce


Full Tutorial Series with videos, free apps, live sessions, salesforce consulting and much more.


Telegram logo   Join our Telegram Channel

Sunday, 9 November 2025

Salesforce Winter '26: The 6 Must-See Features That Will Change How You Work

Aloha Trailblazers!

Today, we're diving headfirst into the new features of the Salesforce Winter '26 Release. This release is packed with enhancements that will supercharge the way we build and automate on the platform.

Here are six of the most impactful features I've selected which you need to know about.

1. Use LWC Components for Local Actions in Screen Flows

Your LWC components can be used as actions in Screen Flows. The key here is: lightning__FlowAction keyword. Let's take a look at the below code:
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
   <apiVersion>64.0</apiVersion>
   <isExposed>true</isExposed>
   <targets>
       <target>lightning__FlowAction</target>
   </targets>
   <targetConfigs>
       <targetConfig targets="lightning__FlowAction">
           <property name="firstName" type="String" label="First Name" />
           <property name="lastName" type="String" label="Last Name" />
       </targetConfig>
   </targetConfigs>
</LightningComponentBundle>
Here I've mentioned the target as lightning__FlowAction and there are some target configs defined for this target. The functionality of the lightning component depends on the code of that component. Its just that this component can now be used as an action in a screen flow.

For our sample LWC component, its functionality is to use the firstName and lastName property and show a toast. You can see the js file code below:
import { api, LightningElement } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';


export default class ShowToastExampleComponent extends LightningElement {
   @api firstName;
   @api lastName;

   @api invoke(cancelToken) {

       this.dispatchEvent(new ShowToastEvent({
           title: 'Hey!',
           message: this.firstName + ' ' +this.lastName,
       }));
   }
}
I've defined an invoke() which will be called automatically by our flow as it moves to the action calling this LWC component. Let's take a look at the sample flow I created:


This flow is simply querying the First Name and Last Name of a contact record and passing it to my LWC component which will show a toast. I'm also sharing the configuration of this flow below for reference:

Create a simple text variable in your flow named recordId:
Use it to query the contact record (we're going to place this screen flow on the contact's lightning record page)
I'm querying only the FirstName and LastName of the record as I only need that. There's an option to store all the fields automatically but I still feel this query is optimized and might save some time (Its my personal preference and I don't have any evidence though)
Now, as you select an Action element in this flow, you'll have an option to choose ShowToastExampleComponent as an action because we've exposed this LWC as a flow action. We can choose it and pass the First Name and Last Name of my queried contact record as input values to my LWC component properties which we defined at the beginning of this section:
The value of First Name field should be {!Get_Contact_using_Record_Id.FirstName}  and Last Name field should be {!Get_Contact_using_Record_Id.LastName}. You can simply activate the flow and put it in your contact lightning record page as shown below:
If you notice above, I'm passing the contact record id to our recordId variable which we defined in the flow so that it can query the correct contact record. As you save and activate this page, you'll see the toast as you open any contact record:
There's a very good documentation by Salesforce on How to configure API callouts in LWC flow actions. You can go through it to understand it in detail. The LWC in this documentation returns a Promise. When the promise is resolved, the flow moves on to the next element and when the promise is rejected/timeout, the flow will execute the fault path connected to your LWC local action element. The error message that you'll return in your reject() will be set in $Flow.FaultMessage. You can also cancel the ongoing API callout if its taking too long using the cancelToken parameter which can be passed to the invoke() method. I mentioned it in our LWC code above but we didn't use it for our example.

2. Standardize Apex Documentation with the ApexDoc Comment Format

Clean, well-documented code is the hallmark of a professional developer. However, there has been a lack of a standardized way to document Apex. With Winter '26, Salesforce is providing a built-in standard for Apex documentation by officially supporting the ApexDoc comment format which is based on JavaDoc standard.

This means you can now use special tokens like @author@param and @return in your Apex comments, and they will be recognized by the platform. This will help standardize documentation across projects and teams, making it easier for new developers (as well as AI) to understand your code and for existing developers to maintain it. Below are a few important points about ApexDoc comments:

  1. Normal multi-line comments begin with /* and end with */. However, ApexDoc comments begin with /** and end with */.

  2. It spans multiple lines where each line begins with an asterisk (*). Example:
    /**
    * This is a multi-line ApexDoc comment.
    * Each line will start with an "*" as
    * you can notice in this comment.
    */
    public class MyClass {...}
    
    
  3. It is not placed randomly in between some code. It precedes the class, interface, enum, method, constructor or property declaration which it is documenting.

  4. It is supposed to start with the description but there's no @description tag for it. You can simply start your comment with a descrption where the first line should be a summary ending with a period (.) and the other lines can signify details. In the example I shared in point 2 above, This is a multi-line ApexDoc comment. - is a summary as it is ending with a period and the lines following it i.e. Each line will start with an "*" as you can notice in this comment. signify details of the description of our class.

Moving on, there are two types of tags, we should understand about ApexDoc comments:

  1. Block Tags: These tags are used after the main description (first line) of ApexDoc comment. A block tag begin with @ symbol followed by its name like: @param@author etc. Each block tag starts with a new line. Example:
  2. /*
    * Service class for performing HTTP Callouts.
    * @author Rahul Malhotra
    */
    public class HTTPCalloutService {...}
  3. Here @author tag is a block tag

  4. Inline Tags: These tags also begin with @ symbol but they're enclosed in curly braces {}. They can be used within the main description or within the description of a block tag. Example:
    /*
    * Service class for performing HTTP Callouts.
    * This class will use the name of the configuration passed in the constructor
    * to create HTTP Request.
    * @author Rahul Malhotra
    * @example
    * {@code
    * HttpCalloutService service = new HTTPCalloutService('SFDCStopBlogs');
    * System.debug(service.getRequest());
    * System.debug(service.sendRequest().getBody());
    * }
    */
    public class HTTPCalloutService {...}
    Here @author tag and @example tags are block tags, whereas @code tag which is enclosed within curly braces {} is an inline tag.

Some of the tags which might be commonly used apart from the ones I shared above are: 
  • @deprecated tag - if you have managed package code which is getting deprecated in the new release of your package

  • @param tag to specify parameter of a method or constructor

  • @return tag which specifies the value being returned (do not use it for constructors or void methods as they don't return anything)

  • @throws tag which documents an exception that can be thrown etc. Another example showcasing usage of these tags is provided below:
/**
 * Retrieves all Contact records associated with a specific Account ID.
 * This method performs a SOQL query to find contacts where the standard
 * `AccountId` field matches the accountId provided by the user. It validates that the
 * accountId is not null before executing the query.
 * @param accountId The unique ID of the parent Account. Cannot be null.
 * @return A list of Contact records related to the specified Account. Returns an empty list
 *         if no contacts are found.
 * @throws System.IllegalArgumentException If the provided `accountId` is null.
 * @example
 * {@code
 * ID parentAccountId = '001xxxxxxxxxxxxxxx'; // Replace with a valid Account ID
 * try {
 *     List<Contact> relatedContacts = AccountManager.getContactsByAccountId(parentAccountId);
 *     System.debug('Found ' + relatedContacts.size() + ' contacts for the account.');
 * } catch (System.IllegalArgumentException e) {
 *     System.debug('Error: ' + e.getMessage());
 * }
 * }
 */
public static List<Contact> getContactsByAccountId(Id accountId) {
    // Check for null input and throw a standard exception
    if (accountId == null) {
        throw new System.IllegalArgumentException('Account Id cannot be null.');
    }

    // Use a SOQL query to find related contacts
    List<Contact> contacts = [
        SELECT Id, FirstName, LastName, Email, Phone
        FROM Contact
        WHERE AccountId = :accountId
    ];

    return contacts;
}
Note: We're assuming that the above method is a part of class named AccountManager (as specified in example code within the apex doc @example comment). There is also a guideline on how to document common Apex Annotations using apex doc which I'm not covering but you can check it out in the official salesforce documentation here.

3. Unify Testing with the Test Discovery and Test Runner APIs

Testing is a critical part of the development lifecycle, but managing tests can be complex, especially with different testing frameworks. Winter '26 introduces the Test Discovery and Test Runner APIs, which unify the testing process.

These new APIs allow you to programmatically discover and run tests, regardless of whether they are Apex tests or Automated Flow tests.  This provides a single, unified interface for your Continuous Integration/Continuous Deployment (CI/CD) pipelines, enabling you to build more robust and comprehensive automation. By standardizing how you interact with tests, you can simplify your development process and ensure consistent test coverage across your entire application.

Below is an example of Test Discovery API. I just sent a GET request to @{{_endpoint}}/services/data/v{{version}}/tooling/tests where {{_endpoint}} and {{version}} are variables in my postman application specifying the endpoint of my org and the API version. I got the below response from Test Discovery API:
{
    "apexTestClasses": [
        {
            "id": "01pIn000000fd7a",
            "name": "HTTPCalloutAsyncServiceTest",
            "namespacePrefix": "",
            "testMethods": [
                {
                    "name": "testWithCustomMetadata"
                },
                {
                    "name": "testWithCustomMetadataAndNoTimeout"
                },
                {
                    "name": "testWithCustomMetadataRequestLimitExceeded"
                },
                {
                    "name": "testWithoutCustomMetadata"
                },
                {
                    "name": "testWithoutCustomMetadataRequestLimitExceeded"
                }
            ]
        },
        {
            "id": "01pIn000000fd7f",
            "name": "HTTPCalloutServiceTest",
            "namespacePrefix": "",
            "testMethods": [
                {
                    "name": "testWithCustomMetadata"
                },
                {
                    "name": "testWithCustomMetadataAndBlobInBody"
                },
                {
                    "name": "testWithCustomMetadataAndDocumentInBody"
                },
                {
                    "name": "testWithCustomMetadataMultipleRequests"
                },
                {
                    "name": "testWithCustomMetadataWrongCertificate"
                },
                {
                    "name": "testWithWrongCustomMetadata"
                },
                {
                    "name": "testWithoutCustomMetadata"
                }
            ]
        },
        {
            "id": "",
            "name": "Update_account_description_when_contact_is_updated",
            "namespacePrefix": "flowtesting",
            "testMethods": [
                {
                    "name": "CheckAccountisUpdatedonContactCreation"
                }
            ]
        }
    ],
    "message": null,
    "nextRecordsUrl": null,
    "size": 3,
    "testSetSignature": "fb50ed36c70eb07c226e8387a14895bc"
}
If you notice here, there are 3 records being returned under apexTestClasses array. The first two are Apex Test Classes named HTTPCalloutAsyncServiceTest and HTTPCalloutServiceTest, which got introduced in the org when I installed my HTTPCalloutFramework package and the third one is Update_account_description_when_contact_is_updated which is a simple flow in my org that updates an account's description as a contact record is created or updated. This flow has a declarative test with API name as CheckAccountisUpdatedonContactCreation which is specified as the test method in the above response JSON. Notice that the flow test record is coming with a namespace flowtesting. This confirms that, our Test Discovery API returns both the Apex tests as well as the Flow tests that we have defined in our org. We'll need to specify this flowtesting namespace as we want to run this test using the Test Runner API.

The flow details are shared below:


Now, inside this flow, I've created a new test named CheckAccountisUpdatedonContactCreation which you can see below:




If you noticed above, I've created a new Contact record for this test and then verified using an assert that the Update Account element should be visited.

Similarly you can run one/all of these tests together using the runtestsSynchronous and runTestsAsynchronous endpoints which are a part of Test Runner API.

Using runtestsSynchronous endpoint we can run only one test. However, we can run multiple tests simultaneously using runTestsAsynchronous endpoint. Let's use it to run one apex test and one flow test from the above together. I sent a POST request to this API: {{_endpoint}}/services/data/v{{version}}/tooling/runTestsAsynchronous  with the body as shown below:
{
    "tests": [
        {
            "className": "HTTPCalloutServiceTest",
            "testMethods": [
                "testWithCustomMetadata"
            ]
        },
        {
            "className": "flowtesting.Update_account_description_when_contact_is_updated",
            "testMethods": [
                "CheckAccountisUpdatedonContactCreation"
            ]
        }
    ]
}
Here, I'm running testWithCustomMetadata test method of my HTTPCalloutServiceTest class and CheckAccountisUpdatedonContactCreation method of my flowtesting.Update_account_description_when_contact_is_updated  class which is nothing but our flow test. In the response, you'll get a simple AsyncApexJobId as shown below:


The tests are now running in Salesforce. We can use this job id and call the query tooling API to get results from Salesforce. In the above example our job id is: 707In00000PgAyG, so, we're going to perform a simple GET request as: {{_endpoint}}/services/data/v{{version}}/tooling/query/?q=SELECT+Id,ApexClass. Name,MethodName,Outcome,Message,StackTrace+FROM+ApexTestResult+WHERE+AsyncApexJob Id+=+'707In00000PgAyG'

The output of this request is as follows:


As you can see above, we executed two tests and the operation is completed successfully. You can see the same output below for detailed understanding:
{
    "size": 2,
    "totalSize": 2,
    "done": true,
    "queryLocator": null,
    "entityTypeName": "ApexTestResult",
    "records": [
        {
            "attributes": {
                "type": "ApexTestResult",
                "url": "/services/data/v65.0/tooling/sobjects/ApexTestResult/07MIn0000026uVUMAY"
            },
            "Id": "07MIn0000026uVUMAY",
            "ApexClass": {
                "attributes": {
                    "type": "ApexClass",
                    "url": "/services/data/v65.0/tooling/sobjects/ApexClass/01pIn000000fd7fIAA"
                },
                "Name": "HTTPCalloutServiceTest"
            },
            "MethodName": "testWithCustomMetadata",
            "Outcome": "Pass",
            "Message": null,
            "StackTrace": null
        },
        {
            "attributes": {
                "type": "ApexTestResult",
                "url": "/services/data/v65.0/tooling/sobjects/ApexTestResult/07MIn0000026uVZMAY"
            },
            "Id": "07MIn0000026uVZMAY",
            "ApexClass": null,
            "MethodName": "CheckAccountisUpdatedonContactCreation",
            "Outcome": "Pass",
            "Message": null,
            "StackTrace": null
        }
    ]
}
If you notice above, both our apex test: testWithCustomMetadata and our flow test CheckAccountisUpdatedonContactCreation are completed with outcome as Pass. This is how we can query and execute multiple tests related to apex classes and flows together in one go using the Test Discovery and Test Runner APIs.

4. Automate Complicated Decisions in Flows with Generative AI

Artificial intelligence is making its way into every part of the Salesforce platform, and Flow is no exception. With the Winter '26 release, you can now use Generative AI directly within your Flow decisions.

In this update you can let AI analyze a record based on the information present in it and take the right decision. 

Note: This feature isn't supported for some flows like: Record Triggered flows, record triggered flow approval process or marketing cloud flows.

Now, as we're using Generative AI, the field used in a decision element doesn't have to be something like a picklist which can have only a fixed set of values or a boolean, it can be a text field as well. For example: You're building a feature that allows to create a case using a screen flow and you want to check if the case is urgent based on the case description (maybe via keywords like: urgent, as soon as possible, top priority) and subject, then the case priority should automatically set to High. You can do that by giving a right prompt in your flow. Let's see how that works in action:

Screen Flow: Set Case Priority while creating a New Case

Let's create a screen flow using which where we're trying to create a new case. The screen flow should set the case priority based on the Subject and Description of the case.


Name: Set Case Priority while Creating a New Case

For this demo, we're not going to actually create a record, instead we'll navigate the user to different screens with different outputs. That decision is taken using Generative AI decision element. This is how the whole flow looks like:


In the first Create Case screen, the user will enter a Case Subject and Case Description as you can see below:


These details are entered in simple text input fields with API names as Case_Subject and Case_Description. After that in the decision element, we'll choose whether the case priority should be High, Medium or Low based on the subject and description. We'll let AI take this call for us. The decision element is shown below:


Notice that we've selected Let AI Determine Conditions. Below this in the Describe the Decision section, we've added our prompt:


In this prompt, we've used {!Case_Subject} and {!Case_Description} merge fields which makes our prompt more effective and helps AI to take the right decision.

Decision Instructions (Prompt): I want to find out whether the Case priority should be High, Medium or Low based on {!Case_Subject} {!Case_Description}

Finally in the outcome, I've 3 outcomes High, Medium and Low where High and Medium outcomes have outcome instructions whereas Low is the default outcome. Let's see the outcome instructions as well:


If you see above I've added a prompt in my Outcome Instruction for High output as: Execute this path if the case priority should be High. Similarly, you can see the below outcome details for Medium outcome:


Here the outcome instruction is: Execute this path if the case priority should be Medium. Notice that its different from the one used above as it says Medium here instead of High. This outcome instruction is also a prompt and you can improve it even further by using merge fields. For example, you can write something like: Execute this path if the case priority should be High by analyzing the {Case_Subject} and {!Case_Description}. I haven't added a complex prompt for this demo flow but you might need to test your flow outcome and improve your Decision and Outcome instructions accordingly. The third outcome is the default one so we don't have any prompt for it as shown below:


After that, we've added 3 screens for each of these 3 outcomes - High, Medium and Low as follows:




Let's test our flow now!

High Priority Scenario

Case Subject: [Important] Refund for Online Course
Case Description: Hi, I would not like to continue the course I purchased. Please issue a refund as soon as possible!

Outcome

As you can see, the outcome is coming as: Case priority is High

Medium Priority Scenario

Case Subject: Less Quantity of Items Received
Case Description: Hi, I've received most of the items for my order number O-123 but one soap bar isn't there in the package, can you please check and send that?


Outcome


As you can see, the outcome is coming as: Case priority is Medium

Low Priority Scenario

Case Subject: Applying for a new card
Case Description: Hi, please find my card application here.


Outcome

As you can see, the outcome is coming as: Case priority is Low

All these decisions are taken entirely by AI using the prompt we configured in our flow. If you see in general, a refund should be a high priority case, if a soap bar is not delivered it could be a medium priority and if someone applied for a new card, it can be a low priority case as usually the application takes a fews days to process.

5. Get Related Records Across All Levels with Nested Loops (Beta)

For a long time, retrieving related records in a Flow required multiple "Get Records" elements, one for each level of the hierarchy. This could get messy and was difficult to manage. The Winter '26 release changes this with Nested Loops.

Now in beta, you can use a single "Get Records" element and then use nested loops to traverse related records across multiple levels. For example, you can get an Account, then loop through all its related Contacts, and then for each Contact, loop through their related Cases and for each Case, loop through their Case Team members.  This makes your Flows much cleaner, more efficient, and easier to read, especially when dealing with complex data models.

Note: As of now, I could find this feature Only in Autolaunched Flows.

Let's take a look at the below example. We're going to create an Autolaunched flow and select the checkbox, Also get related records (beta) while selecting the object in our Get Records element as shown below:


If you see above, we've a GetRecords element named Query Account with Related Records. Now in this I've selected the object as Account and I've also selected the checkbox Also get related records (beta). As we select that checkbox, we've the section visible with a button as Select Related Records. Salesforce has the below notice as this feature is in Beta currently: 

This feature is a beta service that is subject to the Beta Services Terms at Agreements - Salesforce.com, and applicable terms in the Product Terms Directory. Use of this beta service is at the Customer's sole discretion. After you select related records, they appear here.

As we click the Select Related Records button, we'll have a popup to select related records as shown below:


If you notice above, I've clicked the + button near Account on the left side and selected Contacts from the Objects Related to Account list. Now, as you click the Select Object button, it'll be marked to be queried as a related record. For the base record and each related record that you're querying, you can also select the fields you want to query from the right hand side. You can also add some filters for each object you're querying and in the Preview tab you can see how the hierarchy looks like. As, I selected the related records, my final state looks as shown below:


Here I'm querying Contacts related to Account, Cases related to Contacts and then Case Team Members related to Cases all in a single go. For the sake of this demo, I'm querying a particular account so I've added a filter condition for Account record to query an account based on the given Account Id as you can see below:


Now this Account in my org has a contact related to it, that contact has a case related to it and finally that case has 2 case team members. Therefore, I've selected to query All records for TeamMembers as shown below:


As you save it, you can preview your selections. Under the Selected Objects section, you can see the whole hierarchy as shown below:


However, if you expand Filter, Sort, and Store Records section, you can see the filters and sort conditions you selected for each object you're querying as shown below:



Notice that its showing the condition added for Account Id. Also, we're storing Only the first record for Account, related Contacts and related Cases. However, for related TeamMembers, I'm storing All Records. Now let's loop these records to ensure we've queried everything. 


If you see above, the order is as follows:

1. We're looping Contacts related to the Account. In the Collection Variable box, we have {!Query_Account_with_Related_Records.Contacts} as shown see below:


2. For each Contact, we're looping Cases related to the current Contact. For this, in the Collection Variable box, we have {!Loop_Contacts.Cases}:


3. Now, for each Case, we're looping Case Team Members related to the current Case. Therefore, in the Collection Variable box, we have {!Loop_Cases.TeamMembers}:


4. Finally, for each Case Team Member, we're adding it to a list using an Assignment element. Here in the Variable box we have a list variable named CaseTeamMembers which we created with Data Type as Record and Object as Case Team Member as shown below:


We're adding every Case Team Member record to this list variable using the Add operator and having {!Loop_Case_Team_Members} as the value.


Now, as I debug this flow, I get the below result:


If you notice the output above, on the left hand side, we can see both the the Case Team Members stored in my CaseTeamMembers list. This shows that my nested loops are executed perfectly in this flow.

6. Transform Data for Actions in Element Setup

Data transformation is a common requirement in Flows, but it often involves multiple Assignment elements to prepare data before sending it to an action. This can be cumbersome and adds complexity to your canvas. You can use the separate Transform element as well, though now you have a better option.

Winter '26 introduces a new way to transform data directly while setting up the Action element. It means that when you're calling an Invocable Apex Action, in order to set its variable, you can simply choose an option that says Transform and then click Transform Data Mapping button which allows you to setup a mapping from flow variables (and their further references) to the variables accepted by our Apex Invocable method. I would've definitely shared a full fledged example for this feature as well however, I couldn't find this in my scratch org though its Winter 26 enabled. Therefore, I'm sharing the official salesforce documentation using which you can learn more about this feature. Access the documentation by clicking here.

That's all for this tutorial, I hope you liked it. Let me know your feedback in the comments down below.

Happy Trailblazing!

No comments:

Post a Comment