Creates comprehensive Salesforce integrations including Named Credentials, External Services, REST/SOAP callouts, Platform Events, and Change Data Capture. 120-point scoring across 6 categories. Use this skill when the user needs to connect Salesforce to external systems.
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
Expert integration architect specializing in secure callout patterns, event-driven architecture, and external service registration for Salesforce.
| Insight | Details | Action |
|---|---|---|
| Named Credential Architecture | Legacy (pre-API 61) vs External Credentials (API 61+) | Check org API version first |
| Callouts in Triggers | Synchronous callouts NOT allowed in triggers | Always use async (Queueable, @future) |
| Governor Limits | 100 callouts per transaction, 120s timeout max | Batch callouts, use async patterns |
| External Services | Auto-generates Apex from OpenAPI specs | Requires Named Credential for auth |
| Feature | Legacy Named Credential | External Credential (API 61+) |
|---|---|---|
| API Version | Pre-API 61 | API 61+ (Winter '24+) |
| Principal Concept | Single principal per credential | Named Principal + Per-User Principal |
| OAuth Support | Basic OAuth 2.0 | Full OAuth 2.0 + PKCE, JWT |
| Permissions | Profile-based | Permission Set + Named Principal |
| Recommendation | Legacy orgs only | Use for all new development |
┌─────────────────────────────────────────────────────────────────────────────┐
│ WHEN TO USE WHICH CREDENTIAL TYPE │
├─────────────────────────────────────────────────────────────────────────────┤
│ Use LEGACY Named Credential if: │
│ • Org API version < 61 │
│ • Migrating existing integrations (maintain compatibility) │
│ • Simple API key / Basic Auth (quick setup) │
│ │
│ Use EXTERNAL Credential (API 61+) if: │
│ • New development (recommended) │
│ • OAuth 2.0 with PKCE required │
│ • Per-user authentication needed │
│ • Fine-grained permission control required │
│ • JWT Bearer flow for server-to-server │
└─────────────────────────────────────────────────────────────────────────────┘
Use AskUserQuestion to gather:
Integration Type:
Authentication Method:
External System Details:
Sync vs Async Requirements:
| Integration Need | Template | Location |
|---|---|---|
| OAuth 2.0 Client Credentials | oauth-client-credentials.namedCredential-meta.xml | templates/named-credentials/ |
| OAuth 2.0 JWT Bearer | oauth-jwt-bearer.namedCredential-meta.xml | templates/named-credentials/ |
| Certificate Auth | certificate-auth.namedCredential-meta.xml | templates/named-credentials/ |
| API Key / Basic Auth | custom-auth.namedCredential-meta.xml | templates/named-credentials/ |
| External Credential (OAuth) | oauth-external-credential.externalCredential-meta.xml | templates/external-credentials/ |
| External Service (OpenAPI) | openapi-registration.externalServiceRegistration-meta.xml | templates/external-services/ |
| REST Callout (Sync) | rest-sync-callout.cls | templates/callouts/ |
| REST Callout (Async) | rest-queueable-callout.cls | templates/callouts/ |
| Retry Handler | callout-retry-handler.cls | templates/callouts/ |
| SOAP Callout | soap-callout-service.cls | templates/soap/ |
| Platform Event | platform-event-definition.object-meta.xml | templates/platform-events/ |
| Event Publisher | event-publisher.cls | templates/platform-events/ |
| Event Subscriber | event-subscriber-trigger.trigger | templates/platform-events/ |
| CDC Subscriber | cdc-subscriber-trigger.trigger | templates/cdc/ |
File Locations:
force-app/main/default/
├── namedCredentials/
│ └── {{CredentialName}}.namedCredential-meta.xml
├── externalCredentials/
│ └── {{CredentialName}}.externalCredential-meta.xml
├── externalServiceRegistrations/
│ └── {{ServiceName}}.externalServiceRegistration-meta.xml
├── classes/
│ ├── {{ServiceName}}Callout.cls
│ ├── {{ServiceName}}Callout.cls-meta.xml
│ └── ...
├── objects/
│ └── {{EventName}}__e/
│ └── {{EventName}}__e.object-meta.xml
└── triggers/
├── {{EventName}}Subscriber.trigger
└── {{EventName}}Subscriber.trigger-meta.xml
Validate using scoring system (see Scoring System section)
Deployment Order (CRITICAL):
1. Deploy Named Credentials / External Credentials FIRST
2. Deploy External Service Registrations (depends on Named Credentials)
3. Deploy Apex classes (callout services, handlers)
4. Deploy Platform Events / CDC configuration
5. Deploy Triggers (depends on events being deployed)
Use sf-deploy skill:
Skill(skill="sf-deploy")
Request: "Deploy Named Credential {{Name}} with dry-run first"
CLI Commands:
# Deploy Named Credential
sf project deploy start --metadata NamedCredential:{{Name}} --target-org {{alias}}
# Deploy External Service
sf project deploy start --metadata ExternalServiceRegistration:{{Name}} --target-org {{alias}}
# Deploy all integration components
sf project deploy start --source-dir force-app/main/default/namedCredentials,force-app/main/default/externalServiceRegistrations --target-org {{alias}}
| Auth Type | Use Case | Template | Key Config |
|---|---|---|---|
| OAuth 2.0 Client Credentials | Server-to-server, no user context | oauth-client-credentials.namedCredential-meta.xml | scope, tokenEndpoint |
| OAuth 2.0 JWT Bearer | CI/CD, backend services | oauth-jwt-bearer.namedCredential-meta.xml | Certificate + Connected App |
| Certificate (Mutual TLS) | High-security integrations | certificate-auth.namedCredential-meta.xml | Client cert required |
| Custom (API Key/Basic) | Simple APIs | custom-auth.namedCredential-meta.xml | username/password |
Templates in templates/named-credentials/. ⚠️ NEVER hardcode credentials - always use Named Credentials!
Use Case: Modern OAuth 2.0 with per-user or named principal authentication
Template: templates/external-credentials/oauth-external-credential.externalCredential-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<ExternalCredential xmlns="http://soap.sforce.com/2006/04/metadata">
<label>{{CredentialLabel}}</label>
<authenticationProtocol>Oauth</authenticationProtocol>
<externalCredentialParameters>
<parameterName>clientId</parameterName>
<parameterType>AuthProviderClientId</parameterType>
<parameterValue>{{ClientId}}</parameterValue>
</externalCredentialParameters>
<externalCredentialParameters>
<parameterName>clientSecret</parameterName>
<parameterType>AuthProviderClientSecret</parameterType>
<parameterValue>{{ClientSecret}}</parameterValue>
</externalCredentialParameters>
<principals>
<principalName>{{PrincipalName}}</principalName>
<principalType>NamedPrincipal</principalType>
<sequenceNumber>1</sequenceNumber>
</principals>
</ExternalCredential>
Process:
Template: templates/external-services/openapi-registration.externalServiceRegistration-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<ExternalServiceRegistration xmlns="http://soap.sforce.com/2006/04/metadata">
<label>{{ServiceLabel}}</label>
<namedCredential>{{NamedCredentialName}}</namedCredential>
<schema>{{OpenAPISchemaContent}}</schema>
<schemaType>OpenApi3</schemaType>
<serviceBinding>{{ServiceBindingName}}</serviceBinding>
<status>Complete</status>
</ExternalServiceRegistration>
CLI Alternative:
# Register External Service from URL
sf api request rest /services/data/v62.0/externalServiceRegistrations \
--method POST \
--body '{"label":"{{Label}}","namedCredential":"{{NC}}","schemaUrl":"{{URL}}"}'
External Services generate Apex classes like:
ExternalService.{{ServiceName}}ExternalService.{{ServiceName}}_{{OperationName}}Example Usage:
// Auto-generated class usage
ExternalService.Stripe stripe = new ExternalService.Stripe();
ExternalService.Stripe_createCustomer_Request req = new ExternalService.Stripe_createCustomer_Request();
req.email = 'customer@example.com';
ExternalService.Stripe_createCustomer_Response resp = stripe.createCustomer(req);
Use Case: Need immediate response, NOT triggered from DML
Template: templates/callouts/rest-sync-callout.cls
public with sharing class {{ServiceName}}Callout {
private static final String NAMED_CREDENTIAL = 'callout:{{NamedCredentialName}}';
public static HttpResponse makeRequest(String method, String endpoint, String body) {
HttpRequest req = new HttpRequest();
req.setEndpoint(NAMED_CREDENTIAL + endpoint);
req.setMethod(method);
req.setHeader('Content-Type', 'application/json');
req.setTimeout(120000); // 120 seconds max
if (String.isNotBlank(body)) {
req.setBody(body);
}
Http http = new Http();
return http.send(req);
}
public static Map<String, Object> get(String endpoint) {
HttpResponse res = makeRequest('GET', endpoint, null);
return handleResponse(res);
}
public static Map<String, Object> post(String endpoint, Map<String, Object> payload) {
HttpResponse res = makeRequest('POST', endpoint, JSON.serialize(payload));
return handleResponse(res);
}
private static Map<String, Object> handleResponse(HttpResponse res) {
Integer statusCode = res.getStatusCode();
if (statusCode >= 200 && statusCode < 300) {
return (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
} else if (statusCode >= 400 && statusCode < 500) {
throw new CalloutException('Client Error: ' + statusCode + ' - ' + res.getBody());
} else if (statusCode >= 500) {
throw new CalloutException('Server Error: ' + statusCode + ' - ' + res.getBody());
}
return null;
}
}
Use Case: Callouts triggered from DML (triggers, Process Builder)
Template: templates/callouts/rest-queueable-callout.cls
public with sharing class {{ServiceName}}QueueableCallout implements Queueable, Database.AllowsCallouts {
private List<Id> recordIds;
private String operation;
public {{ServiceName}}QueueableCallout(List<Id> recordIds, String operation) {
this.recordIds = recordIds;
this.operation = operation;
}
public void execute(QueueableContext context) {
if (recordIds == null || recordIds.isEmpty()) {
return;
}
try {
// Query records
List<{{ObjectName}}> records = [
SELECT Id, Name, {{FieldsToSend}}
FROM {{ObjectName}}
WHERE Id IN :recordIds
WITH USER_MODE
];
// Make callout for each record (consider batching)
for ({{ObjectName}} record : records) {
makeCallout(record);
}
} catch (CalloutException e) {
// Log callout errors
System.debug(LoggingLevel.ERROR, 'Callout failed: ' + e.getMessage());
// Consider: Create error log record, retry logic, notification
} catch (Exception e) {
System.debug(LoggingLevel.ERROR, 'Error: ' + e.getMessage());
}
}
private void makeCallout({{ObjectName}} record) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:{{NamedCredentialName}}/{{Endpoint}}');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setTimeout(120000);
Map<String, Object> payload = new Map<String, Object>{
'id' => record.Id,
'name' => record.Name
// Add more fields
};
req.setBody(JSON.serialize(payload));
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() >= 200 && res.getStatusCode() < 300) {
// Success - update record status if needed
} else {
// Handle error
throw new CalloutException('API Error: ' + res.getStatusCode());
}
}
}
Use Case: Handle transient failures with intelligent retry
Template: templates/callouts/callout-retry-handler.cls
public with sharing class CalloutRetryHandler {
private static final Integer MAX_RETRIES = 3;
private static final Integer BASE_DELAY_MS = 1000; // 1 second
public static HttpResponse executeWithRetry(HttpRequest request) {
Integer retryCount = 0;
HttpResponse response;
while (retryCount < MAX_RETRIES) {
try {
Http http = new Http();
response = http.send(request);
// Success or client error (4xx) - don't retry
if (response.getStatusCode() < 500) {
return response;
}
// Server error (5xx) - retry with backoff
retryCount++;
if (retryCount < MAX_RETRIES) {
// Exponential backoff: 1s, 2s, 4s
Integer delayMs = BASE_DELAY_MS * (Integer) Math.pow(2, retryCount - 1);
// Note: Apex doesn't have sleep(), so we schedule retry via Queueable
throw new RetryableException('Server error, retry ' + retryCount);
}
} catch (CalloutException e) {
retryCount++;
if (retryCount >= MAX_RETRIES) {
throw e;
}
}
}
return response;
}
public class RetryableException extends Exception {}
}
Step 1: Generate Apex from WSDL
Step 2: Configure Remote Site Setting or Named Credential
Step 3: Use generated classes in Apex
Template: templates/soap/soap-callout-service.cls
public with sharing class {{ServiceName}}SoapService {
public static {{ResponseType}} callService({{RequestType}} request) {
try {
// Generated stub class
{{WsdlGeneratedClass}}.{{PortType}} stub = new {{WsdlGeneratedClass}}.{{PortType}}();
// Set endpoint (use Named Credential if possible)
stub.endpoint_x = 'callout:{{NamedCredentialName}}';
// Set timeout
stub.timeout_x = 120000;
// Make the call
return stub.{{OperationName}}(request);
} catch (Exception e) {
System.debug(LoggingLevel.ERROR, 'SOAP Callout Error: ' + e.getMessage());
throw new CalloutException('SOAP service error: ' + e.getMessage());
}
}
}
Use Case: Asynchronous, event-driven communication
Template: templates/platform-events/platform-event-definition.object-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<deploymentStatus>Deployed</deploymentStatus>
<eventType>HighVolume</eventType>
<label>{{EventLabel}}</label>
<pluralLabel>{{EventPluralLabel}}</pluralLabel>
<publishBehavior>PublishAfterCommit</publishBehavior>
<fields>
<fullName>{{FieldName}}__c</fullName>
<label>{{FieldLabel}}</label>
<type>Text</type>
<length>255</length>
</fields>
<!-- Add more fields as needed -->
</CustomObject>
Event Types:
StandardVolume: ~2,000 events/hour, standard deliveryHighVolume: Millions/day, at-least-once delivery, 24-hour retentionTemplate: templates/platform-events/event-publisher.cls
public with sharing class {{EventName}}Publisher {
public static void publishEvents(List<{{EventName}}__e> events) {
if (events == null || events.isEmpty()) {
return;
}
List<Database.SaveResult> results = EventBus.publish(events);
for (Integer i = 0; i < results.size(); i++) {
Database.SaveResult sr = results[i];
if (!sr.isSuccess()) {
for (Database.Error err : sr.getErrors()) {
System.debug(LoggingLevel.ERROR,
'Event publish error: ' + err.getStatusCode() + ' - ' + err.getMessage());
}
}
}
}
public static void publishSingleEvent(Map<String, Object> eventData) {
{{EventName}}__e event = new {{EventName}}__e();
// Map fields from eventData
event.{{FieldName}}__c = (String) eventData.get('{{fieldKey}}');
Database.SaveResult sr = EventBus.publish(event);
if (!sr.isSuccess()) {
throw new EventPublishException('Failed to publish event: ' + sr.getErrors());
}
}
public class EventPublishException extends Exception {}
}
Template: templates/platform-events/event-subscriber-trigger.trigger
trigger {{EventName}}Subscriber on {{EventName}}__e (after insert) {
// Get replay ID for resumption
String lastReplayId = '';
for ({{EventName}}__e event : Trigger.new) {
// Store replay ID for potential resume
lastReplayId = event.ReplayId;
try {
// Process event
{{EventName}}Handler.processEvent(event);
} catch (Exception e) {
// Log error but don't throw - allow other events to process
System.debug(LoggingLevel.ERROR,
'Event processing error: ' + e.getMessage() +
' ReplayId: ' + event.ReplayId);
}
}
// Set resume checkpoint (for high-volume events)
EventBus.TriggerContext.currentContext().setResumeCheckpoint(lastReplayId);
}
Enable CDC via Setup → Integrations → Change Data Capture, or via metadata:
Objects supporting CDC: Standard objects, Custom objects
Channel Format: {{ObjectAPIName}}ChangeEvent (e.g., AccountChangeEvent, Order__ChangeEvent)
Template: templates/cdc/cdc-subscriber-trigger.trigger
trigger {{ObjectName}}CDCSubscriber on {{ObjectName}}ChangeEvent (after insert) {
for ({{ObjectName}}ChangeEvent event : Trigger.new) {
// Get change event header
EventBus.ChangeEventHeader header = event.ChangeEventHeader;
String changeType = header.getChangeType();
List<String> changedFields = header.getChangedFields();
String recordId = header.getRecordIds()[0]; // First record ID
System.debug('CDC Event - Type: ' + changeType +
', RecordId: ' + recordId +
', Changed Fields: ' + changedFields);
// Route based on change type
switch on changeType {
when 'CREATE' {
// Handle new record
{{ObjectName}}CDCHandler.handleCreate(event);
}
when 'UPDATE' {
// Handle update
{{ObjectName}}CDCHandler.handleUpdate(event, changedFields);
}
when 'DELETE' {
// Handle delete
{{ObjectName}}CDCHandler.handleDelete(recordId);
}
when 'UNDELETE' {
// Handle undelete
{{ObjectName}}CDCHandler.handleUndelete(event);
}
}
}
}
Template: templates/cdc/cdc-handler.cls
public with sharing class {{ObjectName}}CDCHandler {
public static void handleCreate({{ObjectName}}ChangeEvent event) {
// Sync to external system on create
Map<String, Object> payload = buildPayload(event);
System.enqueueJob(new ExternalSystemSyncQueueable(payload, 'CREATE'));
}
public static void handleUpdate({{ObjectName}}ChangeEvent event, List<String> changedFields) {
// Only sync if relevant fields changed
Set<String> fieldsToWatch = new Set<String>{'Name', 'Status__c', 'Amount__c'};
Boolean relevantChange = false;
for (String field : changedFields) {
if (fieldsToWatch.contains(field)) {
relevantChange = true;
break;
}
}
if (relevantChange) {
Map<String, Object> payload = buildPayload(event);
payload.put('changedFields', changedFields);
System.enqueueJob(new ExternalSystemSyncQueueable(payload, 'UPDATE'));
}
}
public static void handleDelete(String recordId) {
Map<String, Object> payload = new Map<String, Object>{'recordId' => recordId};
System.enqueueJob(new ExternalSystemSyncQueueable(payload, 'DELETE'));
}
public static void handleUndelete({{ObjectName}}ChangeEvent event) {
handleCreate(event); // Treat undelete like create
}
private static Map<String, Object> buildPayload({{ObjectName}}ChangeEvent event) {
return new Map<String, Object>{
'recordId' => event.ChangeEventHeader.getRecordIds()[0],
'commitTimestamp' => event.ChangeEventHeader.getCommitTimestamp(),
// Add event field values
'name' => event.Name
// Add more fields
};
}
}
| Category | Points | Evaluation Criteria |
|---|---|---|
| Security | 30 | Named Credentials used (no hardcoded secrets), OAuth scopes minimized, certificate auth where applicable |
| Error Handling | 25 | Retry logic present, timeout handling (120s max), specific exception types, logging implemented |
| Bulkification | 20 | Batch callouts considered, CDC bulk handling, event batching for Platform Events |
| Architecture | 20 | Async patterns for DML-triggered callouts, proper service layer separation, single responsibility |
| Best Practices | 15 | Governor limit awareness, proper HTTP methods, idempotency for retries |
| Documentation | 10 | Clear intent documented, endpoint versioning noted, API contract documented |
Score: XX/120 Rating
├─ ⭐⭐⭐⭐⭐ Excellent (108-120): Production-ready, follows all best practices
├─ ⭐⭐⭐⭐ Very Good (90-107): Minor improvements suggested
├─ ⭐⭐⭐ Good (72-89): Acceptable with noted improvements
├─ ⭐⭐ Needs Work (54-71): Address issues before deployment
└─ ⭐ Block (<54): CRITICAL issues, do not deploy
📊 INTEGRATION SCORE: XX/120 ⭐⭐⭐⭐ Rating
════════════════════════════════════════════════════
🔐 Security XX/30 ████████░░ XX%
├─ Named Credentials used: ✅
├─ No hardcoded secrets: ✅
└─ OAuth scopes minimal: ✅
⚠️ Error Handling XX/25 ████████░░ XX%
├─ Retry logic: ✅
├─ Timeout handling: ✅
└─ Logging: ✅
📦 Bulkification XX/20 ████████░░ XX%
├─ Batch callouts: ✅
└─ Event batching: ✅
🏗️ Architecture XX/20 ████████░░ XX%
├─ Async patterns: ✅
└─ Service separation: ✅
✅ Best Practices XX/15 ████████░░ XX%
├─ Governor limits: ✅
└─ Idempotency: ✅
📝 Documentation XX/10 ████████░░ XX%
├─ Clear intent: ✅
└─ API versioning: ✅
════════════════════════════════════════════════════
| To Skill | When to Use |
|---|---|
| sf-connected-apps | OAuth Connected App for Named Credential |
| sf-apex | Custom callout service beyond templates |
| sf-metadata | Query existing Named Credentials |
| sf-devops-architect | ⚠️ MANDATORY for ALL deployments |
| sf-ai-agentforce | Agent action using External Service |
| sf-flow | HTTP Callout Flow for agent |
sf-integration → Named Credential + External Service → sf-flow → HTTP Callout wrapper → sf-ai-agentforce → Agent with flow:// target → sf-devops-architect → Deploy all
# List Named Credentials
sf org list metadata --metadata-type NamedCredential --target-org {{alias}}
# Deploy Named Credential
sf project deploy start --metadata NamedCredential:{{Name}} --target-org {{alias}}
# Retrieve Named Credential
sf project retrieve start --metadata NamedCredential:{{Name}} --target-org {{alias}}
# List External Service Registrations
sf org list metadata --metadata-type ExternalServiceRegistration --target-org {{alias}}
# Deploy External Service
sf project deploy start --metadata ExternalServiceRegistration:{{Name}} --target-org {{alias}}
# List Platform Events
sf org list metadata --metadata-type CustomObject --target-org {{alias}} | grep "__e"
# Deploy Platform Event
sf project deploy start --metadata CustomObject:{{EventName}}__e --target-org {{alias}}
| Anti-Pattern | Problem | Correct Pattern |
|---|---|---|
| Hardcoded credentials | Security vulnerability, credential rotation nightmare | Use Named Credentials |
| Sync callout in trigger | CalloutException: Uncommitted work pending | Use Queueable with Database.AllowsCallouts |
| No timeout specified | Default 10s may be too short | Set req.setTimeout(120000) (max 120s) |
| No retry logic | Transient failures cause data loss | Implement exponential backoff |
| Ignoring status codes | Silent failures | Check statusCode and handle 4xx/5xx |
| 100+ callouts per transaction | Governor limit exceeded | Batch callouts, use async |
| No logging | Can't debug production issues | Log all callout requests/responses |
| Exposing API errors to users | Security risk, poor UX | Catch and wrap in user-friendly messages |
MIT License - See LICENSE file for details.