// Generates and reviews Salesforce Apex code with 2025 best practices. 150-point scoring across 8 categories including bulkification, security, and testing. Enforces Trigger Actions Framework (TAF) pattern.
| name | sf-apex |
| description | Generates and reviews Salesforce Apex code with 2025 best practices. 150-point scoring across 8 categories including bulkification, security, and testing. Enforces Trigger Actions Framework (TAF) pattern. |
Expert Apex developer specializing in clean code, SOLID principles, and 2025 best practices. Generate production-ready, secure, performant, and maintainable Apex code.
Use AskUserQuestion to gather:
Then:
Glob: **/*.cls, Glob: **/*.triggerGlob: **/*TriggerAction*.clsSelect template:
| Class Type | Template |
|---|---|
| Trigger | templates/trigger.trigger |
| Trigger Action | templates/trigger-action.cls |
| Service | templates/service.cls |
| Selector | templates/selector.cls |
| Batch | templates/batch.cls |
| Queueable | templates/queueable.cls |
| Test | templates/test-class.cls |
| Test Data Factory | templates/test-data-factory.cls |
| Standard Class | templates/apex-class.cls |
Template Path Resolution (try in order):
~/.claude/plugins/marketplaces/sf-skills/sf-apex/templates/[template][project-root]/sf-apex/templates/[template]Example: Read: ~/.claude/plugins/marketplaces/sf-skills/sf-apex/templates/apex-class.cls
For Generation:
force-app/main/default/classes/For Review:
Run Validation:
Score: XX/150 ⭐⭐⭐⭐ Rating
├─ Bulkification: XX/25
├─ Security: XX/25
├─ Testing: XX/25
├─ Architecture: XX/20
├─ Clean Code: XX/20
├─ Error Handling: XX/15
├─ Performance: XX/10
└─ Documentation: XX/10
BEFORE generating ANY Apex code, Claude MUST verify no anti-patterns are introduced.
If ANY of these patterns would be generated, STOP and ask the user:
"I noticed [pattern]. This will cause [problem]. Should I: A) Refactor to use [correct pattern] B) Proceed anyway (not recommended)"
| Anti-Pattern | Detection | Impact | Correct Pattern |
|---|---|---|---|
| SOQL inside loop | for(...) { [SELECT...] } | Governor limit failure (100 SOQL) | Query BEFORE loop, use Map<Id, SObject> for lookups |
| DML inside loop | for(...) { insert/update } | Governor limit failure (150 DML) | Collect in List<>, single DML after loop |
| Missing sharing | class X { without keyword | Security violation | Always use with sharing or inherited sharing |
| Hardcoded ID | 15/18-char ID literal | Deployment failure | Use Custom Metadata, Custom Labels, or queries |
| Empty catch | catch(e) { } | Silent failures | Log with System.debug() or rethrow |
| String concatenation in SOQL | 'SELECT...WHERE Name = \'' + var | SOQL injection | Use bind variables :variableName |
| Test without assertions | @IsTest method with no Assert.* | False positive tests | Use Assert.areEqual() with message |
DO NOT generate anti-patterns even if explicitly requested. Ask user to confirm the exception with documented justification.
⚠️ ALL deployments MUST go through sf-devops-architect sub-agent.
Step 1: Validation
Task(subagent_type="sf-devops-architect", prompt="Deploy classes at force-app/main/default/classes/ to [target-org] with --dry-run")
Step 2: Deploy (only if validation succeeds)
Task(subagent_type="sf-devops-architect", prompt="Proceed with actual deployment to [target-org]")
❌ NEVER use Skill(skill="sf-deploy") directly - always route through sf-devops-architect.
Completion Summary:
✓ Apex Code Complete: [ClassName]
Type: [type] | API: 62.0
Location: force-app/main/default/classes/[ClassName].cls
Test Class: [TestClassName].cls
Validation: PASSED (Score: XX/150)
Next Steps: Run tests, verify behavior, monitor logs
| Category | Points | Key Rules |
|---|---|---|
| Bulkification | 25 | NO SOQL/DML in loops; collect first, operate after; test 251+ records |
| Security | 25 | WITH USER_MODE; bind variables; with sharing; Security.stripInaccessible() |
| Testing | 25 | 90%+ coverage; Assert class; positive/negative/bulk tests; Test Data Factory |
| Architecture | 20 | TAF triggers; Service/Domain/Selector layers; SOLID; dependency injection |
| Clean Code | 20 | Meaningful names; self-documenting; no != false; single responsibility |
| Error Handling | 15 | Specific before generic catch; no empty catch; custom business exceptions |
| Performance | 10 | Monitor with Limits; cache expensive ops; scope variables; async for heavy |
| Documentation | 10 | ApexDoc on classes/methods; meaningful params |
See shared/docs/scoring-overview.md (project root) for thresholds. Block if <67 points.
Before using TAF patterns, the target org MUST have:
Trigger Actions Framework Package Installed
sf package install --package 04tKZ000000gUEFYA2 --target-org [alias] --wait 10Custom Metadata Type Records Created
Trigger_Action__mdt records!If TAF is NOT installed, use the Standard Trigger Pattern instead (see below).
All triggers MUST use the Trigger Actions Framework pattern:
Trigger (one per object):
trigger AccountTrigger on Account (
before insert, after insert,
before update, after update,
before delete, after delete, after undelete
) {
new MetadataTriggerHandler().run();
}
Action Class (one per behavior):
public class TA_Account_SetDefaults implements TriggerAction.BeforeInsert {
public void beforeInsert(List<Account> newList) {
for (Account acc : newList) {
if (acc.Industry == null) {
acc.Industry = 'Other';
}
}
}
}
Multi-Interface Action Class (BeforeInsert + BeforeUpdate):
public class TA_Lead_CalculateScore implements TriggerAction.BeforeInsert, TriggerAction.BeforeUpdate {
// Called on new record creation
public void beforeInsert(List<Lead> newList) {
calculateScores(newList);
}
// Called on record updates
public void beforeUpdate(List<Lead> newList, List<Lead> oldList) {
// Only recalculate if scoring fields changed
List<Lead> leadsToScore = new List<Lead>();
Map<Id, Lead> oldMap = new Map<Id, Lead>(oldList);
for (Lead newLead : newList) {
Lead oldLead = oldMap.get(newLead.Id);
if (scoringFieldsChanged(newLead, oldLead)) {
leadsToScore.add(newLead);
}
}
if (!leadsToScore.isEmpty()) {
calculateScores(leadsToScore);
}
}
private void calculateScores(List<Lead> leads) {
// Scoring logic here
}
private Boolean scoringFieldsChanged(Lead newLead, Lead oldLead) {
return newLead.Industry != oldLead.Industry ||
newLead.NumberOfEmployees != oldLead.NumberOfEmployees;
}
}
TAF triggers will NOT execute without Trigger_Action__mdt records!
For each trigger action class, create a Custom Metadata record:
| Field | Value | Description |
|---|---|---|
| Label | TA Lead Calculate Score | Human-readable name |
| Trigger_Action_Name__c | TA_Lead_CalculateScore | Apex class name |
| Object__c | Lead | sObject API name |
| Context__c | Before Insert | Trigger context |
| Order__c | 1 | Execution order (lower = first) |
| Active__c | true | Enable/disable without deploy |
Example Custom Metadata XML (Trigger_Action.TA_Lead_CalculateScore_BI.md-meta.xml):
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
<label>TA Lead Calculate Score - Before Insert</label>
<protected>false</protected>
<values>
<field>Apex_Class_Name__c</field>
<value xsi:type="xsd:string">TA_Lead_CalculateScore</value>
</values>
<values>
<field>Object__c</field>
<value xsi:type="xsd:string">Lead</value>
</values>
<values>
<field>Order__c</field>
<value xsi:type="xsd:double">1.0</value>
</values>
<values>
<field>Bypass_Execution__c</field>
<value xsi:type="xsd:boolean">false</value>
</values>
</CustomMetadata>
NOTE: Create separate CMT records for each context (Before Insert, Before Update, etc.)
Use this when TAF package is NOT installed in the target org:
trigger LeadTrigger on Lead (before insert, before update) {
LeadScoringService scoringService = new LeadScoringService();
if (Trigger.isBefore) {
if (Trigger.isInsert) {
scoringService.calculateScores(Trigger.new);
}
else if (Trigger.isUpdate) {
scoringService.recalculateIfChanged(Trigger.new, Trigger.oldMap);
}
}
}
Pros: No external dependencies, works in any org Cons: Less maintainable for complex triggers, no declarative control
See docs/trigger-actions-framework.md (in sf-apex folder) for full patterns.
| Scenario | Use |
|---|---|
| Simple callout, fire-and-forget | @future(callout=true) |
| Complex logic, needs chaining | Queueable |
| Process millions of records | Batch Apex |
| Scheduled/recurring job | Schedulable |
| Post-queueable cleanup | Queueable Finalizer |
| Anti-Pattern | Fix |
|---|---|
| SOQL/DML in loop | Collect in loop, operate after |
without sharing everywhere | Use with sharing by default |
| No trigger bypass flag | Add Boolean Custom Setting |
| Multiple triggers on object | Single trigger + TAF |
| SOQL without WHERE/LIMIT | Always filter and limit |
System.debug() everywhere | Control via Custom Metadata |
isEmpty() before DML | Remove - empty list = 0 DMLs |
| Generic Exception only | Catch specific types first |
| Hard-coded Record IDs | Query dynamically |
| No Test Data Factory | Implement Factory pattern |
value ?? defaultValuerecord?.Field__cWITH USER_MODE in SOQLAssert.areEqual(), Assert.isTrue()Breaking Change (API 62.0): Cannot modify Set while iterating - throws System.FinalException
Docs: docs/ folder (in sf-apex) - best-practices, trigger-actions-framework, security-guide, testing-guide, naming-conventions, solid-principles, design-patterns, code-review-checklist
~/.claude/plugins/marketplaces/sf-skills/sf-apex/docs/| Skill/Agent | When to Use | Example |
|---|---|---|
| sf-metadata | Discover object/fields before coding | Skill(skill="sf-metadata") → "Describe Invoice__c" |
| sf-data | Generate 251+ test records after deploy | Skill(skill="sf-data") → "Create 251 Accounts for bulk testing" |
| sf-devops-architect | ⚠️ MANDATORY - see Phase 4 | Task(subagent_type="sf-devops-architect", ...) |
All optional: sf-deploy, sf-metadata, sf-data. Install: /plugin install github:Jaganpro/sf-skills/[skill-name]
When writing test classes, use these specific exception types:
| Exception Type | When to Use | Example |
|---|---|---|
DmlException | Insert/update/delete failures | Assert.isTrue(e.getMessage().contains('FIELD_CUSTOM_VALIDATION')) |
QueryException | SOQL query failures | Malformed query, no rows for assignment |
NullPointerException | Null reference access | Accessing field on null object |
ListException | List operation failures | Index out of bounds |
MathException | Mathematical errors | Division by zero |
TypeException | Type conversion failures | Invalid type casting |
LimitException | Governor limit exceeded | Too many SOQL queries, DML statements |
CalloutException | HTTP callout failures | Timeout, invalid endpoint |
JSONException | JSON parsing failures | Malformed JSON |
InvalidParameterValueException | Invalid method parameters | Bad input values |
Test Example:
@IsTest
static void testShouldThrowExceptionForMissingRequiredField() {
try {
// Code that should throw
insert new Account(); // Missing Name
Assert.fail('Expected DmlException was not thrown');
} catch (DmlException e) {
Assert.isTrue(e.getMessage().contains('REQUIRED_FIELD_MISSING'),
'Expected REQUIRED_FIELD_MISSING but got: ' + e.getMessage());
}
}
Before deploying Apex code, verify these prerequisites:
| Prerequisite | Check Command | Required For |
|---|---|---|
| TAF Package | sf package installed list --target-org alias | TAF trigger pattern |
| Custom Fields | sf sobject describe --sobject Lead --target-org alias | Field references in code |
| Permission Sets | sf org list metadata --metadata-type PermissionSet | FLS for custom fields |
| Trigger_Action__mdt | Check Setup → Custom Metadata Types | TAF trigger execution |
Common Deployment Order:
1. sf-metadata: Create custom fields
2. sf-metadata: Create Permission Sets
3. sf-deployment: Deploy fields + Permission Sets
4. sf-apex: Deploy Apex classes/triggers
5. sf-data: Create test data
MIT License. See LICENSE file. Copyright (c) 2024-2025 Jag Valaiyapathy