> ## Content Index
> Fetch the complete content index at: https://www.bulkifiedthinking.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Salesforce Winter `27 Is Here: All Developers Need to Know
- URL: https://www.bulkifiedthinking.com/salesforce-winter-27-is-here-all-developers-need-to-know/
- Published: 2026-08-25T03:36:12.000Z
- Updated: 2026-08-25T03:36:12.000Z
- Description: A developer walkthrough of the Salesforce Winter '27 release: bigger Apex heap limits, integration tests that call real HTTP endpoints, the Apex Symbol API, elastic Batch Apex limits and two LWC features that finally went GA.
- Author: Andrii Sukhetskyi
- Tags: Winter '27, Developers, Apex, JavaScript, Agentforce

While we’re still licking our wounds from Summer ’26 and the various security updates that ran us over several times this summer, it’s time to talk about the next stage of our suffering.

The Winter ’27 release is around the corner, and today we’re going to talk about the **good** things in this release that will give all developers a little dose of ecstasy.

[Create a Preview Org](https://developer.salesforce.com/signup?ref=bulkifiedthinking.com) [Read the Release Notes](https://help.salesforce.com/s/articleView?id=release-notes.salesforce%5Frelease%5Fnotes.htm&type=5&ref=bulkifiedthinking.com) 

## Apex

One of the biggest surprises this time is the increase of the existing transaction **heap size** limit, which, just to be clear, does not happen very often.

### Increased Apex Heap Size Limits

Now you are able to consume up to 10 MB (from 6 MB) of heap in a synchronous transaction, and up to 25 MB (from 12 MB) in asynchronous transactions.

The exact numbers are 10,000,000 and 25,000,000 bytes.

0:00 

/0:14 

1× 

How to check heap size in Apex?

What it means is that now in Apex you can store much larger datasets in a single transaction, which helps a lot of old features to be resurrected and new features to be used even more broadly, as they receive more room to operate within.

It also helps with managing files in Salesforce.

You can now naturally retrieve larger files and if you’re using external storage: partial upload or download becomes faster, because one chunk of a file is simply bigger than it used to be.

And I haven’t even mentioned relatively new features like the [Compression namespace](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex%5Fnamespace%5Fcompression.htm?ref=bulkifiedthinking.com), which was originally introduced to create and extract zip files, and which can now feel much more confident handling multiple files and compressing them into something that actually makes sense to your users.

```Apex
// Synchronous
System.debug(Limits.getLimitHeapSize()); // 10000000
System.debug(Limits.getLimitCpuTime()); // 10000

// Inside a Queueable, Future and Batch execute()
System.debug(Limits.getLimitHeapSize()); // 25000000
System.debug(Limits.getLimitCpuTime()); // 60000
```

Now, so you wouldn’t get too excited about this, my takes:

- **The CPU time limit has not been changed.** It is still 10 seconds synchronous and 60 seconds asynchronous. You might be able to store large datasets and big media now, but that does **not** mean you will be able to process them. You’ll probably hit the CPU time just as good as you used to hit the heap size. Thank me for spoiling the party!
- **I refuse to believe they were so worried about your and my struggling that they increased the heap size limit.** It’s definitely because of AI, it has already given us things like the [Models API](https://developer.salesforce.com/docs/ai/agentforce/guide/models-api.html?ref=bulkifiedthinking.com) to open a route for more interaction with LLMs and to give them more context from Apex, and I suspect there is much more coming in this direction, so Salesforce had to prepare itself for those changes by increasing the heap. Thank me for reminding you that your struggling makes no difference to the enterprise world!

### **Call HTTP Endpoints in Apex Tests (Developer Preview)**

Another quite surprising upgrade goes to Apex test classes. In the new release, you’ll be able to create **true** integration tests and verify a real connection with third-party endpoints.

> Unit testing validates small, isolated pieces of code, **while integration testing** verifies that components, services and external dependencies work together correctly.

Before this release, the idea of working with a third-party endpoint was simple: you create a mock for it and try to predict what result it might return, how authentication works and every edge case related to the service.

```Apex
@IsTest
private class ExternalServiceCalloutTest {
    @IsTest
    private static void testHttpCallout() {
        // It's all fake and never triggers a real endpoint
        new HttpMock().get('/external/service').body('null').statusCodeBadRequest().mock();

        Test.startTest();
        HttpResponse result = ExternalServiceCalloutMock.getData();
        Test.stopTest();

        Assert.areEqual('null', result.getBody());
    }
}
```

In the Winter ’27 release, you are given an option to call a real HTTP endpoint right from your Apex test class, without creating any mock services **at all**.

```Apex
// This works in Apex Tests
Http protocol = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint('callout:NamedCredential/verify/' + acc.Id);
request.setMethod('GET');
HttpResponse response = protocol.send(request);
```

It has slightly different syntax though, and requires a few annotations to be added to the class that behaves as an integration test.

```Apex
@IntegrationTest
public class ExternalEndpointTest {
    @IntegrationTest
    public static void testExternalEndpoint() {
        // ...
    }
}
```

💡

Existing Apex test classes are ****not** affected, because they use different syntax and serve a different purpose.

If you want a little more interesting insights: **Nothing rolls back.** 

I ran a few scenarios inserting records in `@BeforeClass` and a few more inside the test method, and after the run every single one of them was still sitting in the org.

Regular Apex tests roll back and you never think about it. Integration tests commit, and your org keeps whatever you inserted.

Which is exactly why `@TearDown` is something you'd like to use:

```Apex
@IntegrationTest
public with sharing class ExternalEndpointTest {
    @BeforeClass
    public static void setup() {
        Account newCompany = new Account(Name = 'PaymentTestAccount');
        insert as user newCompany;
    }

    @IntegrationTest
    public static void testExternalEndpoint() {
        Account acc = [
            SELECT Id
            FROM Account
            WHERE Name = 'PaymentTestAccount'
            WITH USER_MODE
        ];

        Http protocol = new Http();
        HttpRequest request = new HttpRequest();
        request.setEndpoint('callout:PaymentGateway/verify/' + acc.Id);
        request.setMethod('GET');
        HttpResponse response = protocol.send(request);

        Assert.areEqual(200, response.getStatusCode());
    }

    @TearDown
    public static void tearDown() {
        delete [SELECT Id FROM Account WHERE Name = 'PaymentTestAccount'];
    }
}
```

For all the updates, retirements and more security updates, head over to the [Posts](https://www.bulkifiedthinking.com/posts/?important=true) tab to stay up to date on what could break your organization.

[Learn more ](https://www.bulkifiedthinking.com/posts/?important=true) 

### Additional Limits for Batch Apex (Beta)

Your organization has a rolling 24-hour limit on how many asynchronous Apex jobs it can run. Once you cross it, new jobs are rejected and your code receives a limit exception.

Elastic limits change what happens at that point. 

Instead of rejecting jobs the moment you reach the standard limit, the org keeps accepting them and reduces the processing rate instead.

The idea itself is **not new**. Summer ’26 introduced elastic limits for Queueable jobs and future methods. What Winter ’27 adds is **Batch Apex**.

![](https://storage.ghost.io/c/92/d5/92d55211-2cfe-45c9-b8c4-b7c7b1595cdb/content/images/2026/08/image-7.png)

Setup | Apex Settings

You’ll find the switch in **Setup → Custom Code → Apex Settings → Use elastic limits for asynchronous Apex jobs (Beta)**. Hover over the help icon next to it and the org gives you your own numbers. With the setting off, it says:

> The elastic limits setting is disabled. Your standard asynchronous job limit is **N** jobs executed per 24-hour period.

And with the setting on, it says:

> The elastic limits setting is enabled. Your elastic asynchronous job limit is **N+E** jobs executed per 24-hour period.

Me personally, I have never hit those limits in my entire life. And if you do, most likely you’re doing something wrong with the architecture or implementation of your Salesforce organization. For those, you can read more about how to design an org that does not suck here: [Design Your Org The Right Way](https://www.bulkifiedthinking.com/salesforce-design-your-org-the-right-way/)

### Apex Symbol API (Beta)

This one will primarily help your AI agents to understand Salesforce better.

It gives you a new endpoint `/services/data/vXX.0/tooling/symbols`, which you or your agent can call to retrieve detailed information about the standard and custom Apex classes, interfaces and triggers that are available for use.

```HTTP
GET /services/data/v68.0/tooling/symbols?category=builtin&namespace=System&name=ApexPages
Authorization: Bearer <session-id>
```

The `category` parameter is mandatory and takes `builtin`, `database` or `dynamic`. 

What comes back is everything the compiler knows about the type: every method, every parameter, every modifier and every annotation.

It also reads your ApexDoc. The `@description` tag becomes the leading text, and every `@param` is pulled out a second time into the parameter it belongs to:

```JSON
{
  "name": "snapshot",
  "returnType": { "name": "String" },
  "documentation": "Builds a one-line snapshot of the current governor limits.\n@param ctx ...",
  "parameters": [
    { "name": "ctx", "documentation": "A label identifying the execution context." }
  ]
}
```

So you don't write ApexDoc, an agent reading this API has **nothing** to work with beyond your method names, and we all know how descriptive your methods are.

I have no clear opinion on this capability yet. It needs time to show itself and whether it does bring a benefit.

That said, the fact that Salesforce APIs are extending is definitely good news, and I’ll be posting updates on the [Posts](https://www.bulkifiedthinking.com/posts/?important=true) section of this newsletter to keep you informed.

### API Versions Retirement

We also have a retirement coming for old Apex.

If you’re using classes and triggers marked with **versions 9.0 through 19.0**, first of all, you suck. Second of all, you must upgrade those classes to a newer version to avoid any compilation errors moving forward.

To help you with identifying them, Salesforce now returns a warning when you compile or deploy at those versions:

```
LegacyWarnProbe: 1,14 SEVERE [1:14]: Apex API version 16.0 is scheduled for
retirement. Update to the latest API version to avoid compilation errors.
```

## Salesforce CLI

The command-line interface has no such exciting updates this time, and mostly has small changes to current behavior which will not affect you. Maybe 😊

### Credentials are no longer printed in command output

As of CLI 2.136.8, `org display`, `org list --json`, `org create scratch --json` and several login commands stopped returning secrets. What you get instead is this:

```
"accessToken": "[REDACTED] Use 'sf org auth show-access-token' to view"
```

### **Use Unverified Email Domains in Scratch Orgs**

Not that long ago, Salesforce has introduced a new restriction to its email service restricting users from sending emails from unverified email domains.

We all went through this enforcement and activated quite a few DKIM keys for each domain that is being used in the org.

That rule makes sense in production, and it is extremely annoying in a scratch org you are going to delete in a few days. So the CLI added a switch for it:

```JSON
{
  "orgName": "Winter27 Unverified Domains",
  "edition": "Developer",
  "settings": {
    "emailAuthorizationSettings": {
      "enableSubstituteFromAddress": true
    }
  }
}
```

What I notices is that if you retrieve `EmailAuthorizationSettings` from a Winter ’27 scratch org that was created without that setting, it comes back as `true` anyway.

![](https://storage.ghost.io/c/92/d5/92d55211-2cfe-45c9-b8c4-b7c7b1595cdb/content/images/2026/08/image-8.png)

So it is already on by default, and `Messaging.sendEmail` goes through either way.

## SOQL

There is exactly one new thing in here, it is a beta, and it does **not** work in any org I could put it into.

### **Compare Values Between Fields in SOQL (Beta)**

The feature here is `FORMULA()`, which lets you do arithmetic directly in a `WHERE` clause and compare two fields against each other, without creating a formula field.

```Apex
SELECT Id, Name FROM Opportunity
WHERE FORMULA(Amount - ExpectedRevenue) > 0
```

That's good... I'd say if it worked for me. 

The release notes say the beta is available in sandboxes, Developer Edition and scratch orgs on API 68.0 and later.

And yet, if you run that query in a Winter ’27 preview org or in a freshly created Winter ’27 Developer Edition scratch org, every one of those routes gives you the same answer:

```
MALFORMED_QUERY: unexpected token: 'FORMULA(AnnualRevenue -'
```

So I can’t vouch for it in practice yet. But on paper, **it looks really good**, and I will definitely keep an eye on it.

## Lightning Web Components

Lightning Web Components did not receive anything dramatically new this time. Instead, this is the release where a couple of things that have been sitting in beta for a while finally became generally available (GA).

### Use Complex Template Expressions (Generally Available)

Complex JavaScript expressions inside HTML templates went beta in Spring ’26 and are generally available now. I've already said on LinkedIn that you have to be very careful with this, and it's always better to consolidate logic into JavaScript only, but for small things - I suppose it works.

```HTML
<template>
    <p>{items.length > 2 ? 'many' : 'few'}</p>
    <p>{`${firstName} ${lastName}`}</p>
    <p>{contact?.account?.name}</p>
    <p>{label.toUpperCase()}</p>
</template>
```

More interesting is where the wall actually is. These five are rejected:

| Expression                 | Compiler                                                  |
| -------------------------- | --------------------------------------------------------- |
| {count = 5}                | Field mutations are only permitted within arrow functions |
| {count++}                  | Field mutations are only permitted within arrow functions |
| {new Date().getFullYear()} | Use of object instantiation is disallowed                 |
| {(count, label)}           | Use of comma operators is disallowed                      |

The pattern behind it is actually simple. If an expression mutates state or creates an object - it is rejected. If it only reads and calculates - it is allowed.

What the compiler does not check though, is whether the thing you are referencing actually exists. Inside a template expression you only have access to your own component, and every global is undefined in there, including `window`, `document`, `Math` and `JSON`.

![](https://storage.ghost.io/c/92/d5/92d55211-2cfe-45c9-b8c4-b7c7b1595cdb/content/images/2026/08/image-6.png)

Template Expression Error

So you have to be extremely precise about what you put in those expressions, and reference only the properties that actually exist on your component.

### Use Third-Party Web Components in LWC (Generally Available)

You can now render third-party custom elements inside LWC templates without rewriting them, by using `lwc:external`. This one is also generally available with no changes since the last beta.

The way it works is you upload the already-built JavaScript file and then load it at runtime with `loadScript` from `lightning/platformResourceLoader`.

It has to be a plain browser bundle in [IIFE](https://developer.mozilla.org/en-US/docs/Glossary/IIFE?ref=bulkifiedthinking.com) or [UMD](https://github.com/umdjs/umd?ref=bulkifiedthinking.com) format, because `loadScript` does not support ECMAScript modules, and you cannot import a web component from `npm` into your LWC bundle either.

```JavaScript
import { LightningElement } from 'lwc';
import { loadScript } from 'lightning/platformResourceLoader';
import FANCY from '@salesforce/resourceUrl/fancyWidget';

export default class ExternalWidget extends LightningElement {
    hasLoaded = false;

    renderedCallback() {
        if (this.hasLoaded) {
            return;
        }
      
        this.hasLoaded = true;

        loadScript(this, FANCY).catch((error) => {
            // ...
        });
    }
}
```

And the template, where the only new thing is the directive itself:

```HTML
<template>
    <fancy-widget lwc:external data-label="external"></fancy-widget>
</template>
```

### **Control Navigation Items in Lightning Console Apps**

The last one is a capability that must have been released waayy back in the day.

The `lightning/platformNavigationItemApi` module is the LWC equivalent of the old `lightning:navigationItemAPI` Aura component, and it gives you five methods:

```JavaScript
import {
    getNavigationItems,
    getSelectedNavigationItem,
    setSelectedNavigationItem,
    focusNavigationItem,
    refreshNavigationItem
} from 'lightning/platformNavigationItemApi';

// ...

const items = await getNavigationItems();
await setSelectedNavigationItem('standard-Account');
```

It only works in Lightning console apps, so there is nothing to see outside of one, but if you have been keeping an Aura component alive purely to reach the item menu - you can finally delete it.

Farewell, Aura.

## **Where that leaves us**

Winter ’27 is a small release with a few genuinely a few good things: a bigger heap, integration tests that talk to external systems, template expressions that I suggest you avoid, and an API that hands your whole type system over to an agent.

This is good. This walkthrough was supposed to be good and not too pessimistic, so I kept that promise...

So why don’t I leave it entirely up to you? As long as you remember one simple rule: the fact that a feature exists doesn’t mean you should use it. The fact that a feature doesn’t exist doesn’t mean you shouldn’t blame Salesforce.