Sending Emails — Workers Binding & Agents SDK
Send emails from Cloudflare Workers using the native binding, or from AI agents using the Agents SDK. If your app is NOT on Workers, use the REST API instead.
Workers Binding
Configuration
// wrangler.jsonc
{
"send_email": [
{ "name": "EMAIL" }
]
}
For local development, add "remote": true so email sends are proxied to the real service:
{ "send_email": [{ "name": "EMAIL", "remote": true }] }
Run npx wrangler types to auto-generate the Env interface with your EMAIL binding. This produces a worker-configuration.d.ts with the real SendEmail, EmailAttachment, EmailAddress, and related types from the workerd runtime. Always use these generated types — do not define them manually.
Note: Workers binding uses email in the from object (EmailAddress type). REST API uses address. See rest-api.md.
send()
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const response = await env.EMAIL.send({
to: "user@example.com",
from: { email: "welcome@yourdomain.com", name: "My App" },
subject: "Welcome!",
html: "<h1>Welcome!</h1><p>Thanks for signing up.</p>",
text: "Welcome! Thanks for signing up.",
});
return new Response(`Sent: ${response.messageId}`);
},
} satisfies ExportedHandler<Env>;
Multiple recipients (max 50 combined to + cc + bcc):
const response = await env.EMAIL.send({
to: ["user1@example.com", "user2@example.com"],
cc: ["manager@company.com"],
bcc: ["archive@company.com"],
from: { email: "orders@yourdomain.com", name: "Orders" },
replyTo: "support@yourdomain.com",
subject: "Order Confirmation #12345",
html: "<h1>Your order is confirmed</h1>",
text: "Your order is confirmed",
});
Attachments
Important: The Workers binding and REST API handle attachment content differently:
- Workers binding:
contentacceptsstring | ArrayBuffer | ArrayBufferView. Strings are treated as raw content (not base64). For text files, pass the raw string. For binary files (images, PDFs), pass anArrayBuffer. Note:ArrayBufferdoes not work with"remote": truein local dev — deploy to test binary attachments. - REST API:
contentis always a base64-encoded string. See rest-api.md.
// Text file attachment — pass raw string content
const response = await env.EMAIL.send({
to: "customer@example.com",
from: "invoices@yourdomain.com",
subject: "Your Report",
html: "<h1>Report attached</h1>",
text: "Report attached.",
attachments: [{
content: "Name,Amount\nWidget A,100\nWidget B,250", // Raw text, NOT base64
filename: "report.csv",
type: "text/csv",
disposition: "attachment",
}],
});
// Binary file attachment — use ArrayBuffer
const pdfBytes = await fetchPdfFromSomewhere(); // Returns ArrayBuffer
const response = await env.EMAIL.send({
to: "customer@example.com",
from: "invoices@yourdomain.com",
subject: "Your Invoice",
html: "<h1>Invoice attached</h1>",
text: "Invoice attached.",
attachments: [{
content: pdfBytes,
filename: "invoice-12345.pdf",
type: "application/pdf",
disposition: "attachment",
}],
});
// Inline image — reference in HTML with cid:<contentId>
const imageBytes = await fetchImageFromSomewhere(); // Returns ArrayBuffer
const response = await env.EMAIL.send({
to: "user@example.com",
from: "marketing@yourdomain.com",
subject: "New Product",
html: '<img src="cid:product-hero" alt="Product" />',
attachments: [{
content: imageBytes,
filename: "product.png",
type: "image/png",
disposition: "inline",
contentId: "product-hero",
}],
});
Total email size (body + attachments) cannot exceed 25 MiB.
Custom Headers
Only whitelisted headers allowed. See the headers reference.
const response = await env.EMAIL.send({
to: "user@example.com",
from: "notifications@yourdomain.com",
subject: "Your weekly digest",
html: "<h1>Weekly Digest</h1>",
headers: {
"In-Reply-To": "<original-message-id@yourdomain.com>",
"List-Unsubscribe": "<https://yourdomain.com/unsubscribe?id=abc123>",
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
"X-Campaign-ID": "weekly-digest-2026-03",
},
});
Legacy EmailMessage API
The EmailMessage API remains supported. Uses raw MIME via mimetext:
import { EmailMessage } from "cloudflare:email";
import { createMimeMessage } from "mimetext";
const msg = createMimeMessage();
msg.setSender({ name: "Sender", addr: "sender@yourdomain.com" });
msg.setRecipient("recipient@example.com");
msg.setSubject("Hello");
msg.addMessage({ contentType: "text/html", data: "<h1>Hello</h1>" });
await env.EMAIL.send(new EmailMessage("sender@yourdomain.com", "recipient@example.com", msg.asRaw()));
Requires npm install mimetext and "nodejs_compat" in compatibility flags.
Agents SDK Email
Agents can receive and reply to emails natively via the Agents SDK.
// wrangler.jsonc
{
"durable_objects": {
"bindings": [{ "name": "EmailAgent", "class_name": "EmailAgent" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["EmailAgent"] }],
"send_email": [{ "name": "EMAIL", "destination_address": "reply@yourdomain.com" }]
}
import { Agent } from "agents";
import { type AgentEmail } from "agents/email";
import PostalMime from "postal-mime";
export class EmailAgent extends Agent<Env, State> {
async onEmail(email: AgentEmail) {
const parsed = await PostalMime.parse(await email.getRaw());
await this.replyToEmail(email, {
fromName: "My Agent",
subject: `Re: ${parsed.subject}`,
body: "Thanks for your email! I'll look into this.",
});
}
}
Route emails to agents with resolvers:
import { routeAgentEmail } from "agents";
import { createAddressBasedEmailResolver } from "agents/email";
export default {
async email(message, env) {
await routeAgentEmail(message, env, {
resolver: createAddressBasedEmailResolver("EmailAgent"),
});
},
};
Resolver types: createAddressBasedEmailResolver (recipient → instance name), createSecureReplyEmailResolver(secret) (HMAC-signed replies), createCatchAllEmailResolver("Agent", "default") (single inbox). Use isAutoReplyEmail(email.headers) to skip vacation responders.
Error Handling
try {
const response = await env.EMAIL.send({ /* ... */ });
} catch (error) {
// error.code is one of the E_* error codes
console.error(`Failed: ${error.code} - ${error.message}`);
}
These error codes are for the Workers binding (thrown as Error objects with .code and .message). The REST API returns standard Cloudflare API numeric error codes instead — see rest-api.md.
| Error Code | What It Means | What to Do |
|---|---|---|
E_VALIDATION_ERROR |
Invalid payload | Check email format, required fields |
E_FIELD_MISSING |
Required field missing | Add to, from, or subject |
E_TOO_MANY_RECIPIENTS |
Combined to/cc/bcc exceeds 50 | Split into multiple sends |
E_SENDER_NOT_VERIFIED |
Domain not onboarded | Run wrangler email sending enable yourdomain.com or onboard in Dashboard |
E_RECIPIENT_NOT_ALLOWED |
Recipient not in allowed list | Add to allowed_destination_addresses |
E_RECIPIENT_SUPPRESSED |
Address bounced or reported spam | Remove from your list; check suppression list in Dashboard |
E_SENDER_DOMAIN_NOT_AVAILABLE |
Domain not available for sending | Complete domain onboarding |
E_CONTENT_TOO_LARGE |
Content exceeds 25 MiB | Reduce attachments or body |
E_RATE_LIMIT_EXCEEDED |
Rate limit hit | Retry with exponential backoff |
E_DAILY_LIMIT_EXCEEDED |
Daily quota reached | Wait or request limit increase |
E_DELIVERY_FAILED |
SMTP delivery failure | Check recipient address, retry if transient |
E_INTERNAL_SERVER_ERROR |
Service temporarily unavailable | Retry with exponential backoff |
E_HEADER_NOT_ALLOWED |
Header not on whitelist | Use an allowed header; see headers reference |
E_HEADER_USE_API_FIELD |
Must use API field instead | Set From, To, etc. via the dedicated API fields, not headers |
E_HEADER_VALUE_INVALID |
Header value is malformed or empty | Fix the value format (e.g., List-Unsubscribe needs angle-bracket URIs) |
E_HEADER_VALUE_TOO_LONG |
Header value exceeds 2,048 bytes | Shorten the header value |
E_HEADER_NAME_INVALID |
Invalid header name | Fix characters or keep under 100 bytes |
E_HEADERS_TOO_LARGE |
Total headers exceed 16 KB | Reduce number or size of custom headers |
E_HEADERS_TOO_MANY |
More than 20 non-X headers | Reduce to 20 or fewer whitelisted headers |
For E_RATE_LIMIT_EXCEEDED and E_DELIVERY_FAILED, retry with exponential backoff. For validation errors (E_VALIDATION_ERROR, E_FIELD_MISSING, E_SENDER_NOT_VERIFIED), fix the request — retrying won't help.
Restricted Bindings
Restrict which from addresses a binding can use:
{
"send_email": [{
"name": "RESTRICTED_EMAIL",
"allowed_sender_addresses": ["noreply@yourdomain.com", "support@yourdomain.com"]
}]
}