Antigravity-awesome-skills salesforce-development

Expert patterns for Salesforce platform development including

install
source · Clone the upstream repo
git clone https://github.com/sickn33/antigravity-awesome-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/sickn33/antigravity-awesome-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/antigravity-awesome-skills/skills/salesforce-development" ~/.claude/skills/sickn33-antigravity-awesome-skills-salesforce-development-d2c879 && rm -rf "$T"
manifest: plugins/antigravity-awesome-skills/skills/salesforce-development/SKILL.md
safety · automated scan (medium risk)
This is a pattern-based risk scan, not a security review. Our crawler flagged:
  • global npm install
  • references .env files
Always read a skill's source content before installing. Patterns alone don't mean the skill is malicious — but they warrant attention.
source content

Salesforce Development

Expert patterns for Salesforce platform development including Lightning Web Components (LWC), Apex triggers and classes, REST/Bulk APIs, Connected Apps, and Salesforce DX with scratch orgs and 2nd generation packages (2GP).

Patterns

Lightning Web Component with Wire Service

Use @wire decorator for reactive data binding with Lightning Data Service or Apex methods. @wire fits LWC's reactive architecture and enables Salesforce performance optimizations.

// myComponent.js import { LightningElement, wire, api } from 'lwc'; import { getRecord, getFieldValue } from 'lightning/uiRecordApi'; import getRelatedRecords from '@salesforce/apex/MyController.getRelatedRecords'; import ACCOUNT_NAME from '@salesforce/schema/Account.Name'; import ACCOUNT_INDUSTRY from '@salesforce/schema/Account.Industry';

const FIELDS = [ACCOUNT_NAME, ACCOUNT_INDUSTRY];

export default class MyComponent extends LightningElement { @api recordId; // Passed from parent or record page

// Wire to Lightning Data Service (preferred for single records) @wire(getRecord, { recordId: '$recordId', fields: FIELDS }) account;

// Wire to Apex method (for complex queries) @wire(getRelatedRecords, { accountId: '$recordId' }) wiredRecords({ error, data }) { if (data) { this.relatedRecords = data; this.error = undefined; } else if (error) { this.error = error; this.relatedRecords = undefined; } }

get accountName() { return getFieldValue(this.account.data, ACCOUNT_NAME); }

get isLoading() { return !this.account.data && !this.account.error; }

// Reactive: changing recordId automatically re-fetches }

// myComponent.html <template> <lightning-card title={accountName}> <template if:true={isLoading}> <lightning-spinner alternative-text="Loading"></lightning-spinner> </template>

<template if:true={account.data}>
  <p>Industry: {industry}</p>
</template>

<template if:true={error}>
  <p class="slds-text-color_error">{error.body.message}</p>
</template>
</lightning-card> </template>

// MyController.cls public with sharing class MyController { @AuraEnabled(cacheable=true) public static List<Contact> getRelatedRecords(Id accountId) { return [ SELECT Id, Name, Email, Phone FROM Contact WHERE AccountId = :accountId WITH SECURITY_ENFORCED LIMIT 100 ]; } }

Context

