> ## Documentation Index
> Fetch the complete documentation index at: https://nuntly.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# AI agent inboxes

> Build AI agents that receive, process, and respond to emails. Understand how agent inboxes work, manage conversation memory, and see a complete example with the Claude API.

An AI agent inbox turns email into a structured input channel for your AI models. Instead of polling for new emails or building a custom IMAP listener, you assign an agent identifier to an inbox and receive a webhook event every time a message arrives. Your agent reads the message, decides what to do, and responds, all through the API.

This pattern works for any scenario where you want an LLM to handle email autonomously: customer support, intake triage, lead qualification, invoice processing, order acknowledgment, or any custom workflow that starts with an inbound email.

## How it works

<Steps>
  <Step title="Assign an agent to an inbox">
    Set the `agentId` field on an inbox to your agent's identifier. This can be any string, a name, a UUID, or a reference to your agent registry.
  </Step>

  <Step title="Receive the webhook">
    When a message arrives at an agent-enabled inbox, Nuntly emits both `message.received` and `message.agent.triggered`. Your server receives the
    event with the message ID and thread context.
  </Step>

  <Step title="Read and process">
    Your agent fetches the message content through the API, full text, HTML, and attachments. It loads any stored conversation memory, then calls your
    AI model with the message and context.
  </Step>

  <Step title="Respond and remember">
    Your agent sends a reply through the API, which is added to the same thread and delivered via email. It then saves updated conversation memory so
    the next message in the thread has the full context.
  </Step>
</Steps>

## Assign an agent to an inbox

Set `agentId` when creating or updating an inbox.

```typescript theme={null}
import { Nuntly } from '@nuntly/sdk';

const nuntly = new Nuntly({
  apiKey: process.env.NUNTLY_API_KEY,
});

// On creation
const inbox = await nuntly.inboxes.create({
  domainId: 'your-domain-id',
  address: 'ai-support',
  name: 'AI Support Agent',
  agentId: 'support-agent-v1',
});

// Or update an existing inbox
await nuntly.inboxes.update('ibx_01kabn43yqyxn2bx4ve84mczd3', {
  agentId: 'support-agent-v1',
});

// Remove the agent from an inbox
await nuntly.inboxes.update('ibx_01kabn43yqyxn2bx4ve84mczd3', {
  agentId: null,
});
```

## Webhook event

When a message arrives at an inbox with an `agentId`, Nuntly emits a `message.agent.triggered` event in addition to `message.received`. Subscribe to this event in your [webhook configuration](/docs/guides/webhooks).

See [receiving events](/docs/guides/receiving-webhooks) for the full event payload.

## Agent memory

The agent memory API lets your agent persist data between messages. This is how your agent maintains context across a multi-email conversation: customer preferences, ticket status, prior summaries, or any structured data your model needs.

Memory is scoped at three levels:

| Scope          | When to use                                                                   |
| -------------- | ----------------------------------------------------------------------------- |
| **Global**     | Preferences or configuration that apply across all of the agent's inboxes.    |
| **Per inbox**  | Memory shared across all threads in an inbox (for example, team settings).    |
| **Per thread** | Conversation context specific to one exchange. This is the most common scope. |

Memory is a JSON object with primitive values (`string`, `number`, `boolean`, `null`, or arrays of primitives). Limits vary by scope:

| Scope      | Memory | Summary | Max keys |
| ---------- | ------ | ------- | -------- |
| Global     | 8 KB   | 4 KB    | 50       |
| Per inbox  | 8 KB   | 4 KB    | 50       |
| Per thread | 4 KB   | 2 KB    | 20       |

The `summary` string is useful for a condensed narrative of the conversation that you pass to your model without replaying the full history.

### Get memory

```typescript theme={null}
// Thread-scoped memory (most common)
const stored = await nuntly.agents.getMemory('support-agent-v1', {
  inboxId: 'ibx_01kabn43yqyxn2bx4ve84mczd3',
  threadId: 'thr_01kabn43yqyxn2bx4ve84mczd3',
});

console.log('Stored memory:', stored.data.memory);
console.log('Summary:', stored.data.summary);
```

### Upsert memory

```typescript theme={null}
await nuntly.agents.upsertMemory('support-agent-v1', {
  inboxId: 'ibx_01kabn43yqyxn2bx4ve84mczd3',
  threadId: 'thr_01kabn43yqyxn2bx4ve84mczd3',
  memory: {
    status: 'awaiting-customer-reply',
    ticketId: 'TICKET-1234',
    priority: 'high',
  },
  summary: 'Customer reported a billing discrepancy. Escalated to billing team and awaiting confirmation.',
});
```

## Complete example with Claude

