All skills
jeffallan avatar

/salesforce-developer

@efebc44
by jeffallanjeffallan/claude-skills12k stars
1,124

Writes and debugs Apex code, builds Lightning Web Components, optimizes SOQL queries, implements triggers, batch jobs, platform events, and integrations on the Salesforce platform. Use when developing Salesforce applications, customizing CRM workflows, managing governor limits, bulk processing, or setting up Salesforce DX and CI/CD pipelines.

Use this Skill: https://skilld.dev/gh/jeffallan/claude-skills/salesforce-developer

This session only. Nothing lands on disk.

referenceslightning-web-components.md

≈6k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Lightning Web Components


Component Structure

Basic LWC Anatomy

Every Lightning Web Component consists of three files:

myComponent/
├── myComponent.html     # Template
├── myComponent.js       # JavaScript controller
└── myComponent.js-meta.xml  # Configuration

HTML Template

<!-- accountCard.html -->
<template>
    <lightning-card title={cardTitle} icon-name="standard:account">
        <div class="slds-p-around_medium">
            <!-- Conditional rendering -->
            <template lwc:if={isLoading}>
                <lightning-spinner alternative-text="Loading"></lightning-spinner>
            </template>

            <template lwc:else>
                <!-- Iteration -->
                <template for:each={accounts} for:item="account">
                    <div key={account.Id} class="slds-m-bottom_small">
                        <lightning-tile
                            label={account.Name}
                            href={account.recordUrl}>
                            <p class="slds-truncate">{account.Industry}</p>
                            <p class="slds-text-body_small">
                                Revenue: <lightning-formatted-number
                                    value={account.AnnualRevenue}
                                    format-style="currency"
                                    currency-code="USD">
                                </lightning-formatted-number>
                            </p>
                        </lightning-tile>
                    </div>
                </template>

                <!-- Empty state -->
                <template lwc:if={isEmpty}>
                    <div class="slds-align_absolute-center slds-p-around_large">
                        <p>No accounts found</p>
                    </div>
                </template>
            </template>
        </div>

        <!-- Footer slot -->
        <div slot="footer">
            <lightning-button
                label="Refresh"
                onclick={handleRefresh}
                disabled={isLoading}>
            </lightning-button>
        </div>
    </lightning-card>
</template>

JavaScript Controller

// accountCard.js
import { LightningElement, api, wire, track } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { refreshApex } from '@salesforce/apex';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';

export default class AccountCard extends NavigationMixin(LightningElement) {

    // Public properties - exposed to parent components
    @api recordId;
    @api maxRecords = 10;

    // Private reactive properties
    accounts = [];
    error;
    isLoading = true;

    // Cached wire result for refresh
    wiredAccountsResult;

    // Computed properties
    get cardTitle() {
        return `Accounts (${this.accounts.length})`;
    }

    get isEmpty() {
        return this.accounts.length === 0;
    }

    // Wire service - reactive data binding
    @wire(getAccounts, { recordId: '$recordId', maxRecords: '$maxRecords' })
    wiredAccounts(result) {
        this.wiredAccountsResult = result;
        const { data, error } = result;

        if (data) {
            this.accounts = data.map(account => ({
                ...account,
                recordUrl: `/lightning/r/Account/${account.Id}/view`
            }));
            this.error = undefined;
        } else if (error) {
            this.error = error;
            this.accounts = [];
            this.showError('Error loading accounts', this.reduceErrors(error));
        }

        this.isLoading = false;
    }

    // Lifecycle hooks
    connectedCallback() {
        console.log('Component connected, recordId:', this.recordId);
    }

    disconnectedCallback() {
        console.log('Component disconnected');
    }

    renderedCallback() {
        // Called after every render
    }

    errorCallback(error, stack) {
        console.error('Component error:', error, stack);
    }

    // Event handlers
    handleRefresh() {
        this.isLoading = true;
        refreshApex(this.wiredAccountsResult)
            .finally(() => {
                this.isLoading = false;
            });
    }

