Clean quoted replies from email with the Nylas API

작성자

카테고리:

← 피드로
DEV Community · Qasim · 2026-07-21 개발(SW)

Open a five-message reply chain and look at how much of it is actually new. One or two sentences at the top, then a wall of quoted history, a signature, a legal disclaimer, and “Best regards.” For a human skimming, that noise is just clutter. For code, it’s worse: feed that blob to a language model for summarization and you’re paying for tokens of quoted text the model has already seen, and showing it as a preview means the same quoted chain on every message. Cleaning email pulls out just the meaningful text, and this post wires it up with the Email API and the CLI.

It’s a worked use case rather than an endpoint tour, covering the clean conversation endpoint and the cleaned-message webhook from two angles: the HTTP API your backend calls and the nylas CLI for cleaning a message from the terminal. I work on the CLI, so the commands below are the ones I reach for when I want to see what cleaning actually returns.

Two ways to clean a message

There are two paths, and which you pick depends on whether you have a message in hand or want every incoming message cleaned automatically. The on-demand path is a single endpoint: you pass one or more message IDs and get back the cleaned text, which suits a “summarize this thread” button or a one-off processing job. The automatic path is a webhook, message.created.cleaned, that fires a cleaned version of every new message once you enable the Clean Conversations feature for your application.

The distinction matters for how you build. On-demand cleaning is synchronous and you control when it runs, so it’s the natural fit when a user acts on a specific message or you’re backfilling a batch. The webhook is push-based and hands you cleaned content the moment mail arrives, which is what you want when every message flows into a pipeline that needs the text stripped before anything downstream touches it. Many apps use both: the webhook for the steady stream, the endpoint for on-demand reprocessing.

Clean a message on demand

The on-demand endpoint is a PUT /v3/grants/{grant_id}/messages/clean with a message_id array of the messages to clean, up to 20 in a single call. The response returns each message with a conversation field holding the cleaned text, the quoted chain and signature removed, alongside the original message data so you keep the metadata.

curl --request PUT \
  --url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages/clean" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "message_id": ["18df98cadcc8534a"],
    "ignore_links": true,
    "remove_conclusion_phrases": true
  }'

Enter fullscreen mode Exit fullscreen mode

The cleaned result lands in conversation, so that’s the field you read, not body, which still carries the original. Batching up to 20 IDs per call is the detail that makes this practical for a job: cleaning a thread or a day’s worth of messages is a handful of requests, not one per message. Each cleaned message comes back keyed to its ID, so you can match the output to the input when you send several at once.

The operation is non-destructive, which is worth saying plainly: cleaning returns a parsed copy and never modifies the stored message. The original stays intact in the mailbox and in the body field, so you can clean the same message repeatedly with different options, once with links stripped for a model and again with links kept for display, without any risk of losing the source. That makes it safe to call freely in a pipeline, and safe to reprocess later if you change which options you want, since there’s no state to undo. Treat the cleaned conversation as a derived view you can regenerate any time, not a one-way transformation of the message.

Clean a message from the CLI

The terminal command is nylas email clean followed by one or more message IDs, and it does the same parse, returning just the meaningful text with quoted chains, signatures, and conclusion phrases like “Best” and “Regards” removed. By default it strips links, images, tables, and those signature phrases, and the plain-text output has HTML tags removed so it’s readable at a glance.

# Clean one message; keep links in the output
nylas email clean <message-id> --keep-links

# JSON output with the raw cleaned HTML in the "conversation" field
nylas email clean <id-1> <id-2> --json

Enter fullscreen mode Exit fullscreen mode

The --keep-* flags, --keep-links, --keep-images, --keep-tables, and --keep-signatures, turn off individual stripping when you want something retained, and --images-as-markdown returns images as Markdown links instead. Like the endpoint, it cleans up to 20 messages in one call, and --json gives you the raw cleaned HTML in the conversation field for piping into a script. This is the fastest way to see what cleaning does to a real message before you wire the endpoint into anything.

What gets stripped, and how to keep it

Cleaning is opinionated by default, and knowing the defaults saves surprises. The endpoint’s boolean options, ignore_links, ignore_images, ignore_tables, and remove_conclusion_phrases, all default to true, so out of the box a clean strips links, images, tables, and sign-off phrases along with the quoted history. That’s the right default for feeding text to a model, where none of that markup helps, but it’s aggressive if you’re cleaning for display.

To retain something, set its option to false. If you’re cleaning a message to show a readable preview and you want the links live, send ignore_links: false, and the anchor tags survive. The mapping to the CLI is the --keep-* flags, which flip the same switches from the terminal. The thing to internalize is that the defaults remove more than just the quoted chain, so when a cleaned result is missing something you expected, the fix is usually a false on one of these options rather than anything more involved.

