Inbound Email Agent — Agentuity Documentation

Inbound Email Agent

Create an AI email auto-responder with Agentuity + Inbound webhooks.

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/inbound that 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

Add environment variables:

dotenv.env
AGENTUITY_SDK_KEY=your_agentuity_sdk_key
INBOUND_API_KEY=your_inbound_api_key

Project Structure

src/
├── agent/
│   ├── index.ts
│   └── email/
│       └── index.ts
├── api/
│   └── index.ts
app.ts
.env
1

Create the Project

agentuity project create --name inbound-email-agent
cd inbound-email-agent
bun add openai @inboundemail/sdk

The CLI scaffolds a new project and registers it with Agentuity. bun add installs the OpenAI and Inbound SDKs.

2

Create the Email Agent

Create your inbound-email agent in src/agent/email/index.ts:

typescriptsrc/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;
3

Register the Agent

Add the agent to src/agent/index.ts so it is included in the build:

typescriptsrc/agent/index.ts
import email from './email';
 
export default [email];
4

Add the Inbound Webhook Route

Create the route handler in src/api/index.ts:

typescriptsrc/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;
5

Test Locally with curl

Start the app:

agentuity dev

Send 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.

6

Deploy and Connect Inbound

Deploy your app:

agentuity deploy

In 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.

7

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.state to keep multi-email conversation context.
  • Route messages by to address 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 email and nested address/content fields.

Replies are not sending

  • Verify INBOUND_API_KEY is present and valid.
  • Ensure the from address 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