    // Helper methods
    showError(title, message) {
        this.dispatchEvent(new ShowToastEvent({
            title,
            message,
            variant: 'error'
        }));
    }

    reduceErrors(errors) {
        if (!Array.isArray(errors)) {
            errors = [errors];
        }

        return errors
            .filter(error => !!error)
            .map(error => {
                if (typeof error === 'string') {
                    return error;
                }
                if (error.body?.message) {
                    return error.body.message;
                }
                if (error.message) {
                    return error.message;
                }
                return JSON.stringify(error);
            })
            .join(', ');
    }
}

Meta Configuration

<!-- accountCard.js-meta.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>59.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Account Card</masterLabel>
    <description>Displays account information in a card format</description>

    <targets>
        <target>lightning__RecordPage</target>
        <target>lightning__AppPage</target>
        <target>lightning__HomePage</target>
        <target>lightningCommunity__Page</target>
    </targets>

    <targetConfigs>
        <targetConfig targets="lightning__RecordPage">
            <objects>
                <object>Account</object>
                <object>Contact</object>
            </objects>
            <property name="maxRecords" type="Integer" default="10"
                      label="Maximum Records" description="Max accounts to display" />
        </targetConfig>
        <targetConfig targets="lightning__AppPage,lightning__HomePage">
            <property name="maxRecords" type="Integer" default="10"
                      label="Maximum Records" />
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

Wire Service Patterns

Wire with Apex Methods

// Apex Controller
public with sharing class AccountController {

    @AuraEnabled(cacheable=true)
    public static List<Account> getAccounts(Id recordId, Integer maxRecords) {
        return [
            SELECT Id, Name, Industry, AnnualRevenue
            FROM Account
            WHERE Id != :recordId
            ORDER BY AnnualRevenue DESC NULLS LAST
            LIMIT :maxRecords
        ];
    }

    @AuraEnabled
    public static Account updateAccount(Id accountId, Map<String, Object> fields) {
        Account acc = new Account(Id = accountId);
        for (String fieldName : fields.keySet()) {
            acc.put(fieldName, fields.get(fieldName));
        }
        update acc;
        return acc;
    }
}
// LWC using wire service
import { LightningElement, wire } from 'lwc';
import { getRecord, getFieldValue, updateRecord } from 'lightning/uiRecordApi';
import ACCOUNT_NAME from '@salesforce/schema/Account.Name';
import ACCOUNT_INDUSTRY from '@salesforce/schema/Account.Industry';
import ACCOUNT_REVENUE from '@salesforce/schema/Account.AnnualRevenue';

const FIELDS = [ACCOUNT_NAME, ACCOUNT_INDUSTRY, ACCOUNT_REVENUE];

export default class AccountDetail extends LightningElement {
    @api recordId;

    @wire(getRecord, { recordId: '$recordId', fields: FIELDS })
    account;

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

    get industry() {
        return getFieldValue(this.account.data, ACCOUNT_INDUSTRY);
    }

    async handleUpdate() {
        const fields = {};
        fields.Id = this.recordId;
        fields[ACCOUNT_INDUSTRY.fieldApiName] = 'Technology';

        try {
            await updateRecord({ fields });
            this.dispatchEvent(new ShowToastEvent({
                title: 'Success',
                message: 'Account updated',
                variant: 'success'
            }));
        } catch (error) {
            this.dispatchEvent(new ShowToastEvent({
                title: 'Error',
                message: error.body.message,
                variant: 'error'
            }));
        }
    }
}

Imperative Apex Calls

Use when you need control over when data is fetched.

import { LightningElement, api } from 'lwc';
import searchAccounts from '@salesforce/apex/AccountController.searchAccounts';

export default class AccountSearch extends LightningElement {
    searchTerm = '';
    accounts = [];
    isSearching = false;