One honest caveat: cleaning is heuristic. It’s very good at the common signature and quote patterns, but an unusual layout can leave a stray fragment or trim a line you wanted, so spot-check the output on real mail before you trust it in a pipeline. It’s reliable enough to build on, just not so perfect that you should skip eyeballing a sample first.

Get Markdown out for a language model

Since the most common reason to clean a message is to feed it to a model, the API can hand you Markdown directly. Setting images_as_markdown to true, which is itself the default, converts images to Markdown image syntax, and the beta html_as_markdown option converts the whole message to Markdown rather than returning HTML or plain text. One constraint ties them together: html_as_markdown can’t be true while images_as_markdown is false, since converting the document to Markdown but leaving images as raw HTML would be inconsistent.

Markdown is the format most language models handle best, so converting at the cleaning step means the text is model-ready without a second pass through a converter of your own. The plain-text path is still there when you want it, the CLI strips HTML tags from its default output, but for a retrieval or summarization pipeline, asking for Markdown at clean time keeps the formatting cues a model can actually use, like headings and lists, while dropping the HTML scaffolding it can’t.

Clean every message automatically with a webhook

When you want cleaning to happen to every message without calling the endpoint each time, enable Clean Conversations for your application and subscribe to the message.created.cleaned webhook. It fires for each new synced message and delivers the cleaned content in the message body as Markdown, so your pipeline receives stripped text the moment mail lands, with no extra call on your side.

Two behaviors are worth knowing before you rely on it. First, subscribing to message.created.cleaned does not suppress the regular message.created notification, so if you subscribe to both you get two separate webhooks per new message, the raw one and the cleaned one. Subscribe only to the cleaned trigger if the cleaned text is all your pipeline needs. Second, each cleaned notification carries a cleaning_status field, and when it comes back as failed, the body holds the original uncleaned HTML rather than cleaned text, with a cleaning_error describing what went wrong. So branch on cleaning_status before you assume the body is clean.

app.post("/webhooks/nylas", async (req, res) => {
  res.sendStatus(200); // acknowledge fast
  const { type, data } = req.body;
  if (type !== "message.created.cleaned") return;
  const msg = data.object;
  if (msg.cleaning_status !== "success") return; // body is raw HTML on failure
  await indexForRetrieval(msg.id, msg.body); // cleaned markdown
});

Enter fullscreen mode Exit fullscreen mode

Where cleaning pays off

The same cleaning step sits under a range of features, and the reason is always the same: downstream code wants the new message, not the thread’s whole history. A few that map straight on:

  • Feeding email to an LLM. Summarization, classification, and retrieval all work better and cost less on cleaned text, since the quoted chain is repeated context the model doesn’t need and you’d otherwise pay to process.
  • Readable previews. A message list that shows the actual new content instead of “On Tuesday, X wrote:” reads far better, and cleaning with links kept gives you that.
  • Reply extraction. Pulling just the latest reply from a long back-and-forth, for a support tool or a CRM note, is exactly what removing the quoted chain does.
  • Search indexing. Indexing cleaned bodies keeps your search index free of duplicated quoted text, so a query doesn’t match the same sentence echoed across ten replies.

Each is the same clean call with different options, the difference being whether you keep links and images for display or strip everything for a model.

Things to keep in mind

A short list of details keeps cleaning predictable.

  • Read the conversation field. The on-demand endpoint returns cleaned text in conversation, while body still holds the original message.
  • Defaults strip aggressively. ignore_links, ignore_images, ignore_tables, and remove_conclusion_phrases all default to true; set one to false or use a --keep-* flag to retain it.
  • Twenty messages per call. Both the endpoint and the CLI clean up to 20 IDs at once, so batch a thread rather than looping one at a time.
  • The cleaned webhook doesn’t replace message.created. Subscribe to both and you get two notifications per message; subscribe only to .cleaned if that’s all you need.
  • Check cleaning_status. On a failed status the webhook body is the original uncleaned HTML, so branch on it before treating the body as clean.
  • Ask for Markdown for models. html_as_markdown returns model-ready Markdown, and it can’t be true when images_as_markdown is false.

Wrapping up

Cleaning turns a noisy reply chain into just the text that matters. Call PUT /v3/grants/{grant_id}/messages/clean with up to 20 message IDs, or run nylas email clean, and read the cleaned result from the conversation field, remembering the defaults strip links, images, tables, and sign-off phrases unless you keep them. For a steady stream, enable Clean Conversations and take the message.created.cleaned webhook, branching on cleaning_status and knowing it arrives alongside the raw message.created, not instead of it. Ask for Markdown when the destination is a language model, and you’ve got message text that’s ready to summarize, index, or display.

Where to go next:

AI-answer pages for agents

When this post is published, link AI agents and crawlers to the retrieval-ready version on cli.nylas.com:

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다