cloudflare-agentic-inbox
Deploy and manage a self-hosted email client with AI agent on Cloudflare Workers
How do I install this agent skill?
npx skills add https://github.com/reason-machines/ai-agent-skills --skill cloudflare-agentic-inboxIs this agent skill safe to install?
- Gen Agent Trust Hubpass
The skill facilitates the deployment of an AI-powered email inbox. It is primarily safe but contains an attack surface for indirect prompt injection due to processing external email content with an AI agent.
- Socketpass
No alerts
- Snykwarn
Risk: MEDIUM · 1 issue
What does this agent skill do?
Cloudflare Agentic Inbox
Skill by ara.so — AI Agent Skills collection.
Agentic Inbox is a self-hosted email client with an AI agent, running entirely on Cloudflare Workers. It uses Email Routing for receiving emails, Durable Objects with SQLite for per-mailbox storage, R2 for attachments, and Workers AI with the Cloudflare Agents SDK for AI-powered email assistance.
Installation & Deployment
Quick Deploy (Recommended)
-
Deploy via button (provisions R2, Durable Objects, Workers AI automatically):
# Visit: https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/agentic-inbox # When prompted, enter your domain: yourdomain.com -
Configure Cloudflare Access (required for production):
- Navigate to Worker Settings → Domains & Routes
- Enable one-click Cloudflare Access
- Note the
POLICY_AUDandTEAM_DOMAINvalues - Set as Worker secrets:
wrangler secret put POLICY_AUD wrangler secret put TEAM_DOMAIN -
Set up Email Routing:
- Go to your domain in Cloudflare dashboard
- Navigate to Email Routing
- Create a catch-all rule forwarding to this Worker
-
Enable Email Service:
- Add
send_emailbinding towrangler.jsonc:
{ "send_email": [ { "name": "SEB", "destination_address": "you@example.com" } ] } - Add
Manual Setup
# Clone repository
git clone https://github.com/cloudflare/agentic-inbox.git
cd agentic-inbox
# Install dependencies
npm install
# Create R2 bucket
wrangler r2 bucket create agentic-inbox
# Configure domain in wrangler.jsonc
# Set DOMAINS variable to your domain
# Deploy
npm run deploy
Configuration
wrangler.jsonc Structure
{
"name": "agentic-inbox",
"main": "worker/index.ts",
"compatibility_date": "2025-01-01",
"compatibility_flags": ["nodejs_compat"],
"vars": {
"DOMAINS": "yourdomain.com"
},
"durable_objects": {
"bindings": [
{
"name": "MAILBOX",
"class_name": "MailboxDurableObject",
"script_name": "agentic-inbox"
},
{
"name": "EMAIL_AGENT",
"class_name": "EmailAgentDurableObject",
"script_name": "agentic-inbox"
}
]
},
"r2_buckets": [
{
"binding": "R2",
"bucket_name": "agentic-inbox"
}
],
"ai": {
"binding": "AI"
},
"send_email": [
{
"name": "SEB",
"destination_address": "fallback@yourdomain.com"
}
]
}
Environment Variables (Secrets)
# Required for Cloudflare Access authentication
wrangler secret put POLICY_AUD
wrangler secret put TEAM_DOMAIN
# TEAM_DOMAIN can be either:
# - Your Access team URL: yourteam.cloudflareaccess.com
# - Full certs URL: yourteam.cloudflareaccess.com/cdn-cgi/access/certs
Development
Local Development
# Start dev server with hot reload
npm run dev
# Access at http://localhost:8787
# Note: Cloudflare Access is disabled in local development
Project Structure
agentic-inbox/
├── app/ # React frontend
│ ├── routes/ # React Router v7 routes
│ ├── components/ # UI components
│ └── lib/ # Utilities, stores (Zustand)
├── worker/ # Cloudflare Worker backend
│ ├── index.ts # Hono router, email handler
│ ├── mailbox-do.ts # Mailbox Durable Object
│ ├── email-agent-do.ts # AI Agent Durable Object
│ └── auth.ts # Access JWT validation
└── wrangler.jsonc # Cloudflare configuration
Key API Patterns
Creating a Mailbox
// POST /api/mailboxes
const response = await fetch('/api/mailboxes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
address: 'hello@yourdomain.com'
})
});
const mailbox = await response.json();
// { id: "uuid", address: "hello@yourdomain.com", createdAt: "..." }
Sending Email
// POST /api/mailboxes/:id/send
const response = await fetch(`/api/mailboxes/${mailboxId}/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
to: ['recipient@example.com'],
subject: 'Hello',
body: '<p>Email content</p>',
cc: [],
bcc: [],
inReplyTo: null,
references: []
})
});
Accessing AI Agent
// WebSocket connection to agent
const ws = new WebSocket(`wss://yourapp.workers.dev/agents/${mailboxId}`);
ws.onopen = () => {
ws.send(JSON.stringify({
type: 'message',
content: 'Summarize my unread emails'
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// Stream: { type: 'text-delta', text: '...' }
// Tools: { type: 'tool-call', toolName: 'read_inbox', args: {...} }
// Result: { type: 'tool-result', result: {...} }
};
Durable Object Implementation
Mailbox Durable Object
import { DurableObject } from 'cloudflare:workers';
export class MailboxDurableObject extends DurableObject {
async fetch(request: Request) {
const url = new URL(request.url);
if (url.pathname === '/emails' && request.method === 'GET') {
const stmt = this.ctx.storage.sql.exec(
'SELECT * FROM emails ORDER BY receivedAt DESC LIMIT 50'
);
return Response.json(stmt.toArray());
}
if (url.pathname === '/emails' && request.method === 'POST') {
const email = await request.json();
const result = this.ctx.storage.sql.exec(
`INSERT INTO emails (id, subject, from_address, to_address, body, receivedAt)
VALUES (?, ?, ?, ?, ?, ?)`,
email.id, email.subject, email.from, email.to, email.body, Date.now()
);
return Response.json({ success: true });
}
return new Response('Not found', { status: 404 });
}
}
Email Agent Durable Object
import { AIChatAgent } from '@cloudflare/agents-sdk';
import { DurableObject } from 'cloudflare:workers';
export class EmailAgentDurableObject extends DurableObject {
private agent?: AIChatAgent;
async fetch(request: Request) {
if (!this.agent) {
this.agent = new AIChatAgent({
model: '@cf/moonshotai/kimi-k2.5',
binding: this.env.AI,
tools: [
{
name: 'read_inbox',
description: 'Read emails from the inbox',
parameters: {
type: 'object',
properties: {
limit: { type: 'number', default: 10 }
}
},
handler: async ({ limit }) => {
// Fetch from mailbox DO
const mailboxId = this.ctx.id.toString();
const emails = await this.fetchMailboxEmails(mailboxId, limit);
return { emails };
}
},
{
name: 'send_email',
description: 'Send an email',
parameters: {
type: 'object',
properties: {
to: { type: 'array', items: { type: 'string' } },
subject: { type: 'string' },
body: { type: 'string' }
},
required: ['to', 'subject', 'body']
},
handler: async ({ to, subject, body }) => {
// Send via Email Service
await this.env.SEB.send({
from: this.getMailboxAddress(),
to,
subject,
content: [{ type: 'text/html', value: body }]
});
return { success: true };
}
}
],
systemPrompt: 'You are an email assistant...'
});
}
// Handle WebSocket upgrade for streaming
const upgradeHeader = request.headers.get('Upgrade');
if (upgradeHeader === 'websocket') {
const [client, server] = Object.values(new WebSocketPair());
this.ctx.acceptWebSocket(server);
return new Response(null, { status: 101, webSocket: client });
}
return new Response('Expected WebSocket', { status: 400 });
}
async webSocketMessage(ws: WebSocket, message: string) {
const { content } = JSON.parse(message);
for await (const chunk of this.agent.stream(content)) {
ws.send(JSON.stringify(chunk));
}
}
}
Email Routing Handler
// worker/index.ts
import { EmailMessage } from 'cloudflare:email';
export default {
async email(message: EmailMessage, env: Env) {
const to = message.to;
const mailboxId = await env.KV.get(`address:${to}`);
if (!mailboxId) {
message.setReject('Mailbox not found');
return;
}
// Forward to Mailbox DO
const id = env.MAILBOX.idFromString(mailboxId);
const stub = env.MAILBOX.get(id);
const emailData = {
id: crypto.randomUUID(),
from: message.from,
to: message.to,
subject: message.headers.get('subject'),
body: await message.text(),
receivedAt: Date.now()
};
await stub.fetch('https://mailbox/emails', {
method: 'POST',
body: JSON.stringify(emailData)
});
}
};
Common Patterns
Access Authentication Middleware
// worker/auth.ts
import * as jose from 'jose';
export async function validateAccessToken(request: Request, env: Env) {
if (!env.POLICY_AUD || !env.TEAM_DOMAIN) {
throw new Error('Cloudflare Access must be configured in production');
}
const token = request.headers.get('Cf-Access-Jwt-Assertion');
if (!token) {
throw new Error('Missing Access token');
}
const certsUrl = env.TEAM_DOMAIN.includes('/cdn-cgi/access/certs')
? env.TEAM_DOMAIN
: `https://${env.TEAM_DOMAIN}/cdn-cgi/access/certs`;
const jwks = jose.createRemoteJWKSet(new URL(certsUrl));
const { payload } = await jose.jwtVerify(token, jwks, {
audience: env.POLICY_AUD,
issuer: env.TEAM_DOMAIN
});
return payload;
}
Agent System Prompt Customization
// Update system prompt per mailbox
const response = await fetch(`/api/mailboxes/${mailboxId}/agent/system-prompt`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
systemPrompt: `You are a professional email assistant for sales@company.com.
Always be polite and concise. When drafting replies, maintain a friendly tone.`
})
});
Attachment Storage in R2
// Store attachment in R2
async function storeAttachment(env: Env, emailId: string, file: File) {
const key = `attachments/${emailId}/${file.name}`;
await env.R2.put(key, file.stream(), {
httpMetadata: {
contentType: file.type
}
});
return key;
}
// Retrieve attachment
async function getAttachment(env: Env, key: string) {
const object = await env.R2.get(key);
if (!object) return null;
return new Response(object.body, {
headers: {
'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream'
}
});
}
Troubleshooting
Invalid or Expired Access Token
Issue: Invalid or expired Access token error when accessing deployed app.
Solution:
# 1. Turn Access off and back on in Worker Settings → Domains & Routes
# 2. Copy new POLICY_AUD and TEAM_DOMAIN values from modal
# 3. Reset secrets
wrangler secret put POLICY_AUD
# Paste new value
wrangler secret put TEAM_DOMAIN
# Paste new value (can be team URL or full certs URL)
# 4. Redeploy
npm run deploy
Emails Not Arriving
Issue: Catch-all rule configured but emails not appearing in inbox.
Checklist:
- Verify Email Routing is enabled for your domain
- Check catch-all rule forwards to the Worker (not an email address)
- Confirm mailbox exists for the recipient address:
# Check via API curl https://yourapp.workers.dev/api/mailboxes - Check Worker logs:
wrangler tail
Agent Not Responding
Issue: WebSocket connection fails or agent doesn't respond.
Solution:
// Verify Workers AI binding in wrangler.jsonc
{
"ai": {
"binding": "AI"
}
}
// Check agent initialization
const ws = new WebSocket(`wss://yourapp.workers.dev/agents/${mailboxId}`);
ws.onerror = (error) => console.error('WebSocket error:', error);
ws.onclose = (event) => console.log('Closed:', event.code, event.reason);
R2 Bucket Not Found
Issue: Attachment upload fails with R2 error.
Solution:
# Create bucket if missing
wrangler r2 bucket create agentic-inbox
# Verify binding in wrangler.jsonc
{
"r2_buckets": [
{
"binding": "R2",
"bucket_name": "agentic-inbox"
}
]
}
# Redeploy
npm run deploy
Send Email Not Working
Issue: send_email binding not available or emails not sending.
Solution:
- Enable Email Service in Cloudflare dashboard for your account
- Add binding to
wrangler.jsonc:{ "send_email": [ { "name": "SEB", "destination_address": "verified@yourdomain.com" } ] } - Verify domain ownership for sending
- Redeploy Worker
Local Development Access Errors
Issue: Access errors when running npm run dev.
Note: Cloudflare Access is intentionally disabled in local development. If you see Access-related errors in production mode locally, set:
// worker/auth.ts - for local testing only
if (env.ENVIRONMENT === 'development') {
return { email: 'dev@localhost' }; // Skip Access validation
}
CLI Commands
# Development
npm run dev # Start local dev server
npm run build # Build frontend and worker
npm run deploy # Deploy to Cloudflare
# Wrangler commands
wrangler tail # Stream Worker logs
wrangler secret put KEY # Set environment secret
wrangler r2 bucket list # List R2 buckets
wrangler d1 execute DB --command "SELECT * FROM emails" # Query D1 (if using D1)
# Debugging
wrangler dev --remote # Debug against production resources
wrangler kv:key list --binding=KV # List KV keys (if using KV)
MCP Server Integration
Agentic Inbox includes an MCP server at /mcp for external AI tools (Claude Code, Cursor):
// claude_desktop_config.json or similar
{
"mcpServers": {
"agentic-inbox": {
"url": "https://yourapp.workers.dev/mcp",
"headers": {
"Cf-Access-Client-Id": "YOUR_SERVICE_TOKEN_ID",
"Cf-Access-Client-Secret": "YOUR_SERVICE_TOKEN_SECRET"
}
}
}
}
MCP tools have access to all mailboxes by passing mailboxId parameter. Security relies on the Cloudflare Access policy.
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/reason-machines/ai-agent-skills/cloudflare-agentic-inbox">View cloudflare-agentic-inbox on skillZs</a>