    handleSearchChange(event) {
        this.searchTerm = event.target.value;
    }

    async handleSearch() {
        if (this.searchTerm.length < 2) {
            return;
        }

        this.isSearching = true;

        try {
            this.accounts = await searchAccounts({ searchTerm: this.searchTerm });
        } catch (error) {
            console.error('Search error:', error);
            this.accounts = [];
        } finally {
            this.isSearching = false;
        }
    }

    // Debounced search for better UX
    debounceTimeout;
    handleSearchInput(event) {
        clearTimeout(this.debounceTimeout);
        this.searchTerm = event.target.value;

        this.debounceTimeout = setTimeout(() => {
            this.handleSearch();
        }, 300);
    }
}

Component Communication

Parent to Child (Public Properties)

// Parent component
// parent.html
<template>
    <c-child-component
        account-id={selectedAccountId}
        display-mode="compact"
        onselect={handleChildSelect}>
    </c-child-component>
</template>
// Child component
// childComponent.js
import { LightningElement, api } from 'lwc';

export default class ChildComponent extends LightningElement {
    @api accountId;
    @api displayMode = 'full'; // Default value

    // Public method callable by parent
    @api
    refresh() {
        // Refresh logic
    }

    @api
    validate() {
        const input = this.template.querySelector('lightning-input');
        return input.reportValidity();
    }
}

Child to Parent (Custom Events)

// Child component dispatching event
export default class ChildComponent extends LightningElement {

    handleAccountSelect(event) {
        const accountId = event.target.dataset.id;

        // Simple event
        this.dispatchEvent(new CustomEvent('select', {
            detail: { accountId }
        }));

        // Bubbling event (crosses shadow DOM)
        this.dispatchEvent(new CustomEvent('accountselected', {
            detail: { accountId, accountName: this.accountName },
            bubbles: true,
            composed: true
        }));
    }
}
// Parent component handling event
// parent.html
<template>
    <c-child-component onselect={handleSelect}></c-child-component>
</template>

// parent.js
handleSelect(event) {
    const { accountId } = event.detail;
    console.log('Selected account:', accountId);
}

Sibling Communication (Lightning Message Service)

// messageChannel/AccountSelected.messageChannel-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningMessageChannel xmlns="http://soap.sforce.com/2006/04/metadata">
    <masterLabel>Account Selected</masterLabel>
    <isExposed>true</isExposed>
    <description>Channel for account selection events</description>
    <lightningMessageFields>
        <fieldName>accountId</fieldName>
        <description>Selected Account ID</description>
    </lightningMessageFields>
    <lightningMessageFields>
        <fieldName>source</fieldName>
        <description>Component that sent message</description>
    </lightningMessageFields>
</LightningMessageChannel>
// Publisher component
import { LightningElement, wire } from 'lwc';
import { publish, MessageContext } from 'lightning/messageService';
import ACCOUNT_SELECTED from '@salesforce/messageChannel/AccountSelected__c';

export default class AccountPublisher extends LightningElement {
    @wire(MessageContext)
    messageContext;

    handleAccountClick(event) {
        const accountId = event.target.dataset.id;

        const payload = {
            accountId: accountId,
            source: 'AccountPublisher'
        };

        publish(this.messageContext, ACCOUNT_SELECTED, payload);
    }
}
// Subscriber component
import { LightningElement, wire } from 'lwc';
import { subscribe, unsubscribe, MessageContext } from 'lightning/messageService';
import ACCOUNT_SELECTED from '@salesforce/messageChannel/AccountSelected__c';

export default class AccountSubscriber extends LightningElement {
    subscription = null;
    selectedAccountId;

    @wire(MessageContext)
    messageContext;

    connectedCallback() {
        this.subscribeToMessageChannel();
    }

    disconnectedCallback() {
        this.unsubscribeFromMessageChannel();
    }

