SfSkills Domains Docs GitHub

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

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:

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.

  1. 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).

  2. Add or update the Trigger_Setting__mdt record (or Custom Permission

    assignment) — never edit the trigger code.

  3. Wire the handler's run() entry point to call

    TriggerControl.isActive(sObjectName, handlerName) and short-circuit.

  4. For programmatic bypass during a cascade, wrap the inner DML in

    try { TriggerControl.bypass(...); ... } finally { TriggerControl.restore(...); }.

  5. Emit an Application_Log__c entry whenever a bypass actually fires.
  6. Write tests that prove (a) the trigger runs by default, (b) the kill

    switch disables it, and (c) restore() reverts the static-state bypass.

  7. Document the kill-switch runbook entry — who can flip it, how to flip it,

    how to verify, how to revert.

Review Checklist

Salesforce-Specific Gotchas

  1. CMDT cache lag. Trigger_Setting__mdt changes can take a few seconds

    to propagate after deploy because of platform-side metadata caching. Verify by hitting the handler from a fresh transaction.

  2. 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.

  3. Hierarchy CS lookup order is User → Profile → Org Default. Setting

    only the org default leaves admin-test users bypassed unintentionally.

Output Artifacts

ArtifactDescription
Trigger_Setting__mdt recordsPer-handler kill switch toggles
TriggerHandler.run() overrideCalls TriggerControl.isActive(...) first
Programmatic bypass blockstry/finally wrappers around cascade DML
Bypass audit log entriesApplication_Log__c rows on each bypass fire
Kill-switch runbook entryOps doc for incident response

Package files