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:
Create a simple text variable in your flow named recordId:
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:
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.
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.
Name: Set Case Priority while Creating a New Case
In the first Create Case screen, the user will enter a Case Subject and Case Description as you can see below:
Notice that we've selected Let AI Determine Conditions. Below this in the Describe the Decision section, we've added our prompt:
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:
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:
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:
- Normal multi-line comments begin with /* and end with */. However, ApexDoc comments begin with /** and end with */.
- 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 {...} - It is not placed randomly in between some code. It precedes the class, interface, enum, method, constructor or property declaration which it is documenting.
- 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:
- 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:
/* * Service class for performing HTTP Callouts. * @author Rahul Malhotra */ public class HTTPCalloutService {...}Here @author tag is a block tag- 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;
}
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"
}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.
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:
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:
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:
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:
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
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
Low Priority Scenario
Case Subject: Applying for a new card
Case Description: Hi, please find my card application here.
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:
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