close

DEV Community

Qasim
Qasim

Posted on

Organize email with threads in the Nylas API

Nobody thinks about their inbox as a pile of individual emails. They think in conversations: the back-and-forth about Wednesday's dinner, the thread with a client about an invoice. A thread is that conversation, the group of related messages that belong together, and treating it as one object is what makes an inbox feel like an inbox instead of a flat list of disconnected emails. Nylas exposes threads as a first-class resource, so you can list, read, and organize a whole conversation in a single call. This post builds the conversation view with the Email API and the CLI.

It's a worked use case rather than an endpoint tour, covering threads from two angles: the HTTP API your backend calls and the nylas CLI for browsing conversations from the terminal. I work on the CLI, so the commands below are the ones I reach for when I want to see a conversation at a glance.

A thread is a conversation, not a message

A thread groups the messages that form one conversation, and the link between them is the thread's message_ids array, the list of every message that belongs to it. Where a message is a single email, a thread is the whole exchange, so the subject, the participants, and the read state describe the conversation as a unit rather than any one reply in it. That's the mental model the API is built around.

The thread object gives you the conversation's summary without making you fetch every message in it. A single thread carries the subject, a snippet of the latest content, the participants across the conversation, has_attachments and has_drafts flags, an unread and starred state for the whole thread, and three timestamps, earliest_message_date, latest_message_received_date, and latest_message_sent_date. It also includes latest_draft_or_message, the full most-recent message inline, so a conversation list can show the latest activity without a second request.

Threads versus messages: which one to use

The distinction that matters for your code is that threads are for reading and organizing, while messages are for sending. You list threads to render a conversation view, you read a thread to see the exchange, and you organize a thread to act on the whole conversation, but you never create a thread directly. There's no "create thread" endpoint, because a thread isn't something you make; it forms automatically as messages reply to each other.

So the division of labor is clean. Display and organization happen at the thread level: the inbox list, the unread badges, the archive action. Composing happens at the message level: you send and reply with the messages endpoint, and the provider threads your reply into the right conversation based on its headers. When you want to show a conversation, reach for threads; when you want to send into one, reach for messages. Mixing that up, trying to "send a thread," is the most common conceptual stumble with this part of the API.

List conversations

Rendering a conversation view is a GET /v3/grants/{grant_id}/threads request, which returns threads with all their summary fields, enough to build an inbox list without touching the messages endpoint. Each thread comes back with its subject, snippet, participants, unread and starred state, and the inline latest_draft_or_message, so one call populates a whole screen of conversations.

curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/threads?limit=20&unread=true" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"
Enter fullscreen mode Exit fullscreen mode

The same standard filters that work on messages work here: unread, starred, subject, from, in for a folder, and the date filters, so you can list, say, every unread conversation in the inbox. The thread-specific date filters are worth knowing too: latest_message_after and latest_message_before filter by when the conversation last saw activity, which is exactly how you'd sort or page a "most recent conversations" view. Each returned thread is the conversation summarized, ready to render as a row.

Browse conversations from the CLI

The terminal command nylas email threads list shows recent conversations, and it's the quick way to see what's active without building anything. It defaults to 10 threads and takes --unread, --starred, and --subject to narrow the view, plus --limit to fetch more, mapping directly onto the API's filters.

# Recent unread conversations
nylas email threads list --unread

# A specific conversation's full detail
nylas email threads show <thread-id>
Enter fullscreen mode Exit fullscreen mode

Where list gives you the conversation summaries, nylas email threads show <thread-id> prints one thread's detail, its subject, participants, message count, the earliest and latest dates, a snippet, and the list of message_ids (plus a draft count and status when present), so you can see what the conversation holds at a glance in the terminal. This is genuinely useful for triage, scanning unread conversations and opening the one that matters, and it's how I check what a thread actually contains before wiring thread handling into an app. The CLI mirrors the API's read-and-organize model: it lists, shows, marks, and deletes threads, but it doesn't send into them; to reply into a conversation you use nylas email send --reply-to <message-id>, which threads onto the message you're answering.

Organize a whole conversation in one call

Here's where threads earn their place: you can act on an entire conversation at once. A PUT /v3/grants/{grant_id}/threads/{thread_id} request updates the whole thread, so setting unread to false marks every message in the conversation read, starred stars the conversation, and a folders array moves all of its messages to a folder in a single operation.

curl --request PUT \
  --url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/threads/<THREAD_ID>" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{ "unread": false, "starred": true }'
