Salesforce platform developer 2
Skill toddkasper/expert-skills/salesforce/skills/salesforce-platform-developer-2
Agent skills that give AI agents the operational competence of expert practitioners (Salesforce, AWS, GitHub Actions, web). SKILL.md format; Claude Code plugins. Not test-prep — certification is the scaffold and benchmark, not the product.
npx -y skills add toddkasper/expert-skills --skill salesforce-platform-developer-2Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Advanced Apex, integration, async processing, and performance work on the Salesforce platform — design patterns (Singleton, Strategy, fflib, Bulk State Transition), asynchronous patterns (Batch/Queueable/Future/Schedulable, chaining), SOQL selectivity and Large Data Volume tuning, Platform Events, Change Data Capture, REST/Bulk API integration, dynamic Apex with FLS/CRUD enforcement, and test mocking (Stub API). Use when designing or reviewing advanced Apex, integration, or performance-critical code. Not Apex fundamentals (see salesforce-platform-developer-1) or declarative config (see salesforce-administrator). Scoped and benchmarked by the Platform Developer II (Plat-Dev-301) blueprint.
SKILL.md
35.4 KB, as published. Nobody here has run it
Salesforce Platform Developer II — Skills Reference
Overview
The Salesforce Certified Platform Developer II (PD2 / PDII) certification validates advanced proficiency in designing, building, and optimizing Salesforce applications using Apex, Lightning Web Components, Aura, Visualforce, and the full integration and automation stack. It targets developers who write correct, performant, and maintainable code at enterprise scale.
The credential has two parts: a multiple-choice exam and three Trailhead Superbadges (Apex Specialist, Data Integration Specialist, Advanced Apex Specialist) [volatile — verify live].
This file is an operational playbook, not an exam outline. Rules are stated as actionable instructions with concrete limits, decision criteria, and anti-patterns to catch in review.
Load this skill when… designing or reviewing advanced Apex (design patterns, async chaining, LDV processing); building or debugging REST/Bulk API integrations or Platform Events; optimizing SOQL selectivity or Large Data Volume performance; implementing dynamic Apex with programmatic FLS/CRUD enforcement. Not this skill: Apex fundamentals (triggers, basic SOQL, LWC essentials, 75% test coverage) → see
salesforce-platform-developer-1; declarative config (profiles, Flow, sharing rules) → seesalesforce-administrator.
Verify steps assume nothing about your tooling — use your project's Salesforce MCP connection, the Salesforce CLI (
sf), or the Salesforce setup UI, in that order of preference.
Credential logistics and study path: see references/study-resources.md.
Uncertainty & Escalation
- Always re-verify live: all governor limit numbers (SOQL/DML/CPU/heap/callout/Queueable chain depth)
[volatile — verify live]; Platform Event retention windows and replay behavior[volatile — verify live]; SOQL selectivity thresholds[volatile — verify live]; Batch API and Bulk API 2.0 limits and behaviors; any quota or cap cited in this skill. - Live wins: if this skill's numbers or rules conflict with Apex runtime behavior, Query Plan output, or official Salesforce release notes, the live system is authoritative. Log the discrepancy immediately using the Feedback protocol below.
- Escalate to a human before proceeding: production deploys of async chaining patterns (Queueable/Batch) not validated in a sandbox; any code that writes to setup objects (User, PermissionSetAssignment) in a non-test context; adding
without sharingto a class processing PII or financial data; deploying a Platform Event subscriber that could duplicate-write production records if CDC events replay. - Confidence taxonomy: facts in this skill are stable unless tagged
[volatile — verify live]or[opinion — house style]. When in doubt, use Query Plan or run a test transaction in a sandbox before assuming a limit applies.
Governor Limits — Memorize These Numbers
These are the hard ceilings every decision below is measured against. Per-transaction unless noted; "async" = future/queueable/batch-execute/schedulable context.
| Limit | Synchronous | Asynchronous |
|---|---|---|
| SOQL queries | 100 [volatile — verify live] | 200 |
| SOQL rows retrieved | 50,000 | 50,000 |
| DML statements | 150 [volatile — verify live] | 150 |
| DML rows | 10,000 | 10,000 |
| SOSL queries | 20 | 20 |
| HTTP callouts | 100 | 100 |
| CPU time | 10,000 ms [volatile — verify live] | 60,000 ms |
| Heap size | 6 MB [volatile — verify live] | 12 MB |
| Future calls | 50 | n/a (can't call future from future) |
| Queueable jobs enqueued | 50 (sync) / 1 (from queueable) [volatile — verify live] | 1 from within execute |
| Batch QueryLocator rows | n/a | 50,000,000 [volatile — verify live] |
| Aggregate query rows | 2,000 grouped rows | same |
Decision rule: if a transaction could touch more than ~10k records, it must be Batch Apex (QueryLocator, 50M ceiling), never a single synchronous transaction.
Apex Triggers & Automation
Bulkify all DML — the single most-tested rule
Never put SOQL or DML inside a for loop. Query once into a Map, iterate in memory, collect into a List, DML once after the loop.
RED FLAG to catch in review: any [SELECT ...], Database.query(...), insert, update, upsert, delete, or EventBus.publish whose enclosing braces are inside a loop body. Also catch List.contains() inside a loop (O(n²)) — replace with a Map/Set lookup (O(1)).
// WRONG — N+1, blows SOQL limit at 100 records
for (Contact c : Trigger.new) {
Account a = [SELECT Id FROM Account WHERE Id = :c.AccountId]; // SOQL in loop
}
// RIGHT — one query, map lookup
Set<Id> acctIds = new Set<Id>();
for (Contact c : Trigger.new) acctIds.add(c.AccountId);
Map<Id, Account> byId = new Map<Id, Account>([SELECT Id FROM Account WHERE Id IN :acctIds]);
One trigger per object, logic in a handler class
The trigger body is a thin dispatcher; all logic lives in a handler. Anti-pattern: two triggers on the same object — execution order between them is undefined. If you find a second trigger on an object, consolidate.
Recursion control: guard with a static Boolean so a re-entrant save doesn't re-run the handler. Deep dive with patterns and edge cases: references/recursion-control.md — load when diagnosing trigger re-entry or implementing context-scoped guards.
Package trigger framework note: some managed packages own objects through their own trigger framework. To add behavior to such an object, plug into the framework's registration mechanism rather than dropping a second raw trigger (e.g. NPSP uses TDTM; see salesforce-nonprofit-cloud-consultant).
Trigger contexts — pick the right one
| Need | Context |
|---|---|
| Default/validate field values before save (no extra DML, no Id yet on insert) | before insert / before update |
| Mutate the record itself | before (just set the field; do not issue DML on Trigger.new) |
| Create/relate other records, roll-ups, send events | after insert / after update (Id exists) |
| React to deletes / restore | before delete / after delete / after undelete |
Trigger.new is read-write only in before contexts; in after, it's read-only (assigning to it throws). Use Trigger.newMap/oldMap keyed by Id for old-vs-new comparison (the Bulk State Transition pattern — only act on records whose watched field actually changed).
Declarative vs. programmatic — choose deliberately
| Scenario | Tool | Why |
|---|---|---|
| Required-field / cross-field validation, no DML | Validation Rule | Cheapest, runs first, no code |
| Field updates, record creation, simple branching, email | Record-Triggered Flow | Admin-maintainable, no test coverage burden |
| Complex branching, recursion control, bulk algorithmic logic, callouts, dynamic SOQL | Apex Trigger + handler | Flow can't do callouts inline or complex collection logic safely |
| Scheduled/async heavy processing | Batch/Queueable/Scheduled Apex | Governor headroom + chaining |
One-automation-per-object principle: don't have a Flow AND a trigger both mutating the same object on the same event — ordering is hard to reason about and you double the limit consumption.
Verify before extending: describe the object and check existing automation; on a package-managed org, query the package's handler-registration table (e.g. Trigger_Handler__c for NPSP).
Error Handling & Transactional Integrity
- Partial-success DML:
Database.insert(records, false)lets good rows commit while bad rows fail; inspectDatabase.SaveResult[]→.isSuccess()/.getErrors(). Plaininsertis all-or-nothing and throwsDmlExceptionon any failure. - Savepoints:
Savepoint sp = Database.setSavepoint(); ... Database.rollback(sp);to undo partial work. Setting and rolling back still counts against DML statement limits. - Surface errors to UI:
record.addError('msg')orrecord.Field__c.addError('msg')in abeforetrigger blocks the save and shows the message inline. - Unhandled exceptions roll back the entire transaction — including prior successful DML. Catch at the boundary, log, decide whether to re-throw.
- Capture, don't lose: when an integration write fails, persist the payload somewhere durable rather than dropping it.
Asynchronous Apex — Pick the Right Tool
| Tool | Use when | Key limits / notes |
|---|---|---|
@future(callout=true) | Fire-and-forget callout, simple primitive args | No sObject params (pass Ids/JSON), no chaining, no return, can't call from batch/future. Prefer Queueable in new code. |
Queueable | Async work needing complex state, chaining, or callouts | Pass sObjects/objects via constructor. Implement Database.AllowsCallouts for HTTP. Chain via System.enqueueJob() in execute() — only 1 child per execute. Monitorable via AsyncApexJob. |
Batchable | >10k records / LDV processing | start() returns Database.QueryLocator (50M rows). Default 200 records/execute; tune scope down for heavy logic. Database.Stateful to accumulate state across chunks. Each execute gets fresh governor limits. |
Schedulable | Time-based kickoff | System.schedule('name', cronExpr, job). Cron is sec min hr day month weekday [year]. Query CronTrigger/CronJobDetail. Usually schedules a Batch/Queueable. |
Mixed DML error: you cannot DML a setup object (User, Group, PermissionSetAssignment) and a non-setup object (Contact) in the same synchronous transaction. Workaround: do the setup-object DML in a @future method.
RED FLAG: synchronous HTTP callout in a trigger/UI path — move it async to avoid the 10s CPU / callout-in-trigger restrictions and to keep the UI responsive.
Invocable Apex (@InvocableMethod): exposes Apex to Flow. Method must be static, in a non-inner class; input/output are List<T> (Flow always passes a collection). Process the full input list at once — never SOQL per input. Deep dive with callout rules and @InvocableVariable patterns: references/invocable-apex.md — load when writing or debugging Flow-invoked Apex actions.
SOQL, SOSL & Selectivity
Write selective queries
A query is selective when its WHERE filters on an indexed field returning under the threshold [volatile — verify live]:
- Standard index: <30% of the first 1M records, <15% of records beyond 1M (max 1M rows examined total).
- Custom index: <10% of the first 1M records, <5% of records beyond 1M (max ~333k rows examined total).
Non-selective queries on large objects throw QueryException: Non-selective query against large object type.
Indexed by default: Id, Name, CreatedDate, SystemModstamp, lookup/master-detail fields, External Id fields, Unique fields. Custom indexes are requested via Salesforce Support.
Kills the index (forces full scan) — RED FLAGS:
- Leading wildcard:
LIKE '%smith' - Negation:
!=,NOT IN,NOT LIKE NULLchecks on non-indexed fields- Functions/operations on the indexed field in the filter
ORacross mixed indexed/non-indexed fields
Verify: use the Query Plan tool in Developer Console (or EXPLAIN via the REST Query Plan resource) to confirm Leading Operation Type = Index.
SOQL features worth knowing
FOR UPDATE— row lock to prevent concurrent-update races (releases at transaction end).WITH SECURITY_ENFORCED— strips fields/objects the running user can't see; throws if any selected field is inaccessible. UseWITH USER_MODE(newer) for full FLS+sharing enforcement in queries and DML.- Aggregates:
COUNT(),SUM(),GROUP BY,HAVING— capped at 2,000 grouped rows. - Relationship queries: parent→child (subquery), child→parent dot-walk up to 5 levels.
- Semi/anti-join:
WHERE Id IN (SELECT ...)/NOT IN (...).
SOSL vs SOQL
Use SOSL for full-text search across multiple objects/fields (FIND 'term' IN ALL FIELDS RETURNING Contact(Id,Name), Lead(Id)). It hits the search index, not the row store. Use SOQL when you know the object and are filtering on structured fields.
Dynamic Apex & FLS/CRUD Enforcement
- Runtime metadata:
Schema.getGlobalDescribe(),obj.getDescribe(),field.getDescribe(). Describes are not free — cachegetGlobalDescribe()results; each call is expensive. - Dynamic SOQL:
Database.query(String). Injection risk — never concatenate user input; use bind variables (:var) orString.escapeSingleQuotes(). - Dynamic instantiation:
Type.forName('MyClass').newInstance()(Strategy/Factory patterns). - Programmatic FLS/CRUD checks:
Schema.sObjectType.Contact.fields.Email.isAccessible()/isUpdateable()/isCreateable(), orSecurity.stripInaccessible()to drop fields the user can't touch before DML. PD2 expects FLS enforcement in code that runswithout sharingor in API context.
Sharing & Security
- Three independent layers: object CRUD → FLS → record access (OWD + role hierarchy + sharing rules + manual/Apex shares). All three must pass. FLS is independent of object access.
- Sharing keywords:
with sharing(enforce record visibility),without sharing(ignore sharing — use sparingly),inherited sharing(adopt caller's context; safe default for reusable classes). - Apex managed sharing: insert
AccountShare/OpportunityShare/MyObject__Sharerows with a customRowCause(Apex Sharing Reason) so recalculation can find them.
Deployment gotcha: SFDX field-meta.xml grants FLS to no one. You must add <fieldPermissions> in a permission set or profile, or every query returns "Invalid field." Salesforce rejects <fieldPermissions> on <required>true</required> fields — omit them. Verify with a FieldPermissions SOQL query, not by trusting the deploy success message.
Platform Events & Integration
Platform Events
- Define as
MyEvent__e; publish withEventBus.publish(events); subscribe via an after-insert trigger on the event (only after-insert is valid), a Flow, or CometD/external subscriber. publish-after-commit(default): suppressed on rollback.publish-immediately: delivered regardless of transaction outcome.ReplayIdenables durable replay. High-volume events: 72-hour retention (the default for all new events). Standard-volume events retain for 24 hours but are scheduled for retirement in Winter '27 (October 2026) — after which publishing/subscribing to standard-volume events will be blocked; migrate to high-volume.[volatile — verify live]- Use for decoupled, fire-and-forget integration.
Change Data Capture (CDC): publishes change events (AccountChangeEvent, MyObject__ChangeEvent, etc.) on record changes; subscribe via after-insert trigger on the change-event object. Key payload: ChangeEventHeader.changeType (CREATE/UPDATE/DELETE/UNDELETE), changedFields, recordIds. CDC events are NOT suppressed on transaction rollback — design consumers to be idempotent. Deep dive with enabling, payload, and testing: references/cdc.md — load when implementing CDC-based integration or debugging delivery behavior.
Inbound integration — pick the API
| Need | API |
|---|---|
| Standard CRUD, single/few records | REST API (sObject endpoints) |
| Custom server-side endpoint | @RestResource Apex (@HttpGet/Post/Put/Patch/Delete) |
| Large batch loads (CSV, 1000s–millions) | Bulk API 2.0 (async, job-based) |
| Several dependent ops in one round-trip | Composite / Composite Graph API |
| Real-time push to subscribers | Streaming API / Platform Events / CDC |
Idempotent upsert on an External Id is the canonical inbound pattern: define a custom field marked External Id and Unique, and have the integration client upsert against it. Re-running the same payload updates rather than duplicates.
Outbound callouts
Http/HttpRequest/HttpResponse. Store endpoints + auth in Named Credentials (never hardcode secrets/URLs). Callout limit 100/transaction; total timeout 120s, 10s default per call. Every callout must be mocked in tests (HttpCalloutMock).
Lightning Web Components
Wire vs imperative Apex
| Use | When |
|---|---|
@wire(apexMethod, {params}) | Read-only, reactive, cacheable data for display. Method must be @AuraEnabled(cacheable=true) and side-effect-free. Re-invokes when reactive params ($param) change. |
Imperative (import m from '@salesforce/apex/...'; await m({})) | On-demand fresh data, mutations (DML), or when you need to call in an event handler / control timing. |
cacheable=true Apex cannot do DML. For mutations, call imperatively.
Component communication
| Direction | Mechanism |
|---|---|
| Parent → child | @api properties / @api methods |
| Child → parent | this.dispatchEvent(new CustomEvent('name', {detail})) |
| Unrelated components | Lightning Message Service: MessageChannel, publish, subscribe |
@api = public reactive; @track only needed for deep mutation of objects/arrays. Lifecycle order: constructor → connectedCallback → render → renderedCallback (fires after every render — never put unguarded state changes here, causes infinite re-render).
Framework selection
LWC (default) > Aura (only for platform features still requiring it; Aura can host LWC, not vice-versa) > Visualforce (PDF rendering via renderAs="pdf", legacy). Don't write new Aura/VF unless a specific platform capability forces it.
Visualforce — PD2 exam scope (~20% of User Interface domain)
Visualforce is explicitly tested in PD2. Core tested topics: StandardSetController (paginated list views), controller extensions, partial page refresh (<apex:actionFunction> + reRender), {!$Component.id} for JS DOM targeting, ApexPages.addMessage + <apex:pageMessages/> for error display, and <apex:page renderAs="pdf"> for PDF output. Deep dive: references/visualforce.md — load when building or reviewing Visualforce pages or working through PD2 UI exam topics.
Testing, Debugging & Deployment
Fundamentals (75% coverage gate, Test.startTest()/stopTest(), seeAllData=false, HttpCalloutMock) are in salesforce-platform-developer-1. PD2-specific additions:
- Meaningful coverage: assert concrete outcomes and async state — coverage that only executes lines without asserting results does not catch regressions.
StubProvider: dynamic mocking without a hand-rolled mock class. Use when the mock logic varies per test or when mocking a service interface.- LWC Jest:
createElement+appendChild,shadowRoot.querySelector, mock@wirewith@salesforce/wire-service-jest-util. Apex Replay Debugger requiresFINESTlog level; never log PII viaSystem.debug. - Deploy order: objects → fields → permsets that reference them. Run
sf project deploy startfrom the SFDX project root (sfdx-project.jsondirectory) or getInvalidProjectWorkspaceError. - Quick Action cache: cache-bust by editing non-field-list metadata (
<description>) and redeploying.
Performance & Large Data Volumes
- Selectivity first (see SOQL section) — the #1 LDV lever.
- CPU/heap: build strings with
List<String>+String.join()(not+=in loop);Map/Setfor lookups; null out large collections when done (6MB sync / 12MB async). - LDV tools: Skinny Tables · custom indexes (both via Support) · Big Objects (
__b, billions of rows, Async SOQL) · archiving. - Skew — RED FLAG: owner skew (one user owning >10k records → sharing-recalc lock contention); lookup skew (many children under one parent → DML lock contention).
- N+1 detection: query/DML inside loop, SOQL count scaling with input,
List.contains()in loop, repeatedgetGlobalDescribe(). - UI perf: prefer cached
@wire; keeprenderedCallbackcheap and idempotent.
Advanced Fundamentals
CMT vs Custom Settings decision, multi-currency handling, and enterprise design patterns (Singleton, Strategy, fflib Service/Selector/Domain): references/advanced-fundamentals.md — load when choosing a config storage mechanism or implementing an enterprise-layer architecture.
One-line decision rules (load references for full detail):
- Config ships with app / needs deployment → Custom Metadata Type (
__mdt) (free SOQL, packageable). - Per-user/profile runtime toggle → Hierarchy Custom Setting (in-memory, no SOQL cost).
- Multi-currency: guard with
UserInfo.isMultiCurrencyOrganization(); never hardcode currency math.
Executable Workflows
1. Build a Queueable chain safely (one child per execute, depth/limits)
- Define the first Queueable class with its state (Ids, parameters) passed via constructor. Implement
Queueable(andDatabase.AllowsCalloutsif making HTTP calls). → gate: class compiles; constructor stores only serializable state (avoid holding live SObject lists if heap is a concern). - In the
execute()method, do the work (query once outside loops, DML once after loops). At the end ofexecute(), enqueue exactly one child:System.enqueueJob(new NextStepQueueable(state)). → gate: code review confirms exactly oneSystem.enqueueJobcall perexecute()path; never enqueue inside a loop. - Add a depth guard: pass a
depthinteger via constructor and abort ifdepth > maxDepth(typically keep chains ≤ 5 levels for safety). → gate: unit test withdepth = maxDepth + 1confirms the chain stops and logs a meaningful message. - Test: wrap in
Test.startTest()/stopTest()to flush the first job synchronously. Assert state after the first step; test each step class independently. → gate: all test methods pass; noSystem.LimitException; assertions on final state are concrete (not just "no exception"). - Deploy and monitor via
SELECT Id, Status, JobType FROM AsyncApexJob WHERE JobType='Queueable' ORDER BY CreatedDate DESC. → gate: each enqueued job showsStatus = Completed; noFailedrows for the chain.
2. Stand up a Platform Event producer + subscriber
- Define the Platform Event object (
MyEvent__e) in Object Manager with the required fields. SetPublish BehaviortoPublish After Commit(default) unless immediate delivery on rollback is required. → gate:MyEvent__eappears insf sobject listoutput; describe confirms expected fields. - Write the producer:
EventBus.publish(new MyEvent__e(Field__c = value))inside anafter insert/updatetrigger or invocable method. Capture and log publish results. → gate: in a sandbox, trigger the producer and confirmEventBus.publishreturnsisSuccess = true; verify the event appears in Event Monitoring or a debug log. - Write the subscriber trigger:
trigger MyEventSub on MyEvent__e (after insert). ProcessTrigger.newas a collection (bulkify). Avoid SOQL/DML inside loops. → gate: subscriber trigger compiles; queryEventBusSubscriberor check Setup → Platform Events to confirm the subscriber is registered. - Write test: mock the event in a test method (
Test.startTest(); EventBus.publish(...); Test.stopTest();). Assert subscriber side effects. → gate: test passes; subscriber logic assertions are on concrete field values, not just "no error." - Deploy producer and subscriber together. Smoke-test end-to-end in the sandbox: trigger the produce action and confirm subscriber side effects (e.g. related record created/updated). → gate: side-effect record exists with expected values; no debug-log errors.
3. Make a slow SOQL query selective (Query Plan → index/selective filter → re-check)
- Open Developer Console → Query Plan tool (or REST API
EXPLAINendpoint). Paste the slow SOQL query and run the plan. → gate: note theLeading Operation Type; a result ofTableScanmeans the query is non-selective. - Identify the filter condition causing the table scan: leading wildcard (
LIKE '%...'), negation (!=,NOT IN), non-indexed field, orORacross mixed indexed/non-indexed. → gate: locate the specific clause in the WHERE that produces the scan. - Rewrite the filter to use an indexed field (Id, Name, External Id, lookup, audit fields like
CreatedDate) and an equality or range predicate. If no native index exists, request a custom index via Salesforce Support. → gate: re-run Query Plan on the rewritten query —Leading Operation Typemust change toIndex. - If a custom index was requested, confirm it is active: describe the field and check
filterable+sortableattributes, or run the Query Plan again after Salesforce confirms the index is built. → gate: Query Plan showsIndexas leading operation; estimated row count is within the selectivity threshold (custom index: <10% of first 1M records, <5% beyond). - Load-test the rewritten query with production-scale data (use a Full sandbox): confirm query returns in under 5 seconds and does not throw
QueryException: Non-selective query. → gate: query executes without error; response time acceptable under peak load in Full sandbox.
Decision Scenarios
The two scenarios below cover the highest-value PD2 operational gotchas. Additional scenarios: references/scenarios.md — load for platform event rollback, Flow/trigger recursion, and dynamic SOQL injection patterns.
Scenario 1 — Async chaining and the "one child per execute" rule
Situation: A nightly data-quality job must process 2 million Contact records in stages: first normalize phone formats, then deduplicate, then update a roll-up on Account. A developer proposes three separate Batch Apex jobs chained by calling
Database.executeBatch(new DeduplicateBatch())at the end of the phone-format batch'sfinish()method.Competent move: Chaining from
finish()is the correct Batchable pattern —finish()runs in a fresh transaction, soDatabase.executeBatchthere is allowed and does not violate the "one Queueable child per execute" rule (that applies to Queueable, not Batch). Model each stage as its ownBatchableclass; pass state viaDatabase.Statefulor a shared Custom Object. Tune scope size conservatively (50–100) for SOQL-heavy stages.Tempting-but-wrong: Calling
System.enqueueJob(...)from inside a Batchexecute()— blocked; only allowed fromfinish(). Also: a developer may try chaining multiple Queueable children from oneexecute(), but each execute may enqueue exactly one child.Verify: Check
AsyncApexJobrecords after a test run — each stage should appear as a separate job withStatus = Completed. In unit tests, wrap each stage inTest.startTest()/stopTest()to force synchronous execution and assert intermediate state.
Scenario 2 — FLS after a successful SFDX deploy
Situation: A developer deploys a new custom field
Loan_Rate__con the Opportunity object via SFDX source push. The deploy log showsDeploy Succeeded. The developer then writes an Apex class that queriesLoan_Rate__cand runs it as a System Administrator. The query returnsnullfor every row even though the field has data in the org.Competent move: SFDX field metadata grants the field's existence — it does not grant FLS to any profile or permission set, including System Administrator. Add an explicit
<fieldPermissions>entry (readable: true,editable: true) to a permission set and deploy it. Verify withSELECT Id, Field, PermissionsRead FROM FieldPermissions WHERE SobjectType = 'Opportunity' AND Field = 'Opportunity.Loan_Rate__c'.Tempting-but-wrong: Assuming System Administrator bypasses FLS in Apex queries. It does not — fields the running user can't read are stripped from query results silently (returning
null, not an error, making this bug hard to spot). Also: adding<fieldPermissions>for a<required>true</required>field fails deploy — required fields are always accessible; omit them.Verify: Query
FieldPermissionsin Developer Console or via the REST API. A missing row means no permission was granted — the deploy succeeded at the metadata layer but FLS was never assigned.
Operational Rules Quick Reference
Read this first. Each is imperative and concrete.
- DO query once into a
Mapand DML once outside loops. DON'T ever put SOQL/DML/EventBus.publishinside aforloop (100 SOQL / 150 DML / 50k rows ceiling). - DO keep one trigger per object with logic in a handler class. DON'T add a second trigger to an object, or a raw trigger to an object owned by a package's trigger framework (register through the framework instead).
- DO mutate records in
beforecontexts; create related records / roll-ups inafter. DON'T assign toTrigger.newin anaftertrigger (throws). - DO use Validation Rule → Flow → Apex in increasing order of complexity; one automation owner per (object, event). DON'T have a Flow and a trigger both mutating the same object on the same event.
- DO check how an object distinguishes record kinds before branching (Record Types vs. boolean flags) and branch accordingly. DON'T assume every object uses Record Types.
- DO make queries selective on indexed fields and verify with Query Plan (standard index: <30%/first-1M, <15%/beyond; custom index: <10%/first-1M, <5%/beyond
[volatile — verify live]). DON'T use leading wildcards,!=,NOT IN, or functions on indexed fields on large objects. - DO move HTTP callouts off the synchronous/UI path (Queueable +
Database.AllowsCallouts). DON'T call out synchronously from a trigger. - DO pass Ids/JSON to
@future; preferQueueablefor anything with state or chaining. DON'T pass sObjects to@future, or try to chain from a future. - DO use Batch Apex (
QueryLocator, 50M ceiling) above ~10k records. DON'T process LDV in a single synchronous transaction. - DO explicitly grant FLS via
<fieldPermissions>in a permset/profile, and omit<required>true</required>fields from it. DON'T assume a deployed field is visible — SFDX field XML grants FLS to no one. - DO store endpoints/secrets in Named Credentials. DON'T hardcode URLs/keys in Apex.
- DO mock every callout (
HttpCalloutMock/WebServiceMock) and assert real outcomes. DON'T rely on line-execution alone for the 75% coverage gate. - DO keep
seeAllData=falseand build data in@testSetup. DON'T useseeAllData=truefor record data. - DO wrap async work in
Test.startTest()/stopTest()to flush and assert it. DON'T assert async results withoutstopTest(). - DO use
@wire+cacheable=truefor reads, imperative Apex for mutations. DON'T do DML in acacheable=truemethod, or change state unguarded inrenderedCallback(infinite render). - DO enforce FLS/CRUD in code (
WITH USER_MODE/Security.stripInaccessible/ describe checks) in API/without sharingpaths. DON'T trust object access to imply field access. - DO use CMT for deployable/packaged config (free SOQL), Hierarchy Custom Settings for per-user runtime toggles.
- DO run
sf project deployfrom the SFDX project root. DON'T run it from a parent repo root (InvalidProjectWorkspaceError). - DO verify changes against the live org via API query before trusting a deploy/config "success" message. DON'T assume a UI tool edited the right place.
- DON'T log PII (names, addresses, DOB, medical, file contents) via
System.debug.
References
- references/study-resources.md — credential logistics, study path, official links, superbadge requirements.
- references/recursion-control.md — static Boolean guard patterns, context-scoped guards, package framework interaction.
- references/invocable-apex.md —
@InvocableMethodsignature rules, callout paths, bulkification, testing patterns. - references/cdc.md — Change Data Capture: enabling, payload structure, rollback behavior, Apex testing.
- references/scenarios.md — additional decision scenarios (platform event rollback, Flow/trigger recursion, dynamic SOQL injection).
- references/advanced-fundamentals.md — CMT vs Custom Settings decision, multi-currency handling, and enterprise design patterns (Singleton, Strategy, fflib).
- references/visualforce.md — Visualforce PD2 exam topics: StandardSetController pagination, controller extensions, partial page refresh, JS DOM targeting, error handling, PDF rendering.
Feedback protocol
Using this skill and hit a wall? If you find a claim contradicted by the live system or official docs, a missing rule that cost you a wrong attempt, or a decision this skill gave no criteria for — append an entry in the moment to .skill-feedback/salesforce-platform-developer-2.md at the project root (create it if absent):
date | skill last-reviewed | claim or gap | what you observed instead | evidence (error text / doc URL / query output) | suggested fix
These are harvested back into the skill via the learning loop. When the live system and this file disagree, trust the live system.
Changelog
- 2026-06-10 — Cycle-4 curation (inbox): corrected SOQL selectivity thresholds (standard 30%/15%, custom 10%/5% — was inverted 10%/33%); added Platform Events Winter '27 standard-volume retirement note; updated superbadge count 4→3 (Lightning Component Framework Specialist removed per current requirements); updated stale PDF exam-guide link to live Trailhead Help page; added Visualforce PD2 exam scope section (StandardSetController, extensions, partial-page refresh, JS targeting, error handling).
- 2026-06-09 — Conformed to the 12-dimension skill standard: task-vocab description + Scope block, Uncertainty & Escalation guidance with inline
[volatile — verify live]marks, executable workflows, tool-agnostic verify steps, and the feedback protocol above. Exam logistics relocated to references/study-resources.md;last-reviewedset to 2026-06-09.
Independent educational content to upskill AI agents. Not affiliated with or endorsed by Salesforce; "Salesforce," "Apex," "Lightning," and all related marks are trademarks of Salesforce, Inc., used here solely to identify the subject matter. All trademarks belong to their respective owners. Guidance only — verify against official Salesforce documentation and live orgs before acting. No certification outcome is implied or guaranteed.