Apex Review Skill
Review lens for Salesforce Apex and SOQL/SOSL changes in a pull request. Every
rule below is a concrete violation you can point at in the diff.
Apex is the single highest-value language for an LLM-backed reviewer to get right: it is
under-represented in training data, and its governor limits make patterns that look
perfectly fine in Java/C# (a query inside a loop) silently brick production once data
volume crosses a threshold. A generic reviewer misses these constantly. This skill encodes
the Apex-specific failure modes, sourced from the PMD Apex ruleset (BSD-2) and Salesforce's
own governor-limit guidance, as diff-checkable rules.
How to apply this skill (read first)
You are reviewing a unified diff, not a live org. Therefore:
- You cannot run anything. No
sf project deploy, no anonymous Apex, no test run, no
PMD execution. Judge from the changed lines alone. Never emit a finding whose
justification is "run X to confirm."
- Findings apply to the diff, not the whole org. Only flag Apex that was added or
modified in this PR.
- Every finding must trace to a rule below. If you cannot point to a specific rule in
this file that the changed code violates, drop it, the generic "issues" agent handles
ordinary bugs. This skill only fires on the Apex-specific rules enumerated here.
- Loops include hidden ones.
for, while, do-while, and trigger bodies (a trigger
processes up to 200 records per invocation, its top level is effectively a loop over
Trigger.new). A query in a method called from inside a loop counts too.
Mapping a finding to the SkillsFinding contract
| This skill's severity |
action_level |
category (typical) |
| Blocking, will hit a governor limit, leak data, or allow injection |
action_required |
Correctness or Security |
| Recommended, likely wrong, context-dependent |
remediation_recommended |
Correctness / Maintainability |
| Optional, maintainability nudge |
informational |
Maintainability |
Each finding's evidence.citations MUST include a SkillCitation with
source = "apex-review". Put the offending span in diff_pointer. Give a concrete
fix_suggestion (the Fix line of each rule is your starting point).
Rule set
1. Governor limits, the #1 Apex footgun (mostly Blocking)
Apex runs in a multi-tenant environment with hard per-transaction limits: 100 SOQL
queries, 150 DML statements, 50,000 rows retrieved, plus CPU time. Code that
performs these per record instead of in bulk works in a unit test with one record and
dies in production on a 200-record batch. This is the mistake LLMs make most.
- SOQL or SOSL query inside a loop. → Blocking (Correctness). [PMD
OperationWithLimitsInLoop]. Hits the 100-query limit. Fix: move the query outside the
loop; query once with a bulk WHERE id IN :ids, build a Map<Id, SObject>, look up
inside the loop.
- DML statement (
insert/update/delete/upsert/Database.*) inside a loop. →
Blocking (Correctness). [PMD OperationWithLimitsInLoop]. Hits the 150-DML limit.
Fix: accumulate records into a List<SObject> in the loop, perform a single DML on the
list after the loop.
- Other limit-consuming calls inside a loop,
@future/Queueable/Batch enqueue,
Approval.process, Messaging.sendEmail, async scheduling. → Blocking (Correctness).
[PMD OperationWithLimitsInLoop]. Fix: hoist out of the loop; batch the work.
- Expensive Schema/describe calls inside a loop (
Schema.getGlobalDescribe(),
getDescribe() per iteration). → Recommended (Performance). [PMD
OperationWithHighCostInLoop]. Fix: call once before the loop, cache the result.
- Unfiltered SOQL/SOSL,
SELECT ... FROM X with no WHERE and no LIMIT on a large
object. → Recommended (Correctness). [PMD AvoidNonRestrictiveQueries]. Risks the
50k-row limit. Fix: add a selective WHERE and/or LIMIT.
2. Security, CRUD / FLS / sharing / injection (mostly Blocking)
- DML or SOQL in a class with no explicit sharing declaration. → Blocking
(Security). [PMD
ApexSharingViolations]. Without with sharing the code runs in system
context and ignores record-level access. Fix: declare with sharing (or inherited sharing for library classes) on classes that perform DML/SOQL.
- Object/field access without a CRUD/FLS check, direct
insert/update/SELECT on
user-reachable objects with no Schema.sObjectType.X.isCreateable()/isAccessible()/...
guard or WITH SECURITY_ENFORCED / Security.stripInaccessible. → Blocking
(Security). [PMD ApexCRUDViolation]. Fix: add the CRUD/FLS check, or use WITH SECURITY_ENFORCED in the SOQL, or Security.stripInaccessible before DML.
- Dynamic SOQL built by string-concatenating an untrusted variable ,
Database.query('... ' + var + ' ...'). → Blocking (Security). [PMD
ApexSOQLInjection]. Fix: use bind variables (:var), or String.escapeSingleQuotes()
for identifiers that cannot be bound.
- Hardcoded credentials / endpoints in callouts. → Blocking (Security). [PMD
ApexSuggestUsingNamedCred]. Fix: use a Named Credential.
- Plain
http:// endpoint in a callout. → Blocking (Security). [PMD
ApexInsecureEndpoint]. Fix: use https://.
- Redirect to a user-controlled location (open redirect). → Blocking (Security).
[PMD
ApexOpenRedirect]. Fix: validate/whitelist the target.
- URL parameter used without escaping (
ApexPages.currentPage().getParameters().get(..)
flowing into output). → Blocking (Security). [PMD ApexXSSFromURLParam]. Fix:
escape/sanitize before use.
addError() called with escaping disabled (escape=false). → Blocking
(Security). [PMD ApexXSSFromEscapeFalse]. Fix: leave escaping on.
- Hardcoded crypto keys/IVs in
Crypto calls. → Blocking (Security). [PMD
ApexBadCrypto]. Fix: use randomly generated keys/IVs.
3. Triggers & error-prone patterns
- Hardcoded Salesforce ID (
'001...', '00D...', record-type/profile IDs as string
literals). → Blocking (Correctness). [PMD AvoidHardcodingId]. IDs differ across
sandbox/prod/orgs and break on deploy. Fix: query the record dynamically or use a Custom
Setting/Metadata.
- Direct indexed access to
Trigger.new[0] / Trigger.old[0] instead of iterating the
collection. → Recommended (Correctness). [PMD AvoidDirectAccessTriggerMap]. Assumes
a single record; breaks on bulk. Fix: iterate Trigger.new, key into Trigger.oldMap.
- Business logic written directly in a trigger body. → Recommended (Maintainability).
[PMD
AvoidLogicInTrigger]. Fix: delegate to a handler class.
- DML in a constructor or initializer. → Recommended (Correctness). [PMD
ApexCSRF].
Merely loading a page executes it, a CSRF surface. Fix: move DML out of the constructor.
- Empty
catch block (exception swallowed, nothing logged/rethrown). → Recommended
(Correctness). [PMD EmptyCatchBlock]. Fix: handle, log with context, or rethrow.
Map keyed by an interface type where an abstract class defines equals/hashCode.
→ Recommended (Correctness). [PMD AvoidInterfaceAsMapKey]. Lookups misbehave. Fix:
key by a concrete type / stable field.
- Class/enum/interface named the same as a
System or Schema builtin. → Recommended
(Correctness). [PMD TypeShadowsBuiltInNamespace]. Shadows the namespace. Fix: rename.
4. Async & global surface
Queueable implemented without attaching a Finalizer. → Recommended
(Reliability). [PMD QueueableWithoutFinalizer]. No failure handling for the async job.
Fix: System.attachFinalizer(...).
- New
@future method where Queueable would be more capable. → Optional
(Maintainability). [PMD AvoidFutureAnnotation]. @future is legacy (no chaining, limited
args). Fix: prefer Queueable.
- New
global class/method outside a managed-package boundary. → Recommended
(Maintainability). [PMD AvoidGlobalModifier]. global signatures can never be changed or
deleted. Fix: use public unless the cross-package surface is truly required.
5. Tests (lower severity, but real)
- New
@isTest class using seeAllData=true. → Recommended (Correctness). [PMD
ApexUnitTestShouldNotUseSeeAllDataTrue]. Tests then depend on org data and break
unpredictably. Fix: create test data in the test (or @testSetup).
- New test method with no assertion at all. → Recommended (Correctness). [PMD
ApexUnitTestClassShouldHaveAsserts]. Fix: assert the actual outcome.
testMethod keyword instead of the @isTest annotation. → Optional
(Maintainability). [PMD ApexUnitTestMethodShouldHaveIsTestAnnotation]. testMethod is
deprecated. Fix: use @isTest.
What NOT to flag
- Generic bugs, logic errors, or non-Apex issues, those belong to the issues agent.
- Code style/formatting unless a rule above names it.
- Anything in files not changed by this PR.
- Anything requiring deployment or a test run to confirm (you have no org).
- A "SOQL in loop" finding when the query is demonstrably already bulkified (collected
before the loop, looked up via a Map inside it), read the surrounding changed lines
before flagging.
Source & attribution
Rules are derived from the PMD Apex ruleset
(https://github.com/pmd/pmd, BSD-2-Clause), specifically the performance, security,
errorprone, and bestpractices categories, and from Salesforce's published Apex
governor-limit guidance. Rule identifiers in brackets (e.g. OperationWithLimitsInLoop) map
back to PMD rules so a reviewer can consult the upstream rationale and examples. This skill
is original prose authored for PR review and is licensed Apache-2.0; it copies no PMD source
text verbatim.
For writing Salesforce code (Apex, LWC, OmniStudio, Data Cloud, Agentforce), see
Salesforce's official skills at https://github.com/forcedotcom/sf-skills, those are
authoring/generation skills and complement this review-only skill.
1---2name: apex-review3description: Use when a PR diff adds or modifies Salesforce Apex (.cls / .trigger) or SOQL/SOSL, flags governor-limit killers (SOQL/DML inside loops), missing CRUD/FLS & sharing enforcement, SOQL injection, hardcoded IDs, trigger anti-patterns, and swallowed exceptions in the CHANGED code only. Skip for non-Apex diffs.4license: Apache-2.05---67# Apex Review Skill89Review lens for Salesforce **Apex** and **SOQL/SOSL** changes in a pull request. Every10rule below is a **concrete violation you can point at in the diff**.1112Apex is the single highest-value language for an LLM-backed reviewer to get right: it is13under-represented in training data, and its **governor limits** make patterns that look14perfectly fine in Java/C# (a query inside a loop) silently brick production once data15volume crosses a threshold. A generic reviewer misses these constantly. This skill encodes16the Apex-specific failure modes, sourced from the PMD Apex ruleset (BSD-2) and Salesforce's17own governor-limit guidance, as diff-checkable rules.1819## How to apply this skill (read first)2021You are reviewing a **unified diff**, not a live org. Therefore:2223- **You cannot run anything.** No `sf project deploy`, no anonymous Apex, no test run, no24 PMD execution. Judge from the changed lines alone. Never emit a finding whose25 justification is "run X to confirm."26- **Findings apply to the diff, not the whole org.** Only flag Apex that was *added or27 modified* in this PR.28- **Every finding must trace to a rule below.** If you cannot point to a specific rule in29 this file that the changed code violates, drop it, the generic "issues" agent handles30 ordinary bugs. This skill only fires on the Apex-specific rules enumerated here.31- **Loops include hidden ones.** `for`, `while`, `do-while`, *and* trigger bodies (a trigger32 processes up to 200 records per invocation, its top level is effectively a loop over33 `Trigger.new`). A query in a method called from inside a loop counts too.3435## Mapping a finding to the SkillsFinding contract3637| This skill's severity | `action_level` | `category` (typical) |38|-----------------------|----------------|----------------------|39| **Blocking**, will hit a governor limit, leak data, or allow injection | `action_required` | `Correctness` or `Security` |40| **Recommended**, likely wrong, context-dependent | `remediation_recommended` | `Correctness` / `Maintainability` |41| **Optional**, maintainability nudge | `informational` | `Maintainability` |4243Each finding's `evidence.citations` MUST include a `SkillCitation` with44`source = "apex-review"`. Put the offending span in `diff_pointer`. Give a concrete45`fix_suggestion` (the **Fix** line of each rule is your starting point).4647---4849## Rule set5051### 1. Governor limits, the #1 Apex footgun (mostly Blocking)5253> Apex runs in a multi-tenant environment with hard per-transaction limits: **100 SOQL54> queries**, **150 DML statements**, **50,000 rows retrieved**, plus CPU time. Code that55> performs these *per record* instead of *in bulk* works in a unit test with one record and56> dies in production on a 200-record batch. This is the mistake LLMs make most.5758- **SOQL or SOSL query inside a loop.** → **Blocking** (Correctness). [PMD59 `OperationWithLimitsInLoop`]. Hits the 100-query limit. Fix: move the query outside the60 loop; query once with a bulk `WHERE id IN :ids`, build a `Map<Id, SObject>`, look up61 inside the loop.62- **DML statement (`insert`/`update`/`delete`/`upsert`/`Database.*`) inside a loop.** →63 **Blocking** (Correctness). [PMD `OperationWithLimitsInLoop`]. Hits the 150-DML limit.64 Fix: accumulate records into a `List<SObject>` in the loop, perform a single DML on the65 list after the loop.66- **Other limit-consuming calls inside a loop**, `@future`/Queueable/Batch enqueue,67 `Approval.process`, `Messaging.sendEmail`, async scheduling. → **Blocking** (Correctness).68 [PMD `OperationWithLimitsInLoop`]. Fix: hoist out of the loop; batch the work.69- **Expensive Schema/describe calls inside a loop** (`Schema.getGlobalDescribe()`,70 `getDescribe()` per iteration). → **Recommended** (Performance). [PMD71 `OperationWithHighCostInLoop`]. Fix: call once before the loop, cache the result.72- **Unfiltered SOQL/SOSL**, `SELECT ... FROM X` with no `WHERE` and no `LIMIT` on a large73 object. → **Recommended** (Correctness). [PMD `AvoidNonRestrictiveQueries`]. Risks the74 50k-row limit. Fix: add a selective `WHERE` and/or `LIMIT`.7576### 2. Security, CRUD / FLS / sharing / injection (mostly Blocking)7778- **DML or SOQL in a class with no explicit sharing declaration.** → **Blocking**79 (Security). [PMD `ApexSharingViolations`]. Without `with sharing` the code runs in system80 context and ignores record-level access. Fix: declare `with sharing` (or `inherited81 sharing` for library classes) on classes that perform DML/SOQL.82- **Object/field access without a CRUD/FLS check**, direct `insert`/`update`/`SELECT` on83 user-reachable objects with no `Schema.sObjectType.X.isCreateable()/isAccessible()/...`84 guard or `WITH SECURITY_ENFORCED` / `Security.stripInaccessible`. → **Blocking**85 (Security). [PMD `ApexCRUDViolation`]. Fix: add the CRUD/FLS check, or use `WITH86 SECURITY_ENFORCED` in the SOQL, or `Security.stripInaccessible` before DML.87- **Dynamic SOQL built by string-concatenating an untrusted variable** , 88 `Database.query('... ' + var + ' ...')`. → **Blocking** (Security). [PMD89 `ApexSOQLInjection`]. Fix: use bind variables (`:var`), or `String.escapeSingleQuotes()`90 for identifiers that cannot be bound.91- **Hardcoded credentials / endpoints in callouts.** → **Blocking** (Security). [PMD92 `ApexSuggestUsingNamedCred`]. Fix: use a Named Credential.93- **Plain `http://` endpoint in a callout.** → **Blocking** (Security). [PMD94 `ApexInsecureEndpoint`]. Fix: use `https://`.95- **Redirect to a user-controlled location** (open redirect). → **Blocking** (Security).96 [PMD `ApexOpenRedirect`]. Fix: validate/whitelist the target.97- **URL parameter used without escaping** (`ApexPages.currentPage().getParameters().get(..)`98 flowing into output). → **Blocking** (Security). [PMD `ApexXSSFromURLParam`]. Fix:99 escape/sanitize before use.100- **`addError()` called with escaping disabled** (`escape=false`). → **Blocking**101 (Security). [PMD `ApexXSSFromEscapeFalse`]. Fix: leave escaping on.102- **Hardcoded crypto keys/IVs** in `Crypto` calls. → **Blocking** (Security). [PMD103 `ApexBadCrypto`]. Fix: use randomly generated keys/IVs.104105### 3. Triggers & error-prone patterns106107- **Hardcoded Salesforce ID** (`'001...'`, `'00D...'`, record-type/profile IDs as string108 literals). → **Blocking** (Correctness). [PMD `AvoidHardcodingId`]. IDs differ across109 sandbox/prod/orgs and break on deploy. Fix: query the record dynamically or use a Custom110 Setting/Metadata.111- **Direct indexed access to `Trigger.new[0]` / `Trigger.old[0]`** instead of iterating the112 collection. → **Recommended** (Correctness). [PMD `AvoidDirectAccessTriggerMap`]. Assumes113 a single record; breaks on bulk. Fix: iterate `Trigger.new`, key into `Trigger.oldMap`.114- **Business logic written directly in a trigger body.** → **Recommended** (Maintainability).115 [PMD `AvoidLogicInTrigger`]. Fix: delegate to a handler class.116- **DML in a constructor or initializer.** → **Recommended** (Correctness). [PMD `ApexCSRF`].117 Merely loading a page executes it, a CSRF surface. Fix: move DML out of the constructor.118- **Empty `catch` block** (exception swallowed, nothing logged/rethrown). → **Recommended**119 (Correctness). [PMD `EmptyCatchBlock`]. Fix: handle, log with context, or rethrow.120- **`Map` keyed by an interface type** where an abstract class defines `equals`/`hashCode`.121 → **Recommended** (Correctness). [PMD `AvoidInterfaceAsMapKey`]. Lookups misbehave. Fix:122 key by a concrete type / stable field.123- **Class/enum/interface named the same as a `System` or `Schema` builtin.** → **Recommended**124 (Correctness). [PMD `TypeShadowsBuiltInNamespace`]. Shadows the namespace. Fix: rename.125126### 4. Async & global surface127128- **`Queueable` implemented without attaching a `Finalizer`.** → **Recommended**129 (Reliability). [PMD `QueueableWithoutFinalizer`]. No failure handling for the async job.130 Fix: `System.attachFinalizer(...)`.131- **New `@future` method** where Queueable would be more capable. → **Optional**132 (Maintainability). [PMD `AvoidFutureAnnotation`]. `@future` is legacy (no chaining, limited133 args). Fix: prefer Queueable.134- **New `global` class/method** outside a managed-package boundary. → **Recommended**135 (Maintainability). [PMD `AvoidGlobalModifier`]. `global` signatures can never be changed or136 deleted. Fix: use `public` unless the cross-package surface is truly required.137138### 5. Tests (lower severity, but real)139140- **New `@isTest` class using `seeAllData=true`.** → **Recommended** (Correctness). [PMD141 `ApexUnitTestShouldNotUseSeeAllDataTrue`]. Tests then depend on org data and break142 unpredictably. Fix: create test data in the test (or `@testSetup`).143- **New test method with no assertion at all.** → **Recommended** (Correctness). [PMD144 `ApexUnitTestClassShouldHaveAsserts`]. Fix: assert the actual outcome.145- **`testMethod` keyword** instead of the `@isTest` annotation. → **Optional**146 (Maintainability). [PMD `ApexUnitTestMethodShouldHaveIsTestAnnotation`]. `testMethod` is147 deprecated. Fix: use `@isTest`.148149---150151## What NOT to flag152153- Generic bugs, logic errors, or non-Apex issues, those belong to the issues agent.154- Code style/formatting unless a rule above names it.155- Anything in files not changed by this PR.156- Anything requiring deployment or a test run to confirm (you have no org).157- A "SOQL in loop" finding when the query is demonstrably already bulkified (collected158 before the loop, looked up via a Map inside it), read the surrounding changed lines159 before flagging.160161## Source & attribution162163Rules are derived from the **PMD Apex ruleset**164(https://github.com/pmd/pmd, BSD-2-Clause), specifically the `performance`, `security`,165`errorprone`, and `bestpractices` categories, and from Salesforce's published Apex166governor-limit guidance. Rule identifiers in brackets (e.g. `OperationWithLimitsInLoop`) map167back to PMD rules so a reviewer can consult the upstream rationale and examples. This skill168is original prose authored for PR review and is licensed Apache-2.0; it copies no PMD source169text verbatim.170171**For *writing* Salesforce code** (Apex, LWC, OmniStudio, Data Cloud, Agentforce), see172Salesforce's official skills at https://github.com/forcedotcom/sf-skills, those are173authoring/generation skills and complement this review-only skill.