Enter fullscreen mode Exit fullscreen mode

This is the operation that would be tedious at the message level. Marking a ten-message conversation read by hand means ten message updates; at the thread level it's one PUT. The same goes for moving a conversation to an archive folder or starring it for follow-up, you act on the thread and every message inside it comes along. From the CLI, nylas email threads mark <thread-id> --read and --star do the same whole-conversation update, which is the natural way to clear or flag an entire exchange.

Deletion works the same way, on the whole conversation. A DELETE on the thread moves the entire exchange, every message included, to the Trash rather than erasing it outright, and nylas email threads delete <thread-id> does it from the terminal, so a "delete conversation" action is one call rather than a loop over every message. As with everything at the thread level, the operation fans out to all the messages the thread holds, which is the point: the conversation is the unit you act on.

Filter to the conversations that matter

A conversation view is only as good as its filtering, and threads support the filters that map onto real inbox actions. Beyond unread and starred for the obvious views, you filter by participant with from or any_email to find every conversation involving a particular person, by in to scope to a folder, and by has_attachment to surface conversations carrying files. Each narrows the thread list to a meaningful slice.

For ordering and paging a conversation view, the date filters are the ones you reach for. latest_message_after and latest_message_before bound the list by the conversation's most recent activity, which is how a "recent conversations" screen stays current, and they pair with limit and cursor paging to walk a long inbox. Because the thread already carries latest_message_received_date, you have the value to sort on without opening any message. One provider caveat: Microsoft, IMAP, iCloud, Yahoo, and EWS return threads ordered reverse-chronologically by latest message received, but Google doesn't guarantee that ordering because of a Gmail API limitation, though setting the in folder filter raises the odds of it noticeably.

How replies keep a conversation together

Since you can't create a thread directly, it's worth knowing how messages end up in the same one. When you send a reply with the messages endpoint, the provider groups it into the existing conversation using the message's headers, the references that tie a reply to what it answers. Nylas surfaces the result as the thread's growing message_ids array, so the conversation you see reflects the provider's own threading.

The practical upshot is that you reply at the message level and trust the threading to happen. To continue a conversation, you send a message in reply to one already in the thread, and it lands in the same thread rather than starting a new one. This is why the API has no "add to thread" operation: threading is a property of how messages reference each other, not something you set, so your job is to send the reply correctly and read the updated thread afterward. Get the reply headers right and the conversation stays whole.

Where threads fit

The same conversation model sits under a range of email features, and the thread is almost always the right unit for the UI. A few that map straight on:

  • A conversation inbox. Render threads, not messages, so the list shows conversations with a subject, snippet, and participant set, the way every modern mail client does.
  • Bulk triage. Mark a whole conversation read or move it to a folder in one PUT, so clearing an inbox is per-conversation rather than per-message.
  • Follow-up flagging. Star a thread for follow-up and filter by starred to build a "needs reply" view across conversations.
  • Activity sorting. Sort and page on latest_message_received_date for a recency-ordered conversation list without opening a single message, a guaranteed ordering on every provider except Google.

Each treats the thread as the conversation unit, with the messages endpoint reserved for composing and the thread endpoint for everything you display and organize.

Things to keep in mind

A short list of details keeps thread handling predictable.

  • Threads are read and organize, not create. There's no create-thread endpoint; conversations form as messages reply to each other.
  • Organize at the thread level. A single PUT marks every message read, stars the conversation, or moves all its messages to a folder, which beats updating messages one by one.
  • The thread carries the summary. Subject, snippet, participants, and latest_draft_or_message come back in the list call, so a conversation view needs no per-message fetch.
  • Send with messages, display with threads. Compose and reply through the messages endpoint; use threads for the conversation view and organization.
  • Sort on latest_message_received_date. The thread already includes it, so a recency-ordered list needs no extra request.
  • Replies thread by headers. A correctly addressed reply lands in the existing conversation; there's no manual "add to thread."

Wrapping up

Threads are how you build an inbox that works in conversations instead of loose emails. List them with GET /v3/grants/{grant_id}/threads to render a conversation view complete with subject, snippet, participants, and the latest message, and organize a whole exchange with a single PUT that marks it read, stars it, or moves all its messages at once. From the terminal, nylas email threads list and mark do the same. Remember the division of labor: you read and organize at the thread level and send at the message level, and conversations form themselves as replies reference what came before.

Where to go next:

Top comments (0)