  • building LWC components
  • fetching Salesforce data
  • reactive UI

Bulkified Apex Trigger with Handler Pattern

Apex triggers must be bulkified to handle 200+ records per transaction. Use handler pattern for separation of concerns, testability, and recursion prevention.

// AccountTrigger.trigger trigger AccountTrigger on Account ( before insert, before update, before delete, after insert, after update, after delete, after undelete ) { new AccountTriggerHandler().run(); }

// TriggerHandler.cls (base class) public virtual class TriggerHandler { // Recursion prevention private static Set<String> executedHandlers = new Set<String>();

public void run() { String handlerName = String.valueOf(this).split(':')[0];

// Prevent recursion
String contextKey = handlerName + '_' + Trigger.operationType;
if (executedHandlers.contains(contextKey)) {
  return;
}
executedHandlers.add(contextKey);

switch on Trigger.operationType {
  when BEFORE_INSERT { this.beforeInsert(); }
  when BEFORE_UPDATE { this.beforeUpdate(); }
  when BEFORE_DELETE { this.beforeDelete(); }
  when AFTER_INSERT { this.afterInsert(); }
  when AFTER_UPDATE { this.afterUpdate(); }
  when AFTER_DELETE { this.afterDelete(); }
  when AFTER_UNDELETE { this.afterUndelete(); }
}

}

// Override in child classes protected virtual void beforeInsert() {} protected virtual void beforeUpdate() {} protected virtual void beforeDelete() {} protected virtual void afterInsert() {} protected virtual void afterUpdate() {} protected virtual void afterDelete() {} protected virtual void afterUndelete() {} }

// AccountTriggerHandler.cls public class AccountTriggerHandler extends TriggerHandler { private List<Account> newAccounts; private List<Account> oldAccounts; private Map<Id, Account> newMap; private Map<Id, Account> oldMap;

public AccountTriggerHandler() { this.newAccounts = (List<Account>) Trigger.new; this.oldAccounts = (List<Account>) Trigger.old; this.newMap = (Map<Id, Account>) Trigger.newMap; this.oldMap = (Map<Id, Account>) Trigger.oldMap; }

protected override void afterInsert() { createDefaultContacts(); notifySlack(); }

protected override void afterUpdate() { handleIndustryChange(); }

// BULKIFIED: Query once, update once private void createDefaultContacts() { List<Contact> contactsToInsert = new List<Contact>();

for (Account acc : newAccounts) {
  if (acc.Type == 'Prospect') {
    contactsToInsert.add(new Contact(
      AccountId = acc.Id,
      LastName = 'Primary Contact',
      Email = 'contact@' + acc.Website
    ));
  }
}

if (!contactsToInsert.isEmpty()) {
  insert contactsToInsert;  // Single DML for all
}

}

private void handleIndustryChange() { Set<Id> changedAccountIds = new Set<Id>();

for (Account acc : newAccounts) {
  Account oldAcc = oldMap.get(acc.Id);
  if (acc.Industry != oldAcc.Industry) {
    changedAccountIds.add(acc.Id);
  }
}

if (!changedAccountIds.isEmpty()) {
  // Queue async processing for heavy work
  System.enqueueJob(new IndustryChangeQueueable(changedAccountIds));
}

}

private void notifySlack() { // Offload callouts to async List<Id> accountIds = new List<Id>(newMap.keySet()); System.enqueueJob(new SlackNotificationQueueable(accountIds)); } }

Context

  • apex triggers
  • data operations
  • automation

Queueable Apex for Async Processing

Use Queueable Apex for async processing with support for non-primitive types, monitoring via AsyncApexJob, and job chaining. Limit: 50 jobs per transaction, 1 child job when chaining.

// IndustryChangeQueueable.cls public class IndustryChangeQueueable implements Queueable, Database.AllowsCallouts { private Set<Id> accountIds; private Integer retryCount;

public IndustryChangeQueueable(Set<Id> accountIds) { this(accountIds, 0); }

public IndustryChangeQueueable(Set<Id> accountIds, Integer retryCount) { this.accountIds = accountIds; this.retryCount = retryCount; }

public void execute(QueueableContext context) { try { // Query with fresh data List<Account> accounts = [ SELECT Id, Name, Industry, OwnerId FROM Account WHERE Id IN :accountIds WITH SECURITY_ENFORCED ];

  // Process and make callout
  for (Account acc : accounts) {
    syncToExternalSystem(acc);
  }

  // Update records
  updateRelatedOpportunities(accountIds);

} catch (Exception e) {
  handleError(e);
}

}

private void syncToExternalSystem(Account acc) { HttpRequest req = new HttpRequest(); req.setEndpoint('callout:ExternalCRM/accounts'); req.setMethod('POST'); req.setHeader('Content-Type', 'application/json'); req.setBody(JSON.serialize(new Map<String, Object>{ 'salesforceId' => acc.Id, 'name' => acc.Name, 'industry' => acc.Industry }));

Http http = new Http();
HttpResponse res = http.send(req);

if (res.getStatusCode() != 200 && res.getStatusCode() != 201) {
  throw new CalloutException('Sync failed: ' + res.getBody());
}

}

private void updateRelatedOpportunities(Set<Id> accIds) { List<Opportunity> oppsToUpdate = [ SELECT Id, Industry__c, AccountId FROM Opportunity WHERE AccountId IN :accIds WITH SECURITY_ENFORCED ];

Map<Id, Account> accountMap = new Map<Id, Account>([
  SELECT Id, Industry FROM Account WHERE Id IN :accIds
]);

for (Opportunity opp : oppsToUpdate) {
  opp.Industry__c = accountMap.get(opp.AccountId).Industry;
}

if (!oppsToUpdate.isEmpty()) {
  update oppsToUpdate;
}

}

private void handleError(Exception e) { // Log error System.debug(LoggingLevel.ERROR, 'Queueable failed: ' + e.getMessage());

// Retry with exponential backoff (max 3 retries)
if (retryCount < 3) {
  // Chain new job for retry
  System.enqueueJob(new IndustryChangeQueueable(accountIds, retryCount + 1));
} else {
  // Create error record for monitoring
  insert new Integration_Error__c(
    Type__c = 'Industry Sync',
    Message__c = e.getMessage(),
    Stack_Trace__c = e.getStackTraceString(),
    Record_Ids__c = String.join(new List<Id>(accountIds), ',')
  );
}

} }

