.cursor/skills/sf-trigger-constraints/SKILL.md
Enforce one-trigger-per-object, handler delegation, bulkification, and recursion prevention. Use when writing or reviewing ANY Apex trigger or handler. Do NOT use for non-trigger Apex, LWC, or Flow.
npx skillsauth add jiten-singh-shahi/salesforce-claude-code sf-trigger-constraintsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill auto-activates when writing, reviewing, or modifying any Apex trigger or trigger handler. It enforces one-trigger-per-object, handler delegation, bulkification, and recursion prevention rules for all trigger artifacts.
Hard rules that every Apex trigger and trigger handler must satisfy. Violations are blockers -- flag them before any other review feedback.
Reference: @../_reference/TRIGGER_PATTERNS.md (order of execution, context variables, framework comparison).
These are absolute prohibitions. Any occurrence is a defect.
| ID | Rule | Why |
|----|------|-----|
| N1 | No logic in the trigger body | Trigger files contain only the handler invocation (new Handler().run() or fflib_SObjectDomain.triggerHandler(Domain.class)). Zero conditionals, zero loops, zero DML. |
| N2 | No multiple triggers per object | Multiple triggers on the same sObject have no guaranteed execution order (see @../_reference/TRIGGER_PATTERNS.md, Step 5/9). Consolidate into one trigger file per object. |
| N3 | No DML inside loops | insert/update/delete/upsert/Database.* calls inside for loops hit governor limits. Collect records first, DML once outside the loop. |
| N4 | No SOQL inside loops | Queries inside for loops risk the per-transaction SOQL limit (see @../_reference/GOVERNOR_LIMITS.md). Query before the loop, store results in a Map<Id, SObject>. |
| N5 | No hardcoded IDs | Record IDs, profile IDs, or record-type IDs must never appear as string literals. Use Schema.SObjectType.*.getRecordTypeInfosByDeveloperName(), Custom Metadata, or Custom Labels. |
| N6 | No direct callouts | Apex triggers cannot make HTTP callouts synchronously. Use @future(callout=true) or Queueable with Database.AllowsCallouts. |
| N7 | No Trigger.new modification in after triggers | Trigger.new is read-only in after contexts. Field updates in after triggers must go through a separate DML statement on queried/cloned records. |
Every trigger implementation must include these elements.
| ID | Rule | How |
|----|------|-----|
| A1 | Delegate to a handler class | Trigger body calls handler: new AccountTriggerHandler().run();. All logic lives in the handler or in service classes the handler calls. |
| A2 | Bulkify all logic | Every method must handle List<SObject> (up to 200 records per chunk). No assumption of single-record input. Iterate Trigger.new / Trigger.old, never index [0] alone. |
| A3 | Use a recursion guard | Prevent infinite re-entry. Recommended: static Set<Id> of processed IDs (allows workflow re-fire for unprocessed records while blocking true recursion). Alternatives: depth counter in base class, setMaxLoopCount(). See @../_reference/TRIGGER_PATTERNS.md recursion patterns. |
| A4 | Use Trigger.newMap / Trigger.oldMap for comparisons | When detecting field changes in update triggers, compare Trigger.newMap.get(id).Field__c against Trigger.oldMap.get(id).Field__c. Never rely on list index alignment. |
| A5 | Register all events in one trigger | The single trigger file should subscribe to all seven events (before insert, before update, before delete, after insert, after update, after delete, after undelete) even if the handler only overrides a subset today. This prevents needing a trigger file redeploy when new events are handled later. |
| A6 | Include a bypass mechanism | Support disabling the handler without a code deploy. Use TriggerHandler.bypass() / .clearBypass(), Custom Metadata (Trigger_Setting__mdt), or Hierarchy Custom Settings. Always reset bypass state in a finally block. |
| A7 | Keep handler methods focused | Each onBeforeInsert(), onAfterUpdate(), etc. should call named service methods. If a handler method exceeds ~30 lines, extract to a service class. |
| Anti-Pattern | Example | Correct Alternative |
|---|---|---|
| Logic in trigger body | trigger T on Account (before insert) { for (Account a : Trigger.new) { a.Name = 'X'; } } | trigger T on Account (...) { new AccountTriggerHandler().run(); } with logic in handler |
| Two triggers on same object | AccountTrigger.trigger + AccountOwnerTrigger.trigger | Single AccountTrigger.trigger delegating to one handler |
| DML in loop | for (Account a : accts) { update a; } | update accts; outside loop |
| SOQL in loop | for (Account a : accts) { Contact c = [SELECT ...]; } | Map<Id, Contact> cMap = new Map<Id, Contact>([SELECT ...]); // before loop |
| Hardcoded ID | if (acc.RecordTypeId == '012000000000001') | Schema.SObjectType.Account.getRecordTypeInfosByDeveloperName().get('Customer').getRecordTypeId() |
| No recursion guard | After-update handler updates same records with no static check | private static Set<Id> processedIds = new Set<Id>(); -- skip IDs already in set |
| Boolean recursion flag | static Boolean hasRun = false; if (hasRun) return; | Set<Id> -- boolean flag blocks legitimate workflow re-fire for unprocessed records |
| Modifying Trigger.new in after context | for (Account a : Trigger.new) { a.Status__c = 'Done'; } in onAfterInsert | Query records, update separately: update [SELECT Id FROM Account WHERE Id IN :newMap.keySet()] |
Use when writing or reviewing a trigger PR:
.trigger file per sObjectTriggerHandler (or FFLIB fflib_SObjectDomain)List<SObject>, not a single recordSet<Id> pattern)Trigger.new mutation in after-trigger methodssf-trigger-frameworks -- Framework patterns, base class code, migration guidesf-architect -- Interactive trigger design guidancedevelopment
Update Salesforce platform reference docs with latest release features and deprecation announcements. Use when SessionStart hook warns docs are outdated or a new Salesforce release has shipped. Do NOT use for Apex or LWC development.
development
Use when syncing documentation after Salesforce Apex code changes. Update README, API docs, and deploy metadata references to match the current org codebase.
development
Use when managing context during long Salesforce Apex development sessions. Suggests manual compaction at logical intervals to preserve deploy and org context across phases.
tools
Visualforce development — pages, controllers, extensions, ViewState, JS Remoting, LWC migration. Use when maintaining VF pages, building PDFs, or planning VF-to-LWC migration. Do NOT use for LWC, Aura, or Flow.