The following example shows a complete agent handler using the [Claude API](https://docs.anthropic.com/en/api). It receives the `message.agent.triggered` webhook, reads the email, loads stored conversation memory, generates a reply with Claude, sends the reply, and saves updated memory.

```typescript theme={null}
import Anthropic from '@anthropic-ai/sdk';
import { Nuntly } from '@nuntly/sdk';

const nuntly = new Nuntly({ apiKey: process.env.NUNTLY_API_KEY });
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

const AGENT_ID = 'support-agent-v1';

export async function handleAgentTriggered(event: {
  type: 'message.agent.triggered';
  data: { messageId: string; threadId: string; inboxId: string };
}) {
  const { messageId, threadId, inboxId } = event.data;

  // 1. Retrieve message metadata and content
  const [message, content] = await Promise.all([
    nuntly.messages.retrieve(messageId),
    nuntly.messages.getContent(messageId, { format: ['text'] }),
  ]);

  const bodyText = content.data.text?.downloadUrl
    ? await fetch(content.data.text.downloadUrl).then((r) => r.text())
    : '(no plain text body)';

  // 2. Load stored conversation memory for this thread
  const stored = await nuntly.agents.getMemory(AGENT_ID, { inboxId, threadId });
  const previousSummary = stored?.data?.summary ?? null;

  // 3. Build the prompt and call Claude
  const systemPrompt = [
    'You are a helpful customer support agent. Reply professionally and concisely.',
    previousSummary ? `Previous conversation summary: ${previousSummary}` : null,
  ]
    .filter(Boolean)
    .join('\n\n');

  const aiResponse = await anthropic.messages.create({
    model: 'claude-opus-4-5',
    max_tokens: 1024,
    system: systemPrompt,
    messages: [
      {
        role: 'user',
        content: `From: ${message.data.from.address}\nSubject: ${message.data.subject}\n\n${bodyText}`,
      },
    ],
  });

  const replyText = aiResponse.content[0].type === 'text' ? aiResponse.content[0].text : '';

  // 4. Send the reply, it is added to the same thread
  await nuntly.messages.reply(messageId, {
    text: replyText,
    html: `<p>${replyText.replace(/\n\n/g, '</p><p>').replace(/\n/g, '<br>')}</p>`,
  });

  // 5. Update conversation memory with a fresh summary
  const summaryResponse = await anthropic.messages.create({
    model: 'claude-haiku-4-5-20251001',
    max_tokens: 256,
    messages: [
      {
        role: 'user',
        content: `Summarize this support exchange in 2-3 sentences:\n\nCustomer: ${bodyText}\n\nAgent: ${replyText}`,
      },
    ],
  });

  const newSummary = summaryResponse.content[0].type === 'text' ? summaryResponse.content[0].text : '';

  await nuntly.agents.upsertMemory(AGENT_ID, {
    inboxId,
    threadId,
    memory: {
      lastMessageId: messageId,
      lastAction: 'replied',
      messageCount: (stored?.data?.memory?.messageCount ?? 0) + 1,
    },
    summary: newSummary,
  });
}
```

<Note>
  The summary is generated by a faster model to keep latency low. It replaces the previous summary on each turn, acting as a rolling memory window
  rather than a growing history.
</Note>

## Multi-agent platforms

You can run multiple specialized agents across different inboxes, each with its own `agentId` and memory. Use namespaces to group an agent's inboxes together.

For example, a platform with a support agent, a billing agent, and a sales agent might look like:

```typescript theme={null}
// Each agent has its own namespace for isolation
const supportNamespace = await nuntly.namespaces.create({ name: 'Support Agent', externalId: 'agent:support' });
const billingNamespace = await nuntly.namespaces.create({ name: 'Billing Agent', externalId: 'agent:billing' });

// Inboxes are scoped to the right namespace and agent
await nuntly.inboxes.create({
  domainId: 'your-domain-id',
  address: 'support',
  namespaceId: supportNamespace.data.id,
  agentId: 'support-agent-v1',
});

await nuntly.inboxes.create({
  domainId: 'your-domain-id',
  address: 'billing',
  namespaceId: billingNamespace.data.id,
  agentId: 'billing-agent-v1',
});
```

Each agent receives only its own `message.agent.triggered` events and manages its own memory independently.

## Next steps

<CardGroup cols={2}>
  <Card title="Receiving events" icon="webhook" href="/docs/guides/receiving-webhooks">
    See the full payload for agent-triggered events
  </Card>

  <Card title="Send, reply, and forward" icon="reply" href="/docs/guides/receiving-send-reply">
    Learn how agents can send replies from inboxes
  </Card>

  <Card title="Namespaces" icon="folder-tree" href="/docs/guides/receiving-namespaces">
    Organize agent inboxes with namespaces
  </Card>
</CardGroup>
