Apex Scenario

Apex Code Breaks or Returns Fewer Records After Bumping to API v67 — Summer '26

Published 3 June 2026 · 13 min read · Advanced

The situation

You upgrade an Apex class to API version 67.0 — either deliberately or because a deployment tool bumped it — and one of these happens:

  • The class fails to compile with an error on a line containing WITH SECURITY_ENFORCED
  • The class compiles but a SOQL query that previously returned 500 records now returns 40, with no error
  • A previously working method suddenly throws System.QueryException or a field-level security exception
  • A batch job that ran fine for years starts skipping records once its class is moved to v67
  • An @AuraEnabled method returns an empty list to the Lightning component for some users but not others

Nothing changed in your data or your sharing rules. The only thing that changed is the API version on the class. This is expected behaviour in Summer ‘26 — and it is the platform doing what it is now designed to do.


Root cause

API version 67.0 introduces a secure-by-default paradigm. Three distinct things change when an Apex class is compiled at v67, plus one versionless change to triggers. Each can independently cause the symptoms above.

1. Database operations run in user mode by default. In API v66 and earlier, plain SOQL, SOSL, and DML ran in system mode — ignoring the running user’s object permissions, field-level security, and sharing rules. In v67, those same operations now respect the running user’s access automatically. A query that used to see every record now sees only the records the running user can access. That is why the row count drops with no error: the platform is silently filtering to the user’s sharing.

2. Classes without a sharing keyword default to with sharing. Previously, a class declared without with sharing or without sharing inherited the sharing mode of whatever called it — which could be anything, and in many entry-point classes meant without sharing. In v67, an omitted declaration means with sharing. A class you relied on to run unrestricted because it had no keyword will now enforce sharing.

3. WITH SECURITY_ENFORCED is removed. The SOQL clause no longer compiles in v67. This is the hard compile error. Salesforce wants explicit WITH USER_MODE or WITH SYSTEM_MODE instead, because WITH USER_MODE covers the full query — including polymorphic fields like Owner and Task.WhatId — not just the selected fields.

4. Triggers always run in system mode — on every API version. This change is not tied to v67. Previously there were edge cases where sharing rules were enforced inside trigger context. Now all triggers run in system mode consistently. If you were unintentionally relying on sharing being enforced in a trigger, that assumption is gone.

The reason a v66 class is unaffected is that all four version-specific behaviours key off the compiled API version of the individual class. Upgrading the org does not rewrite your classes. The behaviour only changes for a class once that class is bumped to v67 — which is exactly why a targeted deployment or a single version bump can produce a surprising regression.


Diagnosis

Find every class still using the removed clause

Before bumping anything, find all code referencing the removed clause. Using Salesforce CLI against your source:

grep -rn "WITH SECURITY_ENFORCED" force-app/

Or query the Tooling API to find classes by API version:

SELECT Id, Name, ApiVersion
FROM ApexClass
WHERE ApiVersion < 67.0
ORDER BY ApiVersion

Identify entry-point classes with no sharing keyword

These are the ones most at risk from change 2. A class with no with sharing / without sharing / inherited sharing keyword that is invoked directly — an @AuraEnabled controller, an @InvocableMethod, a REST endpoint, a batch class — will flip to with sharing on v67. Audit those first.

If a query returns fewer rows after the bump, run the same query in Anonymous Apex as the affected user (via the Run As context or a user-mode test) and compare against the system-mode count:

// User-mode count (new v67 default)
Integer userModeCount = [SELECT COUNT() FROM Account WITH USER_MODE];

// System-mode count (old v66 default)
Integer systemModeCount = [SELECT COUNT() FROM Account WITH SYSTEM_MODE];

System.debug('User mode: ' + userModeCount + ' / System mode: ' + systemModeCount);

A large gap between the two confirms the row drop is caused by the user-mode default, not by missing data.


Resolution

The goal is not to suppress the new security — it is to make each query’s access mode explicit and intentional, so behaviour no longer depends on the API version.

Fix 1 — Replace the removed clause

Every WITH SECURITY_ENFORCED becomes WITH USER_MODE:

// Before (fails to compile on v67)
List<Account> accs = [SELECT Id, Name FROM Account WITH SECURITY_ENFORCED];

