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.QueryExceptionor a field-level security exception - A batch job that ran fine for years starts skipping records once its class is moved to v67
- An
@AuraEnabledmethod 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.
Confirm the symptom is access-related
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_MODEexplicitly
// 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:
- Confirm it compiles with no
WITH SECURITY_ENFORCEDreferences remaining - Run the full test suite — pay attention to tests that assert on record counts
- Run a user-mode versus system-mode count comparison for any query whose result set feeds a UI or an integration
- 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:
| Check | Expected |
|---|---|
| Compilation | No errors; no WITH SECURITY_ENFORCED remaining |
| Every class | Has an explicit sharing keyword |
| Every query | Has an explicit WITH USER_MODE or WITH SYSTEM_MODE |
| Trigger logic | Sharing-dependent logic lives in a handler class, not the trigger body |
| Low-privilege user test | Returns 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.