    subscribeToMessageChannel() {
        if (!this.subscription) {
            this.subscription = subscribe(
                this.messageContext,
                ACCOUNT_SELECTED,
                (message) => this.handleMessage(message)
            );
        }
    }

    unsubscribeFromMessageChannel() {
        unsubscribe(this.subscription);
        this.subscription = null;
    }

    handleMessage(message) {
        this.selectedAccountId = message.accountId;
        console.log('Received from:', message.source);
    }
}

Form Handling

Lightning Record Edit Form

<template>
    <lightning-record-edit-form
        record-id={recordId}
        object-api-name="Account"
        onsuccess={handleSuccess}
        onerror={handleError}
        onsubmit={handleSubmit}>

        <lightning-messages></lightning-messages>

        <lightning-input-field field-name="Name"></lightning-input-field>
        <lightning-input-field field-name="Industry"></lightning-input-field>
        <lightning-input-field field-name="AnnualRevenue"></lightning-input-field>

        <div class="slds-m-top_medium">
            <lightning-button
                type="submit"
                variant="brand"
                label="Save">
            </lightning-button>
        </div>
    </lightning-record-edit-form>
</template>
import { LightningElement, api } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

export default class AccountForm extends LightningElement {
    @api recordId;

    handleSubmit(event) {
        event.preventDefault();
        const fields = event.detail.fields;

        // Modify fields before submission
        fields.Description = `Updated on ${new Date().toISOString()}`;

        this.template.querySelector('lightning-record-edit-form').submit(fields);
    }

    handleSuccess(event) {
        const updatedRecord = event.detail.id;
        this.dispatchEvent(new ShowToastEvent({
            title: 'Success',
            message: 'Account saved successfully',
            variant: 'success'
        }));
    }

    handleError(event) {
        console.error('Form error:', event.detail);
    }
}

Custom Form Validation

<template>
    <lightning-card title="Custom Form">
        <div class="slds-p-around_medium">
            <lightning-input
                label="Account Name"
                value={accountName}
                onchange={handleNameChange}
                required
                class="validate">
            </lightning-input>

            <lightning-combobox
                label="Industry"
                value={industry}
                options={industryOptions}
                onchange={handleIndustryChange}
                required
                class="validate">
            </lightning-combobox>

            <lightning-input
                type="number"
                label="Annual Revenue"
                value={revenue}
                onchange={handleRevenueChange}
                min="0"
                formatter="currency"
                class="validate">
            </lightning-input>

            <div class="slds-m-top_medium">
                <lightning-button
                    label="Save"
                    variant="brand"
                    onclick={handleSave}>
                </lightning-button>
            </div>
        </div>
    </lightning-card>
</template>
import { LightningElement } from 'lwc';
import createAccount from '@salesforce/apex/AccountController.createAccount';

export default class CustomAccountForm extends LightningElement {
    accountName = '';
    industry = '';
    revenue = 0;

    industryOptions = [
        { label: 'Technology', value: 'Technology' },
        { label: 'Healthcare', value: 'Healthcare' },
        { label: 'Finance', value: 'Finance' },
        { label: 'Manufacturing', value: 'Manufacturing' }
    ];

    handleNameChange(event) {
        this.accountName = event.target.value;
    }

    handleIndustryChange(event) {
        this.industry = event.detail.value;
    }

    handleRevenueChange(event) {
        this.revenue = event.target.value;
    }

    validateForm() {
        const inputs = [...this.template.querySelectorAll('.validate')];
        const isValid = inputs.reduce((valid, input) => {
            input.reportValidity();
            return valid && input.checkValidity();
        }, true);

        return isValid;
    }

    async handleSave() {
        if (!this.validateForm()) {
            return;
        }

        try {
            const account = await createAccount({
                name: this.accountName,
                industry: this.industry,
                revenue: this.revenue
            });

            this.dispatchEvent(new ShowToastEvent({
                title: 'Success',
                message: `Account ${account.Name} created`,
                variant: 'success'
            }));

            this.resetForm();
        } catch (error) {
            this.dispatchEvent(new ShowToastEvent({
                title: 'Error',
                message: error.body.message,
                variant: 'error'
            }));
        }
    }

