Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/cloudflare/agents/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Cloudflare Agents provides utilities for routing incoming emails to Agent instances with support for address-based routing, secure reply flows, and catch-all patterns.
import { routeAgentEmail, createAddressBasedEmailResolver } from "agents";

export default {
  async email(message, env) {
    await routeAgentEmail(message, env, {
      resolver: createAddressBasedEmailResolver("EmailAgent")
    });
  }
};

routeAgentEmail()

Route an email to the appropriate Agent.
email
ForwardableEmailMessage
required
The email to route (from Email Workers)
env
Env
required
Environment containing Agent bindings
options
EmailRoutingOptions<Env>
required
resolver
EmailResolver<Env>
required
Function that determines which Agent to route to
onNoRoute
(email: ForwardableEmailMessage) => void | Promise<void>
Called when no routing information is found. If not provided, a warning is logged and the email is dropped.
export default {
  async email(message: ForwardableEmailMessage, env: Env) {
    await routeAgentEmail(message, env, {
      resolver: createAddressBasedEmailResolver("SupportAgent"),
      onNoRoute: async (email) => {
        console.warn("No route for:", email.from, "→", email.to);
        email.setReject("Invalid recipient");
      }
    });
  }
};
Returns: Promise<void>

Email Resolvers

createAddressBasedEmailResolver()

Route based on email address (sub-address or local part).
defaultAgentName
string
required
Default agent name to use if email doesn’t contain sub-address
const resolver = createAddressBasedEmailResolver("SupportAgent");
Routing patterns:
  • support+ticket123@example.comSupportAgent instance ticket123
  • support@example.comSupportAgent instance support
  • agent+room@example.comagent instance room
Returns: EmailResolver<Env>

createSecureReplyEmailResolver()

Route secure reply emails with signature verification.
secret
string
required
Secret key for HMAC verification (must match signAgentHeaders)
options
SecureReplyResolverOptions
maxAge
number
default:"2592000"
Maximum signature age in seconds (default: 30 days)
onInvalidSignature
(email, reason) => void
Called when signature verification fails
const secureResolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {
  maxAge: 7 * 24 * 60 * 60, // 7 days
  onInvalidSignature: (email, reason) => {
    console.warn(`Invalid signature from ${email.from}: ${reason}`);
  }
});
Returns: EmailResolver<Env>
Use signAgentHeaders() when sending outbound emails to enable secure reply routing.

createCatchAllEmailResolver()

Route all emails to a single Agent instance.
agentName
string
required
Agent class name
agentId
string
required
Agent instance name
const resolver = createCatchAllEmailResolver("InboxAgent", "main");
Returns: EmailResolver<Env>

Combining Resolvers

Try multiple resolvers in sequence:
export default {
  async email(message: ForwardableEmailMessage, env: Env) {
    const secureResolver = createSecureReplyEmailResolver(env.EMAIL_SECRET);
    const addressResolver = createAddressBasedEmailResolver("SupportAgent");

    await routeAgentEmail(message, env, {
      resolver: async (email, env) => {
        // Try secure reply routing first
        const replyRouting = await secureResolver(email, env);
        if (replyRouting) return replyRouting;

        // Fall back to address-based routing
        return addressResolver(email, env);
      }
    });
  }
};

Secure Reply Flow

Signing Outbound Emails

Use signAgentHeaders() to sign emails for secure reply routing:
secret
string
required
Secret key for HMAC signing (store in environment variables)
agentName
string
required
Agent class name (kebab-case)
agentId
string
required
Agent instance name
import { signAgentHeaders } from "agents/email";

const headers = await signAgentHeaders(
  env.EMAIL_SECRET,
  "support-agent",
  this.name
);

// Use headers when sending email
// Headers: X-Agent-Name, X-Agent-ID, X-Agent-Sig, X-Agent-Sig-Ts
Returns: Promise<Record<string, string>>

replyToEmail()

Reply to an email from within an Agent:
email
AgentEmail
required
The email to reply to
options
ReplyOptions
required
fromName
string
required
Sender name
subject
string
Email subject (defaults to “Re: original subject”)
body
string
required
Email body
contentType
string
default:"text/plain"
MIME content type
headers
Record<string, string>
Additional headers
secret
string | null
Secret for signing headers. Required if email was routed via createSecureReplyEmailResolver. Pass null to opt out.
class SupportAgent extends Agent {
  async onEmail(email: AgentEmail) {
    await this.replyToEmail(email, {
      fromName: "Support Team",
      body: "Thank you for your message!",
      secret: this.env.EMAIL_SECRET
    });
  }
}
Returns: Promise<void>
If the email was routed via createSecureReplyEmailResolver, you must pass a secret to sign replies. Otherwise, replies cannot be routed back securely.

Email Utilities

isAutoReplyEmail()

Check if an email is an auto-reply (to avoid reply loops).
headers
EmailHeader[]
required
Headers array from postal-mime or similar
import { isAutoReplyEmail } from "agents/email";
import PostalMime from "postal-mime";

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

    if (isAutoReplyEmail(parsed.headers)) {
      console.log("Skipping auto-reply");
      return;
    }

    // Process email
  }
}
Returns: boolean Checks for:
  • Auto-Submitted header (RFC 3834)
  • X-Auto-Response-Suppress header
  • Precedence: bulk/junk/list header

Email Handler

onEmail()

Override to handle incoming emails in your Agent:
import type { AgentEmail } from "agents/email";
import PostalMime from "postal-mime";

