# How to handle email replies in an agent loop

Source: https://developer.nylas.com/docs/cookbook/agent-accounts/handle-replies/

The agent sends an email. Hours later, the recipient replies. If the agent doesn't connect that reply to the original conversation, it either ignores it or treats it as a brand-new message — both wrong.

This recipe detects replies via webhook, pulls the thread context, and routes the reply into the right part of your agent's logic. It assumes you have an Agent Account with a `message.created` webhook already subscribed. If not, start with [Give your agent its own email](/docs/v3/getting-started/agent-own-email/).

## Detect the reply

Every `message.created` webhook payload includes a `thread_id`. If your agent sent the original outbound message, that thread already exists in your state. The check is straightforward: look up the thread ID in your mapping.

```js
// Node.js / Express handler
app.post("/webhooks/nylas", async (req, res) => {
  // Verify X-Nylas-Signature here -- see /docs/v3/notifications/.
  res.status(200).end();

  const event = req.body;
  if (event.type !== "message.created") return;

  const msg = event.data.object;
  if (msg.grant_id !== AGENT_GRANT_ID) return;

  // Is this a reply to a conversation we're tracking?
  const context = await db.getThreadContext(msg.thread_id);

  if (context) {
    // Reply to an active conversation.
    await handleReply(msg, context);
  } else {
    // New inbound message -- triage it.
    await handleNewMessage(msg);
  }
});
```

The `thread_id` approach works because Nylas groups messages by their `In-Reply-To` and `References` headers. When the recipient's mail client sets those headers (which it always does on a reply), Nylas adds the inbound message to the same thread as the original outbound.

You don't need to parse `In-Reply-To` headers yourself. The Threads API already did the work.

## Fetch the conversation history

Before the agent decides how to respond, it needs to know what was said. Fetch the full thread and the latest message body.

```js
async function handleReply(msg, context) {
  // Get the full message body (the webhook only carries summary fields).
  const fullMessage = await nylas.messages.find({
    identifier: AGENT_GRANT_ID,
    messageId: msg.id,
  });

  // Get the thread for the full conversation chain.
  const thread = await nylas.threads.find({
    identifier: AGENT_GRANT_ID,
    threadId: msg.thread_id,
  });

  // Build the conversation history the agent needs.
  const history = await buildConversationHistory(thread.data.messageIds);

  // Route based on the agent's internal state.
  await routeReply(fullMessage.data, history, context);
}
```

Fetching the full thread gives the agent conversation context -- what it said, what the recipient said back, how many exchanges have happened. An LLM deciding how to reply to "sounds good, let's do Thursday" needs to know what was proposed.

## Route based on agent state

The context you stored when the agent sent the original message tells you where in the workflow this reply lands. Different states need different handling.

```js
async function routeReply(message, history, context) {
  switch (context.step) {
    case "awaiting_confirmation":
      // The agent proposed something and is waiting for a yes/no.
      await handleConfirmation(message, history, context);
      break;

    case "awaiting_info":
      // The agent asked a question and needs the answer.
      await handleInfoResponse(message, history, context);
      break;

    case "closed":
      // The conversation was resolved but the person wrote back.
      await handleReopenedThread(message, history, context);
      break;

    default:
      // Unknown state -- log and escalate.
      await escalateToHuman(message, context);
  }
}
```

## Reply in-thread

When the agent responds, pass `reply_to_message_id` so the reply threads correctly in the recipient's mail client.

```js
async function sendReply(originalMessage, body, context) {
  const sent = await nylas.messages.send({
    identifier: AGENT_GRANT_ID,
    requestBody: {
      replyToMessageId: originalMessage.id,
      to: originalMessage.from,
      subject: `Re: ${originalMessage.subject}`,
      body: body,
    },
  });

  // Update the conversation state.
  await db.updateThreadContext(originalMessage.threadId, {
    ...context,
    step: "awaiting_reply",
    lastSentAt: Date.now(),
    lastSentMessageId: sent.data.id,
  });
}
```

By passing `reply_to_message_id`, Nylas sets the `In-Reply-To` and `References` headers on the outbound message. The recipient sees a threaded reply, not a disconnected new email.

## Things to know

- **The webhook fires for outbound too.** When the agent sends a reply via the API, `message.created` fires for that sent message as well. Filter on `msg.from` at the top of the handler to avoid the agent responding to its own messages.
- **Multiple replies can arrive on the same thread.** The recipient might send two quick messages, or two people in a CC'd thread might both reply. Process each one independently and check whether the agent has already responded since the last inbound.
- **Fetch the body — don't rely on the webhook payload.** The `message.created` webhook carries summary fields like `subject`, `from`, and `snippet`. For the full body, always fetch the message from the API. If the body exceeds ~1 MB, the webhook type becomes `message.created.truncated` and the body is omitted entirely.
- **Consider a cooldown before replying.** In fast-moving threads, the recipient might send a correction seconds after their first reply. A short delay (30–60 seconds) before the agent responds lets you batch consecutive messages into one response instead of replying to each one individually.
- **Don't ship a reply loop without dedup.** Webhook redelivery and concurrent workers will both re-trigger your handler. See the duplicate-replies recipe linked below.

## Next steps

- [Build a multi-turn email conversation](/docs/cookbook/agent-accounts/multi-turn-conversations/) — the full state machine for conversations that span days
- [Prevent duplicate agent replies](/docs/cookbook/agent-accounts/prevent-duplicate-replies/) — dedup patterns for high-volume agent inboxes
- [Migrate from transactional email](/docs/cookbook/agent-accounts/migrate-from-transactional-email/) — context if you're moving from SendGrid/Resend/Postmark
- [Email threading for agents](/docs/v3/agent-accounts/email-threading/) — how Message-ID, In-Reply-To, and References headers work
- [Using webhooks with Nylas](/docs/v3/notifications/) — signature verification, retries, and delivery guarantees