    resetForm() {
        this.accountName = '';
        this.industry = '';
        this.revenue = 0;
    }
}

Data Tables

Lightning Datatable

<template>
    <lightning-card title="Accounts">
        <lightning-datatable
            key-field="Id"
            data={accounts}
            columns={columns}
            sorted-by={sortedBy}
            sorted-direction={sortedDirection}
            onsort={handleSort}
            onrowaction={handleRowAction}
            show-row-number-column
            hide-checkbox-column={hideCheckbox}
            selected-rows={selectedRows}
            onrowselection={handleRowSelection}
            default-sort-direction="asc"
            enable-infinite-loading
            onloadmore={loadMoreData}>
        </lightning-datatable>
    </lightning-card>
</template>
import { LightningElement, wire } from 'lwc';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';

const COLUMNS = [
    {
        label: 'Account Name',
        fieldName: 'accountUrl',
        type: 'url',
        typeAttributes: {
            label: { fieldName: 'Name' },
            target: '_blank'
        },
        sortable: true
    },
    { label: 'Industry', fieldName: 'Industry', sortable: true },
    {
        label: 'Annual Revenue',
        fieldName: 'AnnualRevenue',
        type: 'currency',
        sortable: true,
        cellAttributes: { alignment: 'right' }
    },
    {
        label: 'Created Date',
        fieldName: 'CreatedDate',
        type: 'date',
        typeAttributes: {
            year: 'numeric',
            month: 'short',
            day: '2-digit'
        }
    },
    {
        type: 'action',
        typeAttributes: {
            rowActions: [
                { label: 'View', name: 'view' },
                { label: 'Edit', name: 'edit' },
                { label: 'Delete', name: 'delete' }
            ]
        }
    }
];

export default class AccountTable extends LightningElement {
    columns = COLUMNS;
    accounts = [];
    selectedRows = [];
    sortedBy = 'Name';
    sortedDirection = 'asc';
    hideCheckbox = false;

    @wire(getAccounts)
    wiredAccounts({ data, error }) {
        if (data) {
            this.accounts = data.map(account => ({
                ...account,
                accountUrl: `/lightning/r/Account/${account.Id}/view`
            }));
        }
    }

    handleSort(event) {
        const { fieldName, sortDirection } = event.detail;
        this.sortedBy = fieldName;
        this.sortedDirection = sortDirection;

        this.sortData(fieldName, sortDirection);
    }

    sortData(fieldName, direction) {
        const parseValue = (value) => {
            if (typeof value === 'string') return value.toLowerCase();
            return value || '';
        };

        const data = [...this.accounts];
        const reverse = direction === 'asc' ? 1 : -1;

        data.sort((a, b) => {
            const aValue = parseValue(a[fieldName]);
            const bValue = parseValue(b[fieldName]);
            return reverse * ((aValue > bValue) - (bValue > aValue));
        });

        this.accounts = data;
    }

    handleRowAction(event) {
        const action = event.detail.action;
        const row = event.detail.row;

        switch (action.name) {
            case 'view':
                this.navigateToRecord(row.Id);
                break;
            case 'edit':
                this.editRecord(row.Id);
                break;
            case 'delete':
                this.deleteRecord(row.Id);
                break;
        }
    }

    handleRowSelection(event) {
        this.selectedRows = event.detail.selectedRows;
    }

    loadMoreData(event) {
        // Implement infinite scrolling
    }
}

Performance Optimization

Best Practices

// 1. Use wire service for cacheable data
@wire(getAccounts, { recordId: '$recordId' })
accounts;

// 2. Avoid unnecessary re-renders
// Bad - creates new array every render
get processedAccounts() {
    return this.accounts.map(a => ({ ...a, processed: true }));
}

