Summarize long email threads for agent context Nylas engineer demonstrates a technique for summarizing long email threads to fit within AI agent context windows, using the Nylas API to fetch threads, compress older messages into a brief, and feed only the brief plus the latest message to the model. The approach avoids token overflow while preserving key context like agreements and promises, with tradeoffs noted for detail loss. Most "AI email" demos hand the model the whole inbox and hope for the best. That works in a screenshot. It falls apart the first time a real conversation runs past a dozen messages, because every one of those messages — quoted replies, signatures, disclaimers, the recipient's entire previous email pasted underneath theirs — gets shoved into the prompt verbatim. A five-message sales thread can easily be 8,000 tokens of mostly-redundant text. A twenty-message support escalation is a context-window grenade. The naive fix is "just truncate." Keep the last message, drop the rest. That's worse: the agent loses the thing it actually needs, which is what was already agreed, asked, or promised earlier in the thread . The recipient says "yes, the 14th works" and your agent has no idea what the 14th refers to. The right move is summarization for context budget : fetch the full thread, compress the older turns into a running brief, and feed the model only brief + the latest message verbatim . The brief is a few hundred tokens. The latest message is whatever it is. You stay well inside budget and the agent keeps the plot. I work on the Nylas CLI, so the terminal commands below are the exact ones I reach for when I'm poking at a live Agent Account. I'll show every step both ways — the raw HTTP call and the CLI equivalent — because that's how I actually debug this stuff. The data plane here is nothing exotic. An Agent Account is just a Nylas grant with a grant id , and it works with every grant-scoped endpoint — Messages, Threads, Drafts, the lot. There's nothing new to learn on the API side. If you've used the Threads and Messages endpoints before, you already know the whole surface area. The flow is four steps: thread id . message ids array — that's your list of turns.The summarization itself is your code calling your model. Nylas doesn't summarize anything for you, and it shouldn't — the brief is application state, and where you keep it is your call. A few concrete reasons, beyond "it's smaller": The tradeoff, stated honestly: summaries lose detail. If your workflow needs an exact quote from message three, a summary won't have it — so keep the last few messages verbatim and accept that anything older is lossy. For most conversational agents that's the correct tradeoff. For a legal-discovery bot, it isn't; keep everything and pay for the tokens. You need an Agent Account. If you don't have one, provision it on a domain you've registered with Nylas a custom domain or a .nylas.email trial subdomain . Over HTTP, that's a POST /v3/connect/custom with the nylas provider: curl --request POST \ --url "https://api.us.nylas.com/v3/connect/custom" \ --header "Authorization: Bearer $NYLAS API KEY" \ --header "Content-Type: application/json" \ --data '{ "provider": "nylas", "settings": { "email": "support@yourcompany.com" }, "name": "Support Agent" }' From the CLI: nylas agent account create support@yourcompany.com --name "Support Agent" That returns a grant. The API auto-creates a default workspace and policy alongside it, so there's nothing else to wire up to start reading mail. Hold onto the grant id — every call below uses it. Full details: https://developer.nylas.com/docs/v3/agent-accounts/ https://developer.nylas.com/docs/v3/agent-accounts/ . You also need a message.created webhook subscribed at the app level so you know when new mail lands. Webhooks on Nylas are application-scoped, not grant-scoped — you subscribe once with POST /v3/webhooks , and events for every grant arrive at that one endpoint, each payload carrying a grant id you filter on. There's no per-account webhook to manage. Over HTTP: curl --request POST \ --url "https://api.us.nylas.com/v3/webhooks" \ --header "Authorization: Bearer $NYLAS API KEY" \ --header "Content-Type: application/json" \ --data '{ "trigger types": "message.created" , "webhook url": "https://yourapp.com/webhooks/nylas" }' Or from the CLI: nylas webhook create \ --url https://yourapp.com/webhooks/nylas \ --triggers message.created One thing to internalize before we go further: don't rely on the webhook payload for the body. Fetch the full message with GET /v3/grants/{grant id}/messages/{message id} when you need the real content, and branch on message.created.truncated — that's the event type you get when a message is large enough that Nylas doesn't inline it. For a summarization pipeline this matters double, because the long threads you most want to summarize are exactly the ones with bodies big enough to get truncated. When message.created fires, the payload includes a thread id . That's your entry point. Fetch the thread and you get back a message ids array — the ordered list of every message in the conversation. That array is how you enumerate the turns. Over HTTP: curl --request GET \ --url "https://api.us.nylas.com/v3/grants/$GRANT ID/threads/$THREAD ID" \ --header "Authorization: Bearer $NYLAS API KEY" The response includes the fields you care about for budgeting: { "request id": "...", "data": { "id": "thread-abc123", "subject": "Re: Onboarding question about SSO", "message ids": "msg-001", "msg-002", "msg-003", "msg-004", "msg-005" , "snippet": "Thanks, that worked. One more thing about...", "participants": { "email": "alice@customer.com", "name": "Alice Chen" }, { "email": "support@yourcompany.com" } , "latest message received date": 1718900000 } } From the CLI, the same fetch — pass --json so you get the structured object and not the pretty table: nylas email threads show "$THREAD ID" --json message ids.length is your first budget signal. Five messages? Just feed them all verbatim, summarization is overkill. Twenty-five? You're summarizing. I usually branch on a threshold — under roughly 6 messages, send everything; over it, compress. The thread gives you ids, not bodies. To reconstruct what was actually said, fetch each message. Don't try to shortcut this with the thread snippet — that's a 100-character preview, useless for summarizing meaning. Over HTTP, one call per id: curl --request GET \ --url "https://api.us.nylas.com/v3/grants/$GRANT ID/messages/msg-003" \ --header "Authorization: Bearer $NYLAS API KEY" The message object gives you from , to , subject , body , and the date field a Unix timestamp . That date is what you sort on to get chronological order — don't assume message ids is always perfectly time-ordered across providers; sort by date to be safe. From the CLI: nylas email read msg-003 --json If you want the unprocessed body without HTML-to-text munging, nylas email read takes a --raw flag, and --headers if you need to inspect threading headers directly. For summarization I usually take the default processed body — it's already closer to plain text, which is what the model wants. In code, you fan these out in parallel. Here's the shape of it in Node: js // thread.data.message ids came from the threads.show call above. const messages = await Promise.all thread.data.messageIds.map id = nylas.messages.find { identifier: GRANT ID, messageId: id } , , ; // Sort chronologically on the date field, then normalize into turns. const turns = messages .map m = m.data .sort a, b = a.date - b.date .map m = { role: m.from 0 .email === AGENT EMAIL ? "agent" : "contact", date: m.date, body: stripQuotedText m.body , // drop the quoted-reply tail } ; That stripQuotedText step is worth a sentence. Email clients append the previous message under the new one. Before you summarize, trim everything after the first On