- Node.js 20+
- npm
- optional for remote environment work: Cloudflare account with D1, R2, and Queues
- optional for real outbound validation: AWS account with SES API access keys or a Resend API key
npm installFor the default purely local workflow, you can usually keep the top-level local bindings in wrangler.toml as-is.
Only update wrangler.toml when you want to:
- point remote commands at real Cloudflare environments
- change local bucket or queue names
- adjust
OUTBOUND_PROVIDER,SES_REGION,SES_FROM_DOMAIN,SES_CONFIGURATION_SET, orRESEND_API_BASE_URL
Copy .dev.vars.example to .dev.vars and fill in the
values you need for your local workflow:
Required for the default local API flow:
WEBHOOK_SHARED_SECRETAPI_SIGNING_SECRETADMIN_API_SECRET
Optional outbound-provider selection:
OUTBOUND_PROVIDER
Optional for real outbound SES-backed validation:
SES_ACCESS_KEY_IDSES_SECRET_ACCESS_KEY
Optional for real outbound Resend-backed validation:
RESEND_API_KEYRESEND_API_BASE_URL
Optional for contact inbox, alias-management, or Email Routing admin flows:
CLOUDFLARE_API_TOKENCLOUDFLARE_ZONE_IDCLOUDFLARE_EMAIL_DOMAINCLOUDFLARE_EMAIL_WORKERCONTACT_ALIAS_ROUTING_BOOTSTRAP_ENABLED
Wrangler automatically loads .dev.vars for local development.
Do not commit .dev.vars; it is intentionally gitignored.
The default local values for ADMIN_ROUTES_ENABLED, DEBUG_ROUTES_ENABLED,
and idempotency retention windows already live in wrangler.toml.
You only need to override them in .dev.vars if you intentionally want different
local behavior.
npm run d1:migrate:localThis applies the full local schema in the local D1 instance persisted under
.wrangler/state, including:
migrations/0001_initial.sqlmigrations/0002_agent_registry.sqlmigrations/0002_idempotency_keys.sqlmigrations/0003_agent_deployment_history.sqlmigrations/0004_token_reissue_requests.sqlmigrations/0005_draft_origin_audit.sql
The entire .wrangler/ directory is local-only and is intentionally gitignored.
npm run d1:seed:localThis inserts:
- demo tenant
t_demo - demo mailbox
mbx_demo - demo agent
agt_demo - seeded inbound thread
thr_demo_inbound - seeded inbound message
msg_demo_inbound - mailbox binding and basic policy
The SQL lives in seeds/0001_demo.sql.
npm run dev:localDefault local URL is typically http://127.0.0.1:8787.
Create a token with the admin secret:
curl -X POST http://127.0.0.1:8787/v1/auth/tokens \
-H 'content-type: application/json' \
-H 'x-admin-secret: replace-with-admin-api-secret' \
-d '{
"sub": "local-dev",
"tenantId": "t_demo",
"scopes": [
"agent:create",
"agent:read",
"agent:update",
"agent:bind",
"task:read",
"mail:read",
"mail:replay",
"draft:create",
"draft:read",
"draft:send"
],
"mailboxIds": ["mbx_demo"],
"expiresInSeconds": 86400
}'Store the returned token and use it as:
export TOKEN="REPLACE_WITH_TOKEN"The seeded agt_demo row is enough for the mailbox read, send, and reply
examples below.
If you want a real R2-backed config object for direct agent or deployment experiments, create another agent through the API:
curl -X POST http://127.0.0.1:8787/v1/agents \
-H 'content-type: application/json' \
-H "authorization: Bearer $TOKEN" \
-d '{
"tenantId": "t_demo",
"name": "Support Agent",
"mode": "assistant",
"config": {
"systemPrompt": "You are a helpful support agent.",
"defaultModel": "gpt-5",
"tools": ["reply_email"]
}
}'Store the returned agent id if you want to use direct agent routes such as
POST /v1/agents/{agentId}/drafts.
Do not bind that new agent to mbx_demo unless you also replace the seeded
agt_demo binding through the deployment flow; otherwise the older seeded
binding remains the mailbox fallback execution target.
List messages:
curl -X GET "http://127.0.0.1:8787/v1/mailboxes/self/messages?limit=10" \
-H "authorization: Bearer $TOKEN"Read a single message body:
curl -X GET http://127.0.0.1:8787/v1/mailboxes/self/messages/msg_demo_inbound/content \
-H "authorization: Bearer $TOKEN"curl -X POST http://127.0.0.1:8787/v1/messages/send \
-H 'content-type: application/json' \
-H "authorization: Bearer $TOKEN" \
-d '{
"to": ["user@example.com"],
"subject": "Hello from Mailagents",
"text": "This is a local high-level send test.",
"idempotencyKey": "send-message-001"
}'curl -X POST http://127.0.0.1:8787/v1/messages/msg_demo_inbound/reply \
-H 'content-type: application/json' \
-H "authorization: Bearer $TOKEN" \
-d '{
"text": "Thanks for your message. This is a local reply test.",
"idempotencyKey": "reply-message-001"
}'Create:
curl -X POST http://127.0.0.1:8787/v1/agents/agt_demo/drafts \
-H 'content-type: application/json' \
-H "authorization: Bearer $TOKEN" \
-d '{
"tenantId": "t_demo",
"mailboxId": "mbx_demo",
"from": "agent@mail.example.com",
"to": ["user@example.com"],
"subject": "Hello from Mailagents",
"text": "This is a local test draft."
}'If you completed the optional R2-backed agent step above, you can substitute
that returned agent id for agt_demo here.
Send:
curl -X POST http://127.0.0.1:8787/v1/drafts/REPLACE_WITH_DRAFT_ID/send \
-H 'content-type: application/json' \
-H "authorization: Bearer $TOKEN" \
-d '{
"idempotencyKey": "send-draft-001"
}'Use the explicit draft path only when you need a visible review step or direct control over the draft lifecycle.
curl -X POST http://127.0.0.1:8787/v1/messages/REPLACE_WITH_MESSAGE_ID/replay \
-H 'content-type: application/json' \
-H "authorization: Bearer $TOKEN" \
-d '{
"mode": "normalize",
"idempotencyKey": "replay-normalize-001"
}'npm run d1:migrate:localnpm run d1:seed:localnpm run dev:local- mint a token
- create agent via API
- bind mailbox
- read mailbox messages
- send or reply through the high-level routes
- use explicit drafts only when the workflow needs review
- trigger replay/tests when debugging or recovering state
- The current parser is MVP-grade, not a full RFC-complete MIME parser.
- Outbound now chooses provider-specific rich send behavior for reply headers or attachments. SES uses
RawMIME; Resend uses headers plus attachment upload in the API payload. - Sends to active Mailagents mailboxes are routed internally through the local inbound ingest path and do not require SES or Resend credentials.
- Idempotency cleanup can be triggered manually with
POST /admin/api/maintenance/idempotency-cleanup. - For remote D1, use the environment-specific scripts such as
npm run d1:migrate:remote:devandnpm run d1:seed:remote:dev. - With fake or sandbox-limited provider credentials, local send tests can still exercise the accepted-to-retry path without proving external delivery.