Context

  • async processing
  • long-running operations
  • callouts from triggers

REST API Integration with Connected App

External integrations use Connected Apps with OAuth 2.0. JWT Bearer flow for server-to-server, Web Server flow for user-facing apps. Always use Named Credentials for secure callout configuration.

// Node.js - JWT Bearer Flow (server-to-server) import jwt from 'jsonwebtoken'; import fs from 'fs';

class SalesforceClient { private accessToken: string | null = null; private instanceUrl: string | null = null; private tokenExpiry: number = 0;

constructor( private clientId: string, private username: string, private privateKeyPath: string, private loginUrl: string = 'https://login.salesforce.com' ) {}

async authenticate(): Promise<void> { // Check if token is still valid (5 min buffer) if (this.accessToken && Date.now() < this.tokenExpiry - 300000) { return; }

const privateKey = fs.readFileSync(this.privateKeyPath, 'utf8');

// Create JWT assertion
const claim = {
  iss: this.clientId,
  sub: this.username,
  aud: this.loginUrl,
  exp: Math.floor(Date.now() / 1000) + 300  // 5 minutes
};

const assertion = jwt.sign(claim, privateKey, { algorithm: 'RS256' });

// Exchange JWT for access token
const response = await fetch(`${this.loginUrl}/services/oauth2/token`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
    assertion
  })
});

if (!response.ok) {
  const error = await response.json();
  throw new Error(`Auth failed: ${error.error_description}`);
}

const data = await response.json();
this.accessToken = data.access_token;
this.instanceUrl = data.instance_url;
this.tokenExpiry = Date.now() + 7200000;  // 2 hours

}

async query(soql: string): Promise<any> { await this.authenticate();

const response = await fetch(
  `${this.instanceUrl}/services/data/v59.0/query?q=${encodeURIComponent(soql)}`,
  {
    headers: {
      'Authorization': `Bearer ${this.accessToken}`,
      'Content-Type': 'application/json'
    }
  }
);

if (!response.ok) {
  await this.handleError(response);
}

return response.json();

}

async createRecord(sobject: string, data: object): Promise<any> { await this.authenticate();

const response = await fetch(
  `${this.instanceUrl}/services/data/v59.0/sobjects/${sobject}`,
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${this.accessToken}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
  }
);

if (!response.ok) {
  await this.handleError(response);
}

return response.json();

}

private async handleError(response: Response): Promise<never> { const error = await response.json();

if (response.status === 401) {
  // Token expired, clear and retry
  this.accessToken = null;
  throw new Error('Session expired, retry required');
}

throw new Error(`API Error: ${JSON.stringify(error)}`);

} }

