Handling Email Replies in an Agent Loop
You built the outbound half of an email agent. It sends a well-crafted message, the recipient writes back six hours later... and your agent has no idea. The reply either gets ignored or — arguably worse — gets treated as a brand-new conversation, and the agent reintroduces itself to someone it emailed yesterday. That gap between "can send" and "can converse" is where most email agents stall. Closing it takes four pieces: detection, context, routing, and a threaded response. Here's each one, using a Nylas Agent Account (in beta) as the mailbox — a hosted address the agent owns outright. Step 1: know a reply when you see one Every message.created webhook payload carries a thread_id . If the agent sent the original message, that thread already exists in your state store. So detection is a lookup, not a parsing exercise: app . post ( " /webhooks/nylas " , async ( req , res ) => { // Verify X-Nylas-Signature here. 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 ; const context = await db . getThreadContext ( msg . thread_id ); if ( context ) { await handleReply ( msg , context ); // active conversation } else { await handleNewMessage ( msg ); // fresh inbound — triage it } }); Why does this work without touching a single header? Because the threading already happened upstream: messages get grouped by their In-Reply-To and References headers, which every mail client sets on a reply. You never parse them yourself — the Threads API did the work. Step 2: pull the full conversation The webhook payload is a summary — subject , from , snippet . Before an LLM decides how to answer "sounds good, let's do Thursday," it needs to know what was proposed. Fetch the full message body and the thread: const fullMessage = await nylas . messages . find ({ identifier : AGENT_GRANT_ID , messageId : msg . id , }); const thread = await nylas . thread