# Deprecated Salesforce Patterns — Do NOT Generate These > AI: Read this file FIRST before generating any Salesforce code. Every pattern here is wrong. Use the GOOD alternative instead. > Release: Summer '26 | API: v67.0 | Updated: 2026-06 --- ## Apex ### SOQL in loops - BAD: ```apex for (Account acc : accounts) { List contacts = [SELECT Id FROM Contact WHERE AccountId = :acc.Id]; } ``` - GOOD: ```apex Map> contactsByAccount = new Map>(); for (Contact c : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]) { if (!contactsByAccount.containsKey(c.AccountId)) { contactsByAccount.put(c.AccountId, new List()); } contactsByAccount.get(c.AccountId).add(c); } ``` - WHY: Governor limit — 100 SOQL queries per transaction. Loops hit this instantly with bulk data. ### DML without error handling - BAD: ```apex insert contacts; ``` - GOOD: ```apex List results = Database.insert(contacts, false); for (Database.SaveResult sr : results) { if (!sr.isSuccess()) { for (Database.Error err : sr.getErrors()) { System.debug(LoggingLevel.ERROR, err.getStatusCode() + ': ' + err.getMessage()); } } } ``` - WHY: Partial success handling. `insert` throws on first failure, losing all remaining records. ### Business logic in triggers - BAD: ```apex trigger AccountTrigger on Account (before insert, before update) { for (Account acc : Trigger.new) { if (acc.Industry == 'Technology') { acc.Rating = 'Hot'; } } } ``` - GOOD: ```apex trigger AccountTrigger on Account (before insert, before update, after insert, after update) { AccountTriggerHandler.run(Trigger.operationType, Trigger.new, Trigger.oldMap); } ``` ```apex public with sharing class AccountTriggerHandler { public static void run(System.TriggerOperation operation, List newList, Map oldMap) { switch on operation { when BEFORE_INSERT, BEFORE_UPDATE { setRatingForTechAccounts(newList); } } } private static void setRatingForTechAccounts(List accounts) { for (Account acc : accounts) { if (acc.Industry == 'Technology') { acc.Rating = 'Hot'; } } } } ``` - WHY: One trigger per object. Logic in handler classes for testability and maintainability. ### Hardcoded Record Type IDs - BAD: ```apex acc.RecordTypeId = '012000000000001'; ``` - GOOD: ```apex acc.RecordTypeId = Schema.SObjectType.Account.getRecordTypeInfosByDeveloperName() .get('Enterprise').getRecordTypeId(); ``` - WHY: IDs differ between orgs and sandboxes. Hardcoded IDs break on deploy. ### Hardcoded IDs - BAD: ```apex Id priceBookId = '01s000000000001'; ``` - GOOD: ```apex Id priceBookId = [SELECT Id FROM Pricebook2 WHERE IsStandard = true LIMIT 1].Id; // Or use Custom Metadata Types for configurable IDs MyConfig__mdt config = MyConfig__mdt.getInstance('Default'); Id priceBookId = config.PriceBookId__c; ``` - WHY: IDs are org-specific. Use queries, Custom Metadata Types, or Custom Labels. ### Missing explicit sharing declaration - BAD: ```apex public class AccountService { public List getAccounts() { return [SELECT Id, Name FROM Account]; } } ``` - GOOD: ```apex public with sharing class AccountService { public List getAccounts() { return [SELECT Id, Name FROM Account]; } } ``` - WHY: Since API v67.0 (Summer '26), classes without an explicit sharing declaration default to `with sharing`. In v66.0 and earlier, they default to `without sharing`. Always declare sharing explicitly (`with sharing`, `without sharing`, or `inherited sharing`) to make intent clear and avoid behavior changes on API version upgrade. ### WITH SECURITY_ENFORCED (removed in v67.0) - BAD: ```apex List accounts = [SELECT Id, Name FROM Account WITH SECURITY_ENFORCED]; ``` - GOOD: ```apex List accounts = [SELECT Id, Name FROM Account WITH USER_MODE]; ``` - WHY: `WITH SECURITY_ENFORCED` is removed in API v67.0+ — code using it will not compile. Use `WITH USER_MODE` instead. It has better error reporting (returns all FLS errors, not just the first) and handles polymorphic fields correctly. ### Database operations without explicit access mode - BAD: ```apex List accounts = [SELECT Id, Name FROM Account]; insert accounts; ``` - GOOD: ```apex List accounts = [SELECT Id, Name FROM Account WITH USER_MODE]; insert as user accounts; ``` - WHY: Since API v67.0 (Summer '26), database operations default to User Mode — they enforce the current user's CRUD, FLS, and sharing. **Default to User Mode (`WITH USER_MODE` / `AccessLevel.USER_MODE` / `as user`) for any operation acting on a user's behalf — including trigger logic.** Use System Mode (`WITH SYSTEM_MODE` / `as system`) only when you deliberately need to bypass FLS, CRUD, and sharing, and say why in a comment. Setting the mode explicitly is not enough — picking System Mode by default silently runs without security checks. ### String concatenation in SOQL - BAD: ```apex String query = 'SELECT Id FROM Account WHERE Name = \'' + userInput + '\''; List accounts = Database.query(query); ``` - GOOD: ```apex String query = 'SELECT Id FROM Account WHERE Name = :userInput'; List accounts = Database.query(query); ``` - WHY: SOQL injection vulnerability. Always use bind variables. ### Tests without bulk data - BAD: ```apex @isTest static void testInsert() { Account acc = new Account(Name = 'Test'); insert acc; Assert.isNotNull(acc.Id, 'Account should have an Id after insert'); } ``` - GOOD: ```apex @isTest static void testInsertBulk() { List accounts = new List(); for (Integer i = 0; i < 200; i++) { accounts.add(new Account(Name = 'Test ' + i)); } insert accounts; Assert.areEqual(200, [SELECT COUNT() FROM Account], 'All 200 accounts should be inserted'); } ``` - WHY: Triggers and automation fire on bulk operations. Testing with 1 record hides governor limit issues. ### Legacy assertion methods (System.assert*) - BAD: ```apex System.assertEquals(200, results.size()); System.assert(result.isSuccess()); System.assertNotEquals(null, acc.Id); ``` - GOOD: ```apex Assert.areEqual(200, results.size(), 'Expected 200 results'); Assert.isTrue(result.isSuccess(), 'Save should succeed'); Assert.isNotNull(acc.Id, 'Account should have an Id'); ``` - WHY: The `Assert` class (Winter '23) is the modern assertion API. It has explicit, type-safe methods (`areEqual`, `areNotEqual`, `isTrue`, `isFalse`, `isNull`, `isNotNull`, `isInstanceOfType`, `fail`), clearer failure output, and you can't accidentally swap the expected value with the message string. `System.assert*` still compiles but is no longer the recommended style. ### @future for complex async - BAD: ```apex @future public static void processRecords(Set recordIds) { // Can't chain, can't monitor, limited parameters } ``` - GOOD: ```apex public class RecordProcessor implements Queueable { private List recordIds; public RecordProcessor(List recordIds) { this.recordIds = recordIds; } public void execute(QueueableContext context) { // Can chain, can pass complex objects, can monitor via AsyncApexJob } } ``` - WHY: Queueable supports chaining, complex parameters, and job monitoring. `@future` is fire-and-forget only. ### Abstract/override methods without access modifiers - BAD: ```apex public virtual class BaseHandler { virtual void handleRecord(SObject record) { } } public class ChildHandler extends BaseHandler { override void handleRecord(SObject record) { } } ``` - GOOD: ```apex public virtual class BaseHandler { public virtual void handleRecord(SObject record) { } } public class ChildHandler extends BaseHandler { public override void handleRecord(SObject record) { } } ``` - WHY: Since API v65.0 (Winter '26), `abstract` and `override` methods require an explicit access modifier (`protected`, `public`, or `global`). Omitting it causes a compilation error. ### String concatenation for multiline text - BAD: ```apex String json = '{\n' + ' "Name": "' + acc.Name + '",\n' + ' "Type": "' + acc.Type + '"\n' + '}'; ``` - GOOD: ```apex String json = ''' { "Name": "${name}", "Type": "${type}" }'''.template(new Map{ 'name' => acc.Name, 'type' => acc.Type }); ``` - WHY: Apex now supports multiline strings (triple single quotes `'''`) and `String.template()` with named `${variable}` placeholders. Available in all API versions since Summer '26. ### Workflow Rules - BAD: Creating or referencing Workflow Rules for field updates, email alerts, or outbound messages. - GOOD: Use Flow Builder (Record-Triggered Flows) for all automation. - WHY: Workflow Rules deprecated since Spring '23. Salesforce will remove them. Migrate existing ones with the Migrate to Flow tool. ### Process Builder - BAD: Creating or referencing Process Builder for any automation. - GOOD: Use Flow Builder (Record-Triggered Flows) for all automation. - WHY: Process Builder deprecated since Spring '23. Cannot be created in new orgs. Migrate existing ones with the Migrate to Flow tool. --- ## LWC ### Aura components for new development - BAD: ```html

