SfSkills Domains Docs GitHub

Home / apex / continuation-callouts

continuation-callouts

Use when making long-running HTTP callouts from Visualforce or LWC that exceed the synchronous callout timeout limit, using the Apex Continuation class for async callout execution. NOT for ordinary synchronous callouts and Named Credential setup — use apex/callouts-and-http-integrations. NOT for choosing between @future, Queueable, and Continuation — use integration/callout-limits-and-async-patterns.

Open the skill folder on GitHub


Continuation Callouts

Use this skill when a Salesforce page (Visualforce or LWC) needs to call an external HTTP service that is slow enough to make holding the request thread unacceptable, or when you need to fire up to three callouts in parallel without blocking the UI. The Apex Continuation class offloads callout execution to Salesforce infrastructure, freeing the request thread while the external service responds.

What Continuation does and does not buy you. It is not a way to get a longer timeout. A plain synchronous Http.send() can already wait up to 120 seconds — 10 seconds is only the default per-callout timeout, and HttpRequest.setTimeout() accepts 1 to 120,000 ms in synchronous and asynchronous Apex alike, against a cumulative 120-second budget per transaction. What Continuation buys is that the wait happens on Salesforce infrastructure rather than on the user's blocked request thread, and that the response is handled in a fresh Apex transaction with reset governor limits. Choose it for thread occupancy and user experience, not because you think 10 seconds is a ceiling.


Before Starting

Gather this context before working on anything in this domain:


Core Concepts

Concept 1: The Continuation Object and Two-Phase Execution

A Continuation request follows a two-phase pattern:

  1. Start phase — The Visualforce action method (or LWC Apex method) creates a Continuation object, adds one or more HttpRequest objects to it, sets the callback method name, and returns it. Salesforce intercepts the return value, fires the HTTP request(s) off-platform, and releases the request thread.
  2. Callback phase — When the external service responds (or the timeout elapses), Salesforce invokes the callback method in a fresh Apex execution context. The callback receives the response via Continuation.getResponse(label) and returns the final value to the page.

The Salesforce platform acts as a proxy. The request thread is freed between phases, so no page timeout occurs while waiting for the external service.

Concept 2: State Passing Between Phases

Because the callback runs in a new transaction, instance variables on a Visualforce controller are not automatically preserved. State must be passed explicitly:

Concept 3: Parallel Callouts

A single Continuation can encapsulate up to three simultaneous HTTP requests. Each call to continuation.addHttpRequest(req) returns a unique String label. All requests in the batch fire in parallel. The callback receives a List<String> labels and uses Continuation.getResponse(label) to retrieve the individual responses. Order of response arrival is non-deterministic; always retrieve by label, not by index.

Concept 4: Visualforce vs. LWC Invocation

ContextHow to invoke
VisualforceAction method returns Continuation. The page uses action="{!startContinuation}".
LWC (Aura-compatible)Import the Apex method and call invokeAction() from the LWC JavaScript, which wraps the continuation wire. The Apex method must be annotated @AuraEnabled(continuation=true).

Common Patterns

Pattern 1: Single Continuation Callout from Visualforce Controller

When to use: A Visualforce page button triggers a callout to a slow external REST API (e.g., a quote-generation service that routinely takes 30–90 seconds).

How it works:

public class QuoteContinuationController {

    // State to pass between phases — must be serializable
    public String quoteRequestId { get; set; }
    public String quoteResult    { get; set; }

    // Phase 1 — Start the callout, return Continuation
    public Continuation startQuoteCallout() {
        // Max timeout in seconds (1–120). Default is 30.
        Continuation con = new Continuation(60);
        // Name of the callback method on THIS class
        con.continuationMethod = 'processQuoteResponse';

        HttpRequest req = new HttpRequest();
        req.setMethod('POST');
        req.setEndpoint('callout:QuoteService/api/quotes'); // Named Credential
        req.setHeader('Content-Type', 'application/json');
        req.setBody('{"requestId":"' + quoteRequestId + '"}');

        con.addHttpRequest(req);

        // Pass serializable state to the callback
        con.state = quoteRequestId;

        return con; // Salesforce intercepts and fires the HTTP request
    }

