Apex Triggers Always Run in a “without sharing” Context

Apex triggers now always run in a without sharing context. Previously, nested triggers enforced sharing rules in certain edge cases. However, in API version 67.0 and later, database operations within trigger bodies, including SOQL queries, SOSL queries, DML statements, and Database methods, run in user mode unless system mode is explicitly specified. User mode overrides the trigger’s without sharing context and effectively enforces a with sharing context in the trigger body.

Where: Triggers run in a without sharing context in all API versions. Database operations in trigger bodies default to user mode in API version 67.0 and later. The changes apply to Lightning Experience and Salesforce Classic in Enterprise, Performance, Unlimited, and Developer editions.

Why: These changes are part of the new “secure by default” security paradigm. For more information, see Database Operations Run in User Mode by Default, Not System Mode. To control behavior reliably independent of the API version, set explicit access mode on all database operations in your trigger and handler classes. When possible, delegate all business logic to trigger handlers.

How: This example demonstrates trigger database access in API version 67.0 and later.

// API version 67.0

// The trigger itself runs as without sharing (all records accessible by default)
trigger AccountUpdateTrigger on Account (after update) {

    // This SOQL query doesn't specify an access mode, so it defaults to WITH USER_MODE. 
    // Object- and field-level permissions are enforced.
    // User mode reapplies the record-sharing rules of the current user,
    // effectively overriding trigger's own sharing context.
    List<Contact> contactsAccessibleByCurrentUser = [SELECT Id, Name, AccountId 
                                                     FROM Contact 
                                                     WHERE AccountId = :Trigger.newMap.keySet()];

    // This SOQL query explicitly includes a WITH SYSTEM_MODE clause.
    // Object- and field-level are bypassed.
    // Record sharing remains governed by the trigger's own without sharing context, 
    // so all records are visible regardless of the user running the query    

List<Contact> allRelatedContacts = [SELECT Id, Name, AccountId 
                                    FROM Contact 
                                    WHERE AccountId = :Trigger.newMap.keySet() 
                                    WITH SYSTEM_MODE ];

    // Further processing with the related Contacts

}