Home / apex / apex-trigger-bypass-and-killswitch-patterns
apex-trigger-bypass-and-killswitch-patterns
Runtime mechanisms to disable Apex triggers without commenting out code: Custom Metadata kill switches via Trigger_Setting__mdt, Custom Permission gates via FeatureManagement.checkPermission, Hierarchy Custom Settings, and TriggerControl static-state bypass for nested operations. NOT for a handler re-entering itself — use apex/recursive-trigger-prevention. NOT for gating a business feature — use apex/feature-flags-and-kill-switches.
Open the skill folder on GitHub
Apex Trigger Bypass And Killswitch Patterns
Activate when an Apex trigger needs to be disabled at runtime — for a data load, a one-off support rerun, an integration user, or as a break-glass kill switch during an incident — without commenting out code or hot-patching the org. This skill is distinct from apex-recursive-trigger-prevention: recursion prevention stops a handler from re-entering itself; bypass stops a handler from running at all in a chosen scope.
Why Bypasses Matter
- Data loads and migrations. Bulk inserts via Data Loader, ETL, or one-off
scripts often need triggers off so denormalisation, rollups, and outbound callouts don't fire per row.
- Integration users. A nightly sync job from an external system has
already validated and enriched data — the org's enrichment triggers should not re-run for that user.
- Support reruns and backfills. Replaying historical events should not
refire side-effecting integrations.
- Incident response (kill switch). When a trigger is implicated in an
outage, ops needs to disable it in seconds — without a deployment.
Core Concepts
1. Custom Metadata kill switch (canonical)
Trigger_Setting__mdt records carry an Is_Active__c checkbox keyed by Object_API_Name__c + Handler_Class__c. The repo's canonical TriggerControl class (see templates/apex/TriggerControl.cls) reads this CMDT in a cached SOQL on first call per transaction. Toggling Is_Active__c to false and deploying the metadata change disables the handler org-wide. Custom Metadata deploys faster than code and is auditable through the deployment record.
2. Custom Permission gate
FeatureManagement.checkPermission('Bypass_Triggers') is the canonical way to ask "does the running user have the bypass capability?". The Custom Permission is assigned via Permission Set, never directly to a profile. Integration users get the perm set; admins do not (so accidental bypass during clicks is impossible). The repo's TriggerControl already honours TriggerControl_BypassAll.
3. Hierarchy Custom Setting toggle
A Hierarchy Custom Setting (Trigger_Bypass__c with checkbox fields per handler) gives per-user, per-profile, and org-default override semantics. Hierarchy lookups respect User → Profile → Org Default in that order. Useful for short-lived overrides where a Permission Set assignment is too heavyweight, but lacks the audit trail of Custom Metadata.
4. TriggerControl static-state bypass (in-transaction)
When Apex itself needs to suppress a downstream trigger during a cascade — e.g. an Opportunity service that updates Accounts and does not want the Account handler to re-derive territory — use programmatic TriggerControl.bypass('AccountTriggerHandler') then restore(...) in a try/finally. This is transaction-scoped static state; it does not survive across transaction boundaries (Queueable, @future, Batch all start fresh).
5. Test guards
Test.isRunningTest() should NEVER short-circuit business logic. The right pattern is to leave bypasses OFF in tests by default and have specific tests opt in to the bypass via TriggerControl.overrideForTest(...). Tests should prove the trigger fires in the normal path.
Why NEVER A Checkbox On User
Adding Bypass_Triggers__c to the User SObject sounds simple but is wrong:
- Users editing their own record can self-grant the bypass.
- There is no audit of who flipped the bit and when (User field history is
limited and noisy).
- It cannot be assigned through Permission Set Groups, breaking least-privilege
governance.
- It conflates identity (User) with capability (Permission), an
anti-pattern called out in Salesforce Well-Architected Security guidance.
Use a Custom Permission and assign it through a Permission Set instead.
Governance — Every Bypass Must Be Auditable
Every code path that decides to bypass should write a single line to an Application_Log__c (or platform event) — "skipped AccountTriggerHandler at 2026-04-28T14:02:11Z because TriggerControl_BypassAll permission is granted to user 005...". Without this, post-incident forensics is impossible.
Test Pattern
ALWAYS run trigger tests with no bypass applied unless the test is explicitly proving the bypass works. Do this by clearing static state in @TestSetup and asserting TriggerControl.isActive(...) == true before the work.
Recommended Workflow
- Decide bypass scope: org-wide kill switch (CMDT), per-user/integration
(Custom Permission), per-profile or short-lived (Hierarchy CS), or in-transaction cascade suppression (programmatic).
- Add or update the
Trigger_Setting__mdtrecord (or Custom Permissionassignment) — never edit the trigger code.
- Wire the handler's
run()entry point to callTriggerControl.isActive(sObjectName, handlerName)and short-circuit. - For programmatic bypass during a cascade, wrap the inner DML in
try { TriggerControl.bypass(...); ... } finally { TriggerControl.restore(...); }. - Emit an
Application_Log__centry whenever a bypass actually fires. - Write tests that prove (a) the trigger runs by default, (b) the kill
switch disables it, and (c) restore() reverts the static-state bypass.
- Document the kill-switch runbook entry — who can flip it, how to flip it,
how to verify, how to revert.
Review Checklist
- [ ] Handler
run()callsTriggerControl.isActive(...)first - [ ] Custom Permission is
FeatureManagement.checkPermission-gated, not a User field - [ ]
Trigger_Setting__mdtrecord exists for every handler - [ ] Programmatic bypass uses
try/finallyto guarantee restore - [ ] Bypass invocations write to
Application_Log__c - [ ] Tests run with bypass OFF by default
- [ ] No commented-out trigger code anywhere
- [ ] Runbook documents how to flip the kill switch and revert
Salesforce-Specific Gotchas
- CMDT cache lag.
Trigger_Setting__mdtchanges can take a few secondsto propagate after deploy because of platform-side metadata caching. Verify by hitting the handler from a fresh transaction.
- Static state dies at transaction boundaries. A Queueable enqueued from
inside
TriggerControl.bypass(...)runs in a NEW transaction with fresh static state — bypass is gone. - Hierarchy CS lookup order is User → Profile → Org Default. Setting
only the org default leaves admin-test users bypassed unintentionally.
Output Artifacts
| Artifact | Description |
|---|---|
Trigger_Setting__mdt records | Per-handler kill switch toggles |
TriggerHandler.run() override | Calls TriggerControl.isActive(...) first |
| Programmatic bypass blocks | try/finally wrappers around cascade DML |
| Bypass audit log entries | Application_Log__c rows on each bypass fire |
| Kill-switch runbook entry | Ops doc for incident response |
Related Skills
apex/recursive-trigger-prevention— re-entry guards (different problem)apex/trigger-framework— the handler pattern being gatedapex/custom-logging-and-monitoring— the audit log destination