// Usage const sf = new SalesforceClient( process.env.SF_CLIENT_ID!, process.env.SF_USERNAME!, './certificates/server.key' );

const accounts = await sf.query( "SELECT Id, Name FROM Account WHERE CreatedDate = TODAY" );

Context

  • external integration
  • REST API access
  • connected apps

Bulk API 2.0 for Large Data Operations

Use Bulk API 2.0 for operations on 10K+ records. Asynchronous processing with job-based workflow. Part of REST API with streamlined interface compared to original Bulk API.

// Node.js - Bulk API 2.0 insert class SalesforceBulkClient extends SalesforceClient {

async bulkInsert(sobject: string, records: object[]): Promise<any> { await this.authenticate();

// Step 1: Create job
const job = await this.createBulkJob(sobject, 'insert');

try {
  // Step 2: Upload data (CSV format)
  await this.uploadJobData(job.id, records);

  // Step 3: Close job to start processing
  await this.closeJob(job.id);

  // Step 4: Poll for completion
  return await this.waitForJobCompletion(job.id);

} catch (error) {
  // Abort job on error
  await this.abortJob(job.id);
  throw error;
}

}

private async createBulkJob(sobject: string, operation: string): Promise<any> { const response = await fetch(

${this.instanceUrl}/services/data/v59.0/jobs/ingest
, { method: 'POST', headers: { 'Authorization':
Bearer ${this.accessToken}
, 'Content-Type': 'application/json' }, body: JSON.stringify({ object: sobject, operation, contentType: 'CSV', lineEnding: 'LF' }) } );

return response.json();

}

private async uploadJobData(jobId: string, records: object[]): Promise<void> { // Convert to CSV const csv = this.recordsToCSV(records);

await fetch(
  `${this.instanceUrl}/services/data/v59.0/jobs/ingest/${jobId}/batches`,
  {
    method: 'PUT',
    headers: {
      'Authorization': `Bearer ${this.accessToken}`,
      'Content-Type': 'text/csv'
    },
    body: csv
  }
);

}

private async closeJob(jobId: string): Promise<void> { await fetch(

${this.instanceUrl}/services/data/v59.0/jobs/ingest/${jobId}
, { method: 'PATCH', headers: { 'Authorization':
Bearer ${this.accessToken}
, 'Content-Type': 'application/json' }, body: JSON.stringify({ state: 'UploadComplete' }) } ); }

private async waitForJobCompletion(jobId: string): Promise<any> { const maxWaitTime = 10 * 60 * 1000; // 10 minutes const pollInterval = 5000; // 5 seconds const startTime = Date.now();

while (Date.now() - startTime < maxWaitTime) {
  const response = await fetch(
    `${this.instanceUrl}/services/data/v59.0/jobs/ingest/${jobId}`,
    {
      headers: { 'Authorization': `Bearer ${this.accessToken}` }
    }
  );

  const job = await response.json();

  if (job.state === 'JobComplete') {
    // Get results
    return {
      success: job.numberRecordsProcessed - job.numberRecordsFailed,
      failed: job.numberRecordsFailed,
      failedResults: job.numberRecordsFailed > 0
        ? await this.getFailedResults(jobId)
        : []
    };
  }

  if (job.state === 'Failed' || job.state === 'Aborted') {
    throw new Error(`Bulk job failed: ${job.state}`);
  }

  await new Promise(r => setTimeout(r, pollInterval));
}

throw new Error('Bulk job timeout');

}

private async getFailedResults(jobId: string): Promise<any[]> { const response = await fetch(

${this.instanceUrl}/services/data/v59.0/jobs/ingest/${jobId}/failedResults
, { headers: { 'Authorization':
Bearer ${this.accessToken}
} } );

const csv = await response.text();
return this.parseCSV(csv);

}

private recordsToCSV(records: object[]): string { if (records.length === 0) return '';

const headers = Object.keys(records[0]);
const rows = records.map(r =>
  headers.map(h => this.escapeCSV(r[h])).join(',')
);

return [headers.join(','), ...rows].join('\n');

}

