Automated product newsletter with n8n + Google Sheets

Hey everyone,

I wanted to share a workflow I built for anyone who wants to consistently promote products to their audience via email — think affiliate marketing, digital products, or anything you want to put in front of your subscribers regularly.

The idea is simple: instead of manually deciding what to send and when, you just maintain a Google Sheet with your products and send dates. The workflow takes care of the rest automatically.

What it does:

Every morning at 8:00, it:

  • Reads the sheet and picks what’s scheduled for today
  • Builds a clean HTML email with product cards
  • Sends it to your subscriber list via Gmail

The setup:

Two tabs in Google Sheets:

  • newsletter_products — product name, description, image URL, link, send date
  • newsletter_recipients — email addresses

In n8n: Schedule Trigger → Config node (paste your Sheet ID here) → Read + Filter by today → IF node (nothing today? stops cleanly) → Build HTML → Send via Gmail

I’m curious: what features would you add to something like this? I’m thinking about things like A/B testing subject lines or smarter scheduling — but would love to hear what you’d find useful.

1 Like

Nice workflow — this is a solid foundation. A few things I’d add or watch out for:

**1. Gmail sending limits will bite you fast**

Gmail caps at ~500 emails/day (personal) or 2,000/day (Workspace). If your recipients list grows, you will hit the ceiling quickly. Worth switching the send step to a transactional API (Resend, Postmark, or the Brevo node) early — they handle deliverability, bounces, and unsubscribes automatically.

**2. Unsubscribe handling is a legal requirement, not optional**

A Sheets-based recipients list has no unsubscribe mechanism. If anyone clicks spam, Gmail flags your account. At minimum add a second sheet tab for unsubscribes and filter them before sending. Better: use a proper ESP (Brevo has a free tier with native n8n integration) so unsubscribes are handled automatically.

**3. Add an error branch on the IF node**

If the Sheets read fails or returns malformed data, the workflow will silently error. An error output branch that sends a Slack or email alert means you find out before a send day gets missed.

**On the A/B subject line idea:** cleanest n8n implementation is Split In Batches to divide your list in half, two send branches with different subject lines, then a Wait node + Sheets append to log opens if your ESP supports webhooks back.

What ESP are you planning to move to once the list grows?

1 Like

Nice clean pattern. One edge case pirateprentice didn’t mention: duplicate send protection. If the schedule trigger fires twice in a row (server restart, cron overlap), the same row can be processed twice and subscribers get the same email twice. Adding a “sent_date” column to your Sheet and filtering on sent_date IS EMPTY before building the emails - then writing the date back after a successful send - makes the workflow idempotent and safe to retry.

Nice. Before A/B testing, I’d add a dry run and send ledger: render today’s email, show recipient count + product IDs, then write back Gmail/ESP message id, skipped unsubscribes, and errors after send.

Email workflows fail loudly when the API errors. The scary failure is a valid email sent to the wrong list.

The dry run Ahmad described – render the email, verify recipient count, write the send ledger – covers execution failures: API errors, wrong list, partial sends. There is a separate class of failure that none of the suggestions here address: the featured product content being wrong at send time.

If the product URL returns 404 (taken down, URL changed) or the page shows a different price than the Sheet record, the email goes out with a dead link or incorrect pricing. pirateprentice’s move to a transactional ESP helps with deliverability. This is about what lands in the inbox.

The pattern: after the Sheets read step that selects today’s featured product row, and before the email body builder, add an HTTP Request node for each product URL. Set it to GET and grab the response status. If the status is not 200, route that row to a separate branch. That branch writes the URL, status code, and timestamp to a “validation failures” tab in your Sheet, then either skips to the next row (partial send) or halts the workflow entirely (safer if this is a single-product newsletter).

The simplest version is status-check only: you are not scraping price or inventory, just confirming the URL is live. That catches the most common case – a product that was manually unpublished after the newsletter was scheduled. If price accuracy matters enough to validate it, you need a product-specific check (call the store API or scrape a specific element), which is heavier but the same structural pattern.

One practical addition: include the failed URL in the halt notification so whoever owns the newsletter can fix the Sheet and re-run before the send window closes, rather than discovering it from subscriber replies.