// After
List<Account> accs = [SELECT Id, Name FROM Account WITH USER_MODE];

For dynamic SOQL and DML, set the access level explicitly:

// Dynamic query in user mode
List<Account> accs = Database.query(
  'SELECT Id, Name FROM Account WHERE Rating = \'Hot\'',
  AccessLevel.USER_MODE
);

// DML in user mode
Database.insert(newRecords, AccessLevel.USER_MODE);

Fix 2 — Decide each query’s access mode deliberately

For every query in a class moving to v67, decide whether it should run as the user or as the system, and declare it. Do not leave it implicit.

  • A controller returning data the user should only see if they have access → WITH USER_MODE (and let the v67 default reinforce it)
  • A query that legitimately must see all records regardless of the running user — for example a roll-up calculation or a system integration — → WITH SYSTEM_MODE explicitly
// Intentionally elevated — a system roll-up that must see all records
List<Opportunity> allOpps = [
  SELECT Id, Amount FROM Opportunity
  WHERE CloseDate = THIS_QUARTER
  WITH SYSTEM_MODE
];

The point is that WITH SYSTEM_MODE is now a conscious, reviewable decision rather than a silent default.

Fix 3 — Make the sharing keyword explicit on every class

Never rely on the default. Add an explicit keyword to every class so the behaviour is identical whether it is on v66 or v67:

// Explicit — behaviour is now version-independent
public with sharing class AccountController {
    // ...
}

If a class genuinely needs to run unrestricted, declare without sharing explicitly and document why. If it should follow the caller, use inherited sharing.

Fix 4 — Move sharing-dependent logic out of triggers

Because all triggers now run in system mode, any logic that needs to respect the running user’s sharing must not live directly in the trigger body. Delegate it to a handler class with an explicit sharing declaration:

trigger AccountTrigger on Account (after update) {
    // Trigger runs in system mode — delegate to a handler
    // that declares its own sharing/access intent
    new AccountTriggerHandler().handleAfterUpdate(Trigger.new, Trigger.oldMap);
}
public with sharing class AccountTriggerHandler {
    public void handleAfterUpdate(List<Account> updated, Map<Id, Account> oldMap) {
        // Queries here respect sharing because the class declares with sharing
        // Use WITH USER_MODE on queries that must enforce the user's access
    }
}

Verification

Migrate in a sandbox first. For each class moved to v67:

  1. Confirm it compiles with no WITH SECURITY_ENFORCED references remaining
  2. Run the full test suite — pay attention to tests that assert on record counts
  3. Run a user-mode versus system-mode count comparison for any query whose result set feeds a UI or an integration
  4. Test the actual feature as a low-privilege user, not just as a System Administrator — a System Administrator often sees no change because they can access everything

A successful migration looks like this:

CheckExpected
CompilationNo errors; no WITH SECURITY_ENFORCED remaining
Every classHas an explicit sharing keyword
Every queryHas an explicit WITH USER_MODE or WITH SYSTEM_MODE
Trigger logicSharing-dependent logic lives in a handler class, not the trigger body
Low-privilege user testReturns the intended records — no unexpected drop, no over-exposure

Once every class behaves identically regardless of its API version, the migration is safe to promote to production. The behaviour is now explicit and intentional rather than dependent on which version a class happens to be compiled at.

Frequently asked questions

Why does my Apex class return fewer records after upgrading to API v67?

In API v67, database operations run in user mode by default, so queries respect the running user's object permissions, field-level security, and sharing rules. A query that previously ran in system mode and saw all records now sees only what the user can access.

What replaces WITH SECURITY_ENFORCED in API v67?

Use WITH USER_MODE. It covers the full query including polymorphic fields like Owner and Task.WhatId, not just the selected fields. WITH SECURITY_ENFORCED no longer compiles in v67.

Do my existing Apex classes break automatically when the org upgrades to Summer '26?

No. The v67 security changes are version-specific. Classes on API v66 or earlier keep their existing behaviour. The new defaults only apply once a class is bumped to API v67.

Do Apex triggers run in user mode in Summer '26?

No. All Apex triggers run in system mode on every API version. This change is versionless. Move sharing-dependent logic into a handler class with an explicit sharing declaration.