private escapeCSV(value: any): string { if (value === null || value === undefined) return ''; const str = String(value); if (str.includes(',') || str.includes('"') || str.includes('\n')) { return

"${str.replace(/"/g, '""')}"
; } return str; } }

Context

  • large data volumes
  • data migration
  • bulk operations

Salesforce DX with Scratch Orgs

Source-driven development with disposable scratch orgs for isolated testing. Scratch orgs exist 7-30 days and can be created throughout the day, unlike sandbox refresh limits.

// project-scratch-def.json - Scratch org definition { "orgName": "MyApp Dev Org", "edition": "Developer", "features": ["EnableSetPasswordInApi", "Communities"], "settings": { "lightningExperienceSettings": { "enableS1DesktopEnabled": true }, "mobileSettings": { "enableS1EncryptedStoragePref2": false }, "securitySettings": { "passwordPolicies": { "enableSetPasswordInApi": true } } } }

// sfdx-project.json - Project configuration { "packageDirectories": [ { "path": "force-app", "default": true, "package": "MyPackage", "versionName": "ver 1.0", "versionNumber": "1.0.0.NEXT", "dependencies": [ { "package": "SomePackage@2.0.0" } ] } ], "namespace": "myns", "sfdcLoginUrl": "https://login.salesforce.com", "sourceApiVersion": "59.0" }

Development workflow commands

1. Create scratch org

sf org create scratch
--definition-file config/project-scratch-def.json
--alias myapp-dev
--duration-days 7
--set-default

2. Push source to scratch org

sf project deploy start --target-org myapp-dev

3. Assign permission set

sf org assign permset --name MyApp_Admin --target-org myapp-dev

4. Import sample data

sf data import tree --plan data/sample-data-plan.json --target-org myapp-dev

5. Open org

sf org open --target-org myapp-dev

6. Run tests

sf apex run test
--code-coverage
--result-format human
--wait 10
--target-org myapp-dev

7. Pull changes back

sf project retrieve start --target-org myapp-dev

Context

  • development workflow
  • CI/CD
  • testing

2nd Generation Package (2GP) Development

2GP replaces 1GP with source-driven, modular packaging. Requires Dev Hub with 2GP enabled, namespace linked, and 75% code coverage for promoted packages.

Enable Dev Hub and 2GP in Setup:

Setup > Dev Hub > Enable Dev Hub

Setup > Dev Hub > Enable Unlocked Packages and 2GP

Link namespace (required for managed packages)

sf package create
--name "MyManagedPackage"
--package-type Managed
--path force-app
--target-dev-hub DevHub

Create package version (beta)

sf package version create
--package "MyManagedPackage"
--installation-key-bypass
--wait 30
--code-coverage
--target-dev-hub DevHub

Check version status

sf package version list --packages "MyManagedPackage" --target-dev-hub DevHub

Promote to released (requires 75% coverage)

sf package version promote
--package "MyManagedPackage@1.0.0-1"
--target-dev-hub DevHub

Install in sandbox for testing

sf package install
--package "MyManagedPackage@1.0.0-1"
--target-org MySandbox
--wait 20

CI/CD Pipeline (GitHub Actions)

.github/workflows/salesforce-ci.yml

name: Salesforce CI

on: push: branches: [main, develop] pull_request: branches: [main]

jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4

  - name: Install Salesforce CLI
    run: npm install -g @salesforce/cli

  - name: Authenticate Dev Hub
    run: |
      echo "${{ secrets.SFDX_AUTH_URL }}" > auth.txt
      sf org login sfdx-url --sfdx-url-file auth.txt --alias DevHub --set-default-dev-hub

  - name: Create Scratch Org
    run: |
      sf org create scratch \
        --definition-file config/project-scratch-def.json \
        --alias ci-scratch \
        --duration-days 1 \
        --set-default

  - name: Deploy Source
    run: sf project deploy start --target-org ci-scratch

  - name: Run Tests
    run: |
      sf apex run test \
        --code-coverage \
        --result-format human \
        --wait 20 \
        --target-org ci-scratch

  - name: Delete Scratch Org
    if: always()
    run: sf org delete scratch --target-org ci-scratch --no-prompt