// Good - cache computed values
_processedAccounts;
@api
get accounts() {
    return this._accounts;
}
set accounts(value) {
    this._accounts = value;
    this._processedAccounts = value.map(a => ({ ...a, processed: true }));
}
get processedAccounts() {
    return this._processedAccounts;
}

// 3. Debounce frequent operations
handleInput(event) {
    window.clearTimeout(this.delayTimeout);
    const searchKey = event.target.value;
    this.delayTimeout = setTimeout(() => {
        this.searchKey = searchKey;
    }, 300);
}

// 4. Use loading states
isLoading = true;
async loadData() {
    this.isLoading = true;
    try {
        this.data = await getData();
    } finally {
        this.isLoading = false;
    }
}

When to Use

  • Wire service: Read-only data that should be cached
  • Imperative calls: User-triggered actions, data mutations
  • Custom events: Parent-child communication
  • Lightning Message Service: Cross-component communication, sibling components
  • Lightning Data Service: Single record CRUD operations

When NOT to Use

  • LWC for simple displays: Use formula fields or Flow screen components
  • Complex forms: Consider Screen Flow for admin-configurable forms
  • Heavy computation: Move to Apex to avoid client-side performance issues
  • Non-reactive data: Don't use @track for simple primitives (automatic since API v59)

Source: SKILL.md on GitHub

2 alerts16d5 checks · Risk CRITICAL
  • Gen Agent Trust Hub16d

    The skill provides high-quality guidance and code patterns for Salesforce development and DevOps. While automated scanners flagged the author's documentation URLs and the skill file as suspicious, manual review indicates these are likely false positives. The skill follows security best practices, such as using Named Credentials and avoiding SOQL injection, and its CI/CD templates use standard secret management patterns.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer6mo

    6/6 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at efebc44. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 5 months ago
Other metadata
metadata
{
  "author": "https://github.com/Jeffallan",
  "version": "1.1.0",
  "domain": "platform",
  "triggers": "Salesforce, Apex, Lightning Web Components, LWC, SOQL, SOSL, Visualforce, Salesforce DX, governor limits, triggers, platform events, CRM integration, Sales Cloud, Service Cloud",
  "role": "expert",
  "scope": "implementation",
  "output-format": "code",
  "related-skills": "api-designer, java-architect, cloud-architect, devops-engineer"
}
  • apex
  • salesforce
  • lwc
  • soql
  • triggers
  • batch-processing
  • salesforce-dx
  • platform-events
  • crm

README badge

README badge for jeffallan/claude-skills/salesforce-developer

Writes and debugs Apex code, builds Lightning Web Components, optimizes SOQL queries, and manages Salesforce platform governor limits. Covers triggers, batch jobs, platform events, integrations, and Salesforce DX deployment workflows for Sales Cloud and Service Cloud customization.

Generated from the current SKILL.md.

Does this skill cover both Apex and Lightning Web Components?
Yes. The skill covers Apex development (classes, triggers, batch jobs, platform events), SOQL/SOSL query optimization, Lightning Web Components, and Salesforce DX deployment.
What governor limits does this skill address?
The skill enforces bulkification patterns, SOQL/DML counting, heap size, and CPU time constraints. It includes guidance on batch processing, asynchronous patterns, and selective queries to stay within platform limits.
Does this skill handle Salesforce DX and CI/CD?
Yes. The skill includes deployment and DevOps guidance for Salesforce DX, scratch orgs, metadata API, and CI/CD pipeline setup.
What test coverage requirements does this skill enforce?
The skill requires minimum 90% code coverage in test classes and includes patterns for bulk scenario testing (200-record batches) to validate governor limit handling.
Does this skill support Visualforce or only Lightning Web Components?
The skill primarily targets Lightning Web Components and modern Apex patterns but lists Visualforce in its trigger scope, indicating awareness of legacy components without dedicated guidance.

Generated from the current SKILL.md. These answers refresh after source changes.