Home / apex / apex-outbound-email-patterns
apex-outbound-email-patterns
Apex outbound email via Messaging.SingleEmailMessage — OrgWideEmailAddress, ReplyTo and Reply-To header semantics, EmailTemplate merging with whatId/targetObjectId, attachment patterns, daily governor limits, and transactional sends vs Email Alerts. NOT for processing email that arrives into Salesforce — use apex/apex-email-services. NOT for Marketing Cloud sends — use apex/marketing-cloud-api.
Open the skill folder on GitHub
Apex Outbound Email Patterns
Outbound email from Apex looks simple — instantiate Messaging.SingleEmailMessage, populate fields, call Messaging.sendEmail. The patterns that turn it into a reliable production integration are not in the SOAP API guide: they live in the interaction between OrgWideEmailAddress, the whatId/targetObjectId merge contract, the daily governor cap of 5,000 external emails, and the differences between SingleEmailMessage and Email Alerts triggered from Flow.
The recurring questions are: how do we send "from a shared support@" address rather than the running user; how do we make replies go to a routing address rather than the From; how do we merge an EmailTemplate against an Account when the template is keyed to a Contact; and why does setHtmlBody ignore merge fields. These have specific answers — most of them involve setOrgWideEmailAddressId, setReplyTo, or renderStoredEmailTemplate.
Two governor facts shape every design decision. First, the org's daily external-email limit is 5,000 and counts every recipient on every send (a 4-recipient send burns 4 from the bucket). Second, sending email is treated as a non-transactional side effect — if your transaction rolls back, the email still goes out. That last part traps every team at least once.
Questions to Ask Before Configuring
Ask these before choosing Messaging.SingleEmailMessage over a Flow Email Alert. Skipping them produces sends that roll back with the transaction, burn the daily cap, or show merge syntax literally in the body.
| Ask | Why it matters | What a good answer adds |
|---|---|---|
| "Who receives the email — User Id, Contact/Lead Id, raw address, or a list?" | Recipient shape drives bulkification, governor cost, and whether setTargetObjectId is valid | Single vs list send pattern and whether to batch with partial-success handling |
| "Is this transactional (one record context) or operational/bulk?" | Daily external-email limit counts every recipient; trigger loops are a checker finding | Flow Email Alert vs Apex, or a queueable/batch sender for bulk |
| "What should appear as From, and where should replies land?" | Visible From is OrgWideEmailAddress; Reply-To is separate | setOrgWideEmailAddressId plus setReplyTo wired before template merge |
| "Does the body need EmailTemplate merge fields, and against which object?" | setHtmlBody('{!…}') sends merge syntax literally; non-CLU targets need renderStoredEmailTemplate | renderStoredEmailTemplate with the right whatId instead of setTargetObjectId(account.Id) |
| "Must the send survive a transaction rollback?" | Email is a non-transactional side effect — rollback does not unsend | Whether the send belongs after commit, in async, or as an explicit business acceptance of the risk |
What a proper outbound-email design adds over a bare sendEmail call: audited activity timeline entries, predictable From/Reply-To, template merge that actually resolves, and error handling that surfaces per-recipient failures instead of discarding the Messaging.SendEmailResult.
Recommended Workflow
- Decide the send mechanism first. If the trigger is admin-
configurable (a record meeting criteria), use a Flow Email Alert — admins can edit the template, the recipient logic, and the from-address without a deployment. Reach for
Messaging.SingleEmailMessageonly when you need conditional content, attachments computed at runtime, or programmatic recipient selection. - Configure the OrgWideEmailAddress before writing Apex.
Setup → Email → Organization-Wide Addresses, verify the address, note its Id (or query
SELECT Id FROM OrgWideEmailAddress WHERE Address='support@acme.com'). Without this, every send shows the running user as From. - **Build the message with
setOrgWideEmailAddressIdfirst, thensetReplyTo.** The OWE controls the visible From; the ReplyTo controls where replies land. They are distinct concerns and are commonly confused. - **For template-merged sends, use
renderStoredEmailTemplatewhen the merge target is not a Contact/Lead/User.** The built-in
setTemplateId+setTargetObjectIdpath requires a Contact, Lead, or User. Anything else (Account, Case, custom object) needs the explicit render call with awhatId. - Always set
setSaveAsActivity(true)for record-related sends.This writes an EmailMessage record to the related-to object's activity timeline — invaluable for support audits.
- Handle the
Messaging.SendEmailResult[]return value. It isa list parallel to the input — each element carries
isSuccess(),getErrors(), and a status code. Treating the call as fire-and- forget hides per-recipient failures. - Cap and observe daily-limit errors. How the daily-limit failure
reaches you depends on the
allOrNothingargument, and getting this wrong means the handler silently never runs:sendEmail(msgs)orsendEmail(msgs, true)— throwsSystem.EmailException(notSystem.HandledException, which is the generic Visualforce/Aura surface exception and will never match here). CatchEmailExceptionand check forSINGLE_EMAIL_LIMIT_EXCEEDED.sendEmail(msgs, false)— does not throw at all. Inspect eachMessaging.SendEmailResult:isSuccess()andgetErrors(), whose entries carry the status code.
Then degrade gracefully (queue for retry the next day, surface to monitoring). 5,000/day is per-org, not per-user.
SingleEmailMessage vs Email Alert vs MassEmailMessage
- SingleEmailMessage: programmatic; **up to 150 recipients per
message**, counted as the combined total of
toAddresses+ccAddresses+bccAddresses— not 150 in each. Each of those three fields is separately capped at 4,000 bytes, so long address lists can hit the byte cap before the recipient count. Supports attachments, OWE, ReplyTo, custom merge. - Email Alert (Flow): declarative; admin-editable template and
recipient set; respects OWE; cannot conditionally branch attachments.
- MassEmailMessage: deprecated for new work; keep only legacy
references; use SingleEmailMessage in a loop with proper bulkification.
What This Skill Does Not Cover
| Topic | See instead |
|---|---|
| Inbound email handling | apex/inbound-email-handler |
| Marketing Cloud sends from Salesforce | integration/marketing-cloud-rest-api |
| Email-to-Case routing rules | admin/email-to-case-config |
| Email Deliverability settings (Setup → Email) | admin/email-deliverability |
Package files
- references/examples.md
- references/gotchas.md
- references/llm-anti-patterns.md
- references/well-architected.md
- templates/apex-outbound-email-patterns-template.md
- scripts/check_apex_outbound_email_patterns.py
- scripts/fixtures/comment-only/CommentOnlyOutboundEmail.cls
- scripts/fixtures/real-discarded-send/DiscardedSend.cls
- scripts/fixtures/real-discarded-send/README.md