Build an AI email auto-responder: incoming messages trigger an Agentuity agent that classifies intent, drafts a reply, and sends it back through Inbound.
What You'll Build
- An email agent that classifies intent and drafts a response
- A webhook route at
/api/email/inboundthat receives email events from Inbound - An automatic reply flow using
inbound.reply(...) - A deployed endpoint you can connect to an Inbound domain or catch-all address
Prerequisites
- Bun v1.0+
- Agentuity CLI
- Agentuity account
- Inbound account
- A domain you control for inbound email
Add environment variables:
AGENTUITY_SDK_KEY=your_agentuity_sdk_key
INBOUND_API_KEY=your_inbound_api_keyAGENTUITY_SDK_KEY is generated when you run agentuity project create or agentuity project import. Never commit API keys to version control.
Project Structure
src/
├── agent/
│ ├── index.ts
│ └── email/
│ └── index.ts
├── api/
│ └── index.ts
app.ts
.envCreate the Project
agentuity project create --name inbound-email-agent
cd inbound-email-agent
bun add openai @inboundemail/sdkThe CLI scaffolds a new project and registers it with Agentuity. bun add installs the OpenAI and Inbound SDKs.
Create the Email Agent
Create your inbound-email agent in src/agent/email/index.ts:
import { createAgent } from '@agentuity/runtime';
import { s } from '@agentuity/schema';
import OpenAI from 'openai';
export const AgentInput = s.object({
from: s.string().describe('Sender email address'),
subject: s.string().describe('Email subject line'),
body: s.string().describe('Email body content'),
to: s.string().optional().describe('Recipient email address'),
replyTo: s.string().optional().describe('Reply-to address if different from sender'),
});
const ResponseDraft = s.object({
response: s.string().describe('AI-generated email response'),
subject: s.string().describe('Subject line for the response'),
summary: s.string().describe('Brief summary of the original email'),
sentiment: s.string().describe('Detected sentiment of the incoming email'),
});
export const AgentOutput = s.object({
response: s.string().describe('AI-generated email response'),
subject: s.string().describe('Subject line for the response'),
summary: s.string().describe('Brief summary of the original email'),
sentiment: s.string().describe('Detected sentiment of the incoming email'),
threadId: s.string().describe('Thread ID for conversation continuity'),
});
const agent = createAgent('email', {
description: 'Processes inbound emails and generates replies',
schema: {
input: AgentInput,
output: AgentOutput,
},
setup: async () => {
return {
client: new OpenAI(),
};
},
handler: async (ctx, { from, subject, body, to }) => {
const completion = await ctx.config.client.chat.completions.create({
model: 'gpt-5.4-nano',
response_format: {
type: 'json_schema',
json_schema: {
name: 'email_response',
schema: s.toJSONSchema(ResponseDraft, { strict: true }),
strict: true,
},
},
messages: [
{
role: 'system',
content: 'You are a helpful email assistant. Draft concise, professional replies.',
},
{
role: 'user',
content: `From: ${from}\nSubject: ${subject}\n${to ? `To: ${to}` : ''}\n\n${body}`,
},
],
});
const content = completion.choices[0]?.message?.content;
if (!content) {
throw new Error('OpenAI did not return a response');
}
const draft = ResponseDraft.parse(JSON.parse(content));
ctx.logger.info('Drafted email response', {
sentiment: draft.sentiment,
summary: draft.summary,
});
return {
...draft,
threadId: ctx.thread.id,
};
},
});
export default agent;Register the Agent
Add the agent to src/agent/index.ts so it is included in the build:
import email from './email';
export default [email];Add the Inbound Webhook Route
Create the route handler in src/api/index.ts:
import { Hono } from 'hono';
import type { Env } from '@agentuity/runtime';
import { Inbound, isInboundWebhook, verifyWebhookFromHeaders } from '@inboundemail/sdk';
import email from '@agent/email';
const inboundApiKey = process.env.INBOUND_API_KEY;
if (!inboundApiKey) {
throw new Error('INBOUND_API_KEY is required');
}
const inbound = new Inbound(inboundApiKey);
const api = new Hono<Env>();
api.post('/email/inbound', async (c) => {
const verified = await verifyWebhookFromHeaders(c.req.raw.headers, inbound);
if (!verified) {
c.var.logger.warn('Invalid Inbound webhook signature');
return c.json({ error: 'Invalid webhook signature' }, 401);
}
const payload: unknown = await c.req.json();
if (!isInboundWebhook(payload)) {
c.var.logger.error('Invalid Inbound webhook payload');
return c.json({ error: 'Invalid webhook payload' }, 400);
}
const emailData = payload.email;
const agentInput = {
from: emailData.from?.addresses?.[0]?.address ?? '',
subject: emailData.subject ?? '',
body: emailData.cleanedContent?.text ?? emailData.parsedData?.textBody ?? '',
to: emailData.to?.addresses?.[0]?.address ?? '',
};
c.var.logger.info('Processing inbound email', {
from: agentInput.from,
subject: agentInput.subject,
});
const result = await email.run(agentInput);
try {
const { data: reply, error } = await inbound.reply(
emailData,
{
from: agentInput.to,
subject: result.subject,
text: result.response,
},
{
idempotencyKey: `email-reply-${emailData.id}`,
}
);
if (error || !reply) {
const message = error ?? 'Inbound did not return a reply';
c.var.logger.error('Failed to send reply', { error: message });
return c.json({ ...result, replySent: false, error: message }, 500);
}
c.var.logger.info('Reply sent', { replyId: reply.id });
return c.json({ ...result, replySent: true, replyId: reply.id });
} catch (err) {
c.var.logger.error('Failed to send reply', { err: String(err) });
return c.json({ ...result, replySent: false, error: String(err) }, 500);
}
});
export default api;See Webhook Structure for the full payload shape and Reply to Email for reply options.
Test Locally with curl
Start the app:
agentuity devSend a sample inbound payload:
curl -X POST http://localhost:3500/api/email/inbound \
-H "Content-Type: application/json" \
-H "X-Endpoint-ID: endp_test123" \
-H "X-Webhook-Verification-Token: your_endpoint_verification_token" \
-d '{
"event": "email.received",
"timestamp": "2026-04-22T12:00:00Z",
"email": {
"id": "test_123",
"from": { "addresses": [{ "address": "test@example.com" }] },
"to": { "addresses": [{ "address": "hello@yourdomain.com" }] },
"subject": "Test email",
"cleanedContent": { "text": "Hello, this is a test!" }
},
"endpoint": {
"id": "endp_test123",
"name": "Local test",
"type": "webhook"
}
}'Use the endpoint ID and verification token from Inbound. Requests without valid Inbound webhook headers return 401.
Deploy and Connect Inbound
Deploy your app:
agentuity deployIn Inbound, create an endpoint with:
- URL:
https://your-app.agentuity.run/api/email/inbound - Event:
email.received
Then either:
- Attach a specific address (example:
hello@yourdomain.com), or - Configure a catch-all endpoint for your domain.
To attach an address, see Create Email Address.
Send a Real Email and Verify
Send an email to the configured address and verify:
- Inbound shows a successful webhook delivery.
- Your Agentuity app logs show route execution.
- The original sender receives the AI-generated reply.
For debugging deployed behavior, use Debugging Deployments.
Extend the Agent
- Use
ctx.thread.stateto keep multi-email conversation context. - Route messages by
toaddress to different agents. - Process attachment metadata and hand off to specialized workflows.
- Tune prompt/model per mailbox intent (support, sales, billing, etc.).
Troubleshooting
Webhook returns 400
- Confirm endpoint path is exactly
/api/email/inbound. - Confirm request body includes
emailand nested address/content fields.
Replies are not sending
- Verify
INBOUND_API_KEYis present and valid. - Ensure the
fromaddress is valid for your Inbound domain setup. - Check Inbound delivery logs for provider errors.
Domain is not receiving mail
- Re-check MX/TXT records.
- Wait for DNS propagation before retesting.
- Verify the domain is fully verified in Inbound.
Next Steps
- Deploying to the Cloud: Push your agent to Agentuity's hosted runtime
- Debugging Deployments: Inspect logs and traces for deployed agents
- State Management: Persist conversation history across email threads
- Webhook Handler Pattern: Patterns for receiving external webhooks