Email agent

Last updated Aug 17, 2026 Agents can send and receive email with Cloudflare Email Service. This guide shows how to send outbound email with the Workers binding, route inbound mail into Agents, and handle follow-up replies securely.

Prerequisites

Before using email with Agents, you need:

  1. A domain onboarded to Cloudflare Email Service.
  2. A send_email binding in wrangler.jsonc for outbound email.
  3. An Email Service routing rule that sends inbound mail to your Worker.
  4. Optional: an EMAIL_SECRET secret if you want secure reply routing.

Domain setup

  1. Log in to the Cloudflare Dashboard ↗︎.
  2. Go to Compute & AI > Email Service.
  3. Select Onboard Domain and choose your domain.
  4. Add the DNS records (SPF and DKIM) to authorize sending.

DNS changes usually complete within 5-15 minutes for domains using Cloudflare DNS, but can take up to 24 hours to propagate globally.

Wrangler configuration

Add the email binding to your Worker:

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "send_email": [
    {
      "name": "EMAIL",
      "remote": true
    }
  ]
}
[[send_email]]
name = "EMAIL"
remote = true

The remote = true option lets you call the real Email Service API during local development with wrangler dev.

Quick start

import { Agent, callable, routeAgentEmail } from "agents";
import { createAddressBasedEmailResolver } from "agents/email";
import PostalMime from "postal-mime";

export class EmailAgent extends Agent {
    @callable()
    async sendWelcomeEmail(to) {
        await this.sendEmail({
            binding: this.env.EMAIL,
            to,
            from: "support@yourdomain.com",
            replyTo: "support@yourdomain.com",
            subject: "Welcome to our service",
            text: "Thanks for signing up. Reply to this email if you need help.",
        });
    }

async onEmail(email) {
        const raw = await email.getRaw();
        const parsed = await PostalMime.parse(raw);

console.log("Received email from:", email.from);
        console.log("Subject:", parsed.subject);

await this.replyToEmail(email, {
            fromName: "Support Agent",
            body: "Thanks for your email! We received it.",
        });
    }
}

export default {
    async email(message, env) {
        await routeAgentEmail(message, env, {
            resolver: createAddressBasedEmailResolver("EmailAgent"),
        });
    },
};
import { Agent, callable, routeAgentEmail } from "agents";
import { createAddressBasedEmailResolver, type AgentEmail } from "agents/email";
import PostalMime from "postal-mime";

export class EmailAgent extends Agent {
    @callable()
    async sendWelcomeEmail(to: string) {
        await this.sendEmail({
            binding: this.env.EMAIL,
            to,
            from: "support@yourdomain.com",
            replyTo: "support@yourdomain.com",
            subject: "Welcome to our service",
            text: "Thanks for signing up. Reply to this email if you need help.",
        });
    }

async onEmail(email: AgentEmail) {
        const raw = await email.getRaw();
        const parsed = await PostalMime.parse(raw);

console.log("Received email from:", email.from);
        console.log("Subject:", parsed.subject);

await this.replyToEmail(email, {
            fromName: "Support Agent",
            body: "Thanks for your email! We received it.",
        });
    }
}

export default {
    async email(message, env) {
        await routeAgentEmail(message, env, {
            resolver: createAddressBasedEmailResolver("EmailAgent"),
        });
    },
} satisfies ExportedHandler<Env>;

Sending outbound email

Using sendEmail()

sendEmail() sends outbound email through a send_email binding that you pass explicitly. It automatically injects agent routing headers (X-Agent-Name, X-Agent-ID) into every message, and optionally signs them with HMAC-SHA256 so that replies can be routed back to the same agent instance.

class MyAgent extends Agent {
    @callable()
    async sendReceipt(to, orderId) {
        const result = await this.sendEmail({
            binding: this.env.EMAIL,
            to,
            from: { email: "billing@yourdomain.com", name: "Billing Bot" },
            replyTo: "billing@yourdomain.com",
            subject: `Receipt for order ${orderId}`,
            text: `Your receipt for order ${orderId} is ready.`,
            secret: this.env.EMAIL_SECRET,
        });

return result.messageId;
    }
}
class MyAgent extends Agent {
    @callable()
    async sendReceipt(to: string, orderId: string) {
        const result = await this.sendEmail({
            binding: this.env.EMAIL,
            to,
            from: { email: "billing@yourdomain.com", name: "Billing Bot" },
            replyTo: "billing@yourdomain.com",
            subject: `Receipt for order ${orderId}`,
            text: `Your receipt for order ${orderId} is ready.`,
            secret: this.env.EMAIL_SECRET,
        });

return result.messageId;
    }
}

When secret is provided, the agent signs the routing headers so that replies verified by createSecureReplyEmailResolver route back to the same agent instance.

Set replyTo to the mailbox that routes back to your Worker when you want recipients to continue the conversation with the same agent.

Routing inbound mail

Resolvers determine which Agent instance receives an incoming email. Choose the resolver that matches your use case.