    // Phase 2 — Callback after callout completes
    public Object processQuoteResponse(List<String> labels, Object state) {
        // labels[0] is the label for the first (and only) request
        HttpResponse res = Continuation.getResponse(labels[0]);

        if (res.getStatusCode() == 200) {
            quoteResult = res.getBody();
        } else {
            quoteResult = 'Error: ' + res.getStatusCode();
        }

        // Return null or a PageReference to navigate after callback
        return null;
    }
}

Visualforce page binding:

<apex:page controller="QuoteContinuationController">
    <apex:form>
        <apex:commandButton value="Generate Quote"
                            action="{!startQuoteCallout}"
                            reRender="resultPanel"/>
        <apex:outputPanel id="resultPanel">
            <apex:outputText value="{!quoteResult}"/>
        </apex:outputPanel>
    </apex:form>
</apex:page>

Why not the alternative: A synchronous Http.send() in a Visualforce action method blocks the Salesforce request thread for the whole wait. It will also time out at 10 seconds if you leave the timeout at its default — raising it with req.setTimeout(120000) avoids the System.CalloutException: Read timed out, but does nothing about the blocked thread, which is the actual reason to prefer Continuation here.


Pattern 2: Parallel Callouts from a Single Continuation

When to use: A dashboard page needs to aggregate data from three different APIs simultaneously (e.g., ERP inventory, CRM pricing, logistics tracking). Firing them sequentially wastes time.

How it works:

public class DashboardContinuationController {

    public String inventoryData  { get; set; }
    public String pricingData    { get; set; }
    public String trackingData   { get; set; }

    private String inventoryLabel;
    private String pricingLabel;
    private String trackingLabel;

    public Continuation startDashboardLoad() {
        Continuation con = new Continuation(90);
        con.continuationMethod = 'processDashboardResponses';

        HttpRequest invReq = new HttpRequest();
        invReq.setMethod('GET');
        invReq.setEndpoint('callout:ERPService/inventory');
        inventoryLabel = con.addHttpRequest(invReq); // store label

        HttpRequest priceReq = new HttpRequest();
        priceReq.setMethod('GET');
        priceReq.setEndpoint('callout:PricingService/current');
        pricingLabel = con.addHttpRequest(priceReq);

        HttpRequest trackReq = new HttpRequest();
        trackReq.setMethod('GET');
        trackReq.setEndpoint('callout:LogisticsService/track');
        trackingLabel = con.addHttpRequest(trackReq);

        // Pass labels as state so callback can retrieve by name
        con.state = new Map<String, String>{
            'inv'   => inventoryLabel,
            'price' => pricingLabel,
            'track' => trackingLabel
        };

        return con;
    }

    public Object processDashboardResponses(List<String> labels, Object state) {
        Map<String, String> labelMap = (Map<String, String>) state;

        HttpResponse invRes   = Continuation.getResponse(labelMap.get('inv'));
        HttpResponse priceRes = Continuation.getResponse(labelMap.get('price'));
        HttpResponse trackRes = Continuation.getResponse(labelMap.get('track'));

        inventoryData = invRes.getStatusCode()  == 200 ? invRes.getBody()  : 'Error';
        pricingData   = priceRes.getStatusCode() == 200 ? priceRes.getBody() : 'Error';
        trackingData  = trackRes.getStatusCode() == 200 ? trackRes.getBody() : 'Error';

        return null;
    }
}

Why not sequential: Three sequential Continuation calls would require three separate user interactions or a chained pattern. Using a single Continuation with three requests fires them in parallel on Salesforce infrastructure, typically reducing wall-clock latency to the slowest single response rather than the sum of all three.


Pattern 3: LWC with @AuraEnabled Continuation

When to use: A Lightning Web Component (not Visualforce) needs to invoke a long-running callout. Available since Summer '19.

How it works:

// Apex controller
public with sharing class ProductSearchController {

    @AuraEnabled(continuation=true)
    public static Object startSearch(String searchTerm) {
        Continuation con = new Continuation(60);
        con.continuationMethod = 'processSearchResults';

        HttpRequest req = new HttpRequest();
        req.setMethod('GET');
        req.setEndpoint('callout:ProductCatalog/search?q=' +
                        EncodingUtil.urlEncode(searchTerm, 'UTF-8'));
        con.addHttpRequest(req);

        con.state = searchTerm;
        return con;
    }

    @AuraEnabled(continuation=true)
    public static Object processSearchResults(List<String> labels, Object state) {
        HttpResponse res = Continuation.getResponse(labels[0]);
        if (res.getStatusCode() == 200) {
            return res.getBody();
        }
        throw new AuraHandledException('Search failed: ' + res.getStatusCode());
    }
}
// LWC JavaScript — productSearch.js
import { LightningElement } from 'lwc';
import { invokeAction }      from 'lightning/actions'; // Aura-compatible wire
import startSearch            from '@salesforce/apex/ProductSearchController.startSearch';

export default class ProductSearch extends LightningElement {
    results;

    handleSearch(event) {
        const term = event.target.value;
        invokeAction(this, startSearch, { searchTerm: term })
            .then(result => { this.results = result; })
            .catch(error => console.error(error));
    }
}

Decision Guidance

SituationRecommended ApproachReason
Any callout from an Apex trigger, batch, or async contextSynchronous Http.send() (raise setTimeout past the 10 s default if needed) or Queueable ApexContinuation is not supported in triggers or async contexts; 10 s is a default, not a ceiling
Slow callout (roughly > 5–10 s) from VisualforceContinuation object in controller action methodOnly mechanism that frees the VF request thread while waiting
Slow callout (roughly > 5–10 s) from LWC@AuraEnabled(continuation=true) + invokeActionLWC equivalent of VF Continuation; same 120 s ceiling
2–3 independent callouts needed simultaneously, from UISingle Continuation with multiple addHttpRequest callsFires in parallel; reduces latency vs. sequential calls
Callout expected > 120 secondsPolling pattern: trigger async job, poll status endpointPlatform hard ceiling is 120 seconds; Continuation cannot exceed it
From Apex trigger, Batch, Scheduled, @futureStandard synchronous callout or Queueable with callout=trueContinuation requires a user-initiated HTTP request context

Step-by-step instructions for an AI agent or practitioner activating this skill:

  1. Gather context — confirm the org edition, relevant objects, and current configuration state
  2. Review official sources — check the references in this skill's well-architected.md before making changes
  3. Implement or advise — apply the patterns from Core Concepts and Common Patterns sections above
  4. Validate — run the skill's checker script and verify against the Review Checklist below
  5. Document — record any deviations from standard patterns and update the template if needed

Review Checklist

Run through these before marking work in this area complete:


Salesforce-Specific Gotchas

Non-obvious platform behaviors that cause real production problems:

  1. Continuation is not supported in triggers or async Apex — Attempting to instantiate Continuation from a trigger, Batch Apex execute, Scheduled Apex, or @future method throws a runtime exception. The class is only valid in the context of a Visualforce action request or LWC @AuraEnabled(continuation=true) invocation.
  2. Non-serializable state silently fails or throws at callback time — The state property is serialized by the platform between phases. Passing an SObject that includes relationship query results, a Type reference, or an Iterator causes a System.SerializationException at callback invocation. Always pass simple scalar types, Map<String, String>, or a plain class with @TestVisible and primitive fields.
  3. Callback method must be public and on the same class — continuationMethod must be a public (not private) method on the same Apex class. It cannot be a static method. If the name is misspelled or the method is private, the callback silently never fires — no exception is thrown at start time.

Output Artifacts

ArtifactDescription
Apex controller with startRequest/processResponseVisualforce or LWC Apex class implementing the two-phase Continuation pattern
Visualforce page with action bindingPage XML wiring the action method and output panel rerender
LWC component with invokeActionJavaScript component using the continuation-aware import
check_continuation_callouts.py reportStatic analysis of Apex classes for Continuation anti-patterns

Package files