class SupportAgent extends Agent {
  async onEmail(email: AgentEmail) {
    // Parse email
    const raw = await email.getRaw();
    const parser = new PostalMime();
    const parsed = await parser.parse(raw);

    // Skip auto-replies
    if (isAutoReplyEmail(parsed.headers)) {
      return;
    }

    // Extract content
    const subject = email.headers.get("subject") ?? "No subject";
    const text = parsed.text ?? "";

    console.log(`Email from ${email.from}: ${subject}`);
    console.log(`Body: ${text}`);

    // Reply
    await this.replyToEmail(email, {
      fromName: "Support Bot",
      body: `Thanks for your message about: ${subject}`,
      secret: this.env.EMAIL_SECRET
    });
  }
}

AgentEmail Type

The AgentEmail object passed to onEmail():
from
string
required
Sender email address
to
string
required
Recipient email address
headers
Headers
required
Email headers
const subject = email.headers.get("subject");
const messageId = email.headers.get("message-id");
rawSize
number
required
Size of the raw email in bytes
getRaw
() => Promise<Uint8Array>
required
Get the raw email content
const raw = await email.getRaw();
const parser = new PostalMime();
const parsed = await parser.parse(raw);
reply
(options) => Promise<void>
required
Send a reply (use replyToEmail() instead for automatic header signing)
forward
(rcptTo: string, headers?: Headers) => Promise<void>
required
Forward the email to another address
setReject
(reason: string) => void
required
Reject the email with a reason

Full Example

// src/index.ts
import {
  routeAgentEmail,
  createSecureReplyEmailResolver,
  createAddressBasedEmailResolver,
  type AgentEmail,
  isAutoReplyEmail
} from "agents";
import PostalMime from "postal-mime";

export class SupportAgent extends Agent {
  async onEmail(email: AgentEmail) {
    // Parse email
    const raw = await email.getRaw();
    const parser = new PostalMime();
    const parsed = await parser.parse(raw);

    // Skip auto-replies
    if (isAutoReplyEmail(parsed.headers)) {
      return;
    }

    const subject = email.headers.get("subject") ?? "No subject";
    const text = parsed.text ?? "";

    console.log(`Ticket from ${email.from}: ${subject}`);

    // Store in state
    this.setState({
      ...this.state,
      lastEmail: {
        from: email.from,
        subject,
        body: text,
        receivedAt: Date.now()
      }
    });

    // Reply
    await this.replyToEmail(email, {
      fromName: "Support Team",
      subject: `Re: ${subject}`,
      body: `Thank you for contacting support. Your ticket has been created.\n\nOriginal message:\n${text}`,
      secret: this.env.EMAIL_SECRET
    });
  }
}

export default {
  async email(message: ForwardableEmailMessage, env: Env) {
    const secureResolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {
      onInvalidSignature: (email, reason) => {
        console.warn(`Invalid signature: ${reason}`);
      }
    });

    const addressResolver = createAddressBasedEmailResolver("SupportAgent");

    await routeAgentEmail(message, env, {
      resolver: async (email, env) => {
        // Try secure reply first
        const routing = await secureResolver(email, env);
        if (routing) return routing;

        // Fall back to address-based
        return addressResolver(email, env);
      },
      onNoRoute: (email) => {
        console.warn("No route for:", email.to);
        email.setReject("Invalid recipient");
      }
    });
  }
};

wrangler.jsonc Configuration

{
  "name": "email-agent",
  "main": "src/index.ts",
  "compatibility_date": "2026-01-28",
  "compatibility_flags": ["nodejs_compat"],
  "durable_objects": {
    "bindings": [
      {
        "name": "SUPPORT_AGENT",
        "class_name": "SupportAgent",
        "script_name": "email-agent"
      }
    ]
  },
  "vars": {
    "EMAIL_SECRET": "your-secret-here"
  }
}

Security

Signature Verification

Signatures prevent attackers from spoofing email headers to route emails to arbitrary agents:
// ✅ Secure - verifies HMAC signature
const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET);

// ❌ Insecure - trusts attacker-controlled headers (REMOVED)
const resolver = createHeaderBasedEmailResolver(); // Error: removed

Signature Expiration

Signatures expire after maxAge (default: 30 days):
const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {
  maxAge: 7 * 24 * 60 * 60 // 7 days
});

Secret Management

Store secrets in environment variables:
wrangler secret put EMAIL_SECRET
Or in .dev.vars for local development:
EMAIL_SECRET=your-secret-here

Best Practices

Use Secure Resolvers

// ✅ Good - secure reply routing
const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET);

// ❌ Bad - address-based only (no signature verification)
const resolver = createAddressBasedEmailResolver("Agent");

Check Auto-Replies

// ✅ Good - prevent reply loops
if (isAutoReplyEmail(parsed.headers)) {
  return;
}

// ❌ Bad - might create reply loops
// (no auto-reply check)

Handle No Route

// ✅ Good - reject invalid emails
await routeAgentEmail(message, env, {
  resolver,
  onNoRoute: (email) => {
    email.setReject("Invalid recipient");
  }
});

// ❌ Bad - silently drop emails
await routeAgentEmail(message, env, { resolver });

Sign Replies

// ✅ Good - sign for secure routing
await this.replyToEmail(email, {
  fromName: "Bot",
  body: "Reply",
  secret: this.env.EMAIL_SECRET
});

// ❌ Bad - no signature (replies can't be securely routed)
await this.replyToEmail(email, {
  fromName: "Bot",
  body: "Reply",
  secret: null
});

Build docs developers (and LLMs) love