For basic Email Service sending and receiving, createAddressBasedEmailResolver() is enough. The secure reply resolver below is optional and specific to Agents SDK reply signing, not a requirement of Email Service itself.

createAddressBasedEmailResolver

Recommended for inbound mail. Routes emails based on the recipient address.

import { createAddressBasedEmailResolver } from "agents/email";

const resolver = createAddressBasedEmailResolver("EmailAgent");
import { createAddressBasedEmailResolver } from "agents/email";

const resolver = createAddressBasedEmailResolver("EmailAgent");

Routing logic:

Recipient Address Agent Name Agent ID
support@example.com EmailAgent (default) support
sales@example.com EmailAgent (default) sales
NotificationAgent+user123@example.com NotificationAgent user123

The sub-address format (agent+id@domain) allows routing to different agent namespaces and instances from a single email domain.

Note

Agent class names in the recipient address are matched case-insensitively. Email infrastructure often lowercases addresses, so NotificationAgent+user123@example.com and notificationagent+user123@example.com both route to the NotificationAgent class.

createSecureReplyEmailResolver

For reply flows with signature verification. Verifies that incoming emails are authentic replies to your outbound emails, preventing attackers from routing emails to arbitrary agent instances.

import { createSecureReplyEmailResolver } from "agents/email";

const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET);
import { createSecureReplyEmailResolver } from "agents/email";

const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET);

When your agent sends an email with replyToEmail() or sendEmail() and a secret, it signs the routing headers with a timestamp. When a reply comes back, this resolver verifies the signature and checks that it has not expired before routing.

Options:

const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {
    // Maximum age of signature in seconds (default: 30 days)
    maxAge: 7 * 24 * 60 * 60, // 7 days

// Callback for logging/debugging signature failures
    onInvalidSignature: (email, reason) => {
        console.warn(`Invalid signature from ${email.from}: ${reason}`);
        // reason can be: "missing_headers", "expired", "invalid", "malformed_timestamp"
    },
});
const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {
    // Maximum age of signature in seconds (default: 30 days)
    maxAge: 7 * 24 * 60 * 60, // 7 days

// Callback for logging/debugging signature failures
    onInvalidSignature: (email, reason) => {
        console.warn(`Invalid signature from ${email.from}: ${reason}`);
        // reason can be: "missing_headers", "expired", "invalid", "malformed_timestamp"
    },
});

When to use: If your agent initiates email conversations and you need replies to route back to the same agent instance securely.

createCatchAllEmailResolver

For single-instance routing. Routes all emails to a specific agent instance regardless of the recipient address.

import { createCatchAllEmailResolver } from "agents/email";

const resolver = createCatchAllEmailResolver("EmailAgent", "default");
import { createCatchAllEmailResolver } from "agents/email";

const resolver = createCatchAllEmailResolver("EmailAgent", "default");

When to use: When you have a single agent instance that handles all emails (for example, a shared inbox).

Combining resolvers

You can combine resolvers to handle different scenarios:

export default {
    async email(message, env) {
        const secureReplyResolver = createSecureReplyEmailResolver(
            env.EMAIL_SECRET,
        );
        const addressResolver = createAddressBasedEmailResolver("EmailAgent");

await routeAgentEmail(message, env, {
            resolver: async (email, env) => {
                // First, check if this is a signed reply
                const replyRouting = await secureReplyResolver(email, env);
                if (replyRouting) return replyRouting;

// Otherwise, route based on recipient address
                return addressResolver(email, env);
            },

// Handle emails that do not match any routing rule
            onNoRoute: (email) => {
                console.warn(`No route found for email from ${email.from}`);
                email.setReject("Unknown recipient");
            },
        });
    },
};
export default {
    async email(message, env) {
        const secureReplyResolver = createSecureReplyEmailResolver(
            env.EMAIL_SECRET,
        );
        const addressResolver = createAddressBasedEmailResolver("EmailAgent");

await routeAgentEmail(message, env, {
            resolver: async (email, env) => {
                // First, check if this is a signed reply
                const replyRouting = await secureReplyResolver(email, env);
                if (replyRouting) return replyRouting;

// Otherwise, route based on recipient address
                return addressResolver(email, env);
            },

// Handle emails that do not match any routing rule
            onNoRoute: (email) => {
                console.warn(`No route found for email from ${email.from}`);
                email.setReject("Unknown recipient");
            },
        });
    },
} satisfies ExportedHandler<Env>;

Handling emails in your Agent

The AgentEmail interface

When your agent's onEmail method is called, it receives an AgentEmail object:

type AgentEmail = {
    from: string; // Sender's email address
    to: string; // Recipient's email address
    headers: Headers; // Email headers (subject, message-id, etc.)
    rawSize: number; // Size of the raw email in bytes

getRaw(): Promise<Uint8Array>; // Get the full raw email content
    reply(options): Promise<void>; // Send a reply
    forward(rcptTo, headers?): Promise<void>; // Forward the email
    setReject(reason): void; // Reject the email with a reason
};

Parsing email content

Use a library like postal-mime ↗︎ to parse the raw email:

import PostalMime from "postal-mime";

