← All builds
n8n workflow · Live

Mail reply loop: classify inbound mail, write status back, draft the reply

How it worksLive, every 6 hours

Every six hours it reads the inbox, classifies each new message as rejection, interview, acknowledgement or unrelated, matches it to the open application, updates the board, and drafts a gracious reply to rejections. Nothing is ever sent by the workflow.

How it works

The hardest part of a reply loop is not classification. It is never processing the same message twice, and never missing one, across runs that can fail halfway.

This one uses a watermark rather than unread status. An n8n data table holds the timestamp of the last processed message and the ids of everything that shared that exact second, because the mail API has no sub-second precision. Each run reads mail since the watermark, drops anything at or before it, and only advances the watermark after classification and matching succeeded. A run that dies halfway replays rather than skips.

Classification is one JSON-mode call. German rejections are deliberately gentle, so the prompt names the phrases: leider, für einen anderen Kandidaten entschieden, in unserem Talentpool aufnehmen. A request to consent to data retention is an acknowledgement, not a rejection, unless the message also says the application was unsuccessful.

Matching is fuzzy on purpose. The board and the sender rarely spell a company the same way, so it normalises both, allows prefix matches, tries the sender's domain, ignores applications found after the mail arrived, and breaks ties on role words in the subject. If it is still ambiguous, it does not guess.

Key features

  • Watermark-based idempotency stored in an n8n data table, independent of read status or schedule
  • Watermark advances only after the run's work succeeded, so failures replay instead of skip
  • Four-way classification tuned for how German companies actually phrase a no
  • Fuzzy company matching with a date guard and role-word tiebreak; ambiguous matches are left alone
  • Rejections get a drafted reply aimed at the next opening, under 90 words, in the language of the original
  • A sweep mode to replay a backlog from any date

Step by step

  1. Mail PollEvery six hours.
  2. Load Mail Watermark and Mail WindowRead the last processed timestamp and turn it into this run's window.
  3. Get Open Applications and Get Recent MailThe board, and inbox mail since the window, read and unread.
  4. New Mail OnlyDrop anything at or before the watermark, separating same-second messages by id.
  5. Classify ReplyGPT-4.1, JSON mode, temperature 0.
  6. Match ApplicationCompany, domain, date and role tokens against open applications.
  7. Update and draftStatus and response date written to Notion; rejections get a reply drafted in Outlook.
  8. Compute and Save Mail WatermarkOnly now does the watermark move.

The row that read as undefined

Notion rows arrive keyed property_snake_case, not with the property name as written in Notion. Reading Status directly returned undefined and silently dropped every row, which cost 347 messages their status write-back on one run.

Match Application now goes through a helper that accepts either shape, and the same helper went into the outreach workflow the same day.

Want to talk through this one?

Happy to walk through the design, the trade-offs, and what I would do differently.