
Building Sf Integrations
Ship resilient Salesforce HTTP callouts to external APIs with retries, backoff, and clear retryable-error handling in Apex.
Install
npx skills add https://github.com/forcedotcom/sf-skills --skill building-sf-integrationsWhat is this skill?
- CalloutRetryHandler Apex class with up to 3 immediate retries and exponential backoff constants (1s base, 30s cap)
- Treats 6 retryable HTTP status codes: 408, 429, 500, 502, 503, 504
- Separates retryable vs non-retryable failures so agents do not loop on 4xx client errors
- Documents Apex limitation: no Thread.sleep—async backoff via Queueable scheduling for delayed retries
- Jitter-oriented design notes to reduce thundering herd on rate-limited endpoints
Adoption & trust: 688 installs on skills.sh; 513 GitHub stars; 2/3 security scanners passed (skills.sh audits).
Recommended Skills
Entra App Registrationmicrosoft/azure-skills
Azure Aigatewaymicrosoft/azure-skills
Lark Openapi Explorerlarksuite/cli
Supabasesupabase/agent-skills
Firebase Auth Basicsfirebase/agent-skills
Firebase Data Connectfirebase/agent-skills
Journey fit
Primary fit
Canonical shelf is Build because the skill delivers production Apex patterns for wiring Salesforce to external systems via HTTP callouts. Integrations subphase fits outbound API callouts, error classification, and async retry scheduling—not UI or pure data modeling.
Common Questions / FAQ
Is Building Sf Integrations safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.
SKILL.md
READMESKILL.md - Building Sf Integrations
/** * @description Retry Handler with Exponential Backoff for HTTP Callouts * * Use Case: Handle transient failures with intelligent retry * - Network timeouts * - 5xx server errors * - Rate limiting (429) * * Features: * - Exponential backoff between retries * - Configurable retry count * - Jitter to prevent thundering herd * - Distinguishes retryable vs non-retryable errors * * IMPORTANT: Apex doesn't have Thread.sleep(), so retry with backoff * is implemented via Queueable job scheduling for async contexts. * For sync contexts, this provides immediate retry without delay. * * @author {{Author}} * @date {{Date}} */ public with sharing class CalloutRetryHandler { // Configuration private static final Integer MAX_RETRIES = 3; private static final Integer BASE_DELAY_MS = 1000; // 1 second private static final Integer MAX_DELAY_MS = 30000; // 30 seconds // Retryable status codes private static final Set<Integer> RETRYABLE_STATUS_CODES = new Set<Integer>{ 408, // Request Timeout 429, // Too Many Requests (Rate Limited) 500, // Internal Server Error 502, // Bad Gateway 503, // Service Unavailable 504 // Gateway Timeout }; /** * @description Execute HTTP request with retry logic (immediate retries) * @param request HttpRequest to execute * @return HttpResponse from successful call * @throws CalloutException if all retries exhausted */ public static HttpResponse executeWithRetry(HttpRequest request) { return executeWithRetry(request, MAX_RETRIES); } /** * @description Execute HTTP request with configurable retry count * @param request HttpRequest to execute * @param maxRetries Maximum retry attempts * @return HttpResponse from successful call * @throws CalloutException if all retries exhausted */ public static HttpResponse executeWithRetry(HttpRequest request, Integer maxRetries) { Integer retryCount = 0; HttpResponse response; Exception lastException; while (retryCount <= maxRetries) { try { Http http = new Http(); response = http.send(request); Integer statusCode = response.getStatusCode(); // Success - return immediately if (statusCode >= 200 && statusCode < 300) { return response; } // Client error (4xx except 408, 429) - don't retry if (statusCode >= 400 && statusCode < 500 && !RETRYABLE_STATUS_CODES.contains(statusCode)) { throw new NonRetryableException( 'Client Error (' + statusCode + '): ' + response.getBody() ); } // Retryable error - check if we should retry if (RETRYABLE_STATUS_CODES.contains(statusCode)) { retryCount++; if (retryCount > maxRetries) { throw new RetryExhaustedException( 'Max retries exhausted. Last status: ' + statusCode + ', Body: ' + response.getBody() ); } // Log retry attempt System.debug(LoggingLevel.WARN, 'Retryable error (' + statusCode + '). Attempt ' + retryCount + ' of ' + maxRetries); // For 429, check Retry-After header if (statusCode == 429) { String retryAfter = response.getHeader('Retry-After'); if (String.isNotBlank(retryAfter)) { System.debug(LoggingLevel.WARN, 'Rate limited. Retry-After: ' + retryAfter); } } // Continue to next retry (n