Salesforce blocks synchronous callouts from trigger context because:
- Triggers run inside the user's save transaction. A blocking HTTP call to a slow endpoint would freeze the user's UI for seconds.
- Triggers run on every DML, including bulk operations. 200 records in a batch × 1-second callout = 200 seconds blocking, far exceeding the CPU limit.
- Failure semantics — what should happen if the callout fails? Roll back the save? Continue with partial state? Async makes the contract clear.
The platform enforces this by throwing CalloutException if you try.
Workarounds:
1. `@future(callout=true)` — fire and forget:
`apex @future(callout=true) public static void notifyExternal(Set<Id> ids) { // make callout }
// In trigger: trigger AccountTrigger on Account (after update) { Set<Id> ids = new Set<Id>(); for (Account a : Trigger.new) ids.add(a.Id); NotificationService.notifyExternal(ids); } `
Runs in a separate transaction at some point in the future. Limit: only primitive arguments (sObjects not allowed).
2. Queueable with `Database.AllowsCallouts` — modern preferred pattern:
`apex public class NotifyJob implements Queueable, Database.AllowsCallouts { private List<Account> accs; public NotifyJob(List<Account> accs) { this.accs = accs; } public void execute(QueueableContext ctx) { // make callouts } }
// In trigger: System.enqueueJob(new NotifyJob(accountList)); `
Accepts complex types. Can chain.
3. Platform Events — publish an event from the trigger; an external subscriber processes the callout:
apex EventBus.publish(new Notification__e(RecordId__c = '001xxx'));
Decouples Salesforce entirely from the external availability.
4. Outbound Message (legacy) — declarative SOAP message to a configured endpoint, with built-in retry. Good for legacy SOAP integrations.
5. Change Data Capture — let an external system subscribe to record changes and pull what it needs without your trigger doing the callout.
Decision tree:
- Fire-and-forget, simple data ->
@future(callout=true). - Need complex data, chaining, or modern code -> Queueable.
- Decoupled async architecture -> Platform Events or CDC.
- Existing SOAP endpoint with retry -> Outbound Message.
Production triggers should NEVER make synchronous callouts even if Salesforce allowed it — the latency cost is too high.