{!v.greeting}

``` - GOOD: ```html ``` ```javascript import { LightningElement } from 'lwc'; export default class MyComponent extends LightningElement { greeting = 'Hello'; } ``` - WHY: Aura is in maintenance mode. LWC is faster, uses web standards, and is the only actively developed component framework. ### Template expressions with JS logic - BAD: ```html ``` - GOOD: ```html ``` ```javascript get nextCount() { return this.count + 1; } get hasItems() { return this.items.length > 0; } ``` - WHY: LWC templates don't support JavaScript expressions. Use getter properties in the JS class. ### Direct DOM manipulation - BAD: ```javascript this.template.querySelector('.counter').textContent = this.count; ``` - GOOD: ```javascript // Use reactive properties — the template re-renders automatically this.count = newValue; ``` - WHY: LWC uses a reactive rendering model. Direct DOM manipulation bypasses it and causes state inconsistencies. ### Imperative Apex without error handling - BAD: ```javascript import getAccounts from '@salesforce/apex/AccountController.getAccounts'; async connectedCallback() { this.accounts = await getAccounts(); } ``` - GOOD: ```javascript import getAccounts from '@salesforce/apex/AccountController.getAccounts'; async connectedCallback() { try { this.accounts = await getAccounts(); } catch (error) { this.dispatchEvent( new ShowToastEvent({ title: 'Error', message: error.body.message, variant: 'error' }) ); } } ``` - WHY: Apex calls fail (permissions, governor limits, network). Unhandled errors leave the component in a broken state. ### Deprecated template directives - BAD: ```html ``` - GOOD: ```html ``` - WHY: `if:true` and `if:false` are deprecated since Spring '23. Use `lwc:if`, `lwc:elseif`, `lwc:else`. ### lightning/uiGraphQLApi module - BAD: ```javascript import { gql, graphql } from 'lightning/uiGraphQLApi'; ``` - GOOD: ```javascript import { gql, graphql } from 'lightning/graphql'; ``` - WHY: `lightning/graphql` supersedes `lightning/uiGraphQLApi` since Winter '26 (API v65.0). The new module supports optional fields (inaccessible fields are omitted instead of failing), dynamic queries with string interpolation, and mutations (GA in Spring '26). --- ## SOQL ### SELECT * (doesn't exist) - BAD: ```sql SELECT * FROM Account ``` - GOOD: ```sql SELECT Id, Name, Industry, Rating FROM Account ``` - WHY: SOQL has no `SELECT *`. AI models hallucinate this. Always list fields explicitly. ### Queries without LIMIT - BAD: ```sql SELECT Id, Name FROM Account WHERE Industry = 'Technology' ``` - GOOD: ```sql SELECT Id, Name FROM Account WHERE Industry = 'Technology' LIMIT 200 ``` - WHY: Governor limit — 50,000 rows per transaction. Unbounded queries risk hitting this in production. ### Nested subqueries beyond 1 level - BAD: ```sql SELECT Id, (SELECT Id, (SELECT Id FROM Tasks) FROM Contacts) FROM Account ``` - GOOD: ```sql // Query 1: Accounts with Contacts SELECT Id, (SELECT Id FROM Contacts) FROM Account // Query 2: Tasks for those contacts SELECT Id, WhoId FROM Task WHERE WhoId IN :contactIds ``` - WHY: SOQL doesn't support nested subqueries beyond one level. This will throw a runtime error. ### COUNT() with other fields - BAD: ```sql SELECT Name, COUNT() FROM Account GROUP BY Name ``` - GOOD: ```sql SELECT Name, COUNT(Id) cnt FROM Account GROUP BY Name ``` - WHY: `COUNT()` can't be mixed with other fields. Use `COUNT(fieldName)` with an alias in aggregate queries. ### Queries in loops - BAD: ```apex for (Account acc : accounts) { acc.Primary_Contact__c = [SELECT Id FROM Contact WHERE AccountId = :acc.Id LIMIT 1].Id; } ``` - GOOD: ```apex Map primaryContacts = new Map(); for (Contact c : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds ORDER BY CreatedDate DESC]) { if (!primaryContacts.containsKey(c.AccountId)) { primaryContacts.put(c.AccountId, c); } } for (Account acc : accounts) { Contact primary = primaryContacts.get(acc.Id); if (primary != null) { acc.Primary_Contact__c = primary.Id; } } ``` - WHY: Same as SOQL-in-loops above. Query once, filter in memory. --- ## Flow ### BEFORE + AFTER in one Record-Triggered Flow - BAD: A single Record-Triggered Flow that runs in both Before Save and After Save context. - GOOD: Create two separate Record-Triggered Flows — one for Before Save, one for After Save. - WHY: Since Spring '24, combining BEFORE and AFTER in one flow causes unexpected order-of-execution issues. Salesforce recommends separating them. ### Get Records inside loops - BAD: A Loop element containing a Get Records element. - GOOD: Use Get Records before the Loop. Use a Collection Filter or Assignment to process data inside the loop. - WHY: Each Get Records inside a loop counts against the SOQL governor limit. Same principle as SOQL-in-loops in Apex. ### No fault paths - BAD: Flow with DML or callout elements but no Fault connectors. - GOOD: Add Fault connectors on every Create/Update/Delete/Callout element. Route to a Screen or Platform Event that logs the error. - WHY: Flows fail silently in production without fault paths. Users see a generic error page with no actionable information. ### Auto-Launched Flows for record triggers - BAD: Creating an Auto-Launched Flow and wiring it via Process Builder or Workflow Rule. - GOOD: Use Record-Triggered Flows directly. - WHY: Process Builder and Workflow Rules are deprecated. Record-Triggered Flows are the native replacement with better performance and debugging. --- ## API ### Outdated API versions - BAD: Using API version below v67.0 in any new code, metadata, or integration. - GOOD: Use API version v67.0 (Summer '26) for all new work. - WHY: API versions 21.0–30.0 are already retired (calls fail); 31.0–40.0 are deprecated in Summer '27 and retired in Summer '28. Old API versions also miss the v67.0 security model (User Mode by default, sharing by default) and other fixes. See api/versions.md for the full schedule. ### SOAP API for new integrations - BAD: Using SOAP API (enterprise.wsdl / partner.wsdl) for new integrations. - GOOD: Use REST API for real-time operations. Use Bulk API 2.0 for data loading (>2,000 records). - WHY: SOAP API is legacy. REST API is simpler, supports JSON, and has better tooling. Additionally, SOAP API `login()` call for versions 31.0–64.0 is being retired in Summer '27. ### Bulk API 1.0 - BAD: Using Bulk API 1.0 (XML batches, `/job` endpoint). - GOOD: Use Bulk API 2.0 (`/jobs/ingest` endpoint, CSV upload). - WHY: Bulk API 2.0 is simpler (no batch management), supports CSV natively, and handles retry automatically. ### Composite API without limits awareness - BAD: Sending >25 subrequests in a single Composite API call without checking limits. - GOOD: Batch subrequests into groups of 25. Use `allOrNone` parameter for transaction control. - WHY: Composite API maximum is 25 subrequests per call. Exceeding this returns an error. ### Standard Volume Platform Events - BAD: Creating or using Standard Volume Platform Events. - GOOD: Use High Volume Platform Events. Migrate existing ones with the Salesforce migration tool (Tooling API or Metadata API). - WHY: Standard Volume Platform Events are no longer supported starting Winter '27. Migrate before then — unmigrated events will stop working. ### OAuth 2.0 username-password flow - BAD: Using the OAuth 2.0 username-password flow for Connected App integrations. - GOOD: Use the OAuth 2.0 web-server flow (authorization code) or client credentials flow. - WHY: Username-password flow is being retired in Winter '27. It bypasses MFA and is a security risk. ### Salesforce to Salesforce - BAD: Using Salesforce to Salesforce for org-to-org data sharing. - GOOD: Use Partner Cloud, Data Cloud, MuleSoft Anypoint, or MuleSoft for Flow. - WHY: Salesforce to Salesforce is being retired in Spring '27. --- ## General Tooling ### Metadata API for simple deployments - BAD: Using Metadata API directly or Ant Migration Tool for deployments. - GOOD: Use Salesforce CLI: `sf project deploy start --source-dir force-app` - WHY: Ant Migration Tool is deprecated. Salesforce CLI (sf) is the current standard for all deployments. ### Force.com IDE - BAD: Using Force.com IDE, MavensMate, or Illuminated Cloud 1. - GOOD: Use VS Code + Salesforce Extension Pack. - WHY: Force.com IDE discontinued in 2018. VS Code is the only officially supported IDE for Salesforce development. ### Classic UI development - BAD: Building Visualforce pages, JavaScript buttons, or S-Controls for new functionality. - GOOD: Build with LWC (Lightning Web Components). Use Visualforce only for edge cases not yet supported in Lightning. - WHY: Salesforce Classic is in maintenance mode. Lightning Experience is the standard UI platform. # Salesforce Governor Limits — Current Numbers > AI: Reference these numbers when generating Apex code. Never hardcode values that exceed these limits. When in doubt, add LIMIT clauses and bulkify. > Release: Summer '26 | API: v67.0 | Updated: 2026-06 > Source: Verified against Salesforce Apex Developer Guide and Salesforce Developer Limits and Allocations Quick Reference (Summer '26, last updated May 8, 2026). Cursor limits added in Spring '26 (API v66.0). No core limit numbers have changed since Summer '25. --- ## Apex Per-Transaction Limits This table lists synchronous and asynchronous limits. Async = Batch Apex, Queueable, @future, scheduled. | Limit | Sync | Async | |---|---|---| | SOQL queries | 100 | 200 | | SOQL rows retrieved | 50,000 | 50,000 | | Database.getQueryLocator rows | 10,000 | 10,000 | | SOSL queries | 20 | 20 | | SOSL rows per query | 2,000 | 2,000 | | DML statements | 150 | 150 | | DML rows | 10,000 | 10,000 | | Stack depth (recursive triggers via insert/update/delete) | 16 | 16 | | Callouts (HTTP/Web Service) | 100 | 100 | | Callout timeout (cumulative) | 120,000 ms | 120,000 ms | | @future methods invoked | 50 | 0 in batch/future; 50 in queueable | | Queueable jobs enqueued (System.enqueueJob) | 50 | 1 | | sendEmail invocations | 10 | 10 | | Heap size | 6 MB | 12 MB | | CPU time | 10,000 ms | 60,000 ms | | Max transaction execution time | 10 minutes | 10 minutes | | Push notification method calls | 10 | 10 | | Push notifications per call | 2,000 | 2,000 | | EventBus.publish (publish immediately) | 150 | 150 | Notes: - Email services heap size: **50 MB** (override of the 6 MB sync heap for email services). - "Describes" no longer has a hard per-transaction limit — describe operations are cached and don't count. - Scheduled Apex uses synchronous limits despite being async-launched. - For Bulk API/Bulk API 2.0 transactions, the effective limit is the higher of the sync/async values. - **Trigger batch size: 200 for regular DML triggers, 2,000 for Platform Event and Change Data Capture triggers.** AI commonly assumes 200 for all triggers — wrong for PE/CDC. ## Apex Cursor Limits (Spring '26, API v66.0+) Cursors are tracked separately from regular SOQL — they have their own row and call quotas. | Limit | Sync | Async | |---|---|---| | Total rows across all cursors per transaction | 50,000,000 | 50,000,000 | | Cursor.fetch() calls per transaction | 100 | 100 | | New cursors created per 24 hours | 10,000 | 10,000 | | New cursor + pagination rows per 24 hours | 100,000,000 | 100,000,000 | | Pagination cursor instances per transaction | 50 | 50 | | Pagination cursor instances per 24 hours | 200,000 | 200,000 | | Rows across all pagination cursors per transaction | 100,000 | 100,000 | | Rows per page from a pagination cursor | 2,000 | 2,000 | **Elastic Limits (Beta, Summer '26):** Orgs can enable elastic limits for Queueable and @future jobs. The elastic limit is 2× the org's licensed daily async job limit (capped at +10M executions). Jobs exceeding the licensed limit are throttled instead of failing. ## Batch Apex Limits | Limit | Value | |---|---| | Max batch size (scope) | 2,000 | | Default batch size | 200 | | QueryLocator rows | 50,000,000 | | Concurrent batch jobs queued or active | 5 | | Flex queue (Holding status) | 100 | | Concurrent start method executions | 1 | | execute re-tries on failure | 0 | ## Platform / Org-Level Limits These limits aren't per-transaction — they apply across the entire org over time. AI should be aware they exist but doesn't need exact numbers when generating code. | Limit | Value | |---|---| | Daily async Apex executions (batch + queueable + future + scheduled) | 250,000 or 200 × user licenses, whichever is greater | | Concurrent long-running sync transactions (>5s each) | 10–50 (based on org license count) | | Max scheduled Apex classes concurrently | 100 (5 in Developer Edition) | Why this matters: even when a single transaction stays within per-transaction limits, an org can hit daily caps under load. Prefer Batch Apex over many small Queueable jobs when processing huge volumes. ## Platform Events Per-transaction (Apex): | Limit | Value | |---|---| | `EventBus.publish` calls per transaction | 150 | | Per-call payload | 1 event (use list overload to publish multiple) | | Max event message size | 1 MB | | Test method publishing limit (`@isTest`) | 500 event messages | Common allocations (per org, by edition): | Description | Perf/Unlim | Enterprise | Developer | Pro (+API) | |---|---|---|---|---| | Max platform event definitions per org | 100 | 50 | 5 | 5 | | Max concurrent CometD subscribers (all event types, all channels) | 2,000 | 1,000 | 20 | 20 | | Max Process Builder + Flow subscribers per platform event | 4,000 | 4,000 | 4,000 | 5 | | Max **active** PB + Flow subscribers per platform event | 2,000 | 2,000 | 2,000 | 5 | | Max custom channels for Platform Events (excl. Real-Time Event Monitoring) | 100 | 100 | 100 | 100 | | Max custom channels for Real-Time Event Monitoring | 3 | 3 | 3 | 3 | | Max distinct custom platform events per channel (channel members) | 50 | 50 | 5 | 5 | | Max Real-Time Event Monitoring events per channel | 10 | 10 | 10 | 10 | Default event publishing & delivery (no add-on): | Description | Perf/Unlim | Enterprise / Pro (+API) | Developer | |---|---|---|---| | Event delivery — max messages delivered to API subscribers in last 24h (shared with CDC) | 50,000 | 25,000 | 10,000 | | Event publishing — max messages published per hour | 250,000 | 250,000 | 50,000 | | Event retention (high-volume) | 72 hours | 72 hours | 72 hours | Notes: - Delivery allocation applies only to **API subscribers**: Pub/Sub API, CometD, empApi Lightning components, event relays. **Does NOT apply** to Apex triggers, flows, or Process Builder processes. - Delivery allocation is **shared** between high-volume Platform Events and Change Data Capture events. - Standard Volume Platform Events (defined in API v44.0 and earlier) are no longer supported starting **Winter '27**. Migrate to High Volume Platform Events. - A platform event add-on license adds +100,000 events/day delivery, +25,000 events/hour publishing, and a 3M monthly entitlement with grace allocation. - When the publishing limit is exceeded, the publish call fails with `LIMIT_EXCEEDED`. When the delivery limit is exceeded, subscribers receive `403::Organization total events daily limit exceeded` (CometD) or `sfdc.platform.eventbus.grpc.subscription.limit.exceeded` (Pub/Sub API). ### Pub/Sub API | Limit | Value | |---|---| | Max event message size | 1 MB | | Max recommended batch in a `PublishRequest` | 3 MB (gRPC hard cap is 4 MB) | | Recommended events per publish request | ≤ 200 | | Max events per `FetchRequest` / `ManagedFetchRequest` | 100 | | Max managed subscriptions per org | 200 | | gRPC concurrent streams per channel | 1,000 (HTTP/2 underlying connection) | Note: Pub/Sub API is the recommended subscription channel for new integrations. CometD remains supported but is in maintenance mode. ## Change Data Capture Per org, by edition: | Description | Perf/Unlim | Enterprise | Developer | |---|---|---|---| | Max concurrent CometD subscribers (shared across all event types) | 2,000 | 1,000 | 20 | | Max event message size | 1 MB | 1 MB | 1 MB | | Max entities selected for change notifications (across all channels) | 5 | 5 | 5 | | Max custom channels for CDC (separate from Platform Events) | 100 | 100 | 100 | | Event delivery — max messages delivered to API subscribers in last 24h (shared with high-volume PE) | 50,000 | 25,000 | 10,000 | | Event retention | 72 hours | 72 hours | 72 hours | Notes: - **No publishing limit** for CDC — Salesforce generates change events from record DML, not user-published. - 5-entity limit applies to selections you make and selections by unmanaged/managed packages (except AppExchange-released managed packages). - Multiple DML on the same record in the same transaction produce **one** change event with the committed final state, not one per DML. - Use custom channels with stream filtering to reduce delivery allocation usage. - If a change event exceeds 1 MB, a **gap event** is published instead. ## Bulk API 2.0 | Limit | Value | |---|---| | Max file size per upload (base64 encoded) | 150 MB | | Recommended raw data size (pre-encoding) | 100 MB | | Records per batch (auto-split) | 10,000 | | Max fields per record | 5,000 | | Max characters per field | 131,072 | | Max characters per record | 400,000 | | Daily max records per ingest job | 150,000,000 | | Daily batch allocations (shared with Bulk API v1) | 15,000 batches per rolling 24h | | Daily query jobs (Bulk API 2.0) | 10,000 | | Daily query results storage | 1 TB | | Per-batch processing timeout | 5 minutes | | Per-batch retry attempts | 20 | | Bulk API CPU time (isolated from Apex limit) | 60,000 ms | | Job lifespan after terminal state (completed/aborted/failed) | 7 days | | Max time a job can remain open | 24 hours | Note: The Bulk API has no explicit "concurrent jobs" cap. Throughput is governed by the 15,000 daily batches and the 250,000 daily async Apex executions allocation (for any code processing the results). ## REST API — Composite | Limit | Value | |---|---| | Composite Batch subrequests | 25 per call | | Composite Batch timeout | 10 minutes | | Composite Graph total nodes per payload | 500 | | Composite Graph graphs per payload | 75 | | Composite Graph max depth | 15 | | Composite Graph different node types per payload | 15 | | Composite Graph max failed graphs before halt | 14 | Note: Composite Graph counts a node as "different" when it uses a different API version, HTTP method, or sObject type. ## REST API — sObject Tree | Limit | Value | |---|---| | Max records (total across all trees) | 200 | | Max different object types | 5 | | Max tree depth | 5 levels | ## REST API — General | Limit | Value | |---|---| | API calls per 24h | Varies by edition (org-level) | | Query batch size (Sforce-Query-Options header) | 200–2,000 (default 2,000) | ## API Request Limits (REST + SOAP) | Limit | Value | |---|---| | Concurrent inbound requests longer than 20s — Developer Edition / Trial | 5 | | Concurrent inbound requests longer than 20s — Production / Sandbox | 25 | | Concurrent requests shorter than 20s | unlimited | | REST/SOAP call timeout (non-query) | 10 minutes | | Query call timeout (SOQL) | 120 seconds (server) / 32 minutes (total: 2 min execute + 30 min process) | | Combined URI + headers length per REST call | 16,384 bytes | | Stored third-party access/refresh token length | up to 10,000 chars | Daily API call allocation per edition (`Total Calls Per 24-Hour Period`): - Developer Edition: 15,000 - Enterprise / Professional+API: `100,000 + (licenses × per-license calls)` (Salesforce/Platform license = 1,000) - Unlimited / Performance: `100,000 + (licenses × per-license calls)` (Salesforce/Platform license = 5,000) ## Static Apex Limits | Limit | Value | |---|---| | Default callout timeout | 10 seconds (max 120s per `setTimeout`) | | Max callout request/response size | 6 MB (sync) / 12 MB (async) | | Max SOQL query runtime before cancel | 120 seconds | | Max class + trigger code units per deployment | 7,500 | | Max characters per class | 1,000,000 | | Max characters per trigger | 1,000,000 | | Total Apex code per org | 6 MB (10 MB for scratch orgs) | | Method bytecode size | 65,535 instructions (compiled) | | Batch Apex QueryLocator max rows | 50,000,000 | | Daily test classes queued (production) | greater of 500 or 10 × test class count | | Daily test classes queued (sandbox/DE) | greater of 500 or 20 × test class count | Notes: - Org-wide 6 MB code limit excludes managed packages (1GP/2GP) and `@isTest` classes. Increase via support case. - HTTP callout req/resp size counts against heap. ## SOQL & SOSL Specifics | Limit | Value | |---|---| | Max SOQL statement length | 100,000 characters | | Max string in WHERE clause | 4,000 characters | | Max junction IDs per query | 500 (returns MALFORMED_QUERY beyond) | | Max SOQL results per request | 2,000 (API v28+) | | OFFSET clause max | 2,000 rows | | Child-to-parent relationships per query | max 55 | | Parent-to-child relationships per query | max 20 | | Relationship depth (e.g. `Contact.Account.Owner.Name`) | max 5 levels | | Parent-to-child nesting via REST/SOAP/Apex (v58+) | max 5 levels (not for big objects / external objects / Bulk API) | | Cursor / query locator result lifespan | 2 days | | Max SOSL `SearchQuery` length | 10,000 chars (logical operators stripped above 4,000) | | Max SOSL results | 2,000 (API v28+) | ## Flows Flows execute inside an Apex transaction and inherit per-transaction Apex governor limits. | Limit | Value | Source | |---|---|---| | SOQL queries per transaction | Same as Apex (100 sync / 200 async) | inherited | | DML statements per transaction | Same as Apex (150) | inherited | | Records retrieved by SOQL | Same as Apex (50,000) | inherited | | Records processed by DML | Same as Apex (10,000) | inherited | | CPU time per transaction | Same as Apex (10,000 ms sync / 60,000 ms async) | inherited | | Heap size | Same as Apex (6 MB sync / 12 MB async) | inherited | | Max flow interview size | 1,000,000 bytes (~1 MB) | multi-source | | Duplicate updates per batch | 12 | Trailhead "Avoid Flow Limits" | Notes: - **The 2,000-elements-per-flow runtime limit was REMOVED in API v57.0 (Spring '23).** Code generators and older guides may still cite it — don't. - A single flow interview is one running instance of a flow. Multiple interviews can run in the same transaction; one interview can span multiple transactions (via Pause elements or Scheduled Paths). - Bulkification works automatically for `Get Records` / `Create Records` / `Update Records` / `Delete Records` in record-triggered flows — Salesforce pauses interviews at these elements and executes them together as one DML/SOQL operation. **Unverified (not in any AI-accessible source):** - Interviews per transaction cap (commonly cited as 2,000, source unknown) - Scheduled paths per flow (commonly cited as 10) - Scheduled path max time range from trigger (commonly cited as months) - Daily paused/waiting flow interviews cap For these, consult the official Flow Limits and Considerations page in Salesforce Help (`platform.flow_considerations_limit.htm`). ## Data Storage | Limit | Value | |---|---| | Record storage | Varies by edition + per-user allocation | | File storage | Varies by edition + per-user allocation | | Max records returned by report | 2,000 (UI), 2,000 (API) | | Max report filters | 20 | | Big Object max rows per insert | 100 | ## Commonly Hit Limits These are the limits AI-generated code violates most frequently: 1. **SOQL 100 query limit** — #1 cause of production failures. Always bulkify. 2. **DML 150 statement limit** — Don't DML inside loops. 3. **CPU 10s limit** — Watch nested loops and complex string operations. 4. **50,000 row limit** — Always add WHERE clauses and LIMIT. 5. **Heap 6MB limit** — Don't load large datasets into memory. Use Batch Apex for large volumes. # Apex Best Practices — Current Patterns > AI: Use these patterns when generating Apex code. These are the current recommended approaches for Summer '26. > Release: Summer '26 | API: v67.0 | Updated: 2026-06 --- ## Trigger Framework ### One trigger per object, logic in handler class ```apex trigger OpportunityTrigger on Opportunity ( before insert, before update, before delete, after insert, after update, after delete, after undelete ) { OpportunityTriggerHandler.run(Trigger.operationType, Trigger.new, Trigger.old, Trigger.newMap, Trigger.oldMap); } ``` ```apex public with sharing class OpportunityTriggerHandler { public static void run( System.TriggerOperation operation, List newList, List oldList, Map newMap, Map oldMap ) { switch on operation { when BEFORE_INSERT { setDefaults(newList); } when AFTER_INSERT { createRelatedRecords(newList); } when BEFORE_UPDATE { validateChanges(newList, oldMap); } when AFTER_UPDATE { syncExternalSystem(newList, oldMap); } } } private static void setDefaults(List opps) { /* ... */ } private static void createRelatedRecords(List opps) { /* ... */ } private static void validateChanges(List opps, Map oldMap) { /* ... */ } private static void syncExternalSystem(List opps, Map oldMap) { /* ... */ } } ``` **Default to static methods.** The trigger stays logic-free and calls `Handler.run(...)`; the handler exposes a `static run` dispatcher and one focused `private static` method per trigger operation. - BAD: instantiate the handler — `new OpportunityTriggerHandler(Trigger.new).run();` — unless your org already standardizes on an instance-based handler framework. - BAD: one catch-all method handling every operation, or business logic inline in the trigger body. - GOOD: one bulkified `private static` method per operation (`setDefaults`, `validateChanges`, …), as shown above. - WHY: static keeps the call path obvious and stateless; one method per operation keeps each path bulkified and testable. Note: Triggers always run in System Mode (all API versions). The handler class enforces sharing and access control. Always declare `with sharing`, `without sharing`, or `inherited sharing` explicitly on handler classes — since API v67.0, omitting the declaration defaults to `with sharing`. --- ## Bulkification ### Use Maps for record lookups ```apex Map accountMap = new Map( [SELECT Id, Name, Industry FROM Account WHERE Id IN :accountIds] ); for (Contact c : contacts) { Account acc = accountMap.get(c.AccountId); if (acc != null && acc.Industry == 'Technology') { c.Tech_Account__c = true; } } ``` ### Use Sets for ID collections ```apex Set accountIds = new Set(); for (Contact c : Trigger.new) { accountIds.add(c.AccountId); } // One query for all contacts List accounts = [SELECT Id, Name FROM Account WHERE Id IN :accountIds]; ``` ### Collect DML into lists ```apex List tasksToInsert = new List(); for (Opportunity opp : opportunities) { tasksToInsert.add(new Task( WhatId = opp.Id, Subject = 'Follow up', ActivityDate = Date.today().addDays(7) )); } insert tasksToInsert; ``` --- ## Database Methods vs DML Statements ### Use Database methods for partial success ```apex List results = Database.insert(records, false); for (Database.SaveResult sr : results) { if (!sr.isSuccess()) { for (Database.Error err : sr.getErrors()) { System.debug(LoggingLevel.ERROR, err.getStatusCode() + ': ' + err.getMessage()); } } } ``` ### Use DML statements when all-or-nothing is required ```apex Savepoint sp = Database.setSavepoint(); try { insert parentRecord; insert childRecords; } catch (DmlException e) { Database.rollback(sp); throw e; } ``` --- ## Async Patterns ### Queueable — default choice for async ```apex public class DataSyncJob implements Queueable, Database.AllowsCallouts { private List recordIds; public DataSyncJob(List recordIds) { this.recordIds = recordIds; } public void execute(QueueableContext context) { List accounts = [SELECT Id, Name FROM Account WHERE Id IN :recordIds]; // Process and optionally chain if (!remainingIds.isEmpty()) { System.enqueueJob(new DataSyncJob(remainingIds)); } } } // Enqueue System.enqueueJob(new DataSyncJob(recordIds)); ``` ### Batch — for processing large data volumes (>50k records) ```apex public class AccountCleanupBatch implements Database.Batchable { public Database.QueryLocator start(Database.BatchableContext bc) { return Database.getQueryLocator('SELECT Id, Name FROM Account WHERE IsCleanedUp__c = false'); } public void execute(Database.BatchableContext bc, List scope) { for (Account acc : scope) { acc.IsCleanedUp__c = true; } update scope; } public void finish(Database.BatchableContext bc) { System.debug('Batch complete'); } } // Execute with batch size of 200 Database.executeBatch(new AccountCleanupBatch(), 200); ``` ### Schedulable — for recurring jobs ```apex public class WeeklyReportScheduler implements Schedulable { public void execute(SchedulableContext sc) { Database.executeBatch(new WeeklyReportBatch(), 200); } } // Schedule: every Monday at 6 AM System.schedule('Weekly Report', '0 0 6 ? * MON', new WeeklyReportScheduler()); ``` ### When to use which | Pattern | Use when | |---|---| | Queueable | Default async. Supports chaining, complex params, callouts | | Batch | Processing >50k records. QueryLocator supports 50M rows | | Schedulable | Recurring jobs on a cron schedule | | @future | Simple fire-and-forget with primitive params only (avoid for new code) | | Platform Events | Cross-boundary async, guaranteed delivery, event-driven architecture | ### Apex Cursors — flexible large dataset processing (GA, API v66.0+) ```apex Database.Cursor cursor = Database.getCursor( 'SELECT Id, Name FROM Account WHERE Industry = \'Technology\'' ); Integer totalSize = cursor.getNumRecords(); List chunk = (List) cursor.fetch(0, 200); List nextChunk = (List) cursor.fetch(200, 200); ``` Use cursors when you need flexible random-access to large result sets without Batch Apex overhead. Cursors can be serialized and passed between Queueable jobs. Each `fetch()` counts against the SOQL query limit and rows fetched count against the row limit. --- ## Test Patterns ### TestDataFactory ```apex @isTest public class TestDataFactory { public static List createAccounts(Integer count) { List accounts = new List(); for (Integer i = 0; i < count; i++) { accounts.add(new Account(Name = 'Test Account ' + i)); } insert accounts; return accounts; } public static List createContacts(List accounts, Integer perAccount) { List contacts = new List(); for (Account acc : accounts) { for (Integer i = 0; i < perAccount; i++) { contacts.add(new Contact( FirstName = 'Test', LastName = 'Contact ' + i, AccountId = acc.Id )); } } insert contacts; return contacts; } } ``` ### @TestSetup for shared test data ```apex @isTest private class OpportunityServiceTest { @TestSetup static void setup() { List accounts = TestDataFactory.createAccounts(5); TestDataFactory.createContacts(accounts, 2); } @isTest static void testBulkInsert() { List accounts = [SELECT Id FROM Account]; List opps = new List(); for (Account acc : accounts) { opps.add(new Opportunity( AccountId = acc.Id, Name = 'Test Opp', StageName = 'Prospecting', CloseDate = Date.today().addDays(30) )); } Test.startTest(); insert opps; Test.stopTest(); Assert.areEqual(5, [SELECT COUNT() FROM Opportunity], 'All 5 opportunities should be inserted'); } } ``` ### Test.startTest() / Test.stopTest() resets governor limits Always wrap the method under test in `Test.startTest()` / `Test.stopTest()` to get a fresh set of governor limits and to force async jobs to complete synchronously. --- ## Security — Access Modes (Summer '26 / API v67.0) Since API v67.0, database operations run in **User Mode by default** — they enforce the current user's CRUD, FLS, and sharing. In v66.0 and earlier, they default to System Mode. **Default to User Mode** (`WITH USER_MODE`, `AccessLevel.USER_MODE`, `as user`) for any operation acting on a user's behalf — including trigger logic. Reach for System Mode **only** when you deliberately need to bypass security, and comment why. Choosing System Mode by default silently skips FLS/CRUD/sharing — setting the mode explicitly does not make System Mode safe. ### SOQL/SOSL — WITH USER_MODE / WITH SYSTEM_MODE ```apex List accounts = [ SELECT Id, Name, Phone FROM Account WHERE Industry = 'Technology' WITH USER_MODE ]; ``` Use `WITH SYSTEM_MODE` only when intentionally bypassing security (e.g., system-level operations). Note: `WITH SECURITY_ENFORCED` is removed in API v67.0 — it will not compile. Use `WITH USER_MODE` instead. ### DML — as user / as system ```apex Account acc = new Account(Name = 'Acme'); insert as user acc; update as system systemRecord; ``` For Database methods, use the `AccessLevel` parameter: ```apex Database.insert(records, false, AccessLevel.USER_MODE); List results = Database.query( 'SELECT Id, Name FROM Account WHERE Rating = \'Hot\'', AccessLevel.USER_MODE ); ``` ### Security.stripInaccessible() ```apex // Intentional System Mode: query runs unrestricted, then fields are // stripped down to what the running user can actually see. List accounts = [SELECT Id, Name, Phone, Revenue__c FROM Account WITH SYSTEM_MODE]; SObjectAccessDecision decision = Security.stripInaccessible(AccessType.READABLE, accounts); List sanitized = decision.getRecords(); ``` Silently removes inaccessible fields instead of throwing. This is the deliberate System Mode case: query broadly, then filter for a specific user's access. For normal record access, prefer `WITH USER_MODE` directly. ### Sharing declarations — always explicit ```apex public with sharing class AccountService { /* enforces sharing rules */ } public without sharing class SystemService { /* bypasses sharing — use sparingly */ } public inherited sharing class UtilityClass { /* inherits from caller */ } ``` Since API v67.0, classes without an explicit sharing declaration default to `with sharing`. Always declare it explicitly to avoid behavior changes on API version upgrade. --- ## Custom Metadata Types for Configuration ```apex List settings = Integration_Setting__mdt.getAll().values(); Integration_Setting__mdt apiConfig = Integration_Setting__mdt.getInstance('PaymentGateway'); String endpoint = apiConfig.Endpoint__c; String apiKey = apiConfig.API_Key__c; ``` Use Custom Metadata Types instead of Custom Settings (Legacy) or hardcoded values. They are deployable, packageable, and editable in production. --- ## Platform Events ### Publishing ```apex OrderEvent__e event = new OrderEvent__e( OrderId__c = order.Id, Action__c = 'CREATED' ); Database.SaveResult sr = EventBus.publish(event); ``` ### Subscribing (Apex trigger) ```apex trigger OrderEventTrigger on OrderEvent__e (after insert) { List tasks = new List(); for (OrderEvent__e event : Trigger.new) { tasks.add(new Task( Subject = 'Process Order: ' + event.OrderId__c, Status = 'New' )); } insert tasks; } ``` Use Platform Events for: - Cross-system async communication - Loosely coupled integrations - Event-driven architecture - Logging from triggers (commit-independent) --- ## Exception Handling ```apex public with sharing class OrderService { public class OrderServiceException extends Exception {} public static Order__c createOrder(Id accountId, List items) { if (items == null || items.isEmpty()) { throw new OrderServiceException('Order must have at least one item'); } Savepoint sp = Database.setSavepoint(); try { Order__c order = new Order__c(Account__c = accountId, Status__c = 'Draft'); insert order; List lineItems = new List(); for (OrderItem item : items) { lineItems.add(new OrderLineItem__c( Order__c = order.Id, Product__c = item.productId, Quantity__c = item.quantity )); } insert lineItems; return order; } catch (DmlException e) { Database.rollback(sp); throw new OrderServiceException('Failed to create order: ' + e.getMessage()); } } } ``` Custom exception classes for each service. Use Savepoints for multi-step DML. Catch specific exceptions, not generic `Exception`. --- ## Multiline Strings and String Interpolation (Summer '26) ### Multiline strings ```apex String json = ''' { "name": "John Doe", "type": "New Customer" }'''; ``` Use triple single quotes (`'''`) to declare strings spanning multiple lines. Available in all API versions since Summer '26. ### String.template() — named variable interpolation ```apex String body = ''' { "Account": "${accountName}", "Updated": "${date}" }'''.template(new Map{ 'accountName' => acc.Name, 'date' => DateTime.now() }); ``` Use `String.template()` with named `${variable}` placeholders instead of `String.format()` with index-based `{0}` placeholders. Keys map to descriptive names, making the code easier to read and maintain. # LWC Best Practices — Current Patterns > AI: Use these patterns when generating Lightning Web Components. Default to Lightning Data Service over imperative Apex, use the modern `lwc:if`/`lwc:ref` syntax, and respect Lightning Web Security. > Release: Summer '26 | API: v67.0 | Updated: 2026-06 --- ## Data Access — Prefer Lightning Data Service LDS handles caching, FLS, sharing, and cache invalidation across components automatically. Reach for imperative Apex only when LDS can't model the operation. ### Reactive single record with `@wire(getRecord)` ```javascript import { LightningElement, api, wire } from 'lwc'; import { getRecord, getFieldValue } from 'lightning/uiRecordApi'; import NAME_FIELD from '@salesforce/schema/Account.Name'; import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry'; export default class AccountSummary extends LightningElement { @api recordId; @wire(getRecord, { recordId: '$recordId', fields: [NAME_FIELD, INDUSTRY_FIELD] }) account; get name() { return getFieldValue(this.account.data, NAME_FIELD); } get industry() { return getFieldValue(this.account.data, INDUSTRY_FIELD); } } ``` The `$` prefix makes `recordId` reactive — when it changes, the wire re-fires. ### Edit a record with `lightning-record-form` / `lightning-record-edit-form` ```html ``` LDS-managed forms automatically refresh other components wired to the same record. ### Create / update / delete from JS ```javascript import { createRecord, updateRecord, deleteRecord } from 'lightning/uiRecordApi'; async handleCreate() { const recordInput = { apiName: 'Account', fields: { Name: 'Acme', Industry: 'Technology' } }; const account = await createRecord(recordInput); // LDS cache and any @wire(getRecord) on this record are updated automatically } async handleUpdate() { const fields = { Id: this.recordId, Industry: 'Finance' }; await updateRecord({ fields }); } async handleDelete() { await deleteRecord(this.recordId); } ``` ### When to use imperative Apex Use imperative Apex calls when: - The operation isn't a simple CRUD on a standard sObject (custom logic, multi-step workflows) - You need a custom DTO shape - You need a callout (LDS can't call external services) - Aggregate or complex SOQL not expressible via LDS ```javascript import { LightningElement, wire } from 'lwc'; import getTopAccounts from '@salesforce/apex/AccountController.getTopAccounts'; export default class TopAccounts extends LightningElement { @wire(getTopAccounts, { limitSize: 10 }) accounts; } ``` Apex method must be `@AuraEnabled(cacheable=true)` to be `@wire`-compatible. Methods with `cacheable=false` (or omitted) must be invoked imperatively. --- ## Imperative Apex Call ```javascript import { LightningElement } from 'lwc'; import processOrder from '@salesforce/apex/OrderController.processOrder'; import { ShowToastEvent } from 'lightning/platformShowToastEvent'; export default class OrderProcessor extends LightningElement { async handleClick() { try { const result = await processOrder({ orderId: this.recordId }); this.dispatchEvent(new ShowToastEvent({ title: 'Success', message: `Order ${result.orderNumber} processed`, variant: 'success' })); } catch (error) { this.dispatchEvent(new ShowToastEvent({ title: 'Error', message: error.body?.message ?? error.message, variant: 'error' })); } } } ``` --- ## Conditional Rendering — `lwc:if` / `lwc:elseif` / `lwc:else` Use the modern directives. `if:true` / `if:false` were deprecated in v59.0 (Winter '24). ```html ``` --- ## Iteration — `for:each` and `iterator` ```html ``` `key` must be unique and stable. Don't use array index as key — re-renders break. --- ## Querying the DOM — `lwc:ref` `lwc:ref` (Winter '24+) replaces `this.template.querySelector` in most cases. ```html ``` ```javascript validate() { const email = this.refs.email; const phone = this.refs.phone; return email.checkValidity() && phone.checkValidity(); } ``` Use `querySelector` only for dynamic selection (e.g., querying inside an iteration). --- ## Component Lifecycle ```javascript import { LightningElement } from 'lwc'; export default class LifecycleDemo extends LightningElement { connectedCallback() { // Component inserted into DOM — set up subscriptions, fetch initial data } renderedCallback() { // After every render — keep logic minimal and idempotent (guard with a flag) if (this.hasRendered) return; this.hasRendered = true; // one-time post-render setup } disconnectedCallback() { // Component removed — unsubscribe, clear timers, release resources } errorCallback(error, stack) { // Catches errors thrown from child component lifecycle hooks console.error(error, stack); } } ``` Never mutate reactive state in `renderedCallback` without a guard — causes infinite re-render loops. --- ## Reactivity Top-level field assignments are reactive automatically (no `@track` needed since v45). ```javascript export default class ReactiveDemo extends LightningElement { count = 0; // reactive user = { name: '' }; // reactive at top level increment() { this.count++; // ✅ triggers re-render this.user = { ...this.user, name: 'Y' }; // ✅ triggers re-render (new object reference) this.user.name = 'X'; // ❌ no re-render — nested mutation } } ``` For nested object/array mutations to trigger re-render, reassign the top-level field to a new reference. --- ## Custom Events ```javascript // Child component dispatches this.dispatchEvent(new CustomEvent('select', { detail: { accountId: this.accountId }, bubbles: true, // bubble up the DOM composed: false // stay within the shadow boundary (default) })); ``` ```html ``` ```javascript handleSelect(event) { const accountId = event.detail.accountId; } ``` - `composed: true` lets the event cross shadow DOM boundaries. Use sparingly — most cross-component communication should use LMS or `@api` methods on the parent. - Event names: lowercase, no dashes, no event-prefix (`accountselect`, not `account-select` or `onAccountSelect`). --- ## Cross-Component Messaging — Lightning Message Service LMS replaces pubsub for any communication between components that don't share a parent. ```javascript // message channel: force-app/main/default/messageChannels/AccountSelected.messageChannel-meta.xml import { LightningElement, wire } from 'lwc'; import { publish, subscribe, MessageContext } from 'lightning/messageService'; import ACCOUNT_SELECTED from '@salesforce/messageChannel/AccountSelected__c'; export default class Publisher extends LightningElement { @wire(MessageContext) messageContext; notify(accountId) { publish(this.messageContext, ACCOUNT_SELECTED, { accountId }); } } export default class Subscriber extends LightningElement { subscription = null; @wire(MessageContext) messageContext; connectedCallback() { this.subscription = subscribe( this.messageContext, ACCOUNT_SELECTED, (message) => this.handleMessage(message) ); } disconnectedCallback() { if (this.subscription) { unsubscribe(this.subscription); this.subscription = null; } } } ``` --- ## Cache Refresh — `refreshApex` and `getRecordNotifyChange` After mutating data, the cache must be told to refetch. ```javascript import { refreshApex } from '@salesforce/apex'; import { getRecordNotifyChange } from 'lightning/uiRecordApi'; @wire(getAccounts) wiredAccountsResult; async handleSave() { await updateRecord({ fields: { Id: this.recordId, Name: 'New' } }); // ✅ Refresh imperatively-wired Apex result await refreshApex(this.wiredAccountsResult); // ✅ Notify LDS that a record changed (so other components re-fetch) getRecordNotifyChange([{ recordId: this.recordId }]); } ``` `updateRecord` already invalidates LDS cache for that record — you only need `getRecordNotifyChange` when the change happened outside LDS (Apex callout, external system, custom action). --- ## Navigation ```javascript import { NavigationMixin } from 'lightning/navigation'; export default class NavDemo extends NavigationMixin(LightningElement) { openAccount(accountId) { this[NavigationMixin.Navigate]({ type: 'standard__recordPage', attributes: { recordId: accountId, objectApiName: 'Account', actionName: 'view' } }); } openList() { this[NavigationMixin.Navigate]({ type: 'standard__objectPage', attributes: { objectApiName: 'Account', actionName: 'list' }, state: { filterName: 'Recent' } }); } } ``` --- ## Toasts ```javascript import { ShowToastEvent } from 'lightning/platformShowToastEvent'; this.dispatchEvent(new ShowToastEvent({ title: 'Saved', message: 'Account updated successfully', variant: 'success', // 'info' | 'success' | 'warning' | 'error' mode: 'dismissable' // 'dismissable' | 'pester' | 'sticky' })); ``` Toasts only work in Lightning Experience and the Salesforce mobile app — they're no-ops in standalone Lightning Out apps. --- ## Public API — `@api` ```javascript import { LightningElement, api } from 'lwc'; export default class AccountTile extends LightningElement { @api recordId; // public property @api compact = false; // public property with default @api focusFirstField() { // public method this.refs.firstInput.focus(); } // ❌ Don't mutate @api properties inside the component @api count = 0; increment() { this.count++; } // anti-pattern — parent owns this value } ``` Public properties are read-only inside the component — the parent owns the value. --- ## GraphQL — `lightning/graphql` ```javascript import { LightningElement, wire } from 'lwc'; import { gql, graphql } from 'lightning/graphql'; export default class AccountsGraph extends LightningElement { @wire(graphql, { query: gql` query getAccounts { uiapi { query { Account(where: { Industry: { eq: "Technology" } }, first: 10) { edges { node { Id Name { value } AnnualRevenue { value } } } } } } } ` }) accounts; } ``` `lightning/uiGraphQLApi` is removed — use `lightning/graphql` (GA Summer '24). --- ## Loading External Resources ```javascript import { loadScript, loadStyle } from 'lightning/platformResourceLoader'; import CHARTJS from '@salesforce/resourceUrl/chartjs'; async renderedCallback() { if (this.chartInitialized) return; this.chartInitialized = true; await Promise.all([ loadScript(this, CHARTJS + '/chart.min.js'), loadStyle(this, CHARTJS + '/chart.min.css') ]); this.renderChart(); } ``` External libraries must run under **Lightning Web Security** (replaced Locker Service in Spring '23). Most modern libraries work without changes; legacy libraries that rely on `window`-level globals may need adapters. --- ## Common AI-Generated Mistakes | Mistake | Fix | |---|---| | `if:true={x}` / `if:false={x}` | Use `lwc:if={x}` / `lwc:elseif={x}` / `lwc:else` | | `this.template.querySelector('lightning-input')` for static refs | Use `lwc:ref` and `this.refs.name` | | `@track` on every field | Reactive by default since v45; only needed for deep observation of objects with `@track` (rare) | | `import { ... } from 'lightning/uiGraphQLApi'` | Use `lightning/graphql` | | Aura: ``, `c:childCmp` | LWC: `