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 shows how to detect replies via webhook, pull thread context, and route the reply to 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.
What you’ll build
Section titled “What you’ll build”- Receive the
message.createdwebhook when a reply arrives. - Distinguish replies from new conversations using
thread_id. - Fetch the thread to reconstruct what the agent last said.
- Route the reply to the right handler based on your agent’s internal state.
- Reply in-thread so the conversation stays coherent.
Detect the reply
Section titled “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.
// Node.js / Express handlerapp.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
Section titled “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.
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
Section titled “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.
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
Section titled “Reply in-thread”When the agent responds, pass reply_to_message_id so the reply threads correctly in the recipient’s mail client.
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.
Keep in mind
Section titled “Keep in mind”- The webhook fires for outbound too. When the agent sends a reply via the API,
message.createdfires for that sent message as well. Filter onmsg.fromto 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.createdwebhook carries summary fields likesubject,from,snippet. For the full body, always fetch the message from the API. If the body exceeds ~1 MB, the webhook type becomesmessage.created.truncatedand 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.
What’s next
Section titled “What’s next”- Build a multi-turn email conversation — the full state machine for conversations that span days
- Email threading for agents — how Message-ID, In-Reply-To, and References headers work under the hood
- Prevent duplicate agent replies — dedup patterns for high-volume agent inboxes
- Using webhooks with Nylas — signature verification, retries, and delivery guarantees