sf-apex: Salesforce Apex Code Generation and Review Use this skill when the user needs production Apex : new classes, triggers, selectors, services, async jobs, invocable methods, test classes, or evidence-based review of existing / code. When This Skill Owns the Task Use when the work involves: - Apex class generation or refactoring - trigger design and trigger-framework decisions - , Queueable, Batch, Schedulable, or test-class work - review of bulkification, sharing, security, testing, or maintainability Delegate elsewhere when the user is: - editing LWC JavaScript / HTML / CSS → sf-lwc -…

+ fixedAmount + ' off';\n }\n}\n\npublic class TieredDiscount implements DiscountStrategy {\n public Decimal calculate(Decimal amount) {\n if (amount > 1000) return amount * 0.15;\n if (amount > 500) return amount * 0.10;\n if (amount > 100) return amount * 0.05;\n return 0;\n }\n\n public String getDescription() {\n return 'Tiered discount based on amount';\n }\n}\n```\n\n### Usage\n\n```apex\npublic class PricingService {\n private Map\u003cString, DiscountStrategy> strategies;\n\n public PricingService() {\n strategies = new Map\u003cString, DiscountStrategy>{\n 'PERCENTAGE_10' => new PercentageDiscount(10),\n 'FIXED_50' => new FixedAmountDiscount(50),\n 'TIERED' => new TieredDiscount()\n };\n }\n\n public Decimal applyDiscount(String discountType, Decimal amount) {\n DiscountStrategy strategy = strategies.get(discountType);\n if (strategy == null) {\n return 0;\n }\n return strategy.calculate(amount);\n }\n}\n```\n\n---\n\n## Unit of Work Pattern\n\n### Purpose\nManage DML as single transaction, track changes, enable rollback.\n\n### Basic Implementation\n\n```apex\npublic class UnitOfWork {\n private List\u003cSObject> newRecords = new List\u003cSObject>();\n private List\u003cSObject> dirtyRecords = new List\u003cSObject>();\n private List\u003cSObject> deletedRecords = new List\u003cSObject>();\n\n public void registerNew(SObject record) {\n newRecords.add(record);\n }\n\n public void registerNew(List\u003cSObject> records) {\n newRecords.addAll(records);\n }\n\n public void registerDirty(SObject record) {\n dirtyRecords.add(record);\n }\n\n public void registerDeleted(SObject record) {\n deletedRecords.add(record);\n }\n\n public void commitWork() {\n Savepoint sp = Database.setSavepoint();\n try {\n insert newRecords;\n update dirtyRecords;\n delete deletedRecords;\n } catch (Exception e) {\n Database.rollback(sp);\n throw e;\n }\n }\n}\n```\n\n### Usage\n\n```apex\npublic class OrderService {\n public void processOrder(Order__c order, List\u003cOrderItem__c> items) {\n UnitOfWork uow = new UnitOfWork();\n\n // Register all changes\n uow.registerNew(order);\n uow.registerNew(items);\n\n Account acc = [SELECT Id, Order_Count__c FROM Account WHERE Id = :order.Account__c];\n acc.Order_Count__c = (acc.Order_Count__c ?? 0) + 1;\n uow.registerDirty(acc);\n\n // Single commit - all or nothing\n uow.commitWork();\n }\n}\n```\n\n---\n\n## Decorator Pattern\n\n### Purpose\nAdd functionality dynamically without modifying original class. Stack behaviors flexibly.\n\n### Implementation\n\n```apex\n// Base interface\npublic interface NotificationService {\n void send(String message, Id recipientId);\n}\n\n// Core implementation\npublic class EmailNotification implements NotificationService {\n public void send(String message, Id recipientId) {\n Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();\n email.setTargetObjectId(recipientId);\n email.setPlainTextBody(message);\n Messaging.sendEmail(new List\u003cMessaging.SingleEmailMessage>{ email });\n }\n}\n\n// Decorator base - wraps another NotificationService\npublic virtual class NotificationDecorator implements NotificationService {\n protected NotificationService wrapped;\n\n public NotificationDecorator(NotificationService service) {\n this.wrapped = service;\n }\n\n public virtual void send(String message, Id recipientId) {\n wrapped.send(message, recipientId);\n }\n}\n\n// Concrete decorator: Add logging\npublic class LoggingNotificationDecorator extends NotificationDecorator {\n public LoggingNotificationDecorator(NotificationService service) {\n super(service);\n }\n\n public override void send(String message, Id recipientId) {\n System.debug('Sending notification to: ' + recipientId);\n super.send(message, recipientId);\n System.debug('Notification sent successfully');\n }\n}\n\n// Concrete decorator: Add retry logic\npublic class RetryNotificationDecorator extends NotificationDecorator {\n private Integer maxRetries;\n\n public RetryNotificationDecorator(NotificationService service, Integer maxRetries) {\n super(service);\n this.maxRetries = maxRetries;\n }\n\n public override void send(String message, Id recipientId) {\n Integer attempts = 0;\n while (attempts \u003c maxRetries) {\n try {\n super.send(message, recipientId);\n return;\n } catch (Exception e) {\n attempts++;\n if (attempts >= maxRetries) throw e;\n }\n }\n }\n}\n```\n\n### Usage\n\n```apex\n// Stack decorators as needed\nNotificationService service = new EmailNotification();\nservice = new LoggingNotificationDecorator(service);\nservice = new RetryNotificationDecorator(service, 3);\n\n// Now sends with logging + retry + email\nservice.send('Your order shipped!', userId);\n```\n\n### When to Use\n- Adding cross-cutting concerns (logging, caching, validation)\n- When inheritance leads to class explosion\n- Stacking behaviors that can be combined independently\n\n---\n\n## Observer Pattern\n\n### Purpose\nDefine one-to-many dependency where observers are notified of state changes automatically.\n\n### Implementation\n\n```apex\n// Observer interface\npublic interface AccountObserver {\n void onAccountUpdated(Account oldAccount, Account newAccount);\n}\n\n// Subject that notifies observers\npublic class AccountSubject {\n private static List\u003cAccountObserver> observers = new List\u003cAccountObserver>();\n\n public static void attach(AccountObserver observer) {\n observers.add(observer);\n }\n\n public static void detach(AccountObserver observer) {\n Integer index = observers.indexOf(observer);\n if (index >= 0) observers.remove(index);\n }\n\n public static void notifyObservers(Account oldAccount, Account newAccount) {\n for (AccountObserver observer : observers) {\n observer.onAccountUpdated(oldAccount, newAccount);\n }\n }\n}\n\n// Concrete observers\npublic class SalesNotificationObserver implements AccountObserver {\n public void onAccountUpdated(Account oldAcc, Account newAcc) {\n if (newAcc.AnnualRevenue > 1000000 && (oldAcc.AnnualRevenue == null || oldAcc.AnnualRevenue \u003c= 1000000)) {\n // Notify sales team about new enterprise account\n createTask(newAcc.OwnerId, 'New Enterprise Account: ' + newAcc.Name);\n }\n }\n\n private void createTask(Id ownerId, String subject) {\n insert new Task(OwnerId = ownerId, Subject = subject, Status = 'Open');\n }\n}\n\npublic class IntegrationSyncObserver implements AccountObserver {\n public void onAccountUpdated(Account oldAcc, Account newAcc) {\n // Queue sync to external system\n System.enqueueJob(new AccountSyncQueueable(newAcc.Id));\n }\n}\n```\n\n### Usage in Trigger\n\n```apex\n// TriggerHandler or Action class\npublic class AccountTriggerHandler {\n\n static {\n // Register observers once\n AccountSubject.attach(new SalesNotificationObserver());\n AccountSubject.attach(new IntegrationSyncObserver());\n }\n\n public void afterUpdate(List\u003cAccount> newList, Map\u003cId, Account> oldMap) {\n for (Account acc : newList) {\n AccountSubject.notifyObservers(oldMap.get(acc.Id), acc);\n }\n }\n}\n```\n\n### Platform Events Alternative\nFor decoupled, async observers, use Platform Events:\n\n```apex\n// Publish event\nEventBus.publish(new Account_Updated__e(Account_Id__c = acc.Id, Field_Changed__c = 'Status'));\n\n// Subscribe via trigger on platform event\ntrigger AccountUpdatedSubscriber on Account_Updated__e (after insert) {\n // Handle event\n}\n```\n\n---\n\n## Command Pattern\n\n### Purpose\nEncapsulate requests as objects, enabling queuing, logging, undo, and parameterized execution.\n\n### Implementation\n\n```apex\n// Command interface\npublic interface Command {\n void execute();\n void undo();\n String getDescription();\n}\n\n// Concrete command: Update Field\npublic class UpdateFieldCommand implements Command {\n private Id recordId;\n private String fieldName;\n private Object newValue;\n private Object oldValue;\n private SObjectType objectType;\n\n public UpdateFieldCommand(Id recordId, String fieldName, Object newValue) {\n this.recordId = recordId;\n this.fieldName = fieldName;\n this.newValue = newValue;\n this.objectType = recordId.getSObjectType();\n }\n\n public void execute() {\n // Store old value for undo\n SObject record = Database.query(\n 'SELECT ' + fieldName + ' FROM ' + objectType + ' WHERE Id = :recordId'\n );\n this.oldValue = record.get(fieldName);\n\n // Apply new value\n record.put(fieldName, newValue);\n update record;\n }\n\n public void undo() {\n SObject record = objectType.newSObject(recordId);\n record.put(fieldName, oldValue);\n update record;\n }\n\n public String getDescription() {\n return 'Update ' + objectType + '.' + fieldName + ' to ' + newValue;\n }\n}\n\n// Command invoker with history\npublic class CommandInvoker {\n private List\u003cCommand> history = new List\u003cCommand>();\n private List\u003cCommand> queue = new List\u003cCommand>();\n\n public void addToQueue(Command cmd) {\n queue.add(cmd);\n }\n\n public void executeQueue() {\n for (Command cmd : queue) {\n cmd.execute();\n history.add(cmd);\n // Log for audit trail\n System.debug('Executed: ' + cmd.getDescription());\n }\n queue.clear();\n }\n\n public void undoLast() {\n if (!history.isEmpty()) {\n Command lastCommand = history.remove(history.size() - 1);\n lastCommand.undo();\n }\n }\n}\n```\n\n### Usage\n\n```apex\nCommandInvoker invoker = new CommandInvoker();\n\n// Queue multiple field updates\ninvoker.addToQueue(new UpdateFieldCommand(accountId, 'Status__c', 'Active'));\ninvoker.addToQueue(new UpdateFieldCommand(accountId, 'Priority__c', 'High'));\n\n// Execute all\ninvoker.executeQueue();\n\n// Undo last operation\ninvoker.undoLast();\n```\n\n### Use Cases\n- Wizard/multi-step processes with undo\n- Audit trail with replayable operations\n- Batch processing with deferred execution\n- Macro recording and playback\n\n---\n\n## Facade Pattern\n\n### Purpose\nProvide simplified interface to complex subsystems. Reduce coupling between client and implementation details.\n\n### Implementation\n\n```apex\n// Complex subsystems\npublic class CustomerVerificationService {\n public Boolean verifyIdentity(String customerId) {\n // Complex identity verification logic\n return true;\n }\n}\n\npublic class CreditCheckService {\n public Integer getCreditScore(String customerId) {\n // Call external credit bureau\n return 720;\n }\n\n public Decimal getAvailableCredit(String customerId) {\n return 50000.00;\n }\n}\n\npublic class RiskAssessmentService {\n public String assessRisk(Integer creditScore, Decimal requestedAmount) {\n if (creditScore > 700 && requestedAmount \u003c 25000) return 'LOW';\n if (creditScore > 600) return 'MEDIUM';\n return 'HIGH';\n }\n}\n\npublic class LoanApplicationService {\n public Id createApplication(Id accountId, Decimal amount) {\n Loan_Application__c app = new Loan_Application__c(\n Account__c = accountId,\n Amount__c = amount,\n Status__c = 'Pending'\n );\n insert app;\n return app.Id;\n }\n}\n\n// FACADE: Simplified interface\npublic class LoanFacade {\n private CustomerVerificationService verificationService;\n private CreditCheckService creditService;\n private RiskAssessmentService riskService;\n private LoanApplicationService applicationService;\n\n public LoanFacade() {\n this.verificationService = new CustomerVerificationService();\n this.creditService = new CreditCheckService();\n this.riskService = new RiskAssessmentService();\n this.applicationService = new LoanApplicationService();\n }\n\n // Single method hides all complexity\n public LoanResult applyForLoan(Id accountId, String customerId, Decimal amount) {\n LoanResult result = new LoanResult();\n\n // Step 1: Verify customer\n if (!verificationService.verifyIdentity(customerId)) {\n result.success = false;\n result.message = 'Identity verification failed';\n return result;\n }\n\n // Step 2: Check credit\n Integer creditScore = creditService.getCreditScore(customerId);\n Decimal availableCredit = creditService.getAvailableCredit(customerId);\n\n if (amount > availableCredit) {\n result.success = false;\n result.message = 'Requested amount exceeds available credit';\n return result;\n }\n\n // Step 3: Assess risk\n String riskLevel = riskService.assessRisk(creditScore, amount);\n\n // Step 4: Create application\n result.applicationId = applicationService.createApplication(accountId, amount);\n result.success = true;\n result.riskLevel = riskLevel;\n result.message = 'Loan application submitted successfully';\n\n return result;\n }\n\n public class LoanResult {\n public Boolean success;\n public Id applicationId;\n public String riskLevel;\n public String message;\n }\n}\n```\n\n### Usage\n\n```apex\n// Client code is simple - no knowledge of subsystems\nLoanFacade facade = new LoanFacade();\nLoanFacade.LoanResult result = facade.applyForLoan(accountId, 'CUST-123', 15000.00);\n\nif (result.success) {\n System.debug('Loan approved with risk level: ' + result.riskLevel);\n} else {\n System.debug('Loan denied: ' + result.message);\n}\n```\n\n### When to Use\n- Simplifying access to complex subsystems\n- Creating API layers for external integrations\n- Reducing dependencies on multiple services\n- Providing entry points for different client needs\n\n---\n\n## Domain Class Pattern\n\n> 💡 *Principles inspired by \"Clean Apex Code\" by Pablo Gonzalez.\n> [Purchase the book](https://link.springer.com/book/10.1007/979-8-8688-1411-2) for complete coverage.*\n\n### Purpose\n\nEncapsulate business rules in domain-specific classes, making code read like plain English and enabling reuse across the application.\n\n### Implementation\n\n```apex\n/**\n * Domain class encapsulating Account business rules\n * Rules live here, not scattered across triggers/services\n */\npublic class AccountRules {\n\n public static Boolean isStrategicAccount(Account account) {\n return isEnterpriseCustomer(account) &&\n isHighValue(account) &&\n isInTargetMarket(account);\n }\n\n public static Boolean isEnterpriseCustomer(Account account) {\n return account.Type == 'Enterprise' &&\n account.NumberOfEmployees > 500;\n }\n\n public static Boolean isHighValue(Account account) {\n return account.AnnualRevenue != null &&\n account.AnnualRevenue > 1000000;\n }\n\n public static Boolean isInTargetMarket(Account account) {\n Set\u003cString> targetIndustries = new Set\u003cString>{\n 'Technology', 'Finance', 'Healthcare'\n };\n Set\u003cString> targetCountries = new Set\u003cString>{\n 'United States', 'Canada', 'United Kingdom'\n };\n\n return targetIndustries.contains(account.Industry) &&\n targetCountries.contains(account.BillingCountry);\n }\n\n public static Boolean requiresExecutiveApproval(Account account, Decimal dealValue) {\n return isStrategicAccount(account) && dealValue > 500000;\n }\n\n public static Boolean isEligibleForDiscount(Account account) {\n return account.Customer_Since__c != null &&\n account.Customer_Since__c.monthsBetween(Date.today()) > 24 &&\n isHighValue(account);\n }\n}\n```\n\n### Usage\n\n```apex\n// Reads like plain English\npublic void processOpportunity(Opportunity opp, Account account) {\n if (AccountRules.isStrategicAccount(account)) {\n assignToEnterpriseTeam(opp);\n }\n\n if (AccountRules.requiresExecutiveApproval(account, opp.Amount)) {\n routeForApproval(opp);\n }\n\n if (AccountRules.isEligibleForDiscount(account)) {\n applyLoyaltyDiscount(opp);\n }\n}\n```\n\n### When to Use\n\n- Business rules are reused across multiple classes\n- Complex boolean logic needs to be readable\n- Rules change frequently (centralized = easier updates)\n- You want trigger/service code to read like business requirements\n\n### Relationship to Other Patterns\n\n| Pattern | Relationship |\n|---------|--------------|\n| Selector | Domain class uses Selector for data access |\n| Service | Service orchestrates, Domain validates |\n| Repository | Domain class is data-agnostic |\n| Strategy | Domain rules can use Strategy for variations |\n\n---\n\n## Abstraction Level Management\n\n> 💡 *Principles inspired by \"Clean Apex Code\" by Pablo Gonzalez.\n> [Purchase the book](https://link.springer.com/book/10.1007/979-8-8688-1411-2) for complete coverage.*\n\n### Purpose\n\nEnsure each method operates at a consistent level of abstraction. Don't mix high-level orchestration with low-level implementation details.\n\n### The Problem\n\n```apex\n// BAD: Mixed abstraction levels\npublic void processNewCustomer(Account account) {\n // HIGH-LEVEL: Validation\n validateAccount(account);\n\n // LOW-LEVEL: String manipulation (doesn't belong here)\n String sanitizedPhone = account.Phone.replaceAll('[^0-9]', '');\n if (sanitizedPhone.length() == 10) {\n sanitizedPhone = '1' + sanitizedPhone;\n }\n account.Phone = '+' + sanitizedPhone;\n\n // HIGH-LEVEL: Save\n insert account;\n\n // LOW-LEVEL: HTTP details (doesn't belong here)\n HttpRequest req = new HttpRequest();\n req.setEndpoint('https://api.crm.com/customers');\n req.setMethod('POST');\n req.setHeader('Content-Type', 'application/json');\n req.setBody(JSON.serialize(account));\n Http http = new Http();\n HttpResponse res = http.send(req);\n\n // HIGH-LEVEL: Notification\n sendWelcomeEmail(account);\n}\n```\n\n### The Solution\n\n```apex\n// GOOD: Consistent high-level abstraction\npublic void processNewCustomer(Account account) {\n validateAccount(account);\n normalizePhoneNumber(account);\n insert account;\n syncToExternalCRM(account);\n sendWelcomeEmail(account);\n}\n\n// Low-level details extracted to focused methods\nprivate void normalizePhoneNumber(Account account) {\n if (String.isBlank(account.Phone)) return;\n\n String digitsOnly = account.Phone.replaceAll('[^0-9]', '');\n if (digitsOnly.length() == 10) {\n digitsOnly = '1' + digitsOnly;\n }\n account.Phone = '+' + digitsOnly;\n}\n\nprivate void syncToExternalCRM(Account account) {\n CRMIntegrationService.syncCustomer(account);\n}\n```\n\n### Abstraction Layers in Apex\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│ TRIGGER LAYER │\n│ - Routes events to handlers │\n│ - No business logic │\n└─────────────────────────────────────────────────────────────┘\n │\n ▼\n┌─────────────────────────────────────────────────────────────┐\n│ HANDLER/SERVICE LAYER (High-level) │\n│ - Orchestrates business operations │\n│ - Coordinates between components │\n│ - Each step is a method call, not implementation │\n└─────────────────────────────────────────────────────────────┘\n │\n ▼\n┌─────────────────────────────────────────────────────────────┐\n│ DOMAIN LAYER (Business rules) │\n│ - Encapsulates business logic │\n│ - AccountRules, OpportunityRules, etc. │\n│ - Pure logic, no infrastructure │\n└─────────────────────────────────────────────────────────────┘\n │\n ▼\n┌─────────────────────────────────────────────────────────────┐\n│ DATA ACCESS LAYER (Low-level) │\n│ - Selectors for SOQL │\n│ - Repositories for DML │\n│ - Integration services for external calls │\n└─────────────────────────────────────────────────────────────┘\n```\n\n### Guidelines\n\n| Level | Should Contain | Should NOT Contain |\n|-------|---------------|-------------------|\n| High (Orchestration) | Method calls, flow control | SOQL, DML, string parsing |\n| Mid (Domain) | Business rules, validation | HTTP calls, database queries |\n| Low (Data Access) | SOQL, DML, HTTP | Business decisions |\n\n### Signs of Mixed Abstraction\n\n- A method has both `[SELECT ...]` and business logic\n- HTTP request building next to email sending\n- String manipulation in a method that also updates records\n- Governor limit checks scattered among business rules\n\n### Benefits\n\n- Each method is easier to understand in isolation\n- Methods at the same level can be tested with similar techniques\n- Changes to implementation don't affect orchestration\n- Code reads like a high-level description of the process\n\n---\n\n## Pattern Selection Guide\n\n| Need | Pattern |\n|------|---------|\n| Centralize object creation | Factory |\n| Abstract data access | Repository / Selector |\n| Build complex objects | Builder |\n| Single cached instance | Singleton |\n| Interchangeable algorithms | Strategy |\n| Transactional DML | Unit of Work |\n| Add behavior without modification | Decorator |\n| React to state changes | Observer |\n| Queue/undo operations | Command |\n| Simplify complex systems | Facade |\n| Encapsulate business rules | Domain Class |\n| Consistent method structure | Abstraction Levels |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":32992,"content_sha256":"f5a341eddd3f660f9b60cb69b5bfb329eac01618cfe4a2d8ea9b987e224eb32a"},{"filename":"references/flow-integration.md","content":"\u003c!-- Parent: sf-apex/SKILL.md -->\n# Apex Flow Integration Guide\n\nThis guide covers creating Apex classes callable from Salesforce Flows using `@InvocableMethod` and `@InvocableVariable`.\n\n---\n\n## Overview\n\n```\n┌─────────────────────────────────────────────────────────────────────┐\n│ FLOW → APEX INTEGRATION │\n├─────────────────────────────────────────────────────────────────────┤\n│ │\n│ ┌─────────────┐ actionCalls ┌─────────────────────┐ │\n│ │ Flow │ ─────────────────────▶ │ @InvocableMethod │ │\n│ │ Action │ │ Apex Class │ │\n│ └─────────────┘ ◀───────────────────── └─────────────────────┘ │\n│ Response │\n│ │\n│ Input Variables ────▶ Request Wrapper ────▶ Business Logic │\n│ Output Variables ◀──── Response Wrapper ◀──── Return Values │\n│ │\n└─────────────────────────────────────────────────────────────────────┘\n```\n\n---\n\n## Quick Reference\n\n| Annotation | Purpose | Required |\n|------------|---------|----------|\n| `@InvocableMethod` | Marks method as Flow-callable | Yes |\n| `@InvocableVariable` | Marks property as Flow parameter | Yes (for wrappers) |\n\n---\n\n## @InvocableMethod Decorator\n\n### Syntax\n\n```apex\n@InvocableMethod(\n label='Display Name in Flow'\n description='Explanation shown in Flow Builder'\n category='Category for grouping'\n callout=true // If method makes HTTP callouts\n)\npublic static List\u003cResponse> execute(List\u003cRequest> requests) {\n // Implementation\n}\n```\n\n### Parameters\n\n| Parameter | Description | Required |\n|-----------|-------------|----------|\n| `label` | Display name in Flow Builder action list | Yes |\n| `description` | Help text shown when configuring action | No |\n| `category` | Groups actions in Flow Builder | No |\n| `callout` | Set `true` if method makes HTTP callouts | No (default: false) |\n| `configurationEditor` | Custom LWC for configuration UI | No |\n\n### Method Signature Rules\n\n```apex\n// ✅ CORRECT: Static, List input, List output\npublic static List\u003cResponse> execute(List\u003cRequest> requests)\n\n// ❌ WRONG: Non-static method\npublic List\u003cResponse> execute(List\u003cRequest> requests)\n\n// ❌ WRONG: Single object (not List)\npublic static Response execute(Request request)\n\n// ✅ CORRECT: Simple types also allowed\npublic static List\u003cString> execute(List\u003cId> recordIds)\n```\n\n---\n\n## @InvocableVariable Decorator\n\n### Syntax\n\n```apex\npublic class Request {\n @InvocableVariable(\n label='Record ID'\n description='The ID of the record to process'\n required=true\n )\n public Id recordId;\n}\n```\n\n### Parameters\n\n| Parameter | Description | Required |\n|-----------|-------------|----------|\n| `label` | Display name in Flow mapping UI | Yes |\n| `description` | Help text for the variable | No |\n| `required` | Whether Flow must provide a value | No (default: false) |\n\n### Supported Data Types\n\n| Type | Flow Equivalent | Notes |\n|------|-----------------|-------|\n| `Boolean` | Boolean | |\n| `Date` | Date | |\n| `DateTime` | DateTime | |\n| `Decimal` | Number | |\n| `Double` | Number | |\n| `Integer` | Number | |\n| `Long` | Number | |\n| `String` | Text | |\n| `Time` | Time | |\n| `Id` | Text (Record ID) | Stores as 18-char ID |\n| `SObject` | Record | Any standard/custom object |\n| `List\u003cT>` | Collection | Collection of any above type |\n\n---\n\n## Request/Response Pattern\n\nThe recommended pattern uses wrapper classes for clean data exchange:\n\n```apex\npublic class AccountProcessorInvocable {\n\n @InvocableMethod(label='Process Account' category='Account')\n public static List\u003cResponse> execute(List\u003cRequest> requests) {\n List\u003cResponse> responses = new List\u003cResponse>();\n\n for (Request req : requests) {\n Response res = new Response();\n try {\n // Process the request\n res = processRequest(req);\n } catch (Exception e) {\n res.isSuccess = false;\n res.errorMessage = e.getMessage();\n }\n responses.add(res);\n }\n\n return responses;\n }\n\n private static Response processRequest(Request req) {\n // Business logic here\n Response res = new Response();\n res.isSuccess = true;\n res.outputMessage = 'Processed successfully';\n return res;\n }\n\n // ═══════════════════════════════════════════════════════════════\n // REQUEST WRAPPER\n // ═══════════════════════════════════════════════════════════════\n public class Request {\n @InvocableVariable(label='Account ID' required=true)\n public Id accountId;\n\n @InvocableVariable(label='Operation Type')\n public String operation;\n }\n\n // ═══════════════════════════════════════════════════════════════\n // RESPONSE WRAPPER\n // ═══════════════════════════════════════════════════════════════\n public class Response {\n @InvocableVariable(label='Is Success')\n public Boolean isSuccess;\n\n @InvocableVariable(label='Error Message')\n public String errorMessage;\n\n @InvocableVariable(label='Output Message')\n public String outputMessage;\n\n @InvocableVariable(label='Result Record ID')\n public Id outputRecordId;\n }\n}\n```\n\n---\n\n## Bulkification Best Practices\n\nFlows can invoke your method with multiple records. Always bulkify:\n\n```apex\n@InvocableMethod(label='Update Accounts' category='Account')\npublic static List\u003cResponse> execute(List\u003cRequest> requests) {\n List\u003cResponse> responses = new List\u003cResponse>();\n\n // ─────────────────────────────────────────────────────────────\n // STEP 1: Collect all IDs first (avoid SOQL in loop)\n // ─────────────────────────────────────────────────────────────\n Set\u003cId> accountIds = new Set\u003cId>();\n for (Request req : requests) {\n if (req.accountId != null) {\n accountIds.add(req.accountId);\n }\n }\n\n // ─────────────────────────────────────────────────────────────\n // STEP 2: Single bulk query with USER_MODE\n // ─────────────────────────────────────────────────────────────\n Map\u003cId, Account> accountsById = new Map\u003cId, Account>(\n [SELECT Id, Name, Industry, AnnualRevenue\n FROM Account\n WHERE Id IN :accountIds\n WITH USER_MODE]\n );\n\n // ─────────────────────────────────────────────────────────────\n // STEP 3: Collect DML records\n // ─────────────────────────────────────────────────────────────\n List\u003cAccount> accountsToUpdate = new List\u003cAccount>();\n\n for (Request req : requests) {\n Response res = new Response();\n Account acc = accountsById.get(req.accountId);\n\n if (acc == null) {\n res.isSuccess = false;\n res.errorMessage = 'Account not found: ' + req.accountId;\n } else {\n // Process and collect for bulk DML\n acc.Description = 'Processed via Flow';\n accountsToUpdate.add(acc);\n res.isSuccess = true;\n res.outputRecordId = acc.Id;\n }\n\n responses.add(res);\n }\n\n // ─────────────────────────────────────────────────────────────\n // STEP 4: Single bulk DML operation\n // ─────────────────────────────────────────────────────────────\n if (!accountsToUpdate.isEmpty()) {\n update accountsToUpdate;\n }\n\n return responses;\n}\n```\n\n---\n\n## Error Handling\n\n### Return Errors to Flow (Recommended)\n\n```apex\npublic class Response {\n @InvocableVariable(label='Is Success')\n public Boolean isSuccess;\n\n @InvocableVariable(label='Error Message')\n public String errorMessage;\n\n @InvocableVariable(label='Error Type')\n public String errorType;\n}\n\n// In your method:\ntry {\n // Business logic\n res.isSuccess = true;\n} catch (DmlException e) {\n res.isSuccess = false;\n res.errorMessage = e.getDmlMessage(0);\n res.errorType = 'DmlException';\n} catch (Exception e) {\n res.isSuccess = false;\n res.errorMessage = e.getMessage();\n res.errorType = e.getTypeName();\n}\n```\n\n### Throw Exception (Flow Fault Path)\n\n```apex\n// Throwing an exception triggers the Flow's Fault path\n@InvocableMethod(label='Process Account')\npublic static List\u003cResponse> execute(List\u003cRequest> requests) {\n if (requests.isEmpty()) {\n throw new InvocableException('No requests provided');\n }\n // ...\n}\n\npublic class InvocableException extends Exception {}\n```\n\n**Flow Fault Connector:**\n```xml\n\u003cactionCalls>\n \u003cname>Call_Apex\u003c/name>\n \u003cfaultConnector>\n \u003ctargetReference>Handle_Error\u003c/targetReference>\n \u003c/faultConnector>\n \u003c!-- ... -->\n\u003c/actionCalls>\n```\n\n---\n\n## Working with Collections\n\n### Accept Collection Input\n\n```apex\npublic class Request {\n @InvocableVariable(label='Account IDs' required=true)\n public List\u003cId> accountIds; // Flow passes a collection\n}\n```\n\n### Return Collection Output\n\n```apex\npublic class Response {\n @InvocableVariable(label='Processed Accounts')\n public List\u003cAccount> accounts; // Flow receives a collection\n}\n```\n\n### Collection Iteration in Flow\n\nWhen your invocable returns a List inside the Response, Flow can:\n1. Use it directly in data tables\n2. Loop over it with a Loop element\n3. Pass it to another invocable action\n\n---\n\n## Security Considerations\n\n### FLS/CRUD Enforcement\n\n```apex\n// Use USER_MODE for automatic FLS/CRUD checks\nMap\u003cId, Account> accounts = new Map\u003cId, Account>(\n [SELECT Id, Name FROM Account WHERE Id IN :ids WITH USER_MODE]\n);\n\n// Or use Security.stripInaccessible for DML\nSObjectAccessDecision decision = Security.stripInaccessible(\n AccessType.CREATABLE,\n accounts\n);\ninsert decision.getRecords();\n```\n\n### with sharing\n\n```apex\n// Always use 'with sharing' unless there's a specific reason not to\npublic with sharing class AccountInvocable {\n // Respects org-wide defaults and sharing rules\n}\n```\n\n---\n\n## Testing Invocable Methods\n\n```apex\n@IsTest\nprivate class AccountInvocableTest {\n\n @IsTest\n static void testSuccessScenario() {\n // Setup test data\n Account testAccount = new Account(Name = 'Test Account');\n insert testAccount;\n\n // Create request\n AccountInvocable.Request req = new AccountInvocable.Request();\n req.accountId = testAccount.Id;\n req.operation = 'process';\n\n // Execute\n Test.startTest();\n List\u003cAccountInvocable.Response> responses =\n AccountInvocable.execute(new List\u003cAccountInvocable.Request>{ req });\n Test.stopTest();\n\n // Verify\n System.assertEquals(1, responses.size(), 'Should return one response');\n System.assertEquals(true, responses[0].isSuccess, 'Should succeed');\n System.assertNotEquals(null, responses[0].outputRecordId, 'Should return record ID');\n }\n\n @IsTest\n static void testBulkExecution() {\n // Test with multiple records to verify bulkification\n List\u003cAccount> accounts = new List\u003cAccount>();\n for (Integer i = 0; i \u003c 200; i++) {\n accounts.add(new Account(Name = 'Test ' + i));\n }\n insert accounts;\n\n List\u003cAccountInvocable.Request> requests = new List\u003cAccountInvocable.Request>();\n for (Account acc : accounts) {\n AccountInvocable.Request req = new AccountInvocable.Request();\n req.accountId = acc.Id;\n requests.add(req);\n }\n\n Test.startTest();\n List\u003cAccountInvocable.Response> responses = AccountInvocable.execute(requests);\n Test.stopTest();\n\n System.assertEquals(200, responses.size(), 'Should handle bulk records');\n for (AccountInvocable.Response res : responses) {\n System.assertEquals(true, res.isSuccess, 'All should succeed');\n }\n }\n\n @IsTest\n static void testErrorHandling() {\n // Test with invalid ID\n AccountInvocable.Request req = new AccountInvocable.Request();\n req.accountId = '001000000000000AAA'; // Non-existent ID\n\n Test.startTest();\n List\u003cAccountInvocable.Response> responses =\n AccountInvocable.execute(new List\u003cAccountInvocable.Request>{ req });\n Test.stopTest();\n\n System.assertEquals(false, responses[0].isSuccess, 'Should fail for invalid ID');\n System.assertNotEquals(null, responses[0].errorMessage, 'Should have error message');\n }\n}\n```\n\n---\n\n## Flow XML Reference\n\nWhen your Invocable is deployed, Flows call it like this:\n\n```xml\n\u003cactionCalls>\n \u003cname>Process_Account\u003c/name>\n \u003clabel>Process Account\u003c/label>\n \u003cactionName>AccountProcessorInvocable\u003c/actionName>\n \u003cactionType>apex\u003c/actionType>\n \u003cconnector>\n \u003ctargetReference>Next_Element\u003c/targetReference>\n \u003c/connector>\n \u003cfaultConnector>\n \u003ctargetReference>Error_Handler\u003c/targetReference>\n \u003c/faultConnector>\n\n \u003c!-- Map Flow variable to Apex Request property -->\n \u003cinputParameters>\n \u003cname>accountId\u003c/name>\n \u003cvalue>\n \u003celementReference>recordId\u003c/elementReference>\n \u003c/value>\n \u003c/inputParameters>\n\n \u003c!-- Map Apex Response property to Flow variable -->\n \u003coutputParameters>\n \u003cassignToReference>isSuccess\u003c/assignToReference>\n \u003cname>isSuccess\u003c/name>\n \u003c/outputParameters>\n \u003coutputParameters>\n \u003cassignToReference>errorMessage\u003c/assignToReference>\n \u003cname>errorMessage\u003c/name>\n \u003c/outputParameters>\n\u003c/actionCalls>\n```\n\n---\n\n## Cross-Skill Integration\n\n| Integration | See Also |\n|-------------|----------|\n| Flow → LWC → Apex | [triangle-pattern.md](triangle-pattern.md) |\n| Apex → LWC | via @AuraEnabled controller pattern |\n| Agentforce Actions | sf-ai-agentscript skill (similar pattern for agent actions) |\n\n---\n\n## Template\n\nUse the template at `assets/invocable-method.cls` as a starting point.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":16276,"content_sha256":"f382cae8b8fc4bd67cb78822b92700f887b03d700fe307d6e03c8105cacb1296"},{"filename":"references/llm-anti-patterns.md","content":"\u003c!-- Parent: sf-apex/SKILL.md -->\n# LLM-Specific Anti-Patterns in Apex\n\nThis guide documents systematic errors that LLMs (including Claude) commonly make when generating Salesforce Apex code. These patterns are critical to validate in generated code.\n\n> **Source**: [LLM Mistakes in Apex & LWC - Salesforce Diaries](https://salesforcediaries.com/2026/01/16/llm-mistakes-in-apex-lwc-salesforce-code-generation-rules/)\n\n---\n\n## Table of Contents\n\n1. [Non-Existent Methods](#1-non-existent-methods)\n2. [Java Types Instead of Apex Types](#2-java-types-instead-of-apex-types)\n3. [Map Access Without Null Safety](#3-map-access-without-null-safety)\n4. [Missing SOQL Fields](#4-missing-soql-fields)\n5. [Recursive Trigger Loops](#5-recursive-trigger-loops)\n6. [Invalid InvocableVariable Types](#6-invalid-invocablevariable-types)\n7. [Missing @JsonAccess Annotations](#7-missing-jsonaccess-annotations)\n8. [Null Pointer from Missing Checks](#8-null-pointer-from-missing-checks)\n9. [Incorrect DateTime Methods](#9-incorrect-datetime-methods)\n10. [Collection Initialization Patterns](#10-collection-initialization-patterns)\n\n---\n\n\u003ca id=\"1-non-existent-methods\">\u003c/a>\n\n## 1. Non-Existent Methods\n\nLLMs often hallucinate methods that don't exist in Apex, borrowing syntax from Java or other languages.\n\n### Common Hallucinated Methods\n\n| Hallucinated Method | What LLM Expected | Correct Apex Alternative |\n|---------------------|-------------------|--------------------------|\n| `Datetime.addMilliseconds()` | Add milliseconds | `Datetime.addSeconds(ms/1000)` |\n| `String.isEmpty(str)` | Static empty check | `String.isBlank(str)` |\n| `List.stream()` | Java streams | Use `for` loops |\n| `Map.getOrDefault()` | Default value | `map.get(key) ?? defaultValue` |\n| `String.format()` | String formatting | `String.format()` exists but with different syntax |\n| `Object.equals()` | Equality check | Use `==` or custom method |\n| `List.sort(comparator)` | Custom sorting | Implement `Comparable` interface |\n| `String.join(list)` | Join with delimiter | `String.join(list, delimiter)` |\n\n### ❌ BAD: Hallucinated Datetime Method\n\n```apex\n// LLM generates this - DOES NOT EXIST\nDatetime future = Datetime.now().addMilliseconds(500);\n```\n\n### ✅ GOOD: Correct Apex Pattern\n\n```apex\n// Apex has no millisecond precision - use seconds\nDatetime future = Datetime.now().addSeconds(1);\n\n// For sub-second timing, use System.currentTimeMillis()\nLong startMs = System.currentTimeMillis();\n// ... operation ...\nLong elapsedMs = System.currentTimeMillis() - startMs;\n```\n\n### ❌ BAD: Java Stream Syntax\n\n```apex\n// LLM generates this - Java streams don't exist in Apex\nList\u003cString> names = accounts.stream()\n .map(a -> a.Name)\n .collect(Collectors.toList());\n```\n\n### ✅ GOOD: Apex Loop Pattern\n\n```apex\n// Use traditional loops in Apex\nList\u003cString> names = new List\u003cString>();\nfor (Account a : accounts) {\n names.add(a.Name);\n}\n```\n\n---\n\n## 2. Java Types Instead of Apex Types\n\nLLMs trained on Java code often use Java collection types that don't exist in Apex.\n\n### Java Types to Avoid\n\n| Java Type | Apex Equivalent |\n|-----------|-----------------|\n| `ArrayList\u003cT>` | `List\u003cT>` |\n| `HashMap\u003cK,V>` | `Map\u003cK,V>` |\n| `HashSet\u003cT>` | `Set\u003cT>` |\n| `StringBuffer` | `String` (immutable) or `List\u003cString>` + `String.join()` |\n| `StringBuilder` | `String` (immutable) or `List\u003cString>` + `String.join()` |\n| `LinkedList\u003cT>` | `List\u003cT>` |\n| `TreeMap\u003cK,V>` | `Map\u003cK,V>` (no ordering guarantee) |\n| `Vector\u003cT>` | `List\u003cT>` |\n| `Hashtable\u003cK,V>` | `Map\u003cK,V>` |\n\n### ❌ BAD: Java Collection Types\n\n```apex\n// LLM generates these - COMPILE ERROR\nArrayList\u003cAccount> accounts = new ArrayList\u003cAccount>();\nHashMap\u003cId, Contact> contactMap = new HashMap\u003cId, Contact>();\nStringBuilder sb = new StringBuilder();\n```\n\n### ✅ GOOD: Apex Native Types\n\n```apex\n// Apex uses these types\nList\u003cAccount> accounts = new List\u003cAccount>();\nMap\u003cId, Contact> contactMap = new Map\u003cId, Contact>();\n\n// For string concatenation\nString result = '';\nfor (String s : parts) {\n result += s; // OK for small strings\n}\n\n// For large string building\nList\u003cString> parts = new List\u003cString>();\nparts.add('Part 1');\nparts.add('Part 2');\nString result = String.join(parts, '');\n```\n\n### Detection Rule\n\n```\nREGEX: \\b(ArrayList|HashMap|HashSet|StringBuffer|StringBuilder|LinkedList|TreeMap|Vector|Hashtable)\\s*\u003c\nSEVERITY: CRITICAL\nMESSAGE: Java type \"{match}\" does not exist in Apex. Use Apex native collections.\n```\n\n---\n\n## 3. Map Access Without Null Safety\n\nLLMs often use `Map.get()` without checking if the key exists, causing null pointer exceptions.\n\n### ❌ BAD: Unsafe Map Access\n\n```apex\nMap\u003cId, Account> accountMap = new Map\u003cId, Account>([SELECT Id, Name FROM Account]);\n\nfor (Contact c : contacts) {\n // DANGER: If AccountId not in map, .Name throws NPE\n String accountName = accountMap.get(c.AccountId).Name;\n}\n```\n\n### ✅ GOOD: Safe Map Access (Option 1 - containsKey)\n\n```apex\nMap\u003cId, Account> accountMap = new Map\u003cId, Account>([SELECT Id, Name FROM Account]);\n\nfor (Contact c : contacts) {\n if (accountMap.containsKey(c.AccountId)) {\n String accountName = accountMap.get(c.AccountId).Name;\n }\n}\n```\n\n### ✅ GOOD: Safe Map Access (Option 2 - Null Check)\n\n```apex\nMap\u003cId, Account> accountMap = new Map\u003cId, Account>([SELECT Id, Name FROM Account]);\n\nfor (Contact c : contacts) {\n Account acc = accountMap.get(c.AccountId);\n if (acc != null) {\n String accountName = acc.Name;\n }\n}\n```\n\n### ✅ GOOD: Safe Map Access (Option 3 - Safe Navigation)\n\n```apex\nMap\u003cId, Account> accountMap = new Map\u003cId, Account>([SELECT Id, Name FROM Account]);\n\nfor (Contact c : contacts) {\n // Safe navigation operator (?.) - returns null if key not found\n String accountName = accountMap.get(c.AccountId)?.Name;\n if (accountName != null) {\n // Process\n }\n}\n```\n\n### ✅ GOOD: Safe Map Access (Option 4 - Null Coalescing)\n\n```apex\nMap\u003cId, Account> accountMap = new Map\u003cId, Account>([SELECT Id, Name FROM Account]);\n\nfor (Contact c : contacts) {\n // Null coalescing operator (??) for default values\n String accountName = accountMap.get(c.AccountId)?.Name ?? 'Unknown';\n}\n```\n\n---\n\n## 4. Missing SOQL Fields\n\nLLMs often query fields but then access different fields not in the SELECT clause.\n\n### ❌ BAD: Accessing Unqueried Fields\n\n```apex\n// Only querying Id and Name\nList\u003cAccount> accounts = [SELECT Id, Name FROM Account];\n\nfor (Account acc : accounts) {\n // RUNTIME ERROR: Industry was not queried!\n if (acc.Industry == 'Technology') {\n acc.Description = 'Tech company'; // Description also not queried!\n }\n}\n```\n\n### ✅ GOOD: Query All Accessed Fields\n\n```apex\n// Query all fields that will be accessed\nList\u003cAccount> accounts = [SELECT Id, Name, Industry, Description FROM Account];\n\nfor (Account acc : accounts) {\n if (acc.Industry == 'Technology') {\n acc.Description = 'Tech company';\n }\n}\n```\n\n### ❌ BAD: Missing Relationship Fields\n\n```apex\n// Missing Account.Name in query\nList\u003cContact> contacts = [SELECT Id, Name, AccountId FROM Contact];\n\nfor (Contact c : contacts) {\n // RUNTIME ERROR: Account.Name not queried\n System.debug('Account: ' + c.Account.Name);\n}\n```\n\n### ✅ GOOD: Include Relationship Fields\n\n```apex\n// Include relationship fields using dot notation\nList\u003cContact> contacts = [SELECT Id, Name, AccountId, Account.Name FROM Contact];\n\nfor (Contact c : contacts) {\n System.debug('Account: ' + c.Account.Name); // Works!\n}\n```\n\n### Validation Checklist\n\nBefore running code, verify:\n1. All fields accessed in `if` statements are queried\n2. All fields accessed in assignments are queried\n3. All relationship fields (e.g., `Account.Name`) are in SELECT\n4. Parent relationship uses `.` notation in query (e.g., `Contact.Account.Name`)\n\n---\n\n## 5. Recursive Trigger Loops\n\nLLMs often forget to add recursion prevention in triggers, causing infinite loops.\n\n### ❌ BAD: No Recursion Prevention\n\n```apex\n// Trigger that updates related records\ntrigger AccountTrigger on Account (after update) {\n List\u003cContact> contactsToUpdate = new List\u003cContact>();\n\n for (Account acc : Trigger.new) {\n // This update might trigger another trigger, which might update Account...\n for (Contact c : [SELECT Id FROM Contact WHERE AccountId = :acc.Id]) {\n c.MailingCity = acc.BillingCity;\n contactsToUpdate.add(c);\n }\n }\n\n update contactsToUpdate; // Could cause recursion!\n}\n```\n\n### ✅ GOOD: Static Flag Pattern\n\n```apex\n// TriggerHelper.cls\npublic class TriggerHelper {\n private static Boolean isFirstRun = true;\n\n public static Boolean isFirstRun() {\n if (isFirstRun) {\n isFirstRun = false;\n return true;\n }\n return false;\n }\n\n public static void reset() {\n isFirstRun = true;\n }\n}\n\n// AccountTrigger.trigger\ntrigger AccountTrigger on Account (after update) {\n if (!TriggerHelper.isFirstRun()) {\n return; // Skip on recursive calls\n }\n\n // ... trigger logic ...\n}\n```\n\n### ✅ BETTER: Set-Based Recursion Control\n\n```apex\n// TriggerRecursionHandler.cls\npublic class TriggerRecursionHandler {\n private static Set\u003cId> processedIds = new Set\u003cId>();\n\n public static Boolean hasProcessed(Id recordId) {\n return processedIds.contains(recordId);\n }\n\n public static void markProcessed(Id recordId) {\n processedIds.add(recordId);\n }\n\n public static void markProcessed(Set\u003cId> recordIds) {\n processedIds.addAll(recordIds);\n }\n}\n\n// In trigger handler\nfor (Account acc : Trigger.new) {\n if (TriggerRecursionHandler.hasProcessed(acc.Id)) {\n continue;\n }\n TriggerRecursionHandler.markProcessed(acc.Id);\n // Process record...\n}\n```\n\n### ✅ BEST: Trigger Actions Framework\n\n```apex\n// Use TAF for built-in recursion control via metadata\n// See: references/trigger-actions-framework.md\n```\n\n---\n\n## 6. Invalid InvocableVariable Types\n\nLLMs often use unsupported types in `@InvocableVariable` annotations for Flow/Process Builder integration.\n\n### Supported InvocableVariable Types\n\n| Category | Supported Types |\n|----------|----------------|\n| **Primitives** | `Boolean`, `Date`, `DateTime`, `Decimal`, `Double`, `Id`, `Integer`, `Long`, `String`, `Time` |\n| **Collections** | `List\u003cT>` where T is a supported type |\n| **sObjects** | Any standard or custom sObject |\n| **Apex-Defined** | Classes with `@InvocableVariable` on fields |\n\n### Unsupported Types\n\n| Type | Why Unsupported | Alternative |\n|------|----------------|-------------|\n| `Map\u003cK,V>` | Not serializable to Flow | Use List of wrapper class |\n| `Set\u003cT>` | Not serializable to Flow | Use `List\u003cT>` |\n| `Object` | Too generic | Use specific type |\n| `Blob` | Not serializable | Use `String` (Base64) |\n| Custom classes without `@InvocableVariable` | Not marked for Flow | Add annotations |\n\n### ❌ BAD: Unsupported InvocableVariable Types\n\n```apex\npublic class FlowInput {\n @InvocableVariable\n public Map\u003cString, String> options; // NOT SUPPORTED!\n\n @InvocableVariable\n public Set\u003cId> recordIds; // NOT SUPPORTED!\n\n @InvocableVariable\n public Blob fileContent; // NOT SUPPORTED!\n}\n```\n\n### ✅ GOOD: Supported Types with Wrapper Pattern\n\n```apex\npublic class FlowInput {\n @InvocableVariable(label='Record IDs' description='Comma-separated IDs')\n public List\u003cId> recordIds; // List is supported\n\n @InvocableVariable(label='Options JSON' description='JSON string of options')\n public String optionsJson; // Serialize Map to JSON string\n\n @InvocableVariable(label='File Content' description='Base64 encoded content')\n public String fileContentBase64; // Base64 encode Blob\n}\n\n// In your method, deserialize as needed\npublic static void process(List\u003cFlowInput> inputs) {\n for (FlowInput input : inputs) {\n Map\u003cString, String> options = (Map\u003cString, String>)JSON.deserialize(\n input.optionsJson,\n Map\u003cString, String>.class\n );\n\n Blob fileContent = EncodingUtil.base64Decode(input.fileContentBase64);\n }\n}\n```\n\n---\n\n## 7. Missing @JsonAccess Annotations\n\nWhen using `JSON.serialize()` / `JSON.deserialize()` with inner classes or non-public classes, LLMs forget the `@JsonAccess` annotation required since API version 49.0.\n\n### ❌ BAD: Missing JsonAccess\n\n```apex\npublic class AccountService {\n // Inner class without @JsonAccess - JSON.serialize() fails silently\n private class AccountWrapper {\n public String name;\n public String industry;\n }\n\n public static String getAccountsJson() {\n List\u003cAccountWrapper> wrappers = new List\u003cAccountWrapper>();\n // ... populate ...\n return JSON.serialize(wrappers); // Returns \"[]\" or throws error!\n }\n}\n```\n\n### ✅ GOOD: With JsonAccess Annotation\n\n```apex\npublic class AccountService {\n @JsonAccess(serializable='always' deserializable='always')\n private class AccountWrapper {\n public String name;\n public String industry;\n }\n\n public static String getAccountsJson() {\n List\u003cAccountWrapper> wrappers = new List\u003cAccountWrapper>();\n // ... populate ...\n return JSON.serialize(wrappers); // Works correctly!\n }\n}\n```\n\n### When @JsonAccess is Required\n\n| Class Type | Needs @JsonAccess? |\n|------------|-------------------|\n| Public top-level class | No |\n| Private inner class | **Yes** |\n| Protected inner class | **Yes** |\n| Public inner class | No (but recommended for clarity) |\n| Class used only internally | No (unless serialized) |\n\n---\n\n## 8. Null Pointer from Missing Checks\n\nLLMs often chain method calls without null safety, leading to null pointer exceptions.\n\n### ❌ BAD: Chained Calls Without Null Checks\n\n```apex\n// Any of these could be null!\nString city = [SELECT Id, Account.BillingAddress FROM Contact LIMIT 1]\n .Account\n .BillingAddress\n .getCity();\n```\n\n### ✅ GOOD: Safe Navigation Operator\n\n```apex\n// Use ?. for safe navigation\nContact c = [SELECT Id, Account.BillingCity FROM Contact LIMIT 1];\nString city = c?.Account?.BillingCity;\n\n// With default value\nString city = c?.Account?.BillingCity ?? 'Unknown';\n```\n\n### ✅ GOOD: Explicit Null Checks\n\n```apex\nContact c = [SELECT Id, Account.BillingCity FROM Contact LIMIT 1];\n\nString city = 'Unknown';\nif (c != null && c.Account != null && c.Account.BillingCity != null) {\n city = c.Account.BillingCity;\n}\n```\n\n---\n\n## 9. Incorrect DateTime Methods\n\nLLMs confuse Date and DateTime methods, which have different APIs.\n\n### Date vs DateTime Method Confusion\n\n| Operation | Date Method | DateTime Method |\n|-----------|-------------|-----------------|\n| Add days | `addDays(n)` | `addDays(n)` |\n| Add months | `addMonths(n)` | `addMonths(n)` |\n| Add years | `addYears(n)` | N/A (use `addMonths(n*12)`) |\n| Add hours | N/A | `addHours(n)` |\n| Add minutes | N/A | `addMinutes(n)` |\n| Add seconds | N/A | `addSeconds(n)` |\n| Get day | `day()` | `day()` |\n| Get month | `month()` | `month()` |\n| Get year | `year()` | `year()` |\n| Get hour | N/A | `hour()` |\n| Get minute | N/A | `minute()` |\n| Today | `Date.today()` | N/A |\n| Now | N/A | `DateTime.now()` |\n\n### ❌ BAD: Mixing Date/DateTime Methods\n\n```apex\n// Date doesn't have addHours!\nDate d = Date.today();\nDate future = d.addHours(5); // COMPILE ERROR\n\n// DateTime doesn't have a static today()!\nDateTime now = DateTime.today(); // COMPILE ERROR\n```\n\n### ✅ GOOD: Correct Method Usage\n\n```apex\n// For Date operations\nDate d = Date.today();\nDate future = d.addDays(5);\n\n// For DateTime operations\nDateTime now = DateTime.now();\nDateTime future = now.addHours(5);\n\n// Converting between Date and DateTime\nDate d = Date.today();\nDateTime dt = DateTime.newInstance(d, Time.newInstance(0, 0, 0, 0));\n\nDateTime dt = DateTime.now();\nDate d = dt.date();\n```\n\n---\n\n## 10. Collection Initialization Patterns\n\nLLMs sometimes use incorrect patterns for initializing collections from SOQL or other collections.\n\n### ❌ BAD: Incorrect Map Initialization\n\n```apex\n// This doesn't work - can't construct Map from SOQL directly with fields\nMap\u003cId, String> nameMap = new Map\u003cId, String>(\n [SELECT Id, Name FROM Account]\n); // COMPILE ERROR - wrong constructor\n```\n\n### ✅ GOOD: Correct Map Initialization\n\n```apex\n// Map\u003cId, SObject> works directly with SOQL\nMap\u003cId, Account> accountMap = new Map\u003cId, Account>(\n [SELECT Id, Name FROM Account]\n);\n\n// For Map\u003cId, SpecificField>, use a loop\nMap\u003cId, String> nameMap = new Map\u003cId, String>();\nfor (Account acc : [SELECT Id, Name FROM Account]) {\n nameMap.put(acc.Id, acc.Name);\n}\n```\n\n### ❌ BAD: List to Set Conversion\n\n```apex\n// Can't directly convert List to Set in constructor\nList\u003cId> idList = new List\u003cId>{'001...', '001...'};\nSet\u003cId> idSet = new Set\u003cId>(idList); // Actually this DOES work in Apex!\n```\n\n### ✅ GOOD: Collection Conversions\n\n```apex\n// List to Set - this works!\nList\u003cId> idList = new List\u003cId>{'001...', '001...'};\nSet\u003cId> idSet = new Set\u003cId>(idList);\n\n// Set to List\nSet\u003cId> idSet = new Set\u003cId>{'001...', '001...'};\nList\u003cId> idList = new List\u003cId>(idSet);\n\n// Map keys to Set\nMap\u003cId, Account> accountMap = new Map\u003cId, Account>([SELECT Id FROM Account]);\nSet\u003cId> accountIds = accountMap.keySet();\n\n// Map values to List\nList\u003cAccount> accounts = accountMap.values();\n```\n\n---\n\n## Quick Reference: LLM Validation Checklist\n\nBefore accepting LLM-generated Apex code, verify:\n\n### Methods & Types\n- [ ] No Java collection types (ArrayList, HashMap, etc.)\n- [ ] No hallucinated methods (addMilliseconds, stream(), etc.)\n- [ ] Correct Date vs DateTime methods\n\n### Null Safety\n- [ ] Map.get() has null check or uses containsKey()\n- [ ] Chained method calls use safe navigation (?.)\n- [ ] SOQL results checked before accessing\n\n### SOQL\n- [ ] All accessed fields are in SELECT clause\n- [ ] Relationship fields use dot notation in query\n- [ ] Parent records accessed safely\n\n### Flow Integration\n- [ ] InvocableVariable uses supported types\n- [ ] No Map/Set in @InvocableVariable\n- [ ] @JsonAccess on inner classes if serialized\n\n### Triggers\n- [ ] Recursion prevention mechanism in place\n- [ ] Static flag or processed ID tracking\n\n---\n\n## Detection Script\n\nAdd this validation to your CI/CD pipeline:\n\n```python\n# detect_llm_patterns.py\nimport re\n\nJAVA_TYPES = [\n r'\\bArrayList\\s*\u003c',\n r'\\bHashMap\\s*\u003c',\n r'\\bHashSet\\s*\u003c',\n r'\\bStringBuffer\\b',\n r'\\bStringBuilder\\b',\n r'\\bLinkedList\\s*\u003c',\n r'\\bTreeMap\\s*\u003c',\n]\n\nHALLUCINATED_METHODS = [\n r'\\.addMilliseconds\\s*\\(',\n r'\\.stream\\s*\\(\\)',\n r'\\.getOrDefault\\s*\\(',\n r'DateTime\\.today\\s*\\(\\)',\n]\n\ndef validate_apex(content):\n issues = []\n\n for pattern in JAVA_TYPES:\n if re.search(pattern, content):\n issues.append(f\"Java type detected: {pattern}\")\n\n for pattern in HALLUCINATED_METHODS:\n if re.search(pattern, content):\n issues.append(f\"Non-existent method: {pattern}\")\n\n return issues\n```\n\n---\n\n## Reference\n\n- **Existing Anti-Patterns**: See `references/anti-patterns.md` for traditional Apex anti-patterns\n- **Best Practices**: See `references/best-practices.md` for correct patterns\n- **Source**: [Salesforce Diaries - LLM Mistakes](https://salesforcediaries.com/2026/01/16/llm-mistakes-in-apex-lwc-salesforce-code-generation-rules/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":19279,"content_sha256":"8c7f11aed3ac95c99b66d52b4d2ed4555e8a33864a7263074bb3dc3feb357415"},{"filename":"references/naming-conventions.md","content":"\u003c!-- Parent: sf-apex/SKILL.md -->\n# Apex Naming Conventions\n\n## Classes\n\n### Format: PascalCase\n\n| Type | Convention | Example |\n|------|------------|---------|\n| Standard class | `[Domain]Service` | `AccountService` |\n| Controller | `[Page]Controller` | `AccountPageController` |\n| Extension | `[Page]ControllerExt` | `AccountControllerExt` |\n| Trigger Action | `TA_[Object]_[Action]` | `TA_Account_SetDefaults` |\n| Batch | `[Domain]_Batch` | `AccountCleanup_Batch` |\n| Queueable | `[Domain]_Queueable` | `AccountSync_Queueable` |\n| Schedulable | `[Domain]_Schedule` | `DailyReport_Schedule` |\n| Selector | `[Object]Selector` | `AccountSelector` |\n| Domain | `[Object]Domain` | `AccountDomain` |\n| Test class | `[ClassName]Test` | `AccountServiceTest` |\n| Test factory | `TestDataFactory` | `TestDataFactory` |\n| Exception | `[Domain]Exception` | `InsufficientFundsException` |\n| Interface | `I[Name]` or descriptive | `IPaymentProcessor` |\n\n### Examples\n\n```apex\npublic class AccountService { }\npublic class TA_Account_ValidateAddress implements TriggerAction.BeforeInsert { }\npublic class AccountCleanup_Batch implements Database.Batchable\u003cSObject> { }\npublic class AccountSync_Queueable implements Queueable { }\npublic class AccountSelector { }\n```\n\n---\n\n## Methods\n\n### Format: camelCase, Start with Verb\n\n| Purpose | Convention | Example |\n|---------|------------|---------|\n| Get data | `get[Noun]` | `getAccounts()` |\n| Set data | `set[Noun]` | `setAccountStatus()` |\n| Check condition | `is[Adjective]` / `has[Noun]` | `isActive()`, `hasPermission()` |\n| Action | `[verb][Noun]` | `processOrders()`, `validateData()` |\n| Calculate | `calculate[Noun]` | `calculateTotal()` |\n| Create | `create[Noun]` | `createAccount()` |\n| Update | `update[Noun]` | `updateStatus()` |\n| Delete | `delete[Noun]` | `deleteRecords()` |\n\n### Special Verbs\n\n- Use `obtain` instead of `get` for expensive operations\n- Use `specify` instead of `set` for configuration\n- Reserve `get`/`set` for actual property getters/setters\n\n```apex\n// Good\npublic Account getAccount(Id accountId) { }\npublic void processRecords(List\u003cRecord__c> records) { }\npublic Boolean isEligible(Account acc) { }\npublic Decimal calculateTotalRevenue(List\u003cOpportunity> opps) { }\n\n// Better for expensive operations\npublic Account obtainAccountWithRelated(Id accountId) { }\n\n// Boolean methods read as assertions\npublic Boolean hasActiveSubscription() { }\npublic Boolean canModifyRecord() { }\n```\n\n---\n\n## Variables\n\n### Format: camelCase, Descriptive\n\n| Type | Convention | Example |\n|------|------------|---------|\n| Local variable | descriptive noun | `account`, `totalAmount` |\n| Loop iterator | single letter (temp) | `i`, `j`, `k` |\n| Boolean | `is[Adjective]` / `has[Noun]` | `isValid`, `hasError` |\n| Collection | plural noun | `accounts`, `contactList` |\n| Map | `[value]By[key]` | `accountsById`, `contactsByEmail` |\n| Set | `[noun]Set` or `[noun]Ids` | `accountIds`, `processedIdSet` |\n\n### Anti-Patterns to Avoid\n\n```apex\n// BAD: Abbreviations\nString acct; // What is this?\nList\u003cTask> tks; // Unclear\nSObject rec; // Too generic\n\n// GOOD: Descriptive names\nString accountName;\nList\u003cTask> openTasks;\nAccount parentAccount;\n```\n\n### Collection Naming\n\n```apex\n// Lists - plural noun\nList\u003cAccount> accounts;\nList\u003cContact> relatedContacts;\n\n// Sets - noun + Ids or noun + Set\nSet\u003cId> accountIds;\nSet\u003cString> processedEmailSet;\n\n// Maps - value + By + key description\nMap\u003cId, Account> accountsById;\nMap\u003cString, List\u003cContact>> contactsByEmail;\nMap\u003cId, Map\u003cString, Decimal>> metricsByAccountByType;\n```\n\n---\n\n## Constants\n\n### Format: UPPER_SNAKE_CASE\n\n```apex\npublic class Constants {\n public static final String STATUS_ACTIVE = 'Active';\n public static final String STATUS_INACTIVE = 'Inactive';\n public static final Integer MAX_RETRY_COUNT = 3;\n public static final Decimal TAX_RATE = 0.08;\n}\n```\n\n---\n\n## Custom Objects & Fields\n\n### Format: Title_Case_With_Underscores\n\n```apex\n// Objects\nAccount_Score__c\nOrder_Line_Item__c\n\n// Fields\nAccount_Status__c\nTotal_Revenue__c\nIs_Primary__c\n\n// Reference in code (use API names)\naccount.Account_Status__c = 'Active';\n```\n\n---\n\n## Triggers\n\n### Format: [ObjectName]Trigger\n\n```apex\ntrigger AccountTrigger on Account (...) { }\ntrigger ContactTrigger on Contact (...) { }\ntrigger OpportunityTrigger on Opportunity (...) { }\n```\n\n---\n\n## Test Classes\n\n### Format: [TestedClassName]Test\n\n```apex\n// For AccountService.cls\n@isTest\nprivate class AccountServiceTest { }\n\n// For TA_Account_SetDefaults.cls\n@isTest\nprivate class TA_Account_SetDefaultsTest { }\n```\n\n### Test Methods\n\n```apex\n// Format: test[Scenario]\n@isTest\nstatic void testPositiveScenario() { }\n\n@isTest\nstatic void testBulkInsert() { }\n\n@isTest\nstatic void testInvalidInput() { }\n\n@isTest\nstatic void testAsStandardUser() { }\n```\n\n---\n\n## Quick Reference\n\n| Element | Convention | Example |\n|---------|------------|---------|\n| Class | PascalCase | `AccountService` |\n| Interface | I + PascalCase | `IPaymentProcessor` |\n| Method | camelCase verb | `processRecords()` |\n| Variable | camelCase noun | `accountList` |\n| Constant | UPPER_SNAKE | `MAX_RETRIES` |\n| Parameter | camelCase | `accountId` |\n| Boolean | is/has prefix | `isActive` |\n| Map | valueByKey | `accountsById` |\n| Set | nounIds/nounSet | `accountIds` |\n| List | plural noun | `accounts` |\n| Trigger | ObjectTrigger | `AccountTrigger` |\n| Trigger Action | TA_Object_Action | `TA_Account_Validate` |\n| Batch | Domain_Batch | `Cleanup_Batch` |\n| Test | ClassTest | `AccountServiceTest` |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5546,"content_sha256":"a2073ea1fac98452366a8cf91cf100917f7821ca4727ff77683d4cb90373513d"},{"filename":"references/patterns-deep-dive.md","content":"\u003c!-- Parent: sf-apex/SKILL.md -->\n# Apex Patterns Deep Dive\n\nComprehensive guide to advanced Apex patterns including Trigger Actions Framework, Flow Integration, and architectural patterns.\n\n---\n\n## Table of Contents\n\n1. [Trigger Actions Framework (TAF)](#trigger-actions-framework-taf)\n2. [Flow Integration (@InvocableMethod)](#flow-integration-invocablemethod)\n3. [Async Patterns](#async-patterns)\n4. [Service Layer Patterns](#service-layer-patterns)\n\n---\n\n## Trigger Actions Framework (TAF)\n\n### ⚠️ CRITICAL PREREQUISITES\n\n**Before using TAF patterns, the target org MUST have:**\n\n1. **Trigger Actions Framework Package Installed**\n - GitHub: https://github.com/mitchspano/apex-trigger-actions-framework\n - Install via: `sf package install --package 04tKZ000000gUEFYA2 --target-org [alias] --wait 10`\n - Or use unlocked package from repository\n\n2. **Custom Metadata Type Records Created**\n - TAF triggers do NOTHING without `Trigger_Action__mdt` records!\n - Each trigger action class needs a corresponding CMT record\n\n**If TAF is NOT installed, use the Standard Trigger Pattern instead.**\n\n---\n\n### TAF Pattern (Requires Package)\n\nAll triggers MUST use the Trigger Actions Framework pattern when the package is installed:\n\n**Trigger** (one per object):\n```apex\ntrigger AccountTrigger on Account (\n before insert, after insert,\n before update, after update,\n before delete, after delete, after undelete\n) {\n new MetadataTriggerHandler().run();\n}\n```\n\n**Single-Context Action Class** (one interface):\n```apex\npublic class TA_Account_SetDefaults implements TriggerAction.BeforeInsert {\n public void beforeInsert(List\u003cAccount> newList) {\n for (Account acc : newList) {\n if (acc.Industry == null) {\n acc.Industry = 'Other';\n }\n }\n }\n}\n```\n\n**Multi-Context Action Class** (multiple interfaces):\n```apex\npublic class TA_Lead_CalculateScore implements TriggerAction.BeforeInsert, TriggerAction.BeforeUpdate {\n\n // Called on new record creation\n public void beforeInsert(List\u003cLead> newList) {\n calculateScores(newList);\n }\n\n // Called on record updates\n public void beforeUpdate(List\u003cLead> newList, List\u003cLead> oldList) {\n // Only recalculate if scoring fields changed\n List\u003cLead> leadsToScore = new List\u003cLead>();\n Map\u003cId, Lead> oldMap = new Map\u003cId, Lead>(oldList);\n\n for (Lead newLead : newList) {\n Lead oldLead = oldMap.get(newLead.Id);\n if (scoringFieldsChanged(newLead, oldLead)) {\n leadsToScore.add(newLead);\n }\n }\n\n if (!leadsToScore.isEmpty()) {\n calculateScores(leadsToScore);\n }\n }\n\n private void calculateScores(List\u003cLead> leads) {\n // Scoring logic here\n for (Lead l : leads) {\n Integer score = 0;\n if (l.Industry == 'Technology') score += 10;\n if (l.NumberOfEmployees != null && l.NumberOfEmployees > 100) score += 20;\n l.Score__c = score;\n }\n }\n\n private Boolean scoringFieldsChanged(Lead newLead, Lead oldLead) {\n return newLead.Industry != oldLead.Industry ||\n newLead.NumberOfEmployees != oldLead.NumberOfEmployees;\n }\n}\n```\n\n---\n\n### ⚠️ REQUIRED: Custom Metadata Type Records\n\n**TAF triggers will NOT execute without `Trigger_Action__mdt` records!**\n\nFor each trigger action class, create a Custom Metadata record:\n\n| Field | Value | Description |\n|-------|-------|-------------|\n| Label | TA Lead Calculate Score | Human-readable name |\n| Trigger_Action_Name__c | TA_Lead_CalculateScore | Apex class name |\n| Object__c | Lead | sObject API name |\n| Context__c | Before Insert | Trigger context |\n| Order__c | 1 | Execution order (lower = first) |\n| Active__c | true | Enable/disable without deploy |\n\n**Example Custom Metadata XML** (`Trigger_Action.TA_Lead_CalculateScore_BI.md-meta.xml`):\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003cCustomMetadata xmlns=\"http://soap.sforce.com/2006/04/metadata\">\n \u003clabel>TA Lead Calculate Score - Before Insert\u003c/label>\n \u003cprotected>false\u003c/protected>\n \u003cvalues>\n \u003cfield>Apex_Class_Name__c\u003c/field>\n \u003cvalue xsi:type=\"xsd:string\">TA_Lead_CalculateScore\u003c/value>\n \u003c/values>\n \u003cvalues>\n \u003cfield>Object__c\u003c/field>\n \u003cvalue xsi:type=\"xsd:string\">Lead\u003c/value>\n \u003c/values>\n \u003cvalues>\n \u003cfield>Order__c\u003c/field>\n \u003cvalue xsi:type=\"xsd:double\">1.0\u003c/value>\n \u003c/values>\n \u003cvalues>\n \u003cfield>Bypass_Execution__c\u003c/field>\n \u003cvalue xsi:type=\"xsd:boolean\">false\u003c/value>\n \u003c/values>\n\u003c/CustomMetadata>\n```\n\n**NOTE**: Create separate CMT records for each context (Before Insert, Before Update, etc.)\n\n**Deploy Custom Metadata:**\n```bash\nsf project deploy start --metadata CustomMetadata:Trigger_Action.TA_Lead_CalculateScore_BI --target-org myorg\n```\n\n---\n\n\u003ca id=\"standard-trigger-pattern\">\u003c/a>\n\n### Standard Trigger Pattern (No Package Required)\n\n**Use this when TAF package is NOT installed in the target org:**\n\n```apex\ntrigger LeadTrigger on Lead (before insert, before update) {\n\n LeadScoringService scoringService = new LeadScoringService();\n\n if (Trigger.isBefore) {\n if (Trigger.isInsert) {\n scoringService.calculateScores(Trigger.new);\n }\n else if (Trigger.isUpdate) {\n scoringService.recalculateIfChanged(Trigger.new, Trigger.oldMap);\n }\n }\n}\n```\n\n**Service Class:**\n```apex\npublic with sharing class LeadScoringService {\n\n public void calculateScores(List\u003cLead> leads) {\n for (Lead l : leads) {\n Integer score = 0;\n if (l.Industry == 'Technology') score += 10;\n if (l.NumberOfEmployees != null && l.NumberOfEmployees > 100) score += 20;\n l.Score__c = score;\n }\n }\n\n public void recalculateIfChanged(List\u003cLead> newLeads, Map\u003cId, Lead> oldMap) {\n List\u003cLead> leadsToScore = new List\u003cLead>();\n\n for (Lead newLead : newLeads) {\n Lead oldLead = oldMap.get(newLead.Id);\n if (scoringFieldsChanged(newLead, oldLead)) {\n leadsToScore.add(newLead);\n }\n }\n\n if (!leadsToScore.isEmpty()) {\n calculateScores(leadsToScore);\n }\n }\n\n private Boolean scoringFieldsChanged(Lead newLead, Lead oldLead) {\n return newLead.Industry != oldLead.Industry ||\n newLead.NumberOfEmployees != oldLead.NumberOfEmployees;\n }\n}\n```\n\n**Pros**: No external dependencies, works in any org\n**Cons**: Less maintainable for complex triggers, no declarative control\n\n---\n\n### TAF vs Standard Pattern Comparison\n\n| Feature | TAF Pattern | Standard Pattern |\n|---------|-------------|------------------|\n| **Package Required** | Yes | No |\n| **Complexity** | Lower (single-purpose classes) | Higher (monolithic trigger) |\n| **Maintainability** | High (separate files) | Medium (one trigger file) |\n| **Declarative Control** | Yes (CMT records) | No |\n| **Order Control** | Yes (Order__c field) | Manual in code |\n| **Bypass Mechanism** | Built-in (Active__c) | Manual Custom Setting |\n| **Testing** | Easy (test action classes) | Medium (test trigger + service) |\n\n**Recommendation**: Use TAF when available, fall back to Standard Pattern when TAF is not installed.\n\n---\n\n## Flow Integration (@InvocableMethod)\n\nApex classes can be called from Flow using `@InvocableMethod`. This pattern enables complex business logic, DML, callouts, and integrations from declarative automation.\n\n### Quick Reference\n\n| Annotation | Purpose |\n|------------|---------|\n| `@InvocableMethod` | Makes method callable from Flow |\n| `@InvocableVariable` | Exposes properties in Request/Response wrappers |\n\n### Template\n\nUse `assets/invocable-method.cls` for the complete pattern with Request/Response wrappers.\n\n### Basic Example\n\n```apex\npublic with sharing class RecordProcessor {\n\n @InvocableMethod(label='Process Record' category='Custom')\n public static List\u003cResponse> execute(List\u003cRequest> requests) {\n List\u003cResponse> responses = new List\u003cResponse>();\n\n for (Request req : requests) {\n Response res = new Response();\n res.isSuccess = true;\n res.processedId = req.recordId;\n responses.add(res);\n }\n\n return responses;\n }\n\n public class Request {\n @InvocableVariable(label='Record ID' required=true)\n public Id recordId;\n }\n\n public class Response {\n @InvocableVariable(label='Is Success')\n public Boolean isSuccess;\n\n @InvocableVariable(label='Processed ID')\n public Id processedId;\n }\n}\n```\n\n### Advanced Example with Error Handling\n\n```apex\npublic with sharing class AccountValidator {\n\n @InvocableMethod(\n label='Validate Account Data'\n description='Validates account data and returns validation results'\n category='Account Management'\n )\n public static List\u003cValidationResponse> validateAccounts(List\u003cValidationRequest> requests) {\n List\u003cValidationResponse> responses = new List\u003cValidationResponse>();\n\n // Collect all Account IDs for bulk query\n Set\u003cId> accountIds = new Set\u003cId>();\n for (ValidationRequest req : requests) {\n accountIds.add(req.accountId);\n }\n\n // Bulk query\n Map\u003cId, Account> accountMap = new Map\u003cId, Account>(\n [SELECT Id, Name, Industry, AnnualRevenue, Phone\n FROM Account\n WHERE Id IN :accountIds\n WITH USER_MODE]\n );\n\n // Process each request\n for (ValidationRequest req : requests) {\n ValidationResponse res = new ValidationResponse();\n\n Account acc = accountMap.get(req.accountId);\n if (acc == null) {\n res.isValid = false;\n res.errorMessage = 'Account not found';\n responses.add(res);\n continue;\n }\n\n // Validation logic\n List\u003cString> errors = new List\u003cString>();\n\n if (String.isBlank(acc.Name)) {\n errors.add('Name is required');\n }\n if (String.isBlank(acc.Industry)) {\n errors.add('Industry is required');\n }\n if (acc.AnnualRevenue == null || acc.AnnualRevenue \u003c= 0) {\n errors.add('Annual Revenue must be greater than 0');\n }\n\n res.isValid = errors.isEmpty();\n res.errorMessage = errors.isEmpty() ? null : String.join(errors, '; ');\n res.validatedAccountId = acc.Id;\n\n responses.add(res);\n }\n\n return responses;\n }\n\n public class ValidationRequest {\n @InvocableVariable(label='Account ID' description='ID of account to validate' required=true)\n public Id accountId;\n }\n\n public class ValidationResponse {\n @InvocableVariable(label='Is Valid' description='Whether account passed validation')\n public Boolean isValid;\n\n @InvocableVariable(label='Error Message' description='Validation error details')\n public String errorMessage;\n\n @InvocableVariable(label='Validated Account ID')\n public Id validatedAccountId;\n }\n}\n```\n\n### Best Practices\n\n1. **Always use Request/Response wrappers** - Never use primitive types directly\n2. **Bulkify** - Process `List\u003cRequest>` even if Flow passes single record\n3. **Use USER_MODE** - Respect user permissions in SOQL\n4. **Error handling** - Return structured errors in Response, don't throw exceptions\n5. **Label & Category** - Make methods discoverable in Flow Builder\n6. **Description** - Add descriptions to variables for clarity\n\n### Common Patterns\n\n**Pattern 1: DML Operations**\n```apex\n@InvocableMethod(label='Create Related Contacts')\npublic static List\u003cResponse> createContacts(List\u003cRequest> requests) {\n List\u003cContact> contactsToInsert = new List\u003cContact>();\n\n for (Request req : requests) {\n Contact con = new Contact(\n AccountId = req.accountId,\n LastName = req.lastName,\n Email = req.email\n );\n contactsToInsert.add(con);\n }\n\n insert contactsToInsert;\n\n // Return results\n List\u003cResponse> responses = new List\u003cResponse>();\n for (Contact con : contactsToInsert) {\n Response res = new Response();\n res.contactId = con.Id;\n responses.add(res);\n }\n return responses;\n}\n```\n\n**Pattern 2: External Callouts**\n```apex\n@InvocableMethod(label='Send to External System')\npublic static List\u003cResponse> sendData(List\u003cRequest> requests) {\n // Note: Callouts in Flow require @future or Queueable\n // This is a sync example - for async, enqueue from here\n\n List\u003cResponse> responses = new List\u003cResponse>();\n for (Request req : requests) {\n HttpRequest request = new HttpRequest();\n request.setEndpoint('callout:MyNamedCredential/api');\n request.setMethod('POST');\n request.setBody(JSON.serialize(req));\n\n Http http = new Http();\n HttpResponse response = http.send(request);\n\n Response res = new Response();\n res.statusCode = response.getStatusCode();\n res.success = response.getStatusCode() == 200;\n responses.add(res);\n }\n return responses;\n}\n```\n\n**See Also**:\n- [references/flow-integration.md](../references/flow-integration.md) - Complete @InvocableMethod guide\n- [references/triangle-pattern.md](../references/triangle-pattern.md) - Flow-LWC-Apex triangle (Apex perspective)\n\n---\n\n## Async Patterns\n\n### Decision Matrix\n\n| Scenario | Use | Key Advantage | Daily Limit |\n|----------|-----|---------------|-------------|\n| Default async processing | **Queueable** (preferred) | Job ID, chaining, non-primitive types, delays, dedup | 250K or 200× licenses |\n| Process millions of records | Batch Apex | Chunked, off-peak, max 5 concurrent threads | Same pool |\n| Modern batch alternative | **CursorStep** (`Database.Cursor`) | 2000-record chunks, higher throughput | N/A |\n| Scheduled/recurring job | **Scheduled Flow** (preferred) or Schedulable | Flow = deployable metadata; Apex = 100 job limit | — |\n| Post-job cleanup | Queueable Finalizer (`System.Finalizer`) | Runs regardless of success/failure | — |\n| Long-running Lightning callouts | `Continuation` | 3 per txn, 3 parallel | — |\n| Legacy fire-and-forget | `@future` (legacy) | Simpler syntax only | Same pool |\n\n> ⚠️ **Batch Apex max 5 simultaneous threads.** Additional batch jobs queue. Plan accordingly for time-sensitive processing.\n\n> **Scheduled Flow preferred over Apex Schedulable** for most scheduling needs. Schedulable has a hard limit of 100 jobs. Use Schedulable only when chaining to Batch Apex or needing complex Apex-only logic.\n\n> **For delays > 10 minutes** (Queueable max), use `System.scheduleBatch()` to schedule a Batch job at a specific time.\n\n### Queueable Pattern (Preferred)\n\n```apex\npublic class AccountProcessor implements Queueable {\n\n private List\u003cId> accountIds;\n\n public AccountProcessor(List\u003cId> accountIds) {\n this.accountIds = accountIds;\n }\n\n public void execute(QueueableContext context) {\n List\u003cAccount> accounts = [SELECT Id, Name, Industry FROM Account WHERE Id IN :accountIds];\n\n for (Account acc : accounts) {\n acc.Description = 'Processed on ' + System.now();\n }\n\n update accounts;\n\n // Chain another job if needed\n if (!Test.isRunningTest() && accountIds.size() > 200) {\n // Process next batch\n List\u003cId> nextBatch = getNextBatch();\n if (!nextBatch.isEmpty()) {\n System.enqueueJob(new AccountProcessor(nextBatch));\n }\n }\n }\n\n private List\u003cId> getNextBatch() {\n // Logic to get next batch\n return new List\u003cId>();\n }\n}\n\n// Usage:\nSystem.enqueueJob(new AccountProcessor(accountIds));\n```\n\n### Queueable: Configurable Delay\n\nDelay execution up to 10 minutes using `AsyncOptions`:\n\n```apex\nAccountProcessor job = new AccountProcessor(accountIds);\nAsyncOptions options = new AsyncOptions();\noptions.setMinimumQueueableDelayInMinutes(10); // Max 10 min\nSystem.enqueueJob(job, options);\n```\n\n### Queueable: Deduplication Signatures\n\nPrevent duplicate jobs from being enqueued by assigning a dedup signature:\n\n```apex\nAccountProcessor job = new AccountProcessor(accountIds);\nAsyncOptions options = new AsyncOptions();\noptions.setDuplicateSignature(QueueableDuplicateSignature.Builder()\n .addId(accountIds[0])\n .addString('AccountProcessor')\n .build());\nSystem.enqueueJob(job, options);\n// If another job with the same signature is already queued, this is silently ignored\n```\n\n### Queueable with Finalizer (System.Finalizer)\n\n`System.Finalizer` acts as a blanket error handler — it executes regardless of whether the Queueable job succeeds or fails. Use it for logging, notifications, retry logic, and cleanup.\n\n```apex\npublic class DataSyncQueueable implements Queueable {\n\n public void execute(QueueableContext context) {\n // Attach finalizer for cleanup\n System.attachFinalizer(new DataSyncFinalizer(context.getJobId()));\n\n // Main logic\n try {\n // Process data\n Http http = new Http();\n HttpRequest req = new HttpRequest();\n req.setEndpoint('callout:ExternalSystem/sync');\n req.setMethod('POST');\n\n HttpResponse res = http.send(req);\n\n if (res.getStatusCode() != 200) {\n throw new CalloutException('Sync failed: ' + res.getBody());\n }\n } catch (Exception e) {\n // Log error\n System.debug(LoggingLevel.ERROR, 'Sync failed: ' + e.getMessage());\n throw e; // Finalizer will still run\n }\n }\n}\n\npublic class DataSyncFinalizer implements Finalizer {\n\n private Id jobId;\n\n public DataSyncFinalizer(Id jobId) {\n this.jobId = jobId;\n }\n\n public void execute(FinalizerContext context) {\n // This ALWAYS runs, even if job fails\n\n // Log status\n insert new Sync_Log__c(\n Job_Id__c = String.valueOf(jobId),\n Status__c = context.getResult() == ParentJobResult.SUCCESS ? 'Success' : 'Failed',\n Error__c = context.getException()?.getMessage()\n );\n }\n}\n```\n\n### Batch Apex Pattern\n\n```apex\npublic class AccountBatchProcessor implements Database.Batchable\u003cSObject> {\n\n // Start: Define query\n public Database.QueryLocator start(Database.BatchableContext context) {\n return Database.getQueryLocator([\n SELECT Id, Name, Industry, AnnualRevenue\n FROM Account\n WHERE Industry = 'Technology'\n ]);\n }\n\n // Execute: Process each batch (default 200 records)\n public void execute(Database.BatchableContext context, List\u003cAccount> scope) {\n for (Account acc : scope) {\n acc.Description = 'Processed by batch on ' + System.now();\n }\n\n update scope;\n }\n\n // Finish: Post-processing\n public void finish(Database.BatchableContext context) {\n // Send email, log results, chain another batch\n AsyncApexJob job = [\n SELECT Id, Status, NumberOfErrors, JobItemsProcessed, TotalJobItems\n FROM AsyncApexJob\n WHERE Id = :context.getJobId()\n ];\n\n System.debug('Batch completed: ' + job.TotalJobItems + ' items processed');\n }\n}\n\n// Usage:\nDatabase.executeBatch(new AccountBatchProcessor(), 200); // Batch size\n```\n\n### CursorStep Pattern (Database.Cursor)\n\nModern alternative to Batch Apex with higher throughput and 2000-record chunks:\n\n```apex\npublic class AccountCursorStep implements Database.CursorStep {\n\n public Database.Cursor start() {\n return Database.getCursor(\n 'SELECT Id, Name, Industry FROM Account WHERE Industry = \\'Technology\\''\n );\n }\n\n public void execute(Database.CursorStepContext context, List\u003cSObject> scope) {\n List\u003cAccount> accounts = (List\u003cAccount>) scope;\n for (Account acc : accounts) {\n acc.Description = 'Processed via CursorStep on ' + System.now();\n }\n update accounts;\n }\n\n public void finish(Database.CursorStepContext context) {\n System.debug('CursorStep completed');\n }\n}\n\n// Usage:\nDatabase.executeCursorStep(new AccountCursorStep());\n```\n\n> **CursorStep vs Batch**: CursorStep processes 2000 records per chunk (vs 200 default for Batch), doesn't count against the 5 simultaneous batch job limit, and has higher throughput. Use for new development when Batch Apex limits are a concern.\n\n### @future Pattern (Legacy — Prefer Queueable)\n\n> ⚠️ **Legacy pattern.** Prefer Queueable for new development. `@future` cannot return job IDs, cannot chain, cannot pass non-primitive types, and cannot use configurable delays or dedup signatures.\n\n```apex\npublic class CalloutService {\n\n @future(callout=true)\n public static void sendDataToExternalSystem(Set\u003cId> recordIds) {\n // Cannot pass complex objects, only primitives\n List\u003cAccount> accounts = [SELECT Id, Name FROM Account WHERE Id IN :recordIds];\n\n HttpRequest req = new HttpRequest();\n req.setEndpoint('callout:MyNamedCredential/api');\n req.setMethod('POST');\n req.setBody(JSON.serialize(accounts));\n\n Http http = new Http();\n HttpResponse res = http.send(req);\n System.debug('Response: ' + res.getBody());\n }\n}\n```\n\n---\n\n## Service Layer Patterns\n\n### Service Class Structure\n\n```apex\npublic with sharing class AccountService {\n\n // Public interface methods\n public static List\u003cAccount> createAccounts(List\u003cAccountRequest> requests) {\n validateRequests(requests);\n\n List\u003cAccount> accounts = buildAccounts(requests);\n insert accounts;\n\n // Post-processing\n handlePostCreation(accounts);\n\n return accounts;\n }\n\n // Private helper methods\n private static void validateRequests(List\u003cAccountRequest> requests) {\n for (AccountRequest req : requests) {\n if (String.isBlank(req.name)) {\n throw new IllegalArgumentException('Account name is required');\n }\n }\n }\n\n private static List\u003cAccount> buildAccounts(List\u003cAccountRequest> requests) {\n List\u003cAccount> accounts = new List\u003cAccount>();\n for (AccountRequest req : requests) {\n accounts.add(new Account(\n Name = req.name,\n Industry = req.industry\n ));\n }\n return accounts;\n }\n\n private static void handlePostCreation(List\u003cAccount> accounts) {\n // Create related records, send notifications, etc.\n }\n\n // Inner class for structured requests\n public class AccountRequest {\n public String name;\n public String industry;\n }\n}\n```\n\n### Selector Pattern (Data Access Layer)\n\n```apex\npublic inherited sharing class AccountSelector {\n\n public static List\u003cAccount> selectById(Set\u003cId> accountIds) {\n return [\n SELECT Id, Name, Industry, AnnualRevenue, Type\n FROM Account\n WHERE Id IN :accountIds\n WITH USER_MODE\n ];\n }\n\n public static List\u003cAccount> selectByIndustry(String industry) {\n return [\n SELECT Id, Name, Industry, AnnualRevenue\n FROM Account\n WHERE Industry = :industry\n WITH USER_MODE\n LIMIT 200\n ];\n }\n\n public static Map\u003cId, Account> selectByIdWithContacts(Set\u003cId> accountIds) {\n return new Map\u003cId, Account>([\n SELECT Id, Name,\n (SELECT Id, Name, Email FROM Contacts)\n FROM Account\n WHERE Id IN :accountIds\n WITH USER_MODE\n ]);\n }\n}\n```\n\n**Benefits**:\n- Centralized SOQL queries\n- Reusable across multiple classes\n- Easier to test (mock Selector)\n- Consistent field selection\n\n---\n\n## Reference\n\n**Full Documentation**: See `references/` folder for comprehensive guides:\n- `trigger-actions-framework.md` - TAF setup and advanced patterns\n- `design-patterns.md` - 12 Apex design patterns\n- `flow-integration.md` - Complete @InvocableMethod guide\n- `triangle-pattern.md` - Flow-LWC-Apex integration\n\n**Back to Main**: [SKILL.md](../SKILL.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":24293,"content_sha256":"2124fcb9a50ec5d15310033523ff4a174eac5f6e19e62ce846a1432875610135"},{"filename":"references/security-guide.md","content":"\u003c!-- Parent: sf-apex/SKILL.md -->\n# Apex Security Guide\n\nComprehensive guide to Apex security including CRUD/FLS enforcement, sharing rules, SOQL injection prevention, and generation guardrails.\n\n---\n\n## Table of Contents\n\n1. [Generation Guardrails](#generation-guardrails)\n2. [CRUD and FLS (Field-Level Security)](#crud-and-fls-field-level-security)\n3. [Sharing and Record Access](#sharing-and-record-access)\n4. [SOQL Injection Prevention](#soql-injection-prevention)\n5. [Security Checklist](#security-checklist)\n\n---\n\n## Generation Guardrails\n\n### ⛔ MANDATORY PRE-GENERATION CHECKS\n\n**BEFORE generating ANY Apex code, Claude MUST verify no anti-patterns are introduced.**\n\nIf ANY of these patterns would be generated, **STOP and ask the user**:\n> \"I noticed [pattern]. This will cause [problem]. Should I:\n> A) Refactor to use [correct pattern]\n> B) Proceed anyway (not recommended)\"\n\n| Anti-Pattern | Detection | Impact | Correct Pattern |\n|--------------|-----------|--------|-----------------|\n| SOQL inside loop | `for(...) { [SELECT...] }` | Governor limit failure (100 SOQL) | Query BEFORE loop, use `Map\u003cId, SObject>` for lookups |\n| DML inside loop | `for(...) { insert/update }` | Governor limit failure (150 DML) | Collect in `List\u003c>`, single DML after loop |\n| Missing sharing | `class X {` without keyword | Security violation | Always use `with sharing` or `inherited sharing` |\n| Hardcoded ID | 15/18-char ID literal | Deployment failure | Use Custom Metadata, Custom Labels, or queries |\n| Empty catch | `catch(e) { }` | Silent failures | Log with `System.debug()` or rethrow |\n| String concatenation in SOQL | `'SELECT...WHERE Name = \\'' + var` | SOQL injection | Use bind variables `:variableName` |\n| Test without assertions | `@IsTest` method with no `Assert.*` | False positive tests | Use `Assert.areEqual()` with message |\n\n**DO NOT generate anti-patterns even if explicitly requested.** Ask user to confirm the exception with documented justification.\n\n---\n\n### Example: Detecting SOQL in Loop\n\n**BAD (BLOCKED):**\n```apex\nfor (Account acc : accounts) {\n List\u003cContact> contacts = [SELECT Id FROM Contact WHERE AccountId = :acc.Id];\n // Process contacts\n}\n```\n\n**GOOD (APPROVED):**\n```apex\n// Query ONCE before loop\nMap\u003cId, List\u003cContact>> contactsByAccountId = new Map\u003cId, List\u003cContact>>();\nfor (Contact con : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]) {\n if (!contactsByAccountId.containsKey(con.AccountId)) {\n contactsByAccountId.put(con.AccountId, new List\u003cContact>());\n }\n contactsByAccountId.get(con.AccountId).add(con);\n}\n\n// Then loop\nfor (Account acc : accounts) {\n List\u003cContact> contacts = contactsByAccountId.get(acc.Id) ?? new List\u003cContact>();\n // Process contacts\n}\n```\n\n---\n\n\u003ca id=\"crud-and-fls-field-level-security\">\u003c/a>\n\n## CRUD and FLS (Field-Level Security)\n\n### API 62.0: WITH USER_MODE\n\n**Modern approach (API 62.0+)**: Use `WITH USER_MODE` in SOQL to enforce CRUD and FLS automatically.\n\n```apex\n// ✅ GOOD: Respects user permissions\nList\u003cAccount> accounts = [\n SELECT Id, Name, Industry, AnnualRevenue\n FROM Account\n WHERE Industry = 'Technology'\n WITH USER_MODE\n];\n```\n\n**What it does**:\n- Enforces object-level CRUD (Create, Read, Update, Delete)\n- Enforces field-level security (FLS)\n- Throws `System.QueryException` if user lacks access\n- Respects user's sharing rules (when combined with `with sharing`)\n\n**When to use SYSTEM_MODE**:\n```apex\n// Only use SYSTEM_MODE when you explicitly NEED to bypass security\nList\u003cAccount> accounts = [\n SELECT Id, Name, Sensitive_Field__c\n FROM Account\n WITH SYSTEM_MODE // ⚠️ Use with caution!\n];\n```\n\n**Use cases for SYSTEM_MODE**:\n- Background jobs that must process all records regardless of user\n- System integrations\n- Administrative cleanup scripts\n\n**ALWAYS document why SYSTEM_MODE is needed:**\n```apex\n// JUSTIFICATION: This batch job processes all accounts for regulatory reporting,\n// regardless of user's access level. Approved by Security Team on 2025-01-01.\n```\n\n---\n\n### Legacy Approach: Security.stripInaccessible()\n\n**For pre-62.0 compatibility** or when you need to filter fields dynamically:\n\n```apex\n// Query all fields\nList\u003cAccount> accounts = [SELECT Id, Name, Industry, AnnualRevenue FROM Account];\n\n// Strip inaccessible fields\nSObjectAccessDecision decision = Security.stripInaccessible(\n AccessType.READABLE,\n accounts\n);\n\n// Use stripped records\nList\u003cAccount> accessibleAccounts = decision.getRecords();\n\n// Check which fields were removed\nSet\u003cString> removedFields = decision.getRemovedFields().get('Account');\nif (removedFields != null && !removedFields.isEmpty()) {\n System.debug('User lacks access to fields: ' + removedFields);\n}\n```\n\n**Access Types**:\n- `READABLE` - Read access (for queries)\n- `CREATABLE` - Create access (before insert)\n- `UPDATABLE` - Update access (before update)\n- `UPSERTABLE` - Upsert access\n\n**Example: Pre-DML Check**\n```apex\npublic static void createAccounts(List\u003cAccount> accounts) {\n // Check if user can create these fields\n SObjectAccessDecision decision = Security.stripInaccessible(\n AccessType.CREATABLE,\n accounts\n );\n\n if (!decision.getRemovedFields().isEmpty()) {\n throw new SecurityException('User lacks permission to create some fields');\n }\n\n insert decision.getRecords();\n}\n```\n\n---\n\n### Manual CRUD/FLS Checks (Verbose but Explicit)\n\n```apex\n// Check object-level CRUD\nif (!Schema.sObjectType.Account.isAccessible()) {\n throw new SecurityException('User cannot read Accounts');\n}\n\nif (!Schema.sObjectType.Account.isCreateable()) {\n throw new SecurityException('User cannot create Accounts');\n}\n\n// Check field-level security\nif (!Schema.sObjectType.Account.fields.Industry.isAccessible()) {\n throw new SecurityException('User cannot read Industry field');\n}\n\nif (!Schema.sObjectType.Account.fields.Industry.isUpdateable()) {\n throw new SecurityException('User cannot update Industry field');\n}\n```\n\n**When to use**: Legacy codebases, specific error messaging, or when you need fine-grained control.\n\n---\n\n## Sharing and Record Access\n\n### Sharing Keywords\n\n| Keyword | Behavior | When to Use |\n|---------|----------|-------------|\n| `with sharing` | Enforces record-level sharing | Default for user-facing code |\n| `without sharing` | Bypasses record-level sharing | System operations, integrations |\n| `inherited sharing` | Inherits from calling class | Utility classes, shared libraries |\n\n**Default Rule**: If no keyword specified, class runs in `without sharing` mode (pre-API 40 behavior).\n\n**ALWAYS specify a sharing keyword** - implicit behavior is confusing.\n\n---\n\n### with sharing (Recommended Default)\n\n```apex\npublic with sharing class AccountService {\n\n public static List\u003cAccount> getAccountsForUser() {\n // User only sees Accounts they have access to via sharing rules\n return [SELECT Id, Name FROM Account WITH USER_MODE];\n }\n\n public static void updateAccount(Account acc) {\n // Throws exception if user lacks access\n update acc;\n }\n}\n```\n\n**Use cases**:\n- User-facing controllers (LWC, Aura, Visualforce)\n- Service classes handling user requests\n- Trigger actions that respect user context\n\n---\n\n### without sharing (Use Sparingly)\n\n```apex\npublic without sharing class AdminService {\n\n // JUSTIFICATION: This method is only called by system administrators\n // to perform global updates. Access controlled by Custom Permission.\n public static void globalAccountUpdate() {\n List\u003cAccount> allAccounts = [SELECT Id, Name FROM Account];\n // Process ALL accounts, ignoring sharing\n }\n}\n```\n\n**Use cases**:\n- Background jobs\n- System integrations\n- Administrative operations\n\n**Security Note**: Always add access control checks when using `without sharing`:\n```apex\npublic without sharing class AdminService {\n\n public static void globalUpdate() {\n // Check permission before executing\n if (!FeatureManagement.checkPermission('Admin_Global_Update')) {\n throw new SecurityException('Requires Admin_Global_Update permission');\n }\n\n // Now safe to proceed with without sharing logic\n }\n}\n```\n\n---\n\n### inherited sharing (Best for Utilities)\n\n```apex\npublic inherited sharing class StringUtils {\n\n // Inherits sharing from calling class\n public static String sanitize(String input) {\n return String.escapeSingleQuotes(input);\n }\n}\n\n// Called from \"with sharing\" class → runs with sharing\n// Called from \"without sharing\" class → runs without sharing\n```\n\n**Use cases**:\n- Utility classes\n- Helper methods\n- Shared libraries that don't directly query records\n\n---\n\n### Mixing Sharing Contexts\n\n```apex\npublic with sharing class UserFacingService {\n\n public static void processAccount(Id accountId) {\n // This runs WITH sharing\n Account acc = [SELECT Id, Name FROM Account WHERE Id = :accountId];\n\n // Call a without sharing method for specific operation\n SystemOperations.performGlobalCheck(acc);\n }\n}\n\npublic without sharing class SystemOperations {\n\n public static void performGlobalCheck(Account acc) {\n // This runs WITHOUT sharing\n // Can access records the original user couldn't see\n }\n}\n```\n\n**Pattern**: Start with `with sharing`, only escalate to `without sharing` when needed.\n\n---\n\n## SOQL Injection Prevention\n\n### The Problem\n\n**NEVER concatenate user input into SOQL strings:**\n\n```apex\n// ❌ VULNERABLE to SOQL injection\npublic static List\u003cAccount> searchAccounts(String userInput) {\n String query = 'SELECT Id, Name FROM Account WHERE Name = \\'' + userInput + '\\'';\n return Database.query(query);\n}\n\n// Attack: userInput = \"test' OR '1'='1\"\n// Results in: SELECT Id, Name FROM Account WHERE Name = 'test' OR '1'='1'\n// Returns ALL accounts!\n```\n\n---\n\n### Solution 1: Bind Variables (Recommended)\n\n```apex\n// ✅ SAFE: Use bind variables\npublic static List\u003cAccount> searchAccounts(String userInput) {\n return [SELECT Id, Name FROM Account WHERE Name = :userInput WITH USER_MODE];\n}\n\n// Even with malicious input, it's treated as a literal string\n```\n\n**Why it works**: Salesforce treats `:userInput` as a value, not executable SOQL.\n\n---\n\n### Solution 2: String.escapeSingleQuotes()\n\n**When dynamic SOQL is unavoidable** (rare cases):\n\n```apex\n// ✅ SAFE: Escape user input\npublic static List\u003cAccount> dynamicSearch(String userInput) {\n String sanitized = String.escapeSingleQuotes(userInput);\n String query = 'SELECT Id, Name FROM Account WHERE Name = \\'' + sanitized + '\\'';\n return Database.query(query);\n}\n```\n\n**What it does**: Escapes single quotes (`'` → `\\'`) to prevent breaking out of string literals.\n\n**Still prefer bind variables** - escapeSingleQuotes is a backup.\n\n---\n\n### Solution 3: Allowlist Validation\n\n**For field names, operators, or other dynamic query parts:**\n\n```apex\npublic static List\u003cAccount> sortedAccounts(String sortField) {\n // ✅ SAFE: Validate against allowlist\n Set\u003cString> allowedFields = new Set\u003cString>{'Name', 'Industry', 'AnnualRevenue'};\n\n if (!allowedFields.contains(sortField)) {\n throw new IllegalArgumentException('Invalid sort field');\n }\n\n String query = 'SELECT Id, Name, Industry FROM Account ORDER BY ' + sortField;\n return Database.query(query);\n}\n```\n\n**Use case**: Dynamic ORDER BY, dynamic field selection (but NOT WHERE clause values).\n\n---\n\n### Dynamic SOQL Best Practices\n\n**Pattern: Safe Dynamic Query Builder**\n```apex\npublic class SafeQueryBuilder {\n\n private static final Set\u003cString> ALLOWED_FIELDS = new Set\u003cString>{\n 'Id', 'Name', 'Industry', 'AnnualRevenue'\n };\n\n private static final Set\u003cString> ALLOWED_OPERATORS = new Set\u003cString>{\n '=', '!=', '\u003c', '>', '\u003c=', '>=', 'LIKE', 'IN'\n };\n\n public static List\u003cAccount> query(\n String field,\n String operator,\n String value\n ) {\n // Validate field\n if (!ALLOWED_FIELDS.contains(field)) {\n throw new IllegalArgumentException('Invalid field: ' + field);\n }\n\n // Validate operator\n if (!ALLOWED_OPERATORS.contains(operator)) {\n throw new IllegalArgumentException('Invalid operator: ' + operator);\n }\n\n // Use bind variable for value\n String query = 'SELECT Id, Name FROM Account WHERE ' + field + ' ' + operator + ' :value WITH USER_MODE';\n return Database.query(query);\n }\n}\n```\n\n---\n\n## Security Checklist\n\nUse this checklist when generating or reviewing Apex code:\n\n### CRUD/FLS\n\n- [ ] All SOQL queries use `WITH USER_MODE` (or `Security.stripInaccessible()` for pre-62.0)\n- [ ] DML operations check `isCreateable()`, `isUpdateable()`, `isDeletable()` OR use `WITH USER_MODE`\n- [ ] Custom fields have Permission Sets/Profiles granting FLS\n- [ ] System operations using `WITH SYSTEM_MODE` are documented with justification\n\n### Sharing\n\n- [ ] All classes have explicit sharing keyword (`with sharing`, `without sharing`, or `inherited sharing`)\n- [ ] User-facing classes use `with sharing`\n- [ ] `without sharing` classes have documented justification\n- [ ] `without sharing` classes include Custom Permission checks\n\n### SOQL Injection\n\n- [ ] No string concatenation in WHERE clauses with user input\n- [ ] All user input uses bind variables (`:variableName`)\n- [ ] Dynamic SOQL uses allowlist validation for field names/operators\n- [ ] `String.escapeSingleQuotes()` used if concatenation is unavoidable\n\n### General Security\n\n- [ ] No hardcoded credentials or API keys (use Named Credentials)\n- [ ] No hardcoded Record IDs (use Custom Metadata or queries)\n- [ ] Sensitive data (SSN, PII) is encrypted at rest (Platform Encryption)\n- [ ] External callouts use Named Credentials, not plain URLs\n- [ ] Error messages don't leak sensitive information\n\n---\n\n## Advanced Security Patterns\n\n### Custom Permissions\n\n**Check user has specific permission before dangerous operations:**\n\n```apex\npublic without sharing class DataDeletionService {\n\n public static void deleteAllTestData() {\n // Check Custom Permission\n if (!FeatureManagement.checkPermission('Delete_Test_Data')) {\n throw new SecurityException('Requires Delete_Test_Data permission');\n }\n\n // Safe to proceed\n delete [SELECT Id FROM Account WHERE Name LIKE 'TEST%'];\n }\n}\n```\n\n**Create Custom Permission**: Setup → Custom Permissions → New\n\n---\n\n### Secure Remote Actions (@AuraEnabled)\n\n```apex\npublic with sharing class AccountController {\n\n @AuraEnabled(cacheable=true)\n public static List\u003cAccount> getAccounts() {\n // Runs with sharing + user mode = secure\n return [SELECT Id, Name FROM Account WITH USER_MODE LIMIT 50];\n }\n\n @AuraEnabled\n public static void updateAccount(Account acc) {\n // Verify user can update\n if (!Schema.sObjectType.Account.isUpdateable()) {\n throw new AuraHandledException('No update permission');\n }\n\n update acc;\n }\n}\n```\n\n**Security Notes**:\n- Always use `with sharing` for `@AuraEnabled` methods\n- Use `WITH USER_MODE` in SOQL\n- Validate DML permissions before operations\n- Use `AuraHandledException` for friendly error messages\n\n---\n\n### Platform Events Security\n\n```apex\npublic with sharing class EventPublisher {\n\n public static void publishEvent(String message) {\n // Check if user can create Platform Events\n if (!Schema.sObjectType.MyEvent__e.isCreateable()) {\n throw new SecurityException('Cannot publish events');\n }\n\n MyEvent__e event = new MyEvent__e(\n Message__c = message\n );\n\n EventBus.publish(event);\n }\n}\n```\n\n---\n\n## Common Security Vulnerabilities\n\n| Vulnerability | Example | Fix |\n|---------------|---------|-----|\n| **SOQL Injection** | `'WHERE Name = \\'' + input + '\\''` | Use bind variable `:input` |\n| **XSS (Cross-Site Scripting)** | Returning unsanitized HTML | Use `HTMLENCODE()` in VF or LWC escaping |\n| **Insecure Direct Object Reference** | Accepting record ID from user without checking access | Query with `WITH USER_MODE`, verify in `with sharing` |\n| **Hardcoded Credentials** | `String apiKey = 'abc123';` | Use Named Credentials |\n| **Missing FLS** | Directly querying fields without checking | Use `WITH USER_MODE` |\n| **Overly Permissive Sharing** | `without sharing` everywhere | Use `with sharing` by default |\n\n---\n\n## Testing Security\n\n### Test User Mode\n\n```apex\n@IsTest\nstatic void testUserModeEnforcesPermissions() {\n // Create user without Account read access\n User restrictedUser = createRestrictedUser();\n\n System.runAs(restrictedUser) {\n try {\n List\u003cAccount> accounts = [SELECT Id FROM Account WITH USER_MODE];\n Assert.fail('Expected QueryException for user without access');\n } catch (System.QueryException e) {\n Assert.isTrue(e.getMessage().contains('Insufficient privileges'));\n }\n }\n}\n```\n\n### Test Sharing Rules\n\n```apex\n@IsTest\nstatic void testSharingEnforcement() {\n Account acc = new Account(Name = 'Test Account', OwnerId = UserInfo.getUserId());\n insert acc;\n\n // Create user who is NOT the owner\n User otherUser = createStandardUser();\n\n System.runAs(otherUser) {\n // Should NOT see account owned by different user\n List\u003cAccount> visible = [SELECT Id FROM Account WHERE Id = :acc.Id];\n Assert.areEqual(0, visible.size(), 'User should not see account due to sharing rules');\n }\n}\n```\n\n---\n\n## Reference\n\n**Full Documentation**: See `references/` folder for comprehensive guides:\n- `security-guide.md` - Complete security reference (this is an extract)\n- `best-practices.md` - Includes security best practices\n- `code-review-checklist.md` - Security scoring criteria\n\n**Back to Main**: [SKILL.md](../SKILL.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":17866,"content_sha256":"477475f6c6101b50d417196dcbcd207f44e39646cf08d3802f4b91fe16c91ab7"},{"filename":"references/security-quick-reference.md","content":"\u003c!-- Parent: sf-apex/SKILL.md -->\n# Apex Security Guide\n\n## Sharing Modes\n\n### with sharing (Default)\n\nUse for most classes. Enforces record-level security (OWD, sharing rules).\n\n```apex\npublic with sharing class AccountService {\n public List\u003cAccount> getAccounts() {\n // Only returns records user has access to\n return [SELECT Id, Name FROM Account];\n }\n}\n```\n\n### inherited sharing\n\nUse for utility/helper classes. Inherits sharing mode from caller.\n\n```apex\npublic inherited sharing class QueryHelper {\n public static List\u003cSObject> query(String soql) {\n // Sharing determined by calling class\n return Database.query(soql);\n }\n}\n```\n\n### without sharing\n\nUse sparingly. Only for specific system operations that require full access.\n\n```apex\npublic without sharing class SystemService {\n // DOCUMENT WHY this needs without sharing\n public static void updateSystemRecords(List\u003cRecord__c> records) {\n // Updates regardless of sharing rules\n update records;\n }\n}\n```\n\n**Rule**: Keep `without sharing` classes small and isolated. Never expose directly to users.\n\n---\n\n## CRUD/FLS Enforcement\n\n### USER_MODE (Recommended)\n\n```apex\n// SOQL with USER_MODE - enforces CRUD and FLS\nList\u003cAccount> accounts = [\n SELECT Id, Name, AnnualRevenue\n FROM Account\n WITH USER_MODE\n];\n\n// Database methods with AccessLevel\nDatabase.insert(records, AccessLevel.USER_MODE);\nDatabase.update(records, AccessLevel.USER_MODE);\nDatabase.delete(records, AccessLevel.USER_MODE);\n\n// Database.query with USER_MODE\nString query = 'SELECT Id FROM Account WHERE Name = :name';\nList\u003cAccount> accounts = Database.query(query, AccessLevel.USER_MODE);\n```\n\n### Security.stripInaccessible()\n\nRemove inaccessible fields before DML or returning to user:\n\n```apex\n// Strip inaccessible fields before insert\nList\u003cAccount> accounts = getAccountsFromExternalSource();\nSObjectAccessDecision decision = Security.stripInaccessible(\n AccessType.CREATABLE,\n accounts\n);\ninsert decision.getRecords();\n\n// Strip before returning to user\nList\u003cAccount> accounts = [SELECT Id, Name, Secret_Field__c FROM Account];\nSObjectAccessDecision decision = Security.stripInaccessible(\n AccessType.READABLE,\n accounts\n);\nreturn decision.getRecords(); // Secret_Field__c removed if not accessible\n\n// Check which fields were removed\nSet\u003cString> removedFields = decision.getRemovedFields().get('Account');\n```\n\n### Manual Checks (Legacy)\n\n```apex\n// Object-level\nif (!Schema.sObjectType.Account.isCreateable()) {\n throw new SecurityException('No create access to Account');\n}\n\n// Field-level\nif (!Schema.sObjectType.Account.fields.Name.isUpdateable()) {\n throw new SecurityException('No update access to Account.Name');\n}\n```\n\n---\n\n## SOQL Injection Prevention\n\n### Use Bind Variables (Preferred)\n\n```apex\n// SAFE: Bind variable\nString name = userInput;\nList\u003cAccount> accounts = [SELECT Id FROM Account WHERE Name = :name];\n\n// SAFE: Dynamic query with bind\nString query = 'SELECT Id FROM Account WHERE Name = :name';\nList\u003cAccount> accounts = Database.query(query);\n```\n\n### Escape User Input (If Dynamic)\n\n```apex\n// If you must build dynamic SOQL\nString safeName = String.escapeSingleQuotes(userInput);\nString query = 'SELECT Id FROM Account WHERE Name = \\'' + safeName + '\\'';\n```\n\n### Never Do This\n\n```apex\n// VULNERABLE: Direct concatenation\nString query = 'SELECT Id FROM Account WHERE Name = \\'' + userInput + '\\'';\n// Attacker input: ' OR Name != '\n// Results in: SELECT Id FROM Account WHERE Name = '' OR Name != ''\n```\n\n---\n\n## Named Credentials\n\n### Never Hardcode Credentials\n\n```apex\n// BAD: Hardcoded credentials\nHttp http = new Http();\nHttpRequest req = new HttpRequest();\nreq.setEndpoint('https://api.example.com');\nreq.setHeader('Authorization', 'Bearer sk_live_abc123'); // EXPOSED!\n\n// GOOD: Named Credential\nHttpRequest req = new HttpRequest();\nreq.setEndpoint('callout:MyNamedCredential/api/resource');\n// Authorization handled automatically\n```\n\n### Setting Up Named Credentials\n\n1. Setup → Named Credentials\n2. Configure authentication (OAuth, Password, etc.)\n3. Reference in code: `callout:CredentialName/path`\n\n---\n\n## Custom Settings for Bypass Flags\n\nEnable admins to disable automation without code changes:\n\n```apex\npublic class TriggerConfig {\n private static Trigger_Settings__c settings;\n\n public static Boolean isDisabled(String triggerName) {\n if (settings == null) {\n settings = Trigger_Settings__c.getInstance();\n }\n\n return settings?.Disable_All_Triggers__c == true ||\n (Boolean)settings.get('Disable_' + triggerName + '__c') == true;\n }\n}\n\n// Usage in trigger action\nif (TriggerConfig.isDisabled('Account')) {\n return;\n}\n```\n\n---\n\n## Secure Apex for LWC/Aura\n\n### AuraHandledException\n\n```apex\n@AuraEnabled\npublic static Account getAccount(Id accountId) {\n try {\n // Use USER_MODE for security\n return [SELECT Id, Name FROM Account WHERE Id = :accountId WITH USER_MODE];\n } catch (QueryException e) {\n throw new AuraHandledException('Account not found');\n } catch (Exception e) {\n // Log for debugging\n System.debug(LoggingLevel.ERROR, e.getMessage());\n // Return user-friendly message\n throw new AuraHandledException('An error occurred. Please contact support.');\n }\n}\n```\n\n### Cacheable Methods\n\n```apex\n@AuraEnabled(cacheable=true)\npublic static List\u003cAccount> getAccounts() {\n // Cacheable methods cannot have DML\n // Must be idempotent\n return [SELECT Id, Name FROM Account WITH USER_MODE LIMIT 100];\n}\n```\n\n---\n\n## Permission Checks\n\n### Custom Permissions\n\n```apex\npublic static Boolean hasCustomPermission(String permissionName) {\n return FeatureManagement.checkPermission(permissionName);\n}\n\n// Usage\nif (!hasCustomPermission('Access_Sensitive_Data')) {\n throw new SecurityException('Insufficient permissions');\n}\n```\n\n### Profile/Permission Set Checks\n\n```apex\n// Check if user has specific permission\npublic static Boolean canModifyAllData() {\n return [\n SELECT PermissionsModifyAllData\n FROM Profile\n WHERE Id = :UserInfo.getProfileId()\n ].PermissionsModifyAllData;\n}\n```\n\n---\n\n## Security Review Checklist\n\n| Check | Status |\n|-------|--------|\n| Uses `with sharing` by default | ☐ |\n| `without sharing` justified and documented | ☐ |\n| SOQL uses USER_MODE or manual CRUD/FLS checks | ☐ |\n| No SOQL injection vulnerabilities | ☐ |\n| No hardcoded credentials | ☐ |\n| Sensitive data not exposed in debug logs | ☐ |\n| AuraEnabled methods have proper error handling | ☐ |\n| Custom permissions used for sensitive operations | ☐ |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6681,"content_sha256":"3e11cae4fe8efcb6ffc80846bde4cd204447b2d52b38658f4197c1aecbbc81a6"},{"filename":"references/solid-principles.md","content":"\u003c!-- Parent: sf-apex/SKILL.md -->\n# SOLID Principles in Apex\n\n## Overview\n\nSOLID principles guide object-oriented design for maintainable, flexible code.\n\n| Principle | Summary |\n|-----------|---------|\n| **S**ingle Responsibility | One reason to change |\n| **O**pen/Closed | Open for extension, closed for modification |\n| **L**iskov Substitution | Subtypes must be substitutable |\n| **I**nterface Segregation | Small, specific interfaces |\n| **D**ependency Inversion | Depend on abstractions |\n\n---\n\n## S - Single Responsibility Principle\n\n> \"A module should have one, and only one, reason to change.\"\n\n### Problem: Multiple Responsibilities\n\n```apex\n// BAD: Class has multiple reasons to change\npublic class OrderProcessor {\n public void processOrder(Order__c order) {\n // Validate order (reason 1: validation rules change)\n if (order.Total__c \u003c= 0) {\n throw new ValidationException('Invalid total');\n }\n\n // Calculate tax (reason 2: tax rules change)\n Decimal tax = order.Total__c * 0.08;\n order.Tax__c = tax;\n\n // Send email (reason 3: notification requirements change)\n Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();\n email.setToAddresses(new List\u003cString>{order.Customer_Email__c});\n Messaging.sendEmail(new List\u003cMessaging.Email>{email});\n\n // Save to database (reason 4: persistence logic changes)\n update order;\n }\n}\n```\n\n### Solution: Separate Responsibilities\n\n```apex\n// GOOD: Each class has single responsibility\npublic class OrderValidator {\n public void validate(Order__c order) {\n if (order.Total__c \u003c= 0) {\n throw new ValidationException('Invalid total');\n }\n }\n}\n\npublic class TaxCalculator {\n public Decimal calculate(Decimal amount) {\n return amount * 0.08;\n }\n}\n\npublic class OrderNotificationService {\n public void sendConfirmation(Order__c order) {\n // Email logic\n }\n}\n\npublic class OrderService {\n private OrderValidator validator;\n private TaxCalculator taxCalc;\n private OrderNotificationService notifier;\n\n public void processOrder(Order__c order) {\n validator.validate(order);\n order.Tax__c = taxCalc.calculate(order.Total__c);\n update order;\n notifier.sendConfirmation(order);\n }\n}\n```\n\n---\n\n## O - Open/Closed Principle\n\n> \"Software entities should be open for extension, but closed for modification.\"\n\n### Problem: Modifying Existing Code\n\n```apex\n// BAD: Must modify class to add new discount type\npublic class DiscountCalculator {\n public Decimal calculate(String discountType, Decimal amount) {\n if (discountType == 'PERCENTAGE') {\n return amount * 0.1;\n } else if (discountType == 'FIXED') {\n return 50;\n } else if (discountType == 'VIP') { // Added later\n return amount * 0.2;\n }\n // Keep adding else-if for each new type...\n return 0;\n }\n}\n```\n\n### Solution: Extend Without Modifying\n\n```apex\n// GOOD: Add new discount types without changing existing code\npublic interface DiscountStrategy {\n Decimal calculate(Decimal amount);\n}\n\npublic class PercentageDiscount implements DiscountStrategy {\n private Decimal rate;\n\n public PercentageDiscount(Decimal rate) {\n this.rate = rate;\n }\n\n public Decimal calculate(Decimal amount) {\n return amount * rate;\n }\n}\n\npublic class FixedDiscount implements DiscountStrategy {\n private Decimal fixedAmount;\n\n public FixedDiscount(Decimal fixedAmount) {\n this.fixedAmount = fixedAmount;\n }\n\n public Decimal calculate(Decimal amount) {\n return fixedAmount;\n }\n}\n\n// To add VIP discount: create new class, no modification needed\npublic class VIPDiscount implements DiscountStrategy {\n public Decimal calculate(Decimal amount) {\n return amount * 0.2;\n }\n}\n\npublic class DiscountCalculator {\n private Map\u003cString, DiscountStrategy> strategies;\n\n public Decimal calculate(String type, Decimal amount) {\n DiscountStrategy strategy = strategies.get(type);\n return strategy?.calculate(amount) ?? 0;\n }\n}\n```\n\n### Real-World Example: Trigger Actions Framework\n\nTAF follows OCP - add new behaviors via metadata configuration without modifying the trigger or handler.\n\n---\n\n## L - Liskov Substitution Principle\n\n> \"Subtypes must be substitutable for their base types.\"\n\n### Problem: Subtype Breaks Contract\n\n```apex\n// BAD: Lead violates SObject update contract when converted\npublic class RecordUpdater {\n public void updateRecord(SObject record) {\n // This fails for converted Leads!\n if (record instanceof Lead) {\n Lead l = (Lead)record;\n if ([SELECT IsConverted FROM Lead WHERE Id = :l.Id].IsConverted) {\n return; // Can't update converted lead\n }\n }\n update record;\n }\n}\n```\n\n### Solution: Design for Substitutability\n\n```apex\n// GOOD: Interface defines clear contract\npublic interface Updatable {\n Boolean canUpdate();\n void performUpdate();\n}\n\npublic class AccountUpdater implements Updatable {\n private Account record;\n\n public Boolean canUpdate() {\n return true; // Accounts can always be updated\n }\n\n public void performUpdate() {\n update record;\n }\n}\n\npublic class LeadUpdater implements Updatable {\n private Lead record;\n\n public Boolean canUpdate() {\n return ![SELECT IsConverted FROM Lead WHERE Id = :record.Id].IsConverted;\n }\n\n public void performUpdate() {\n if (canUpdate()) {\n update record;\n }\n }\n}\n\n// Consumer doesn't need type checking\npublic class RecordService {\n public void updateRecord(Updatable record) {\n if (record.canUpdate()) {\n record.performUpdate();\n }\n }\n}\n```\n\n---\n\n## I - Interface Segregation Principle\n\n> \"Clients should not be forced to depend on interfaces they don't use.\"\n\n### Problem: Fat Interface\n\n```apex\n// BAD: Interface forces unnecessary implementations\npublic interface RecordProcessor {\n void validate(SObject record);\n void calculate(SObject record);\n void sendNotification(SObject record);\n void createAuditLog(SObject record);\n void syncToExternal(SObject record);\n}\n\n// Simple processor forced to implement everything\npublic class SimpleProcessor implements RecordProcessor {\n public void validate(SObject record) { /* actual logic */ }\n public void calculate(SObject record) { /* actual logic */ }\n\n // Forced to implement these even though not needed\n public void sendNotification(SObject record) { }\n public void createAuditLog(SObject record) { }\n public void syncToExternal(SObject record) { }\n}\n```\n\n### Solution: Small, Focused Interfaces\n\n```apex\n// GOOD: Segregated interfaces\npublic interface Validatable {\n void validate(SObject record);\n}\n\npublic interface Calculable {\n void calculate(SObject record);\n}\n\npublic interface Notifiable {\n void sendNotification(SObject record);\n}\n\npublic interface Auditable {\n void createAuditLog(SObject record);\n}\n\n// Implement only what you need\npublic class SimpleProcessor implements Validatable, Calculable {\n public void validate(SObject record) { /* logic */ }\n public void calculate(SObject record) { /* logic */ }\n}\n\npublic class FullProcessor implements Validatable, Calculable, Notifiable, Auditable {\n public void validate(SObject record) { /* logic */ }\n public void calculate(SObject record) { /* logic */ }\n public void sendNotification(SObject record) { /* logic */ }\n public void createAuditLog(SObject record) { /* logic */ }\n}\n```\n\n### Salesforce Example: Database.Batchable Options\n\n```apex\n// Implement only what you need\npublic class SimpleBatch implements Database.Batchable\u003cSObject> {\n // Just the required interface\n}\n\npublic class StatefulBatch implements Database.Batchable\u003cSObject>, Database.Stateful {\n // Add stateful when needed\n}\n\npublic class CalloutBatch implements Database.Batchable\u003cSObject>, Database.AllowsCallouts {\n // Add callouts when needed\n}\n```\n\n---\n\n## D - Dependency Inversion Principle\n\n> \"High-level modules should not depend on low-level modules. Both should depend on abstractions.\"\n\n### Problem: Direct Dependencies\n\n```apex\n// BAD: High-level class depends on concrete implementation\npublic class OrderService {\n private EmailService emailService; // Concrete class\n private StripePaymentGateway gateway; // Concrete class\n\n public OrderService() {\n this.emailService = new EmailService();\n this.gateway = new StripePaymentGateway();\n }\n\n public void processOrder(Order__c order) {\n gateway.charge(order.Total__c);\n emailService.send(order.Customer_Email__c);\n }\n}\n```\n\n### Solution: Depend on Abstractions\n\n```apex\n// GOOD: Depend on interfaces, inject implementations\npublic interface PaymentGateway {\n PaymentResult charge(Decimal amount);\n}\n\npublic interface NotificationService {\n void send(String recipient, String message);\n}\n\npublic class StripeGateway implements PaymentGateway {\n public PaymentResult charge(Decimal amount) {\n // Stripe-specific logic\n }\n}\n\npublic class EmailNotification implements NotificationService {\n public void send(String recipient, String message) {\n // Email-specific logic\n }\n}\n\n// High-level class depends on abstractions\npublic class OrderService {\n private PaymentGateway gateway;\n private NotificationService notifier;\n\n // Constructor injection\n public OrderService(PaymentGateway gateway, NotificationService notifier) {\n this.gateway = gateway;\n this.notifier = notifier;\n }\n\n public void processOrder(Order__c order) {\n gateway.charge(order.Total__c);\n notifier.send(order.Customer_Email__c, 'Order confirmed');\n }\n}\n\n// Easy to test with mocks\n@isTest\nstatic void testOrderService() {\n PaymentGateway mockGateway = new MockPaymentGateway();\n NotificationService mockNotifier = new MockNotificationService();\n\n OrderService service = new OrderService(mockGateway, mockNotifier);\n // Test without real payment or email\n}\n```\n\n---\n\n## Summary\n\n| Principle | Violation Sign | Solution |\n|-----------|---------------|----------|\n| SRP | Class has multiple reasons to change | Split into focused classes |\n| OCP | Adding features requires modifying existing code | Use strategy pattern, interfaces |\n| LSP | Type checking before using base type | Redesign hierarchy, use composition |\n| ISP | Empty method implementations | Split into smaller interfaces |\n| DIP | Creating concrete dependencies in constructor | Inject dependencies via constructor |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":10728,"content_sha256":"5d04edb02c22a9cf8eb596f9423d9a4918942e62345673089c8ef890cd46ab73"},{"filename":"references/testing-guide.md","content":"\u003c!-- Parent: sf-apex/SKILL.md -->\n# Apex Testing Guide\n\n## Testing Fundamentals\n\n### Coverage Requirements\n- **Minimum**: 75% for deployment\n- **Recommended**: 90%+ for quality\n- **Best Practice**: Maintain buffer above 75%\n\n### Test Class Structure\n\n```apex\n@isTest\nprivate class AccountServiceTest {\n\n @TestSetup\n static void setup() {\n // Create test data once, available to all test methods\n TestDataFactory.createAccounts(10);\n }\n\n @isTest\n static void testPositiveScenario() {\n // Arrange\n Account acc = [SELECT Id FROM Account LIMIT 1];\n\n // Act\n Test.startTest();\n String result = AccountService.processAccount(acc.Id);\n Test.stopTest();\n\n // Assert\n Assert.areEqual('Success', result, 'Should return success');\n }\n\n @isTest\n static void testNegativeScenario() {\n // Test error handling\n Test.startTest();\n try {\n AccountService.processAccount(null);\n Assert.fail('Should have thrown exception');\n } catch (IllegalArgumentException e) {\n Assert.isTrue(e.getMessage().contains('null'), 'Should mention null');\n }\n Test.stopTest();\n }\n\n @isTest\n static void testBulkScenario() {\n // Test with 251+ records (spans two trigger batches)\n List\u003cAccount> accounts = TestDataFactory.createAccounts(251);\n\n Test.startTest();\n List\u003cString> results = AccountService.processAccounts(accounts);\n Test.stopTest();\n\n Assert.areEqual(251, results.size(), 'Should process all records');\n }\n}\n```\n\n---\n\n## Spring '26 Test Annotations\n\n### `@isTest(testFor=ClassName.class)` — Test-to-Source Linking\n\nExplicitly links a test class to the production class it covers. Improves coverage attribution and supports `RunRelevantTests` deployment mode.\n\n```apex\n@isTest(testFor=AccountService.class)\nprivate class AccountServiceTest {\n // All test methods here count toward AccountService coverage\n}\n```\n\n### `@isTest(isCritical=true)` — Always-Run Tests\n\nMarks tests that must always execute, even when using `RunRelevantTests` test level. Use for smoke tests, critical business logic, and integration test entry points.\n\n```apex\n@isTest(isCritical=true)\nprivate class PaymentProcessorTest {\n // Runs even in RunRelevantTests mode\n}\n```\n\nCan combine with `testFor`:\n```apex\n@isTest(testFor=PaymentProcessor.class, isCritical=true)\nprivate class PaymentProcessorTest { }\n```\n\n---\n\n## Assert Class (Winter '23+)\n\n### Preferred Assert Methods\n\n```apex\n// Equality\nAssert.areEqual(expected, actual, 'Optional message');\nAssert.areNotEqual(unexpected, actual);\n\n// Boolean\nAssert.isTrue(condition, 'Should be true');\nAssert.isFalse(condition, 'Should be false');\n\n// Null\nAssert.isNull(value, 'Should be null');\nAssert.isNotNull(value, 'Should not be null');\n\n// Instance type\nAssert.isInstanceOfType(obj, Account.class, 'Should be Account');\n\n// Explicit failure\nAssert.fail('This should not be reached');\n```\n\n### Testing Exceptions\n\n```apex\n@isTest\nstatic void testExceptionThrown() {\n Test.startTest();\n try {\n MyService.riskyOperation();\n Assert.fail('Expected MyCustomException');\n } catch (MyCustomException e) {\n Assert.isTrue(e.getMessage().contains('expected text'));\n }\n Test.stopTest();\n}\n```\n\n---\n\n## Test Data Factory Pattern\n\n### Factory Class\n\n```apex\n@isTest\npublic class TestDataFactory {\n\n public static List\u003cAccount> createAccounts(Integer count) {\n return createAccounts(count, true);\n }\n\n public static List\u003cAccount> createAccounts(Integer count, Boolean doInsert) {\n List\u003cAccount> accounts = new List\u003cAccount>();\n for (Integer i = 0; i \u003c count; i++) {\n accounts.add(new Account(\n Name = 'Test Account ' + i,\n Industry = 'Technology',\n BillingCity = 'San Francisco'\n ));\n }\n if (doInsert) {\n insert accounts;\n }\n return accounts;\n }\n\n public static List\u003cContact> createContacts(Integer count, Id accountId) {\n List\u003cContact> contacts = new List\u003cContact>();\n for (Integer i = 0; i \u003c count; i++) {\n contacts.add(new Contact(\n FirstName = 'Test',\n LastName = 'Contact ' + i,\n Email = 'test' + i + '@example.com',\n AccountId = accountId\n ));\n }\n insert contacts;\n return contacts;\n }\n\n public static User createUser(String profileName) {\n Profile p = [SELECT Id FROM Profile WHERE Name = :profileName LIMIT 1];\n String uniqueKey = String.valueOf(DateTime.now().getTime());\n\n User u = new User(\n Alias = 'test' + uniqueKey.right(4),\n Email = 'test' + uniqueKey + '@example.com',\n EmailEncodingKey = 'UTF-8',\n LastName = 'Test',\n LanguageLocaleKey = 'en_US',\n LocaleSidKey = 'en_US',\n ProfileId = p.Id,\n TimeZoneSidKey = 'America/Los_Angeles',\n Username = 'test' + uniqueKey + '@example.com.test'\n );\n insert u;\n return u;\n }\n}\n```\n\n### Builder Pattern (Complex Objects)\n\n```apex\n@isTest\npublic class AccountBuilder {\n private Account record;\n\n public AccountBuilder() {\n this.record = new Account(\n Name = 'Default Account',\n Industry = 'Other'\n );\n }\n\n public AccountBuilder withName(String name) {\n this.record.Name = name;\n return this;\n }\n\n public AccountBuilder withIndustry(String industry) {\n this.record.Industry = industry;\n return this;\n }\n\n public AccountBuilder withBillingAddress(String city, String state) {\n this.record.BillingCity = city;\n this.record.BillingState = state;\n return this;\n }\n\n public Account build() {\n return this.record;\n }\n\n public Account buildAndInsert() {\n insert this.record;\n return this.record;\n }\n}\n\n// Usage\nAccount acc = new AccountBuilder()\n .withName('Acme Corp')\n .withIndustry('Technology')\n .withBillingAddress('San Francisco', 'CA')\n .buildAndInsert();\n```\n\n---\n\n## @TestSetup\n\n### Benefits\n- Runs once per test class\n- Data available to all test methods\n- Rolled back after each test method\n\n```apex\n@TestSetup\nstatic void setup() {\n // Create accounts\n List\u003cAccount> accounts = TestDataFactory.createAccounts(5);\n\n // Create contacts for first account\n TestDataFactory.createContacts(10, accounts[0].Id);\n\n // Create custom settings\n insert new MySettings__c(SetupOwnerId = UserInfo.getOrganizationId());\n}\n```\n\n### Accessing TestSetup Data\n\n```apex\n@isTest\nstatic void testMethod1() {\n // Query the data created in @TestSetup\n List\u003cAccount> accounts = [SELECT Id, Name FROM Account];\n Assert.areEqual(5, accounts.size());\n}\n```\n\n---\n\n## Testing Async Code\n\n### Test.startTest() / Test.stopTest()\n\n```apex\n@isTest\nstatic void testQueueable() {\n Account acc = TestDataFactory.createAccounts(1)[0];\n\n Test.startTest();\n System.enqueueJob(new MyQueueable(acc.Id));\n Test.stopTest(); // Forces async to complete\n\n // Assert results\n Account updated = [SELECT Status__c FROM Account WHERE Id = :acc.Id];\n Assert.areEqual('Processed', updated.Status__c);\n}\n\n@isTest\nstatic void testFuture() {\n Test.startTest();\n MyService.asyncMethod(); // @future method\n Test.stopTest();\n\n // Assert results after async completes\n}\n\n@isTest\nstatic void testBatch() {\n Test.startTest();\n Database.executeBatch(new MyBatch(), 200);\n Test.stopTest();\n\n // Assert batch results\n}\n```\n\n---\n\n## Mocking HTTP Callouts\n\n### HttpCalloutMock Implementation\n\n```apex\n@isTest\npublic class MockHttpResponse implements HttpCalloutMock {\n private Integer statusCode;\n private String body;\n\n public MockHttpResponse(Integer statusCode, String body) {\n this.statusCode = statusCode;\n this.body = body;\n }\n\n public HTTPResponse respond(HTTPRequest req) {\n HttpResponse res = new HttpResponse();\n res.setStatusCode(this.statusCode);\n res.setBody(this.body);\n return res;\n }\n}\n\n// Usage\n@isTest\nstatic void testCallout() {\n Test.setMock(HttpCalloutMock.class, new MockHttpResponse(200, '{\"success\": true}'));\n\n Test.startTest();\n String result = MyCalloutService.makeCallout();\n Test.stopTest();\n\n Assert.areEqual('success', result);\n}\n```\n\n### Multi-Response Mock\n\n```apex\npublic class MultiMockHttpResponse implements HttpCalloutMock {\n private Map\u003cString, HttpResponse> responses = new Map\u003cString, HttpResponse>();\n\n public void addResponse(String endpoint, Integer statusCode, String body) {\n HttpResponse res = new HttpResponse();\n res.setStatusCode(statusCode);\n res.setBody(body);\n responses.put(endpoint, res);\n }\n\n public HTTPResponse respond(HTTPRequest req) {\n String endpoint = req.getEndpoint();\n if (responses.containsKey(endpoint)) {\n return responses.get(endpoint);\n }\n throw new CalloutException('No mock for: ' + endpoint);\n }\n}\n```\n\n---\n\n## Dependency Injection for Testing\n\n### Factory Pattern\n\n```apex\n// Production code\npublic virtual class Factory {\n private static Factory instance;\n\n public static Factory getInstance() {\n if (instance == null) {\n instance = new Factory();\n }\n return instance;\n }\n\n @TestVisible\n private static void setInstance(Factory mockFactory) {\n instance = mockFactory;\n }\n\n public virtual AccountService getAccountService() {\n return new AccountService();\n }\n}\n\n// Test\n@isTest\nstatic void testWithMock() {\n Factory.setInstance(new MockFactory());\n\n Test.startTest();\n // Code uses MockFactory.getAccountService() which returns mock\n Test.stopTest();\n}\n\nprivate class MockFactory extends Factory {\n public override AccountService getAccountService() {\n return new MockAccountService();\n }\n}\n```\n\n---\n\n## Testing with Different Users\n\n### System.runAs()\n\n```apex\n@isTest\nstatic void testAsStandardUser() {\n User standardUser = TestDataFactory.createUser('Standard User');\n\n System.runAs(standardUser) {\n Test.startTest();\n // Code executes as standardUser\n List\u003cAccount> accounts = AccountService.getAccounts();\n Test.stopTest();\n\n // User only sees records they have access to\n Assert.areEqual(0, accounts.size());\n }\n}\n\n@isTest\nstatic void testAsAdmin() {\n User adminUser = TestDataFactory.createUser('System Administrator');\n\n System.runAs(adminUser) {\n // Test admin-specific functionality\n }\n}\n```\n\n---\n\n## Testing Private Methods\n\n### @TestVisible Annotation\n\n```apex\npublic class MyService {\n @TestVisible\n private static String privateMethod(String input) {\n return input.toUpperCase();\n }\n}\n\n// Test can now access private method\n@isTest\nstatic void testPrivateMethod() {\n String result = MyService.privateMethod('test');\n Assert.areEqual('TEST', result);\n}\n```\n\n---\n\n## Test Checklist\n\n| Scenario | Required |\n|----------|----------|\n| Positive test (happy path) | ✓ |\n| Negative test (error handling) | ✓ |\n| Bulk test (251+ records) | ✓ |\n| Single record test | ✓ |\n| Null/empty input | ✓ |\n| Boundary conditions | ✓ |\n| Different user profiles | ✓ |\n| Assert statements in every test | ✓ |\n| Test.startTest()/stopTest() for async | ✓ |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":11570,"content_sha256":"f8e9c6f384734ad8a6aa624df727048763f04757fc69d69490aa2e20338864d1"},{"filename":"references/testing-patterns.md","content":"\u003c!-- Parent: sf-apex/SKILL.md -->\n# Apex Testing Patterns\n\nComprehensive guide to Apex testing including exception types, test patterns, mocking, and achieving 90%+ code coverage.\n\n---\n\n## Table of Contents\n\n1. [Testing Fundamentals](#testing-fundamentals)\n2. [Common Exception Types](#common-exception-types)\n3. [Test Patterns](#test-patterns)\n4. [Test Data Factory](#test-data-factory)\n5. [Mocking and Stubs](#mocking-and-stubs)\n6. [Bulk Testing](#bulk-testing)\n7. [Code Coverage Strategies](#code-coverage-strategies)\n\n---\n\n## Testing Fundamentals\n\n### Test Class Structure\n\n```apex\n@IsTest\nprivate class AccountServiceTest {\n\n @TestSetup\n static void setup() {\n // Runs ONCE before all test methods\n // Create shared test data\n TestDataFactory.createAccounts(10);\n }\n\n @IsTest\n static void testPositiveCase() {\n // Arrange: Set up test data\n Account acc = new Account(Name = 'Test', Industry = 'Technology');\n\n // Act: Execute code under test\n Test.startTest();\n insert acc;\n Test.stopTest();\n\n // Assert: Verify results\n Account inserted = [SELECT Id, Industry FROM Account WHERE Id = :acc.Id];\n Assert.areEqual('Technology', inserted.Industry, 'Industry should be set');\n }\n\n @IsTest\n static void testNegativeCase() {\n // Test error conditions\n try {\n insert new Account(); // Missing required Name\n Assert.fail('Expected DmlException was not thrown');\n } catch (DmlException e) {\n Assert.isTrue(e.getMessage().contains('REQUIRED_FIELD_MISSING'));\n }\n }\n\n @IsTest\n static void testBulkCase() {\n // Test with 251+ records\n List\u003cAccount> accounts = new List\u003cAccount>();\n for (Integer i = 0; i \u003c 251; i++) {\n accounts.add(new Account(Name = 'Bulk ' + i));\n }\n\n Test.startTest();\n insert accounts;\n Test.stopTest();\n\n Assert.areEqual(251, [SELECT COUNT() FROM Account WHERE Name LIKE 'Bulk%']);\n }\n}\n```\n\n---\n\n### @TestSetup vs Test Methods\n\n| Feature | @TestSetup | Test Methods |\n|---------|------------|--------------|\n| **Runs** | Once before all tests | Once per test |\n| **Data Isolation** | Shared across tests (read-only view) | Isolated per test |\n| **Performance** | Faster (reuses data) | Slower (recreates each time) |\n| **When to Use** | Common baseline data | Test-specific scenarios |\n\n**Example**:\n```apex\n@TestSetup\nstatic void setup() {\n // Common data for all tests\n insert new Account(Name = 'Shared Account');\n}\n\n@IsTest\nstatic void test1() {\n // Can query the Shared Account\n Account acc = [SELECT Id FROM Account WHERE Name = 'Shared Account'];\n acc.Industry = 'Tech';\n update acc; // Only visible in this test\n}\n\n@IsTest\nstatic void test2() {\n // Shared Account still has Industry = null (data rollback between tests)\n Account acc = [SELECT Id, Industry FROM Account WHERE Name = 'Shared Account'];\n Assert.isNull(acc.Industry);\n}\n```\n\n---\n\n### Test.startTest() and Test.stopTest()\n\n**Purpose**: Reset governor limits and execute async code.\n\n```apex\n@IsTest\nstatic void testAsyncOperation() {\n Account acc = new Account(Name = 'Test');\n insert acc;\n\n // Limits reset here\n Test.startTest();\n\n // Enqueue async job\n System.enqueueJob(new AccountProcessor(acc.Id));\n\n // Async job executes synchronously here\n Test.stopTest();\n\n // Verify async results\n Account updated = [SELECT Id, Description FROM Account WHERE Id = :acc.Id];\n Assert.isNotNull(updated.Description);\n}\n```\n\n**Key Points**:\n- Governor limits reset at `Test.startTest()`\n- Async code (@future, Queueable, Batch) executes at `Test.stopTest()`\n- Only one `startTest/stopTest` block per test method\n\n---\n\n## Common Exception Types\n\nWhen writing test classes, use these specific exception types to validate error handling.\n\n### Exception Type Reference\n\n| Exception Type | When to Use | Example |\n|----------------|-------------|---------|\n| `DmlException` | Insert/update/delete failures | `Assert.isTrue(e.getMessage().contains('FIELD_CUSTOM_VALIDATION'))` |\n| `QueryException` | SOQL query failures | Malformed query, no rows for assignment |\n| `NullPointerException` | Null reference access | Accessing field on null object |\n| `ListException` | List operation failures | Index out of bounds |\n| `MathException` | Mathematical errors | Division by zero |\n| `TypeException` | Type conversion failures | Invalid type casting |\n| `LimitException` | Governor limit exceeded | Too many SOQL queries, DML statements |\n| `CalloutException` | HTTP callout failures | Timeout, invalid endpoint |\n| `JSONException` | JSON parsing failures | Malformed JSON |\n| `InvalidParameterValueException` | Invalid method parameters | Bad input values |\n\n---\n\n### Testing DmlException\n\n```apex\n@IsTest\nstatic void testRequiredFieldMissing() {\n try {\n insert new Account(); // Missing Name\n Assert.fail('Expected DmlException was not thrown');\n } catch (DmlException e) {\n Assert.isTrue(\n e.getMessage().contains('REQUIRED_FIELD_MISSING'),\n 'Expected REQUIRED_FIELD_MISSING but got: ' + e.getMessage()\n );\n }\n}\n\n@IsTest\nstatic void testDuplicateValue() {\n Account acc1 = new Account(Name = 'Test', Unique_Field__c = 'ABC123');\n insert acc1;\n\n try {\n Account acc2 = new Account(Name = 'Test2', Unique_Field__c = 'ABC123');\n insert acc2; // Violates unique constraint\n Assert.fail('Expected DmlException');\n } catch (DmlException e) {\n Assert.isTrue(e.getMessage().contains('DUPLICATE_VALUE'));\n }\n}\n\n@IsTest\nstatic void testCustomValidationRule() {\n Account acc = new Account(Name = 'Test', AnnualRevenue = -100);\n\n try {\n insert acc; // Validation rule: Revenue must be > 0\n Assert.fail('Expected DmlException');\n } catch (DmlException e) {\n Assert.isTrue(e.getMessage().contains('FIELD_CUSTOM_VALIDATION_EXCEPTION'));\n }\n}\n```\n\n---\n\n### Testing QueryException\n\n```apex\n@IsTest\nstatic void testNoRowsForAssignment() {\n try {\n // Query expects exactly 1 row but finds 0\n Account acc = [SELECT Id FROM Account WHERE Name = 'Nonexistent'];\n Assert.fail('Expected QueryException');\n } catch (QueryException e) {\n Assert.isTrue(e.getMessage().contains('List has no rows for assignment'));\n }\n}\n\n@IsTest\nstatic void testTooManyRows() {\n TestDataFactory.createAccounts(5); // All with same Name\n\n try {\n // Query expects 1 row but finds 5\n Account acc = [SELECT Id FROM Account WHERE Name LIKE 'Test%'];\n Assert.fail('Expected QueryException');\n } catch (QueryException e) {\n Assert.isTrue(e.getMessage().contains('List has more than 1 row'));\n }\n}\n```\n\n---\n\n### Testing NullPointerException\n\n```apex\n@IsTest\nstatic void testNullReferenceAccess() {\n Account acc = null;\n\n try {\n String name = acc.Name; // NullPointerException\n Assert.fail('Expected NullPointerException');\n } catch (NullPointerException e) {\n Assert.isNotNull(e.getMessage());\n }\n}\n\n@IsTest\nstatic void testSafeNavigationOperator() {\n Account acc = null;\n\n // No exception - returns null\n String name = acc?.Name;\n Assert.isNull(name, 'Safe navigation should return null');\n}\n```\n\n---\n\n### Testing LimitException\n\n```apex\n@IsTest\nstatic void testSoqlLimitExceeded() {\n // Artificially trigger SOQL limit (for demonstration - don't do this in real code!)\n try {\n for (Integer i = 0; i \u003c 101; i++) {\n List\u003cAccount> accounts = [SELECT Id FROM Account LIMIT 1];\n }\n Assert.fail('Expected LimitException');\n } catch (System.LimitException e) {\n Assert.isTrue(e.getMessage().contains('Too many SOQL queries'));\n }\n}\n```\n\n**Note**: In real tests, you should NEVER hit limits - tests should validate limit-safe code.\n\n---\n\n### Testing CalloutException\n\n```apex\n@IsTest\nstatic void testCalloutTimeout() {\n // Set mock callout\n Test.setMock(HttpCalloutMock.class, new TimeoutMock());\n\n Test.startTest();\n try {\n CalloutService.sendData('test');\n Assert.fail('Expected CalloutException');\n } catch (CalloutException e) {\n Assert.isTrue(e.getMessage().contains('Read timed out'));\n }\n Test.stopTest();\n}\n\n// Mock class\nprivate class TimeoutMock implements HttpCalloutMock {\n public HttpResponse respond(HttpRequest req) {\n throw new CalloutException('Read timed out');\n }\n}\n```\n\n---\n\n## Test Patterns\n\n### Pattern 1: Positive, Negative, Bulk (PNB)\n\n**Every feature needs 3 tests:**\n\n```apex\n// 1. POSITIVE: Happy path\n@IsTest\nstatic void testCreateAccountSuccess() {\n Account acc = new Account(Name = 'Test', Industry = 'Tech');\n\n Test.startTest();\n insert acc;\n Test.stopTest();\n\n Account inserted = [SELECT Id, Industry FROM Account WHERE Id = :acc.Id];\n Assert.areEqual('Tech', inserted.Industry);\n}\n\n// 2. NEGATIVE: Error handling\n@IsTest\nstatic void testCreateAccountMissingName() {\n try {\n insert new Account();\n Assert.fail('Expected exception');\n } catch (DmlException e) {\n Assert.isTrue(e.getMessage().contains('REQUIRED_FIELD_MISSING'));\n }\n}\n\n// 3. BULK: 251+ records\n@IsTest\nstatic void testCreateAccountsBulk() {\n List\u003cAccount> accounts = new List\u003cAccount>();\n for (Integer i = 0; i \u003c 251; i++) {\n accounts.add(new Account(Name = 'Bulk ' + i));\n }\n\n Test.startTest();\n insert accounts;\n Test.stopTest();\n\n Assert.areEqual(251, [SELECT COUNT() FROM Account]);\n}\n```\n\n---\n\n### Pattern 2: System.runAs() for Permission Testing\n\n```apex\n@IsTest\nstatic void testUserCannotAccessRestrictedField() {\n // Create user without field permission\n User restrictedUser = TestDataFactory.createStandardUser();\n\n Account acc = new Account(Name = 'Test', Restricted_Field__c = 'Secret');\n insert acc;\n\n System.runAs(restrictedUser) {\n try {\n List\u003cAccount> accounts = [\n SELECT Id, Restricted_Field__c\n FROM Account\n WHERE Id = :acc.Id\n WITH USER_MODE\n ];\n Assert.fail('Expected QueryException due to FLS');\n } catch (QueryException e) {\n Assert.isTrue(e.getMessage().contains('Insufficient privileges'));\n }\n }\n}\n```\n\n---\n\n### Pattern 3: Database Methods for Partial Success\n\n```apex\n@IsTest\nstatic void testPartialInsertSuccess() {\n List\u003cAccount> accounts = new List\u003cAccount>{\n new Account(Name = 'Valid Account'),\n new Account(), // Invalid - missing Name\n new Account(Name = 'Another Valid')\n };\n\n Test.startTest();\n Database.SaveResult[] results = Database.insert(accounts, false); // allOrNone = false\n Test.stopTest();\n\n // Verify results\n Integer successCount = 0;\n Integer failureCount = 0;\n\n for (Database.SaveResult result : results) {\n if (result.isSuccess()) {\n successCount++;\n } else {\n failureCount++;\n for (Database.Error err : result.getErrors()) {\n Assert.areEqual(StatusCode.REQUIRED_FIELD_MISSING, err.getStatusCode());\n }\n }\n }\n\n Assert.areEqual(2, successCount, 'Two accounts should succeed');\n Assert.areEqual(1, failureCount, 'One account should fail');\n}\n```\n\n---\n\n### Pattern 4: Testing Async Code\n\n**Testing @future:**\n```apex\n@IsTest\nstatic void testFutureMethod() {\n Account acc = new Account(Name = 'Test');\n insert acc;\n\n Test.startTest();\n AccountService.updateAsync(acc.Id); // @future method\n Test.stopTest(); // Future executes here\n\n Account updated = [SELECT Id, Description FROM Account WHERE Id = :acc.Id];\n Assert.areEqual('Updated by future', updated.Description);\n}\n```\n\n**Testing Queueable:**\n```apex\n@IsTest\nstatic void testQueueable() {\n Account acc = new Account(Name = 'Test');\n insert acc;\n\n Test.startTest();\n System.enqueueJob(new AccountProcessor(acc.Id));\n Test.stopTest(); // Queueable executes here\n\n Account updated = [SELECT Id, Description FROM Account WHERE Id = :acc.Id];\n Assert.areEqual('Processed', updated.Description);\n}\n```\n\n**Testing Batch:**\n```apex\n@IsTest\nstatic void testBatch() {\n TestDataFactory.createAccounts(200);\n\n Test.startTest();\n Database.executeBatch(new AccountBatchProcessor(), 100);\n Test.stopTest(); // Batch executes here\n\n Integer processed = [SELECT COUNT() FROM Account WHERE Description = 'Processed'];\n Assert.areEqual(200, processed);\n}\n```\n\n**Testing Schedulable:**\n```apex\n@IsTest\nstatic void testSchedulable() {\n String cronExp = '0 0 0 * * ?'; // Daily at midnight\n\n Test.startTest();\n String jobId = System.schedule('Test Job', cronExp, new AccountScheduler());\n Test.stopTest();\n\n // Verify job was scheduled\n CronTrigger ct = [SELECT Id, CronExpression FROM CronTrigger WHERE Id = :jobId];\n Assert.areEqual(cronExp, ct.CronExpression);\n}\n```\n\n---\n\n## Test Data Factory\n\n### Basic Factory Pattern\n\n```apex\n@IsTest\npublic class TestDataFactory {\n\n public static List\u003cAccount> createAccounts(Integer count) {\n return createAccounts(count, true);\n }\n\n public static List\u003cAccount> createAccounts(Integer count, Boolean doInsert) {\n List\u003cAccount> accounts = new List\u003cAccount>();\n\n for (Integer i = 0; i \u003c count; i++) {\n accounts.add(new Account(\n Name = 'Test Account ' + i,\n Industry = 'Technology',\n AnnualRevenue = 1000000\n ));\n }\n\n if (doInsert) {\n insert accounts;\n }\n\n return accounts;\n }\n\n public static List\u003cContact> createContacts(Integer count, Id accountId) {\n return createContacts(count, accountId, true);\n }\n\n public static List\u003cContact> createContacts(Integer count, Id accountId, Boolean doInsert) {\n List\u003cContact> contacts = new List\u003cContact>();\n\n for (Integer i = 0; i \u003c count; i++) {\n contacts.add(new Contact(\n FirstName = 'Test',\n LastName = 'Contact ' + i,\n AccountId = accountId,\n Email = 'test' + i + '@example.com'\n ));\n }\n\n if (doInsert) {\n insert contacts;\n }\n\n return contacts;\n }\n\n public static User createStandardUser() {\n Profile standardProfile = [SELECT Id FROM Profile WHERE Name = 'Standard User' LIMIT 1];\n\n User u = new User(\n FirstName = 'Test',\n LastName = 'User',\n Email = '[email protected]',\n Username = 'testuser' + System.currentTimeMillis() + '@example.com',\n Alias = 'tuser',\n TimeZoneSidKey = 'America/Los_Angeles',\n LocaleSidKey = 'en_US',\n EmailEncodingKey = 'UTF-8',\n LanguageLocaleKey = 'en_US',\n ProfileId = standardProfile.Id\n );\n\n insert u;\n return u;\n }\n}\n```\n\n**Usage:**\n```apex\n@TestSetup\nstatic void setup() {\n // Create 251 accounts\n TestDataFactory.createAccounts(251);\n\n // Create account with 10 contacts\n Account acc = TestDataFactory.createAccounts(1)[0];\n TestDataFactory.createContacts(10, acc.Id);\n}\n```\n\n---\n\n### Advanced Factory with Builder Pattern\n\n```apex\n@IsTest\npublic class AccountBuilder {\n\n private Account record;\n\n public AccountBuilder() {\n record = new Account(\n Name = 'Default Account',\n Industry = 'Technology'\n );\n }\n\n public AccountBuilder withName(String name) {\n record.Name = name;\n return this;\n }\n\n public AccountBuilder withIndustry(String industry) {\n record.Industry = industry;\n return this;\n }\n\n public AccountBuilder withRevenue(Decimal revenue) {\n record.AnnualRevenue = revenue;\n return this;\n }\n\n public Account build() {\n return record;\n }\n\n public Account buildAndInsert() {\n insert record;\n return record;\n }\n}\n```\n\n**Usage:**\n```apex\n@IsTest\nstatic void testWithBuilder() {\n Account acc = new AccountBuilder()\n .withName('Custom Account')\n .withIndustry('Finance')\n .withRevenue(5000000)\n .buildAndInsert();\n\n Assert.areEqual('Finance', acc.Industry);\n}\n```\n\n---\n\n## Mocking and Stubs\n\n### HTTP Callout Mocking\n\n```apex\n@IsTest\npublic class ExternalServiceMock implements HttpCalloutMock {\n\n public HttpResponse respond(HttpRequest req) {\n // Verify request\n Assert.areEqual('POST', req.getMethod());\n Assert.areEqual('https://api.example.com/accounts', req.getEndpoint());\n\n // Create mock response\n HttpResponse res = new HttpResponse();\n res.setHeader('Content-Type', 'application/json');\n res.setBody('{\"status\": \"success\", \"id\": \"12345\"}');\n res.setStatusCode(200);\n\n return res;\n }\n}\n```\n\n**Test:**\n```apex\n@IsTest\nstatic void testExternalCallout() {\n Test.setMock(HttpCalloutMock.class, new ExternalServiceMock());\n\n Test.startTest();\n String result = CalloutService.sendData('test data');\n Test.stopTest();\n\n Assert.areEqual('12345', result);\n}\n```\n\n---\n\n### Multi-Request Callout Mock\n\n```apex\n@IsTest\npublic class MultiCalloutMock implements HttpCalloutMock {\n\n public HttpResponse respond(HttpRequest req) {\n HttpResponse res = new HttpResponse();\n res.setHeader('Content-Type', 'application/json');\n\n // Different responses based on endpoint\n if (req.getEndpoint().endsWith('/accounts')) {\n res.setBody('{\"accounts\": [{\"id\": \"1\"}]}');\n res.setStatusCode(200);\n } else if (req.getEndpoint().endsWith('/contacts')) {\n res.setBody('{\"contacts\": [{\"id\": \"2\"}]}');\n res.setStatusCode(200);\n } else {\n res.setStatusCode(404);\n }\n\n return res;\n }\n}\n```\n\n---\n\n### Stub API (Test Doubles)\n\n```apex\n@IsTest\npublic class AccountSelectorStub implements IAccountSelector {\n\n private List\u003cAccount> stubbedAccounts;\n\n public AccountSelectorStub(List\u003cAccount> accounts) {\n this.stubbedAccounts = accounts;\n }\n\n public List\u003cAccount> selectById(Set\u003cId> accountIds) {\n // Return stubbed data instead of querying\n return stubbedAccounts;\n }\n}\n```\n\n**Test with stub:**\n```apex\n@IsTest\nstatic void testWithStub() {\n // Create stub data (no DML needed!)\n List\u003cAccount> stubbedAccounts = new List\u003cAccount>{\n new Account(Id = TestUtility.getFakeId(Account.SObjectType), Name = 'Stub Account')\n };\n\n IAccountSelector selector = new AccountSelectorStub(stubbedAccounts);\n\n // Inject stub into service\n AccountService service = new AccountService(selector);\n\n Test.startTest();\n List\u003cAccount> results = service.getAccounts();\n Test.stopTest();\n\n Assert.areEqual(1, results.size());\n Assert.areEqual('Stub Account', results[0].Name);\n}\n```\n\n---\n\n## Bulk Testing\n\n### The 251 Record Test\n\n```apex\n@IsTest\nstatic void testBulkTriggerExecution() {\n List\u003cAccount> accounts = new List\u003cAccount>();\n\n for (Integer i = 0; i \u003c 251; i++) {\n accounts.add(new Account(\n Name = 'Bulk Test ' + i,\n Industry = 'Technology'\n ));\n }\n\n Test.startTest();\n\n insert accounts;\n\n Test.stopTest();\n\n // Verify trigger logic executed for all 251\n List\u003cAccount> inserted = [SELECT Id, Description FROM Account WHERE Name LIKE 'Bulk Test%'];\n Assert.areEqual(251, inserted.size());\n\n for (Account acc : inserted) {\n Assert.isNotNull(acc.Description, 'Description should be set by trigger');\n }\n}\n```\n\n---\n\n### Testing Governor Limits\n\n```apex\n@IsTest\nstatic void testDoesNotHitSoqlLimit() {\n TestDataFactory.createAccounts(251);\n\n Integer queriesBefore = Limits.getQueries();\n\n Test.startTest();\n AccountService.processAllAccounts();\n Test.stopTest();\n\n Integer queriesAfter = Limits.getQueries();\n Integer queriesUsed = queriesAfter - queriesBefore;\n\n Assert.isTrue(queriesUsed \u003c= 5, 'Should use no more than 5 SOQL queries, used: ' + queriesUsed);\n}\n```\n\n---\n\n## Code Coverage Strategies\n\n### Achieving 90%+ Coverage\n\n**1. Test all branches (if/else):**\n```apex\n// Code\npublic static String getStatus(Account acc) {\n if (acc.AnnualRevenue > 1000000) {\n return 'Enterprise';\n } else {\n return 'SMB';\n }\n}\n\n// Test BOTH branches\n@IsTest\nstatic void testEnterpriseStatus() {\n Account acc = new Account(Name = 'Test', AnnualRevenue = 2000000);\n Assert.areEqual('Enterprise', AccountService.getStatus(acc));\n}\n\n@IsTest\nstatic void testSmbStatus() {\n Account acc = new Account(Name = 'Test', AnnualRevenue = 500000);\n Assert.areEqual('SMB', AccountService.getStatus(acc));\n}\n```\n\n**2. Test all catch blocks:**\n```apex\n// Code\ntry {\n insert accounts;\n} catch (DmlException e) {\n // Handle DML error\n logError(e);\n}\n\n// Test\n@IsTest\nstatic void testCatchBlock() {\n List\u003cAccount> invalid = new List\u003cAccount>{ new Account() }; // Missing Name\n\n try {\n insert invalid;\n } catch (DmlException e) {\n // Catch block is now covered\n }\n}\n```\n\n**3. Test all methods:**\n```apex\n// Every public/global method needs at least one test\n@IsTest\nstatic void testEveryMethod() {\n AccountService.method1();\n AccountService.method2();\n AccountService.method3();\n // etc.\n}\n```\n\n---\n\n### Identifying Uncovered Code\n\n**Developer Console:**\n1. Open class\n2. Click Tests → New Run\n3. Select test class\n4. View coverage % and red highlights\n\n**VS Code:**\n1. Run tests: `Ctrl+Shift+P` → \"SFDX: Run Apex Tests\"\n2. View coverage in Problems panel\n\n**CLI:**\n```bash\nsf apex run test --code-coverage --result-format human --test-level RunLocalTests\n```\n\n---\n\n## Reference\n\n**Full Documentation**: See `references/` folder for comprehensive guides:\n- `testing-guide.md` - Complete testing reference\n- `best-practices.md` - Test best practices\n- `code-review-checklist.md` - Testing scoring criteria\n\n**Back to Main**: [SKILL.md](../SKILL.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":22182,"content_sha256":"271fdbd6e4259bd51ba0c09cbb49abf9cd290081f769cd23fb7cf75c27dbb2c9"},{"filename":"references/triangle-pattern.md","content":"\u003c!-- Parent: sf-apex/SKILL.md -->\n# Flow-LWC-Apex Triangle: Apex Perspective\n\nThe **Triangle Architecture** is a foundational Salesforce pattern where Flow, LWC, and Apex work together. This guide focuses on the **Apex role** in this architecture.\n\n---\n\n## Architecture Overview\n\n```\n ┌─────────────────────────────────────┐\n │ FLOW │\n │ (Orchestrator) │\n └───────────────┬─────────────────────┘\n │\n ┌──────────────────────────┼──────────────────────────┐\n │ │ │\n │ screens │ actionCalls │\n │ \u003ccomponentInstance> │ actionType=\"apex\" │\n │ │ │\n ▼ ▼ ▲\n┌─────────────────────────┐ ┌─────────────────────────┐ │\n│ LWC │ │ APEX │ │\n│ (UI Component) │───▶│ (Business Logic) │─────────┘\n│ │ │ │\n│ • Rich UI/UX │ │ • @InvocableMethod ◀── YOU ARE HERE\n│ • User Interaction │ │ • @AuraEnabled │\n│ │ │ • Complex Logic │\n│ │ │ • DML Operations │\n│ │ │ • Integration │\n└─────────────────────────┘ └─────────────────────────┘\n │ ▲\n │ @AuraEnabled │\n │ wire / imperative │\n └──────────────────────────┘\n```\n\n---\n\n## Apex's Role in the Triangle\n\n| Communication Path | Apex Annotation | Direction |\n|-------------------|-----------------|-----------|\n| Flow → Apex | `@InvocableMethod` | Request/Response |\n| Apex → Flow | `@InvocableVariable` | Return values |\n| LWC → Apex | `@AuraEnabled` | Async call |\n| Apex → LWC | Return value | Response |\n\n---\n\n## Pattern 1: @InvocableMethod for Flow\n\n**Use Case**: Complex business logic, DML, or external integrations called from Flow.\n\n```\n┌─────────┐ actionCalls ┌─────────┐\n│ FLOW │ ───────────────▶ │ APEX │\n│ Auto- │ List\u003cRequest> │Invocable│\n│ mation │ ◀─────────────── │ Method │\n│ │ List\u003cResponse> │ │\n└─────────┘ └─────────┘\n```\n\n### Apex Class Pattern\n\n```apex\npublic with sharing class RecordProcessor {\n\n @InvocableMethod(label='Process Record' category='Custom')\n public static List\u003cResponse> execute(List\u003cRequest> requests) {\n List\u003cResponse> responses = new List\u003cResponse>();\n\n for (Request req : requests) {\n Response res = new Response();\n try {\n // Business logic here\n res.isSuccess = true;\n res.processedId = req.recordId;\n } catch (Exception e) {\n res.isSuccess = false;\n res.errorMessage = e.getMessage();\n }\n responses.add(res);\n }\n return responses;\n }\n\n public class Request {\n @InvocableVariable(required=true)\n public Id recordId;\n }\n\n public class Response {\n @InvocableVariable\n public Boolean isSuccess;\n @InvocableVariable\n public Id processedId;\n @InvocableVariable\n public String errorMessage;\n }\n}\n```\n\n### Corresponding Flow XML\n\n```xml\n\u003cactionCalls>\n \u003cname>Process_Records\u003c/name>\n \u003cactionName>RecordProcessor\u003c/actionName>\n \u003cactionType>apex\u003c/actionType>\n \u003cinputParameters>\n \u003cname>recordId\u003c/name>\n \u003cvalue>\u003celementReference>var_RecordId\u003c/elementReference>\u003c/value>\n \u003c/inputParameters>\n \u003coutputParameters>\n \u003cassignToReference>var_IsSuccess\u003c/assignToReference>\n \u003cname>isSuccess\u003c/name>\n \u003c/outputParameters>\n \u003cfaultConnector>\n \u003ctargetReference>Handle_Error\u003c/targetReference>\n \u003c/faultConnector>\n\u003c/actionCalls>\n```\n\n---\n\n## Pattern 2: @AuraEnabled for LWC\n\n**Use Case**: LWC needs data or operations beyond Flow context.\n\n```\n┌─────────┐ @wire ┌─────────┐\n│ LWC │ ────────────────▶ │ APEX │\n│ │ imperative │@Aura │\n│ │ ◀──────────────── │Enabled │\n│ │ Promise/data │ │\n└─────────┘ └─────────┘\n```\n\n### Apex Controller\n\n```apex\npublic with sharing class RecordController {\n\n @AuraEnabled(cacheable=true)\n public static List\u003cRecord__c> getRecords(Id parentId) {\n return [\n SELECT Id, Name, Status__c\n FROM Record__c\n WHERE Parent__c = :parentId\n WITH USER_MODE\n ];\n }\n\n @AuraEnabled\n public static Map\u003cString, Object> processRecord(Id recordId) {\n // Process logic (DML operations)\n return new Map\u003cString, Object>{\n 'isSuccess' => true,\n 'recordId' => recordId\n };\n }\n}\n```\n\n### Key Differences\n\n| Annotation | Cacheable | Use For |\n|------------|-----------|---------|\n| `@AuraEnabled(cacheable=true)` | Yes | Read-only queries (SOQL) |\n| `@AuraEnabled` | No | DML operations, mutations |\n\n---\n\n## Testing @InvocableMethod\n\n```apex\n@isTest\nprivate class RecordProcessorTest {\n @isTest\n static void testProcessRecords() {\n Account acc = new Account(Name = 'Test');\n insert acc;\n\n RecordProcessor.Request req = new RecordProcessor.Request();\n req.recordId = acc.Id;\n\n Test.startTest();\n List\u003cRecordProcessor.Response> responses =\n RecordProcessor.execute(new List\u003cRecordProcessor.Request>{ req });\n Test.stopTest();\n\n System.assertEquals(true, responses[0].isSuccess);\n System.assertEquals(acc.Id, responses[0].processedId);\n }\n\n @isTest\n static void testBulkProcessing() {\n // Test with 200+ records for bulkification\n List\u003cAccount> accounts = new List\u003cAccount>();\n for (Integer i = 0; i \u003c 251; i++) {\n accounts.add(new Account(Name = 'Test ' + i));\n }\n insert accounts;\n\n List\u003cRecordProcessor.Request> requests = new List\u003cRecordProcessor.Request>();\n for (Account acc : accounts) {\n RecordProcessor.Request req = new RecordProcessor.Request();\n req.recordId = acc.Id;\n requests.add(req);\n }\n\n Test.startTest();\n List\u003cRecordProcessor.Response> responses = RecordProcessor.execute(requests);\n Test.stopTest();\n\n System.assertEquals(251, responses.size());\n }\n}\n```\n\n---\n\n## Deployment Order\n\nWhen deploying integrated triangle solutions:\n\n```\n1. APEX CLASSES ← Deploy FIRST\n └── @InvocableMethod classes\n └── @AuraEnabled controllers\n\n2. LWC COMPONENTS\n └── Depend on Apex controllers\n\n3. FLOWS\n └── Reference deployed Apex classes\n └── Reference deployed LWC components\n```\n\n---\n\n## Common Anti-Patterns\n\n| Anti-Pattern | Problem | Solution |\n|--------------|---------|----------|\n| Non-bulkified Invocable | Fails for multi-record Flows | Use `List\u003cRequest>` → `List\u003cResponse>` |\n| Missing faultConnector handling | Exceptions crash Flow | Return error in Response, add fault path |\n| Cacheable method with DML | Runtime error | Remove `cacheable=true` for mutations |\n| Mixing concerns | Hard to test | Separate controller (LWC) from service (Flow) classes |\n\n---\n\n## Decision Matrix\n\n| Scenario | Use @InvocableMethod | Use @AuraEnabled |\n|----------|---------------------|------------------|\n| Called from Flow | ✅ | ❌ |\n| Called from LWC | ❌ | ✅ |\n| Needs bulkification | ✅ (always bulk) | Optional |\n| Read-only query | Either | ✅ (cacheable) |\n| DML operations | ✅ | ✅ |\n| External callout | ✅ | ✅ |\n\n---\n\n## Related Documentation\n\n| Topic | Location |\n|-------|----------|\n| @InvocableMethod templates | `sf-apex/assets/invocable-method.cls` |\n| Flow integration guide | `sf-apex/references/flow-integration.md` |\n| LWC triangle perspective | `sf-lwc/references/triangle-pattern.md` |\n| Flow triangle perspective | `sf-flow/references/triangle-pattern.md` |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":9370,"content_sha256":"ec79129b82a19e76afd1e127212b957ebfe6e16d8b9156b9f6254fab2eefe7e0"},{"filename":"references/trigger-actions-framework.md","content":"\u003c!-- Parent: sf-apex/SKILL.md -->\n# Trigger Actions Framework (TAF) Guide\n\n## Overview\n\nThe Trigger Actions Framework provides a metadata-driven approach to trigger management, enabling:\n- One trigger per object\n- Ordered execution of actions\n- Bypass mechanisms (global, transaction, permission-based)\n- Support for both Apex and Flow actions\n\n## Installation\n\nInstall from: https://github.com/mitchspano/trigger-actions-framework\n\n## Basic Setup\n\n### 1. Create the Trigger\n\nOne trigger per object, delegating all logic to the framework:\n\n```apex\ntrigger AccountTrigger on Account (\n before insert, after insert,\n before update, after update,\n before delete, after delete,\n after undelete\n) {\n new MetadataTriggerHandler().run();\n}\n```\n\n### 2. Enable the Object\n\nCreate an `sObject_Trigger_Setting__mdt` record:\n- Label: Account Trigger Setting\n- Object API Name: Account\n- Bypass Execution: unchecked\n\n### 3. Create Action Classes\n\nEach action class handles one specific behavior:\n\n```apex\npublic class TA_Account_SetDefaults implements TriggerAction.BeforeInsert {\n public void beforeInsert(List\u003cAccount> newList) {\n for (Account acc : newList) {\n if (acc.Industry == null) {\n acc.Industry = 'Other';\n }\n }\n }\n}\n```\n\n### 4. Register the Action\n\nCreate a `Trigger_Action__mdt` record:\n- Object: Account\n- Apex Class Name: TA_Account_SetDefaults\n- Order: 1\n- Before Insert: checked\n\n---\n\n## Action Interfaces\n\n### Before Triggers\n\n```apex\n// Before Insert\npublic class MyAction implements TriggerAction.BeforeInsert {\n public void beforeInsert(List\u003cSObject> newList) { }\n}\n\n// Before Update\npublic class MyAction implements TriggerAction.BeforeUpdate {\n public void beforeUpdate(List\u003cSObject> newList, List\u003cSObject> oldList) { }\n}\n\n// Before Delete\npublic class MyAction implements TriggerAction.BeforeDelete {\n public void beforeDelete(List\u003cSObject> oldList) { }\n}\n```\n\n### After Triggers\n\n```apex\n// After Insert\npublic class MyAction implements TriggerAction.AfterInsert {\n public void afterInsert(List\u003cSObject> newList) { }\n}\n\n// After Update\npublic class MyAction implements TriggerAction.AfterUpdate {\n public void afterUpdate(List\u003cSObject> newList, List\u003cSObject> oldList) { }\n}\n\n// After Delete\npublic class MyAction implements TriggerAction.AfterDelete {\n public void afterDelete(List\u003cSObject> oldList) { }\n}\n\n// After Undelete\npublic class MyAction implements TriggerAction.AfterUndelete {\n public void afterUndelete(List\u003cSObject> newList) { }\n}\n```\n\n---\n\n## Common Patterns\n\n### Setting Default Values (Before Insert)\n\n```apex\npublic class TA_Account_SetDefaults implements TriggerAction.BeforeInsert {\n public void beforeInsert(List\u003cAccount> newList) {\n for (Account acc : newList) {\n acc.Industry = acc.Industry ?? 'Other';\n acc.NumberOfEmployees = acc.NumberOfEmployees ?? 0;\n }\n }\n}\n```\n\n### Validation (Before Insert/Update)\n\n```apex\npublic class TA_Account_ValidateData implements TriggerAction.BeforeInsert, TriggerAction.BeforeUpdate {\n\n public void beforeInsert(List\u003cAccount> newList) {\n validate(newList);\n }\n\n public void beforeUpdate(List\u003cAccount> newList, List\u003cAccount> oldList) {\n validate(newList);\n }\n\n private void validate(List\u003cAccount> accounts) {\n for (Account acc : accounts) {\n if (acc.AnnualRevenue != null && acc.AnnualRevenue \u003c 0) {\n acc.AnnualRevenue.addError('Annual Revenue cannot be negative');\n }\n }\n }\n}\n```\n\n### Related Record Updates (After Insert/Update)\n\n```apex\npublic class TA_Account_UpdateContacts implements TriggerAction.AfterUpdate {\n\n public void afterUpdate(List\u003cAccount> newList, List\u003cAccount> oldList) {\n Map\u003cId, Account> oldMap = new Map\u003cId, Account>(oldList);\n Set\u003cId> changedAccountIds = new Set\u003cId>();\n\n for (Account acc : newList) {\n Account oldAcc = oldMap.get(acc.Id);\n if (acc.BillingCity != oldAcc.BillingCity) {\n changedAccountIds.add(acc.Id);\n }\n }\n\n if (!changedAccountIds.isEmpty()) {\n updateContactAddresses(changedAccountIds);\n }\n }\n\n private void updateContactAddresses(Set\u003cId> accountIds) {\n List\u003cContact> contacts = [\n SELECT Id, AccountId, MailingCity\n FROM Contact\n WHERE AccountId IN :accountIds\n WITH USER_MODE\n ];\n\n Map\u003cId, Account> accounts = new Map\u003cId, Account>([\n SELECT Id, BillingCity\n FROM Account\n WHERE Id IN :accountIds\n WITH USER_MODE\n ]);\n\n for (Contact con : contacts) {\n con.MailingCity = accounts.get(con.AccountId).BillingCity;\n }\n\n update contacts;\n }\n}\n```\n\n### Async Processing (After Insert/Update)\n\n```apex\npublic class TA_Account_ProcessAsync implements TriggerAction.AfterInsert {\n\n public void afterInsert(List\u003cAccount> newList) {\n Set\u003cId> accountIds = new Map\u003cId, Account>(newList).keySet();\n System.enqueueJob(new AccountProcessingQueueable(accountIds));\n }\n}\n```\n\n---\n\n## Bypass Mechanisms\n\n### Global Bypass (Metadata)\n\nIn `sObject_Trigger_Setting__mdt`:\n- Set `Bypass_Execution__c = true` to disable all triggers for object\n\n### Transaction Bypass (Apex)\n\n```apex\n// Bypass specific object\nTriggerBase.bypass(Account.SObjectType);\n\n// Bypass specific action\nMetadataTriggerHandler.bypass('TA_Account_SetDefaults');\n\n// Clear bypasses\nTriggerBase.clearAllBypasses();\nMetadataTriggerHandler.clearAllBypasses();\n```\n\n### Permission-Based Bypass\n\nIn `Trigger_Action__mdt`:\n- `Bypass_Permission__c`: Users with this permission skip the action\n- `Required_Permission__c`: Only users with this permission run the action\n\n### Custom Permissions as Preferred Bypass (Recommended)\n\nCustom Permissions provide a scalable, declarative bypass mechanism — preferred over hardcoded profile/role names.\n\n```apex\npublic class TA_Account_SetDefaults implements TriggerAction.BeforeInsert {\n public void beforeInsert(List\u003cAccount> newList) {\n // Check Custom Permission — preferred over hardcoded profile names\n if (FeatureManagement.checkPermission('Bypass_Triggers')) {\n return;\n }\n for (Account acc : newList) {\n acc.Industry = acc.Industry ?? 'Other';\n }\n }\n}\n```\n\n> **Why Custom Permissions > hardcoded profile names**: Custom Permissions can be assigned via Permission Sets (stackable, user-specific), don't break when profiles are renamed, and work across all automation types (Apex, Flow, Validation Rules).\n\n---\n\n## Recursion Prevention\n\n### Field-Value Comparison (Recommended)\n\nThe official recommendation for recursion prevention is to compare old vs new field values, processing only records where the relevant field actually changed. This is more precise than static boolean flags.\n\n```apex\npublic class TA_Account_SyncExternal implements TriggerAction.AfterUpdate {\n\n public void afterUpdate(List\u003cAccount> newList, List\u003cAccount> oldList) {\n Map\u003cId, Account> oldMap = new Map\u003cId, Account>(oldList);\n List\u003cAccount> toProcess = new List\u003cAccount>();\n\n for (Account acc : newList) {\n Account oldAcc = oldMap.get(acc.Id);\n // Only process if the field we care about actually changed\n if (acc.Status__c != oldAcc.Status__c) {\n toProcess.add(acc);\n }\n }\n\n if (!toProcess.isEmpty()) {\n processAccounts(toProcess);\n }\n }\n}\n```\n\n> **Why field-value comparison > static boolean flags**: Static booleans (`hasRun = true`) prevent ALL re-entry, even legitimate re-entry from different field changes. Field-value comparison only skips records where the triggering field didn't change.\n\n### Using TriggerBase (TAF-Specific)\n\nTAF provides built-in recursion tracking via `idToNumberOfTimesSeenAfterUpdate`. Use this when you need to limit execution count per record regardless of which fields changed:\n\n```apex\npublic class TA_Account_PreventRecursion implements TriggerAction.AfterUpdate {\n\n public void afterUpdate(List\u003cAccount> newList, List\u003cAccount> oldList) {\n List\u003cAccount> toProcess = new List\u003cAccount>();\n\n for (Account acc : newList) {\n // Check if already processed in this transaction\n if (!TriggerBase.idToNumberOfTimesSeenAfterUpdate.get(acc.Id).equals(1)) {\n continue;\n }\n toProcess.add(acc);\n }\n\n if (!toProcess.isEmpty()) {\n processAccounts(toProcess);\n }\n }\n}\n```\n\n---\n\n## Flow Actions\n\n### Setup\n\n1. Create an Autolaunched Flow\n2. Add variables:\n - `record` (Input, Record type)\n - `recordPrior` (Input, Record type, for Update triggers)\n\n3. Create `Trigger_Action__mdt`:\n - Apex Class Name: `TriggerActionFlow`\n - Flow Name: `Your_Flow_API_Name`\n\n### Entry Criteria\n\nDefine formula criteria to control when the flow executes:\n```\n{!record.Status__c} = 'Submitted' && {!recordPrior.Status__c} != 'Submitted'\n```\n\n---\n\n## Testing\n\n### Test Action Classes\n\n```apex\n@isTest\nprivate class TA_Account_SetDefaultsTest {\n\n @isTest\n static void testBeforeInsert() {\n Account acc = new Account(Name = 'Test');\n\n Test.startTest();\n insert acc;\n Test.stopTest();\n\n Account result = [SELECT Industry FROM Account WHERE Id = :acc.Id];\n Assert.areEqual('Other', result.Industry, 'Default industry should be set');\n }\n\n @isTest\n static void testBulkInsert() {\n List\u003cAccount> accounts = new List\u003cAccount>();\n for (Integer i = 0; i \u003c 251; i++) {\n accounts.add(new Account(Name = 'Test ' + i));\n }\n\n Test.startTest();\n insert accounts;\n Test.stopTest();\n\n List\u003cAccount> results = [SELECT Industry FROM Account WHERE Id IN :accounts];\n for (Account acc : results) {\n Assert.areEqual('Other', acc.Industry);\n }\n }\n}\n```\n\n### Test with Bypass\n\n```apex\n@isTest\nstatic void testWithBypass() {\n // Bypass the action\n MetadataTriggerHandler.bypass('TA_Account_SetDefaults');\n\n Account acc = new Account(Name = 'Test');\n insert acc;\n\n Account result = [SELECT Industry FROM Account WHERE Id = :acc.Id];\n Assert.isNull(result.Industry, 'Industry should not be set when bypassed');\n\n // Clear bypass for other tests\n MetadataTriggerHandler.clearBypass('TA_Account_SetDefaults');\n}\n```\n\n---\n\n## Naming Convention\n\n```\nTA_[ObjectName]_[ActionDescription]\n\nExamples:\n- TA_Account_SetDefaults\n- TA_Account_ValidateData\n- TA_Contact_UpdateAccountRollup\n- TA_Opportunity_SendNotification\n```\n\n---\n\n## Execution Order\n\nActions execute in the order defined by the `Order__c` field in `Trigger_Action__mdt`.\n\nRecommended ordering:\n1. Validation (10-20)\n2. Default values (30-40)\n3. Field calculations (50-60)\n4. Related record queries (70-80)\n5. Related record updates (90-100)\n6. Async/external calls (110+)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":11050,"content_sha256":"8217b5c73dd01013187ef423925e7c0216a83afeb8c9541f6ce99ec08b88943b"},{"filename":"references/troubleshooting.md","content":"\u003c!-- Parent: sf-apex/SKILL.md -->\n# Apex Troubleshooting Guide\n\nComprehensive guide to debugging Apex code, LSP validation, dependency management, and common deployment issues.\n\n---\n\n## Table of Contents\n\n1. [LSP-Based Validation (Auto-Fix Loop)](#lsp-based-validation-auto-fix-loop)\n2. [Cross-Skill Dependency Checklist](#cross-skill-dependency-checklist)\n3. [Common Deployment Errors](#common-deployment-errors)\n4. [Debug Logs and Monitoring](#debug-logs-and-monitoring)\n5. [Governor Limit Debugging](#governor-limit-debugging)\n6. [Test Failures](#test-failures)\n\n---\n\n\u003ca id=\"lsp-based-validation-auto-fix-loop\">\u003c/a>\n\n## LSP-Based Validation (Auto-Fix Loop)\n\nThe sf-apex skill includes Language Server Protocol (LSP) integration for real-time syntax validation. This enables Claude to automatically detect and fix Apex syntax errors during code authoring.\n\n### How It Works\n\n1. **PostToolUse Hook**: After every Write/Edit operation on `.cls` or `.trigger` files, the LSP hook validates syntax\n2. **Apex Language Server**: Uses Salesforce's official `apex-jorje-lsp.jar` (from VS Code extension)\n3. **Auto-Fix Loop**: If errors are found, Claude receives diagnostics and auto-fixes them (max 3 attempts)\n4. **Two-Layer Validation**:\n - **LSP Validation**: Fast syntax checking (~500ms)\n - **150-Point Validation**: Semantic analysis for best practices\n\n---\n\n### Prerequisites\n\nFor LSP validation to work, users must have:\n\n| Requirement | How to Install |\n|-------------|----------------|\n| **VS Code Salesforce Extension Pack** | VS Code → Extensions → \"Salesforce Extension Pack\" |\n| **Java 11+ (Adoptium recommended)** | https://adoptium.net/temurin/releases/ |\n\n**Verify Installation:**\n```bash\n# Check VS Code extensions\ncode --list-extensions | grep salesforce\n\n# Check Java version\njava -version\n# Should output: openjdk version \"11.x.x\" or higher\n```\n\n---\n\n### Validation Flow\n\n```\nUser writes Apex code → Write/Edit tool executes\n ↓\n ┌─────────────────────────┐\n │ LSP Validation (fast) │\n │ Syntax errors only │\n └─────────────────────────┘\n ↓\n ┌─────────────────────────┐\n │ 150-Point Validation │\n │ Semantic best practices│\n └─────────────────────────┘\n ↓\n Claude sees any errors and auto-fixes\n```\n\n---\n\n### Sample LSP Error Output\n\n```\n============================================================\n🔍 APEX LSP VALIDATION RESULTS\n File: force-app/main/default/classes/MyClass.cls\n Attempt: 1/3\n============================================================\n\nFound 1 error(s), 0 warning(s)\n\nISSUES TO FIX:\n----------------------------------------\n❌ [ERROR] line 4: Missing ';' at 'System.debug' (source: apex)\n\nACTION REQUIRED:\nPlease fix the Apex syntax errors above and try again.\n(Attempt 1/3)\n============================================================\n```\n\n---\n\n### Common LSP Errors\n\n| Error | Cause | Fix |\n|-------|-------|-----|\n| Missing ';' at ... | Statement not terminated | Add semicolon at end of line |\n| Unexpected token ... | Syntax error | Check brackets, quotes, keywords |\n| Unknown type ... | Class/type not found | Ensure class exists, check spelling |\n| Method does not exist ... | Method call on wrong type | Verify method name and signature |\n| Variable not found ... | Undeclared variable | Declare variable before use |\n\n**Example Auto-Fix Loop:**\n\n**Attempt 1 (ERROR):**\n```apex\npublic class MyClass {\n public void doSomething() {\n System.debug('Hello') // Missing semicolon\n }\n}\n```\n\n**LSP Output:**\n```\n❌ [ERROR] line 3: Missing ';' at '}'\n```\n\n**Attempt 2 (SUCCESS):**\n```apex\npublic class MyClass {\n public void doSomething() {\n System.debug('Hello'); // Fixed!\n }\n}\n```\n\n**LSP Output:**\n```\n✅ VALIDATION PASSED\n```\n\n---\n\n### Graceful Degradation\n\nIf LSP is unavailable (no VS Code extension or Java), validation silently skips - the skill continues to work with only 150-point semantic validation.\n\n**Detection Logic:**\n```python\n# hooks/scripts/post-tool-validate.py\ntry:\n result = run_lsp_validation(file_path)\n if result.has_errors:\n print_errors(result)\nexcept LSPNotAvailableException:\n # Silent fallback - continue without LSP\n pass\n```\n\n---\n\n### Manual LSP Validation\n\n**Run LSP validation manually from VS Code:**\n\n1. Open Apex class in VS Code\n2. View → Problems panel (`Cmd+Shift+M` / `Ctrl+Shift+M`)\n3. See syntax errors highlighted in real-time\n\n**Run from CLI (if available):**\n```bash\n# Apex compilation happens automatically during deploy (no standalone compile command)\nsf project deploy start --metadata ApexClass:MyClass --target-org \u003calias> --dry-run --json\n```\n\n---\n\n\u003ca id=\"cross-skill-dependency-checklist\">\u003c/a>\n\n## Cross-Skill Dependency Checklist\n\n**Before deploying Apex code, verify these prerequisites:**\n\n| Prerequisite | Check Command | Required For |\n|--------------|---------------|--------------|\n| **TAF Package** | `sf package installed list --target-org alias` | TAF trigger pattern |\n| **Custom Fields** | `sf sobject describe --sobject Lead --target-org alias` | Field references in code |\n| **Permission Sets** | `sf org list metadata --metadata-type PermissionSet` | FLS for custom fields |\n| **Trigger_Action__mdt** | Check Setup → Custom Metadata Types | TAF trigger execution |\n| **Named Credentials** | Check Setup → Named Credentials | External callouts |\n| **Custom Settings** | Check Setup → Custom Settings | Bypass flags, configuration |\n\n---\n\n### Common Deployment Order\n\n```\n1. sf-metadata: Create custom fields\n └─> sf schema generate field --object Lead --label \"Score\" (then edit XML for Number(3,0))\n\n2. sf-metadata: Create Permission Sets\n └─> Grant FLS on custom fields\n\n3. sf-deploy: Deploy fields + Permission Sets\n └─> sf project deploy start --metadata-dir force-app/main/default/objects\n\n4. sf-apex: Deploy Apex classes/triggers\n └─> sf project deploy start --metadata-dir force-app/main/default/classes\n\n5. sf-data: Create test data\n └─> sf data create record --sobject Account --values \"Name='Test'\"\n```\n\n---\n\n### Verifying Prerequisites\n\n**Check TAF Package:**\n```bash\nsf package installed list --target-org myorg --json\n```\n\n**Output:**\n```json\n{\n \"result\": [\n {\n \"Id\": \"04t...\",\n \"SubscriberPackageName\": \"Trigger Actions Framework\",\n \"SubscriberPackageVersionNumber\": \"1.2.0\"\n }\n ]\n}\n```\n\n**If not installed:**\n```bash\nsf package install --package 04tKZ000000gUEFYA2 --target-org myorg --wait 10\n```\n\n---\n\n**Check Custom Metadata Records:**\n```bash\nsf data query --query \"SELECT DeveloperName, Object__c, Apex_Class_Name__c FROM Trigger_Action__mdt\" --target-org myorg\n```\n\n**Expected Output:**\n```\nDeveloperName Object__c Apex_Class_Name__c\n─────────────────────────────────────────────────────\nTA_Account_SetDefaults Account TA_Account_SetDefaults\nTA_Lead_CalculateScore Lead TA_Lead_CalculateScore\n```\n\n**If missing, create via sf-metadata skill.**\n\n---\n\n## Common Deployment Errors\n\n### Error: \"Field does not exist\"\n\n**Cause**: Apex references a custom field that doesn't exist in target org.\n\n**Example:**\n```\nError: Field Account.Custom_Field__c does not exist\n```\n\n**Fix:**\n1. Verify field exists:\n ```bash\n sf sobject describe --sobject Account --target-org myorg | grep Custom_Field__c\n ```\n\n2. Deploy field first:\n ```bash\n sf project deploy start --metadata CustomField:Account.Custom_Field__c --target-org myorg\n ```\n\n3. Then deploy Apex\n\n---\n\n### Error: \"Invalid type: TriggerAction\"\n\n**Cause**: TAF package not installed in target org.\n\n**Example:**\n```\nError: Invalid type: TriggerAction.BeforeInsert\n```\n\n**Fix:**\n```bash\n# Install TAF package\nsf package install --package 04tKZ000000gUEFYA2 --target-org myorg --wait 10\n\n# Verify\nsf package installed list --target-org myorg\n```\n\n---\n\n### Error: \"Insufficient access rights\"\n\n**Cause**: Deploy user lacks permissions.\n\n**Example:**\n```\nError: Insufficient access rights on object id\n```\n\n**Fix:**\n1. Verify user has \"Modify All Data\" or is System Administrator\n2. Or add specific permissions to user's profile:\n ```bash\n sf org assign permset --name \"Deploy_Permissions\" --target-org myorg\n ```\n\n---\n\n### Error: \"Test coverage less than 75%\"\n\n**Cause**: Production deployment requires 75% test coverage.\n\n**Example:**\n```\nError: Average test coverage across all Apex Classes and Triggers is 68%, at least 75% required\n```\n\n**Fix:**\n1. Identify uncovered classes:\n ```bash\n sf apex run test --code-coverage --result-format human --target-org myorg\n ```\n\n2. Add missing test classes\n\n3. Ensure tests have assertions:\n ```apex\n Assert.areEqual(expected, actual, 'Message');\n ```\n\n---\n\n### Error: \"FIELD_CUSTOM_VALIDATION_EXCEPTION\"\n\n**Cause**: Apex code violates validation rule.\n\n**Example:**\n```\nError: FIELD_CUSTOM_VALIDATION_EXCEPTION: Annual Revenue must be greater than 0\n```\n\n**Fix:**\n1. Check validation rules:\n ```bash\n sf data query --query \"SELECT ValidationName, ErrorDisplayField, ErrorMessage FROM ValidationRule WHERE EntityDefinition.QualifiedApiName = 'Account'\" --target-org myorg\n ```\n\n2. Update Apex to satisfy validation logic:\n ```apex\n acc.AnnualRevenue = 1000000; // Ensure > 0\n ```\n\n---\n\n## Debug Logs and Monitoring\n\n### Enable Debug Logs\n\n**Via Setup:**\n1. Setup → Debug Logs\n2. Click \"New\"\n3. Select User\n4. Set expiration (max 24 hours)\n5. Set log levels:\n - Apex Code: `DEBUG`\n - Database: `INFO`\n - Workflow: `INFO`\n\n**Via CLI:**\n```bash\n# Create trace flag\nsf data create record --sobject TraceFlag --values \"StartDate=2025-01-01T00:00:00Z EndDate=2025-01-02T00:00:00Z LogType=USER_DEBUG TracedEntityId=\u003cUSER_ID> DebugLevelId=\u003cDEBUG_LEVEL_ID>\" --target-org myorg\n\n# Tail logs in real-time\nsf apex tail log --target-org myorg\n```\n\n---\n\n### Reading Debug Logs\n\n**Structure:**\n```\nHH:MM:SS.SSS|EXECUTION_STARTED\nHH:MM:SS.SSS|CODE_UNIT_STARTED|AccountService\nHH:MM:SS.SSS|USER_DEBUG|[3]|DEBUG|Processing account: Test\nHH:MM:SS.SSS|SOQL_EXECUTE_BEGIN|[5]|SELECT Id FROM Account\nHH:MM:SS.SSS|SOQL_EXECUTE_END|[5]|Rows:10\nHH:MM:SS.SSS|DML_BEGIN|[8]|Op:Update|Type:Account|Rows:10\nHH:MM:SS.SSS|DML_END|[8]\nHH:MM:SS.SSS|LIMIT_USAGE_FOR_NS|(default)|SOQL:1/100|DML:1/150\nHH:MM:SS.SSS|EXECUTION_FINISHED\n```\n\n**Key Events:**\n- `USER_DEBUG`: Your `System.debug()` statements\n- `SOQL_EXECUTE_*`: SOQL queries\n- `DML_BEGIN/END`: DML operations\n- `LIMIT_USAGE_FOR_NS`: Governor limit consumption\n\n---\n\n### Strategic Debug Statements\n\n```apex\npublic static void processAccounts(List\u003cAccount> accounts) {\n System.debug(LoggingLevel.INFO, '=== START processAccounts ===');\n System.debug(LoggingLevel.INFO, 'Input size: ' + accounts.size());\n\n // Log limits BEFORE expensive operation\n System.debug('SOQL before: ' + Limits.getQueries() + '/' + Limits.getLimitQueries());\n\n List\u003cContact> contacts = [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds];\n\n // Log limits AFTER\n System.debug('SOQL after: ' + Limits.getQueries() + '/' + Limits.getLimitQueries());\n System.debug('Contacts retrieved: ' + contacts.size());\n\n System.debug(LoggingLevel.INFO, '=== END processAccounts ===');\n}\n```\n\n---\n\n### Log Levels\n\n| Level | When to Use | Example |\n|-------|-------------|---------|\n| `ERROR` | Critical failures | `System.debug(LoggingLevel.ERROR, 'DML failed: ' + e.getMessage())` |\n| `WARN` | Potential issues | `System.debug(LoggingLevel.WARN, 'No contacts found for account')` |\n| `INFO` | Key milestones | `System.debug(LoggingLevel.INFO, 'Processing 251 accounts')` |\n| `DEBUG` | Detailed traces | `System.debug(LoggingLevel.DEBUG, 'Variable value: ' + var)` |\n| `FINE`/`FINER`/`FINEST` | Very detailed | Rarely used |\n\n---\n\n## Governor Limit Debugging\n\n### Monitoring Limits in Code\n\n```apex\npublic static void expensiveOperation() {\n System.debug('=== LIMIT CHECK ===');\n System.debug('SOQL Queries: ' + Limits.getQueries() + '/' + Limits.getLimitQueries());\n System.debug('DML Statements: ' + Limits.getDmlStatements() + '/' + Limits.getLimitDmlStatements());\n System.debug('DML Rows: ' + Limits.getDmlRows() + '/' + Limits.getLimitDmlRows());\n System.debug('CPU Time: ' + Limits.getCpuTime() + '/' + Limits.getLimitCpuTime());\n System.debug('Heap Size: ' + Limits.getHeapSize() + '/' + Limits.getLimitHeapSize());\n}\n```\n\n---\n\n### Common Limit Exceptions\n\n**SOQL Limit (100 queries):**\n```\nSystem.LimitException: Too many SOQL queries: 101\n```\n\n**Fix**: Query BEFORE loops, use Maps for lookups.\n\n**DML Limit (150 statements):**\n```\nSystem.LimitException: Too many DML statements: 151\n```\n\n**Fix**: Collect records in List, DML AFTER loop.\n\n**CPU Time Limit (10 seconds):**\n```\nSystem.LimitException: Maximum CPU time exceeded\n```\n\n**Fix**: Optimize loops, move expensive operations to async, reduce complexity.\n\n**Heap Size Limit (6 MB):**\n```\nSystem.LimitException: Apex heap size too large\n```\n\n**Fix**: Process in batches, clear collections when done, avoid storing large objects in memory.\n\n---\n\n### Using Limits Class for Alerts\n\n```apex\npublic static void monitoredOperation() {\n // Warn if approaching 80% of limit\n Integer queriesUsed = Limits.getQueries();\n Integer queriesLimit = Limits.getLimitQueries();\n\n if (queriesUsed > queriesLimit * 0.8) {\n System.debug(LoggingLevel.WARN, 'Approaching SOQL limit: ' + queriesUsed + '/' + queriesLimit);\n }\n\n // Expensive operation\n List\u003cAccount> accounts = [SELECT Id FROM Account];\n}\n```\n\n---\n\n## Test Failures\n\n### Common Test Failure Patterns\n\n**Pattern 1: No assertions**\n```apex\n@IsTest\nstatic void testCreateAccount() {\n Account acc = new Account(Name = 'Test');\n insert acc;\n // PASSES even if logic is broken!\n}\n```\n\n**Fix**: Add assertions\n```apex\n@IsTest\nstatic void testCreateAccount() {\n Account acc = new Account(Name = 'Test', Industry = 'Tech');\n insert acc;\n\n Account inserted = [SELECT Id, Industry FROM Account WHERE Id = :acc.Id];\n Assert.areEqual('Tech', inserted.Industry, 'Industry should be set');\n}\n```\n\n---\n\n**Pattern 2: Order dependency**\n```apex\n@IsTest\nstatic void test1() {\n insert new Account(Name = 'Shared');\n}\n\n@IsTest\nstatic void test2() {\n // Assumes test1 ran first - BRITTLE!\n Account acc = [SELECT Id FROM Account WHERE Name = 'Shared'];\n}\n```\n\n**Fix**: Use @TestSetup or create data in each test\n```apex\n@TestSetup\nstatic void setup() {\n insert new Account(Name = 'Shared');\n}\n\n@IsTest\nstatic void test2() {\n Account acc = [SELECT Id FROM Account WHERE Name = 'Shared']; // Safe\n}\n```\n\n---\n\n**Pattern 3: Insufficient permissions**\n```apex\n@IsTest\nstatic void testRestrictedUser() {\n User u = TestDataFactory.createStandardUser();\n\n System.runAs(u) {\n // Fails if user lacks permission\n insert new Account(Name = 'Test');\n }\n}\n```\n\n**Fix**: Grant necessary permissions\n```apex\n@TestSetup\nstatic void setup() {\n User u = TestDataFactory.createStandardUser();\n insert new PermissionSetAssignment(\n AssigneeId = u.Id,\n PermissionSetId = [SELECT Id FROM PermissionSet WHERE Name = 'Account_Create'].Id\n );\n}\n```\n\n---\n\n### Running Tests\n\n**VS Code:**\n1. Open test class\n2. Click \"Run Test\" above `@IsTest` method\n3. View results in Output panel\n\n**CLI:**\n```bash\n# Run specific test class\nsf apex run test --tests AccountServiceTest --result-format human --code-coverage --target-org myorg\n\n# Run all tests\nsf apex run test --test-level RunLocalTests --result-format human --code-coverage --target-org myorg\n\n# Run tests and generate coverage report\nsf apex run test --test-level RunLocalTests --code-coverage --result-format json --output-dir test-results --target-org myorg\n```\n\n**Output:**\n```\nTest Summary\n════════════\nOutcome Passed\nTests Ran 12\nPass Rate 100%\nFail Rate 0%\nSkip Rate 0%\nTest Run Coverage 92%\nOrg Wide Coverage 85%\nTest Execution Time 1234 ms\n\nCoverage Warnings\n═════════════════\nAccountService.cls Line 45 not covered by tests\n```\n\n---\n\n## Debugging Strategies\n\n### 1. Binary Search for Errors\n\nWhen unsure where error occurs, add debug statements at midpoints:\n\n```apex\npublic static void complexOperation() {\n System.debug('START');\n\n // Part 1\n List\u003cAccount> accounts = [SELECT Id FROM Account];\n System.debug('CHECKPOINT 1: Retrieved ' + accounts.size() + ' accounts');\n\n // Part 2\n for (Account acc : accounts) {\n acc.Industry = 'Tech';\n }\n System.debug('CHECKPOINT 2: Updated accounts');\n\n // Part 3\n update accounts;\n System.debug('CHECKPOINT 3: DML complete');\n\n System.debug('END');\n}\n```\n\nRun and check logs to see which checkpoint fails.\n\n---\n\n### 2. Isolate in Anonymous Apex\n\n**Execute in Developer Console:**\n```apex\nAccount acc = new Account(Name = 'Debug Test', Industry = 'Tech');\ninsert acc;\n\nSystem.debug('Account ID: ' + acc.Id);\nSystem.debug('Industry: ' + acc.Industry);\n```\n\nOpen Execute Anonymous Window (`Ctrl+E`), paste code, check logs.\n\n---\n\n### 3. Unit Test in Isolation\n\n**Create minimal test case:**\n```apex\n@IsTest\nstatic void debugIssue() {\n Account acc = new Account(Name = 'Test', AnnualRevenue = null);\n\n Test.startTest();\n AccountService.calculateScore(acc); // Isolated method\n Test.stopTest();\n\n System.debug('Score: ' + acc.Score__c);\n}\n```\n\nEasier to debug than full integration test.\n\n---\n\n## Reference\n\n**Full Documentation**: See `references/` folder for comprehensive guides:\n- `best-practices.md` - Debugging best practices\n- `testing-guide.md` - Test troubleshooting\n- `code-review-checklist.md` - Quality checklist\n\n**Back to Main**: [SKILL.md](../SKILL.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":18279,"content_sha256":"063ad82b6573c95c7aae86309acd7a078cac6597759dd3ca09cf51e7d2678db1"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"sf-apex: Salesforce Apex Code Generation and Review","type":"text"}]},{"type":"paragraph","content":[{"text":"Use this skill when the user needs ","type":"text"},{"text":"production Apex","type":"text","marks":[{"type":"strong"}]},{"text":": new classes, triggers, selectors, services, async jobs, invocable methods, test classes, or evidence-based review of existing ","type":"text"},{"text":".cls","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":".trigger","type":"text","marks":[{"type":"code_inline"}]},{"text":" code.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When This Skill Owns the Task","type":"text"}]},{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"sf-apex","type":"text","marks":[{"type":"code_inline"}]},{"text":" when the work involves:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Apex class generation or refactoring","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"trigger design and trigger-framework decisions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"@InvocableMethod","type":"text","marks":[{"type":"code_inline"}]},{"text":", Queueable, Batch, Schedulable, or test-class work","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"review of bulkification, sharing, security, testing, or maintainability","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Delegate elsewhere when the user is:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"editing LWC JavaScript / HTML / CSS → ","type":"text"},{"text":"sf-lwc","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-lwc/SKILL.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"building Flow XML or Flow orchestration → ","type":"text"},{"text":"sf-flow","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-flow/SKILL.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"writing SOQL only → ","type":"text"},{"text":"sf-soql","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-soql/SKILL.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"deploying or validating metadata to orgs → ","type":"text"},{"text":"sf-deploy","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-deploy/SKILL.md","title":null}}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Required Context to Gather First","type":"text"}]},{"type":"paragraph","content":[{"text":"Ask for or infer:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"class type: trigger, service, selector, batch, queueable, schedulable, invocable, test","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"target object(s) and business goal","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"whether code is net-new, refactor, or fix","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"org / API constraints if known","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"expected test coverage or deployment target","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Before authoring, inspect the project shape:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"existing classes / triggers","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"current trigger framework or handler pattern","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"related tests, flows, and selectors","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"whether TAF is already in use","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Recommended Workflow","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1. Discover local architecture","type":"text"}]},{"type":"paragraph","content":[{"text":"Check for:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"existing trigger handlers / frameworks","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"service-selector-domain conventions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"related tests and data factories","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"invocable or async patterns already used in the repo","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. Choose the smallest correct pattern","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Need","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Preferred pattern","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"simple reusable logic","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"service class","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"query-heavy data access","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"selector","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"single object trigger behavior","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"one trigger + handler / TAF action","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Flow needs complex logic","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"@InvocableMethod","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"background processing","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Queueable by default","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"very large datasets","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Batch Apex or ","type":"text"},{"text":"Database.Cursor","type":"text","marks":[{"type":"code_inline"}]},{"text":" patterns","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"repeatable verification","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"dedicated test class + test data factory","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3. Author with guardrails","type":"text"}]},{"type":"paragraph","content":[{"text":"Generate code that is:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"bulk-safe","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sharing-aware","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"CRUD/FLS-safe where applicable","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"testable in isolation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"consistent with project naming and layering","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"4. Validate and score","type":"text"}]},{"type":"paragraph","content":[{"text":"Evaluate against the 150-point rubric before handoff.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"5. Hand off deploy/test next steps","type":"text"}]},{"type":"paragraph","content":[{"text":"When org validation is needed, hand off to:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sf-testing","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-testing/SKILL.md","title":null}}]},{"text":" for test execution loops","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sf-deploy","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-deploy/SKILL.md","title":null}}]},{"text":" for deploy / dry-run / verification","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Generation Guardrails","type":"text"}]},{"type":"paragraph","content":[{"text":"Never generate these without explicitly stopping and explaining the problem:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Anti-pattern","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Why it blocks","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SOQL in loops","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"governor-limit failure","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"DML in loops","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"governor-limit failure","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"missing sharing model","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"security / data exposure risk","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"hardcoded IDs","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"deployment and portability failure","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"empty ","type":"text"},{"text":"catch","type":"text","marks":[{"type":"code_inline"}]},{"text":" blocks","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"silent failure / poor observability","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string-built SOQL with user input","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"injection risk","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"tests without assertions","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"false-positive test suite","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Default fix direction:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"query once, operate on collections","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"use ","type":"text"},{"text":"with sharing","type":"text","marks":[{"type":"code_inline"}]},{"text":" unless justified otherwise","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"use bind variables and ","type":"text"},{"text":"WITH USER_MODE","type":"text","marks":[{"type":"code_inline"}]},{"text":" where appropriate","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"create assertions for positive, negative, and bulk cases","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"references/anti-patterns.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/anti-patterns.md","title":null}}]},{"text":" and ","type":"text"},{"text":"references/security-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/security-guide.md","title":null}}]},{"text":".","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"High-Signal Build Rules","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Trigger architecture","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Prefer ","type":"text"},{"text":"one trigger per object","type":"text","marks":[{"type":"strong"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If TAF is already installed and used, extend it instead of inventing a second trigger pattern.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Triggers should delegate logic; avoid heavy business logic directly in trigger bodies.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Async choice","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Scenario","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Default","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"standard async work","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Queueable","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"very large record processing","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Batch Apex","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"recurring schedule","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Scheduled Flow or Schedulable","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"post-job cleanup","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Finalizer","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"long-running Lightning callouts","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Continuation","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Testing minimums","type":"text"}]},{"type":"paragraph","content":[{"text":"Use the ","type":"text"},{"text":"PNB","type":"text","marks":[{"type":"strong"}]},{"text":" pattern for every feature:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Positive","type":"text","marks":[{"type":"strong"}]},{"text":" path","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Negative","type":"text","marks":[{"type":"strong"}]},{"text":" / error path","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Bulk","type":"text","marks":[{"type":"strong"}]},{"text":" path (251+ records where relevant)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Modern Apex expectations","type":"text"}]},{"type":"paragraph","content":[{"text":"Prefer current idioms when available:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"safe navigation: ","type":"text"},{"text":"obj?.Field__c","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"null coalescing: ","type":"text"},{"text":"value ?? fallback","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Assert.*","type":"text","marks":[{"type":"code_inline"}]},{"text":" over legacy assertion style","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"WITH USER_MODE","type":"text","marks":[{"type":"code_inline"}]},{"text":" and explicit security handling where relevant","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Output Format","type":"text"}]},{"type":"paragraph","content":[{"text":"When finishing, report in this order:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"What was created or reviewed","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Files changed","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Key design decisions","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Risk / guardrail notes","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Test guidance","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Deployment guidance","type":"text","marks":[{"type":"strong"}]}]}]}]},{"type":"paragraph","content":[{"text":"Suggested shape:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"text"},"content":[{"text":"Apex work: \u003csummary>\nFiles: \u003cpaths>\nDesign: \u003cpattern / framework choices>\nRisks: \u003csecurity, bulkification, async, dependency notes>\nTests: \u003cwhat to run / add>\nDeploy: \u003cdry-run or next step>","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"LSP Validation Note","type":"text"}]},{"type":"paragraph","content":[{"text":"This skill supports an LSP-assisted authoring loop for ","type":"text"},{"text":".cls","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":".trigger","type":"text","marks":[{"type":"code_inline"}]},{"text":" files:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"syntax issues can be detected immediately after write/edit","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"the skill can auto-fix common syntax errors in a short loop","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"semantic quality still depends on the 150-point review rubric","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Full guide: ","type":"text"},{"text":"references/troubleshooting.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/troubleshooting.md#lsp-based-validation-auto-fix-loop","title":null}}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Cross-Skill Integration","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Need","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Delegate to","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Reason","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"describe objects / fields first","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sf-metadata","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-metadata/SKILL.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"avoid coding against wrong schema","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"seed bulk or edge-case data","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sf-data","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-data/SKILL.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create realistic test datasets","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"run Apex tests / fix failing tests","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sf-testing","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-testing/SKILL.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"execute and iterate on failures","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"deploy to org","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sf-deploy","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-deploy/SKILL.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"validation and deployment orchestration","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"build Flow that calls Apex","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sf-flow","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-flow/SKILL.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"declarative orchestration","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"build LWC that calls Apex","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sf-lwc","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-lwc/SKILL.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"UI/controller integration","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Reference Map","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Start here","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/patterns-deep-dive.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/patterns-deep-dive.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/security-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/security-guide.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/bulkification-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/bulkification-guide.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/testing-patterns.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/testing-patterns.md","title":null}}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"High-signal checklists","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/code-review-checklist.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/code-review-checklist.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/anti-patterns.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/anti-patterns.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/naming-conventions.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/naming-conventions.md","title":null}}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Specialized patterns","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/trigger-actions-framework.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/trigger-actions-framework.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/automation-density-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/automation-density-guide.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/flow-integration.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/flow-integration.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/triangle-pattern.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/triangle-pattern.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/design-patterns.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/design-patterns.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/solid-principles.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/solid-principles.md","title":null}}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Troubleshooting / validation","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/troubleshooting.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/troubleshooting.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/llm-anti-patterns.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/llm-anti-patterns.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/testing-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/testing-guide.md","title":null}}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Score Guide","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Score","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Meaning","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"120+","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"strong production-ready Apex","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"90–119","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"good implementation, review before deploy","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"67–89","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"acceptable but needs improvement","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\u003c 67","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"block deployment","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"sf-apex","author":"@skillopedia","source":{"stars":416,"repo_name":"sf-skills","origin_url":"https://github.com/jaganpro/sf-skills/blob/HEAD/skills/sf-apex/SKILL.md","repo_owner":"jaganpro","body_sha256":"7bca2bbaa5aaf8fcd284f6fd5b2e344ac014775ca186770dfbb03045bf5d72df","cluster_key":"c73c07005d10368c44d9d7931a9c306bea7897bf93bd99fd515132db2315a9e4","clean_bundle":{"format":"clean-skill-bundle-v1","source":"jaganpro/sf-skills/skills/sf-apex/SKILL.md","attachments":[{"id":"ef34804b-d0fb-55cb-a03a-4ccd50375a5e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ef34804b-d0fb-55cb-a03a-4ccd50375a5e/attachment.md","path":"CREDITS.md","size":4416,"sha256":"332e7f3a917e8c39d50f26b0904b73101f7da37553015b9b182b16584f76ba8d","contentType":"text/markdown; charset=utf-8"},{"id":"308d2bcb-4d23-5e2c-847a-fbba10c8a222","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/308d2bcb-4d23-5e2c-847a-fbba10c8a222/attachment.md","path":"README.md","size":3003,"sha256":"f8bd1ab779ae8be5c50d65c0e477c5488f8e77ad9acedd777a0c8bad58991e92","contentType":"text/markdown; charset=utf-8"},{"id":"941b87ad-38ff-543d-8ba1-620c95fd8645","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/941b87ad-38ff-543d-8ba1-620c95fd8645/attachment.cls","path":"assets/apex-class.cls","size":406,"sha256":"58b2397dae30cbcec4093954fd5383690370bcbafee5c1d5e6c110251811139e","contentType":"text/x-tex"},{"id":"8b3fbc15-b67d-5521-8288-da92edc08292","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8b3fbc15-b67d-5521-8288-da92edc08292/attachment.cls","path":"assets/batch.cls","size":3857,"sha256":"300369dcc89d224aa73ab6492009f9fca110c149cbb0395853bf5580a7b90aa9","contentType":"text/x-tex"},{"id":"78583744-f077-554e-bba7-7e837a445fb0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/78583744-f077-554e-bba7-7e837a445fb0/attachment.cls","path":"assets/invocable-method.cls","size":9500,"sha256":"da1a15a5b8933b4c94a46e6221d1dac043b7848f3ca8783ebf5bf6892ce570a6","contentType":"text/x-tex"},{"id":"3ae0317c-8e3e-5ecd-a089-ef37b452b90b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3ae0317c-8e3e-5ecd-a089-ef37b452b90b/attachment.cls","path":"assets/queueable.cls","size":1797,"sha256":"08e81fa8b515137fa45df52a6a83df65922311201cbe66b16f03019e7f061682","contentType":"text/x-tex"},{"id":"fa2b3fdf-9965-52db-9128-7642c6290f67","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/fa2b3fdf-9965-52db-9128-7642c6290f67/attachment.cls","path":"assets/selector.cls","size":3238,"sha256":"16cf397c272cd53a889ea13af38756ac5a70998d39fdb0f897c5b9982878ed74","contentType":"text/x-tex"},{"id":"fecb8ad0-600f-572a-adb2-54cd816202a9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/fecb8ad0-600f-572a-adb2-54cd816202a9/attachment.cls","path":"assets/service.cls","size":4496,"sha256":"eed4f1d1b077a8c909cfb1645821c1e6655fcca3e9d313e747e0e73d50cd4ac2","contentType":"text/x-tex"},{"id":"7c91e352-c2ae-54ce-90cc-5a55bc89c5d6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7c91e352-c2ae-54ce-90cc-5a55bc89c5d6/attachment.cls","path":"assets/test-class.cls","size":2010,"sha256":"6839f5f6ee4360ca4335b2ab72842cfb4343d2e617ac3f813b06fce47dd1eb3f","contentType":"text/x-tex"},{"id":"b5663d02-bd2c-5f16-8876-baf4fac12845","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b5663d02-bd2c-5f16-8876-baf4fac12845/attachment.cls","path":"assets/test-data-factory.cls","size":3517,"sha256":"e470a3302023bce058640bba51fe9b7d5e23dfddebebe8333ee276a1befafec9","contentType":"text/x-tex"},{"id":"d3a619c9-57af-59f4-96c4-33f7b86963ce","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d3a619c9-57af-59f4-96c4-33f7b86963ce/attachment.cls","path":"assets/trigger-action.cls","size":922,"sha256":"e5b3de77464250eab845f267c5f0e5b9ede81d4c7cd0318e17976d20d27c64fd","contentType":"text/x-tex"},{"id":"3f74340e-fe14-5d71-9a18-cd1eb640806a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3f74340e-fe14-5d71-9a18-cd1eb640806a/attachment.trigger","path":"assets/trigger.trigger","size":349,"sha256":"e74c4a84e6d31674b7cecd2ec58101d76bf96631649ad56b3bcadd066fafd750","contentType":"text/plain; charset=utf-8"},{"id":"70fb0a06-bea2-564d-ad51-004b5b3c14e4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/70fb0a06-bea2-564d-ad51-004b5b3c14e4/attachment.py","path":"hooks/scripts/apex-lsp-validate.py","size":8767,"sha256":"c511f582d09d8c6ce8ea0e2fcb311cac81c9ecdc2103e8f45ff8bd404cca2471","contentType":"text/x-python; charset=utf-8"},{"id":"76202f47-36ad-5ec2-b5ae-b4199c7af4fd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/76202f47-36ad-5ec2-b5ae-b4199c7af4fd/attachment.py","path":"hooks/scripts/llm_pattern_validator.py","size":15519,"sha256":"b199d89af212ce25caf83ad1e4007d2a4445db45fc3d150b55900d205fcf7ef4","contentType":"text/x-python; charset=utf-8"},{"id":"1c4fb40f-70fa-5040-b8e4-c839e0c62b56","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1c4fb40f-70fa-5040-b8e4-c839e0c62b56/attachment.py","path":"hooks/scripts/post-tool-validate.py","size":17651,"sha256":"e59f4f4a3fec818721cd64280395c3d8f8e7327a5c5272689f983d480ec80511","contentType":"text/x-python; charset=utf-8"},{"id":"4aae6efa-dfee-50a9-9f68-3c4d379c7e5e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4aae6efa-dfee-50a9-9f68-3c4d379c7e5e/attachment.py","path":"hooks/scripts/prettier-format.py","size":3521,"sha256":"7ca10c9be2cca898a17fe99cd37edd00dfebed49ae07b647812f0266678f777f","contentType":"text/x-python; charset=utf-8"},{"id":"a385e8c5-046e-594b-a150-5d8f0ef9aa10","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a385e8c5-046e-594b-a150-5d8f0ef9aa10/attachment.py","path":"hooks/scripts/validate_apex.py","size":8096,"sha256":"33c8b6591a208e98e060d25c9425dac7be66b3e50ba10db287dfbf05c25ca12a","contentType":"text/x-python; charset=utf-8"},{"id":"ed76a67b-83f0-5ea7-9222-655367f6d918","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ed76a67b-83f0-5ea7-9222-655367f6d918/attachment.md","path":"references/anti-patterns.md","size":19582,"sha256":"267ad0a95fc8f9049a43fee4c84ad6a1a5f2d441235a218d08b091171cb5290c","contentType":"text/markdown; charset=utf-8"},{"id":"6387a3d9-2793-5451-b0cf-aed7c6753e13","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6387a3d9-2793-5451-b0cf-aed7c6753e13/attachment.md","path":"references/automation-density-guide.md","size":8958,"sha256":"c6063763913133030a2deeabfaf9f5b81dc3f1a96be53e4f5b4c61da8904f651","contentType":"text/markdown; charset=utf-8"},{"id":"62bf72bf-53a0-5f0d-856b-2a4887926010","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/62bf72bf-53a0-5f0d-856b-2a4887926010/attachment.md","path":"references/best-practices.md","size":14457,"sha256":"a5f651a4964da1584be63d78849c0159cfcdffdf5cc2e2627f68424897ea3da6","contentType":"text/markdown; charset=utf-8"},{"id":"540fe4f7-1ada-580e-a996-e24b36d383f4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/540fe4f7-1ada-580e-a996-e24b36d383f4/attachment.md","path":"references/bulkification-guide.md","size":19533,"sha256":"7ee23cf9dbfafbc8c48738d0cc01b74c737cc8834baf6655bb17be9626f846fc","contentType":"text/markdown; charset=utf-8"},{"id":"5ffddd63-6dcb-58f9-9fde-dc5ac07d4f90","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5ffddd63-6dcb-58f9-9fde-dc5ac07d4f90/attachment.md","path":"references/code-review-checklist.md","size":6276,"sha256":"9e3ce14f9e61d98d09223c53a4c7be5654f66625faedaec9773fc1f65639e58b","contentType":"text/markdown; charset=utf-8"},{"id":"396d41d8-84d0-545e-b0d7-60f5c61e05a3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/396d41d8-84d0-545e-b0d7-60f5c61e05a3/attachment.md","path":"references/code-smells-guide.md","size":16269,"sha256":"8e61ea157e3ea253b3872e9ce88ddfe0f6c079b9cf39adf7eed8b4c161e38eb4","contentType":"text/markdown; charset=utf-8"},{"id":"04045e9c-2a12-550c-aba6-6afc3e5789d3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/04045e9c-2a12-550c-aba6-6afc3e5789d3/attachment.md","path":"references/design-patterns.md","size":32992,"sha256":"f5a341eddd3f660f9b60cb69b5bfb329eac01618cfe4a2d8ea9b987e224eb32a","contentType":"text/markdown; charset=utf-8"},{"id":"04d460c2-ebc6-53cd-81cb-e85377ec963f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/04d460c2-ebc6-53cd-81cb-e85377ec963f/attachment.md","path":"references/flow-integration.md","size":16276,"sha256":"f382cae8b8fc4bd67cb78822b92700f887b03d700fe307d6e03c8105cacb1296","contentType":"text/markdown; charset=utf-8"},{"id":"b4b548cc-053f-5d30-99f0-b96bf2293aac","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b4b548cc-053f-5d30-99f0-b96bf2293aac/attachment.md","path":"references/llm-anti-patterns.md","size":19279,"sha256":"8c7f11aed3ac95c99b66d52b4d2ed4555e8a33864a7263074bb3dc3feb357415","contentType":"text/markdown; charset=utf-8"},{"id":"4919c478-f325-5adf-96b3-13d0f5abe190","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4919c478-f325-5adf-96b3-13d0f5abe190/attachment.md","path":"references/naming-conventions.md","size":5546,"sha256":"a2073ea1fac98452366a8cf91cf100917f7821ca4727ff77683d4cb90373513d","contentType":"text/markdown; charset=utf-8"},{"id":"79022b85-b87c-5723-8c28-c84733125ec5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/79022b85-b87c-5723-8c28-c84733125ec5/attachment.md","path":"references/patterns-deep-dive.md","size":24293,"sha256":"2124fcb9a50ec5d15310033523ff4a174eac5f6e19e62ce846a1432875610135","contentType":"text/markdown; charset=utf-8"},{"id":"69df1579-3cde-52d7-bba8-aaf58b2748b9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/69df1579-3cde-52d7-bba8-aaf58b2748b9/attachment.md","path":"references/security-guide.md","size":17866,"sha256":"477475f6c6101b50d417196dcbcd207f44e39646cf08d3802f4b91fe16c91ab7","contentType":"text/markdown; charset=utf-8"},{"id":"4f224ecc-33af-5117-92ce-89183f63e43b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4f224ecc-33af-5117-92ce-89183f63e43b/attachment.md","path":"references/security-quick-reference.md","size":6681,"sha256":"3e11cae4fe8efcb6ffc80846bde4cd204447b2d52b38658f4197c1aecbbc81a6","contentType":"text/markdown; charset=utf-8"},{"id":"f66f1fa0-0613-533b-9d79-79dd2dbbd128","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f66f1fa0-0613-533b-9d79-79dd2dbbd128/attachment.md","path":"references/solid-principles.md","size":10728,"sha256":"5d04edb02c22a9cf8eb596f9423d9a4918942e62345673089c8ef890cd46ab73","contentType":"text/markdown; charset=utf-8"},{"id":"d146e726-e210-5f88-afe7-5611cd7bd3e6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d146e726-e210-5f88-afe7-5611cd7bd3e6/attachment.md","path":"references/testing-guide.md","size":11570,"sha256":"f8e9c6f384734ad8a6aa624df727048763f04757fc69d69490aa2e20338864d1","contentType":"text/markdown; charset=utf-8"},{"id":"8e397ace-0373-54bd-903e-010405b287d1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8e397ace-0373-54bd-903e-010405b287d1/attachment.md","path":"references/testing-patterns.md","size":22182,"sha256":"271fdbd6e4259bd51ba0c09cbb49abf9cd290081f769cd23fb7cf75c27dbb2c9","contentType":"text/markdown; charset=utf-8"},{"id":"e65c0f47-2185-5a02-97d9-d481e79a7dfe","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e65c0f47-2185-5a02-97d9-d481e79a7dfe/attachment.md","path":"references/triangle-pattern.md","size":9370,"sha256":"ec79129b82a19e76afd1e127212b957ebfe6e16d8b9156b9f6254fab2eefe7e0","contentType":"text/markdown; charset=utf-8"},{"id":"70415002-877a-59b5-bb7a-8c77b0ccef6c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/70415002-877a-59b5-bb7a-8c77b0ccef6c/attachment.md","path":"references/trigger-actions-framework.md","size":11050,"sha256":"8217b5c73dd01013187ef423925e7c0216a83afeb8c9541f6ce99ec08b88943b","contentType":"text/markdown; charset=utf-8"},{"id":"80c2f4b2-4cf3-5ba6-9c33-e375a614a4a4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/80c2f4b2-4cf3-5ba6-9c33-e375a614a4a4/attachment.md","path":"references/troubleshooting.md","size":18279,"sha256":"063ad82b6573c95c7aae86309acd7a078cac6597759dd3ca09cf51e7d2678db1","contentType":"text/markdown; charset=utf-8"}],"bundle_sha256":"2b17ac5c8a1c7db8557e2e0a3013aff5e68bbb7709f6781b060219a59c838c35","attachment_count":36,"text_attachments":35,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":1,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/sf-apex/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"testing-qa","category_label":"Testing"},"exact_dupes_collapsed_into_this":0},"license":"MIT","version":"v1","category":"testing-qa","metadata":{"author":"Jag Valaiyapathy","scoring":"150 points across 8 categories","version":"1.1.0"},"import_tag":"clean-skills-v1","description":"Generates and reviews Salesforce Apex code with 150-point scoring. TRIGGER when: user writes, reviews, or fixes Apex classes, triggers, test classes, batch/queueable/schedulable jobs, or touches .cls/.trigger files. DO NOT TRIGGER when: LWC JavaScript (use sf-lwc), Flow XML (use sf-flow), SOQL-only queries (use sf-soql), or non-Salesforce code.\n"}},"renderedAt":1782981210885}

sf-apex: Salesforce Apex Code Generation and Review Use this skill when the user needs production Apex : new classes, triggers, selectors, services, async jobs, invocable methods, test classes, or evidence-based review of existing / code. When This Skill Owns the Task Use when the work involves: - Apex class generation or refactoring - trigger design and trigger-framework decisions - , Queueable, Batch, Schedulable, or test-class work - review of bulkification, sharing, security, testing, or maintainability Delegate elsewhere when the user is: - editing LWC JavaScript / HTML / CSS → sf-lwc -…