class MyAgent extends Agent {
    async onEmail(email) {
        const raw = await email.getRaw();
        const parsed = await PostalMime.parse(raw);

console.log("Subject:", parsed.subject);
        console.log("Text body:", parsed.text);
        console.log("HTML body:", parsed.html);
        console.log("Attachments:", parsed.attachments);
    }
}
import PostalMime from "postal-mime";

class MyAgent extends Agent {
    async onEmail(email: AgentEmail) {
        const raw = await email.getRaw();
        const parsed = await PostalMime.parse(raw);

console.log("Subject:", parsed.subject);
        console.log("Text body:", parsed.text);
        console.log("HTML body:", parsed.html);
        console.log("Attachments:", parsed.attachments);
    }
}

Detecting auto-reply emails

Use isAutoReplyEmail() to detect auto-reply emails and avoid mail loops:

import { isAutoReplyEmail } from "agents/email";
import PostalMime from "postal-mime";

class MyAgent extends Agent {
    async onEmail(email) {
        const raw = await email.getRaw();
        const parsed = await PostalMime.parse(raw);

// Detect auto-reply emails to avoid sending duplicate responses
        if (isAutoReplyEmail(parsed.headers)) {
            console.log("Skipping auto-reply email");
            return;
        }

// Process the email...
    }
}
import { isAutoReplyEmail } from "agents/email";
import PostalMime from "postal-mime";

class MyAgent extends Agent {
    async onEmail(email: AgentEmail) {
        const raw = await email.getRaw();
        const parsed = await PostalMime.parse(raw);

// Detect auto-reply emails to avoid sending duplicate responses
        if (isAutoReplyEmail(parsed.headers)) {
            console.log("Skipping auto-reply email");
            return;
        }

// Process the email...
    }
}

This checks for standard RFC 3834 headers (Auto-Submitted, X-Auto-Response-Suppress, Precedence) that indicate an email is an auto-reply.

Replying to emails

Use this.replyToEmail() to send a reply through the inbound email's reply channel:

class MyAgent extends Agent {
    async onEmail(email) {
        await this.replyToEmail(email, {
            fromName: "Support Bot", // Display name for the sender
            subject: "Re: Your inquiry", // Optional, defaults to "Re: "
            body: "Thanks for contacting us!", // Email body
            contentType: "text/plain", // Optional, defaults to "text/plain"
            headers: {
                // Optional custom headers
                "X-Custom-Header": "value",
            },
            secret: this.env.EMAIL_SECRET, // Optional, signs headers for secure reply routing
        });
    }
}
class MyAgent extends Agent {
    async onEmail(email: AgentEmail) {
        await this.replyToEmail(email, {
            fromName: "Support Bot", // Display name for the sender
            subject: "Re: Your inquiry", // Optional, defaults to "Re: "
            body: "Thanks for contacting us!", // Email body
            contentType: "text/plain", // Optional, defaults to "text/plain"
            headers: {
                // Optional custom headers
                "X-Custom-Header": "value",
            },
            secret: this.env.EMAIL_SECRET, // Optional, signs headers for secure reply routing
        });
    }
}

Deferred replies

replyToEmail() requires a live AgentEmail object, so it only works inside onEmail(). If you need to reply later — from a scheduled task, a callable method, or after a human-in-the-loop approval — store the sender info in state and use sendEmail():

class MyAgent extends Agent {
    async onEmail(email) {
        const raw = await email.getRaw();
        const parsed = await PostalMime.parse(raw);

this.setState({
            ...this.state,
            pendingReply: {
                to: email.from,
                messageId: parsed.messageId,
                subject: parsed.subject,
            },
        });
    }

@callable()
    async sendDelayedReply(body) {
        const { pendingReply } = this.state;
        if (!pendingReply) return;

await this.sendEmail({
            binding: this.env.EMAIL,
            to: pendingReply.to,
            from: "support@yourdomain.com",
            subject: `Re: ${pendingReply.subject}`,
            text: body,
            inReplyTo: pendingReply.messageId,
            secret: this.env.EMAIL_SECRET,
        });
    }
}
class MyAgent extends Agent {
    async onEmail(email: AgentEmail) {
        const raw = await email.getRaw();
        const parsed = await PostalMime.parse(raw);

this.setState({
            ...this.state,
            pendingReply: {
                to: email.from,
                messageId: parsed.messageId,
                subject: parsed.subject,
            },
        });
    }

@callable()
    async sendDelayedReply(body: string) {
        const { pendingReply } = this.state;
        if (!pendingReply) return;

await this.sendEmail({
            binding: this.env.EMAIL,
            to: pendingReply.to,
            from: "support@yourdomain.com",
            subject: `Re: ${pendingReply.subject}`,
            text: body,
            inReplyTo: pendingReply.messageId,
            secret: this.env.EMAIL_SECRET,
        });
    }
}

The inReplyTo field sets the In-Reply-To header so mail clients thread the reply correctly. The secret signs the agent routing headers so that follow-up replies route back to this agent instance via createSecureReplyEmailResolver.