Context

  • packaging
  • ISV development
  • AppExchange

Sharp Edges

Governor Limits Apply Per Transaction, Not Per Record

Severity: CRITICAL

@wire Results Are Cached and May Be Stale

Severity: HIGH

LWC Properties Are Case-Sensitive

Severity: MEDIUM

Null Pointer Exceptions in Apex Collections

Severity: HIGH

Trigger Recursion Causes Infinite Loops

Severity: CRITICAL

Cannot Make Callouts from Synchronous Triggers

Severity: HIGH

Cannot Mix Setup and Non-Setup DML

Severity: HIGH

Dynamic SOQL Is Vulnerable to Injection

Severity: CRITICAL

Scratch Orgs Expire and Lose All Data

Severity: MEDIUM

API Version Mismatches Cause Silent Failures

Severity: MEDIUM

Validation Checks

SOQL Query Inside Loop

Severity: ERROR

SOQL in loops causes governor limit exceptions with bulk data

Message: SOQL query inside loop. Query once outside the loop and use a Map.

DML Operation Inside Loop

Severity: ERROR

DML in loops hits 150 statement limit

Message: DML operation inside loop. Collect records and perform single DML outside loop.

HTTP Callout in Trigger

Severity: ERROR

Synchronous triggers cannot make callouts

Message: Callout in trigger. Use @future(callout=true) or Queueable with Database.AllowsCallouts.

Potential SOQL Injection

Severity: ERROR

Dynamic SOQL with string concatenation is vulnerable

Message: Dynamic SOQL with concatenation. Use bind variables or String.escapeSingleQuotes().

Missing WITH SECURITY_ENFORCED

Severity: WARNING

SOQL should enforce FLS/CRUD permissions

Message: SOQL without security enforcement. Add WITH SECURITY_ENFORCED.

Hardcoded Salesforce ID

Severity: WARNING

Record IDs differ between orgs

Message: Hardcoded Salesforce ID. Query by DeveloperName or ExternalId instead.

Hardcoded Credentials

Severity: ERROR

Credentials must use Named Credentials or Custom Metadata

Message: Hardcoded credentials. Use Named Credentials or Custom Metadata.

Direct DOM Manipulation in LWC

Severity: WARNING

LWC uses shadow DOM, direct manipulation breaks encapsulation

Message: Direct DOM access in LWC. Use this.template.querySelector() or data binding.

Reactive Property Without @track

Severity: INFO

Complex object properties need @track for reactivity

Message: Object assignment may need @track for reactivity (post-Spring '20 objects are auto-tracked).

Wire Without Refresh After DML

Severity: WARNING

Cached wire data becomes stale after updates

Message: DML after @wire without refreshApex. Data may be stale.

Collaboration

Delegation Triggers

  • user needs external API integration -> backend (REST API design, external system sync)
  • user needs complex UI beyond LWC -> frontend (Custom portal with React/Next.js)
  • user needs HubSpot integration -> hubspot-integration (Salesforce-HubSpot sync patterns)
  • user needs data warehouse sync -> data-engineer (ETL from Salesforce to warehouse)
  • user needs payment processing -> stripe-integration (Beyond Salesforce Billing)
  • user needs advanced auth -> auth-specialist (SSO, SAML, custom portals)

When to Use

  • User mentions or implies: salesforce
  • User mentions or implies: sfdc
  • User mentions or implies: apex
  • User mentions or implies: lwc
  • User mentions or implies: lightning web components
  • User mentions or implies: sfdx
  • User mentions or implies: scratch org
  • User mentions or implies: visualforce
  • User mentions or implies: soql
  • User mentions or implies: governor limits
  • User mentions or implies: connected app

Limitations

  • Use this skill only when the task clearly matches the scope described above.
  • Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
  • Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.