How to Automate Email Digests with n8n
The Problem
Inboxes fill with notifications, receipts, alerts and newsletters, and most of it only needs to be glanced at, not answered. Reading each message as it lands breaks focus all day, while ignoring the inbox means missing the two messages that matter. Teams that share several mailboxes feel this twice as hard. This guide shows how to build a daily digest with n8n. A scheduled run collects unread mail from multiple accounts, groups it by sender domain, highlights noisy senders, and sends the whole thing as one tidy email to your team list.
What You Will Need
- A self-hosted n8n instance (Community Edition is free) or an n8n Cloud trial
- IMAP credentials (host, port, app password) for each mailbox you want to include
- SMTP credentials and a distribution list address for the Send Email node
- Estimated setup time: 45 to 60 minutes
How the n8n Workflow Fits Together
This n8n workflow collects mail in a batch and compiles it into one summary email. It runs on a schedule, reads one or more mailboxes with the IMAP Email node, groups messages by sender domain inside a Code node, builds an HTML body, sends it over SMTP, and finally marks the processed mail as read. It is deliberately different from real-time notification flows: nothing happens when a message arrives, and everything happens once each morning.
Because the n8n workflow is a batch job, it is easy to reason about. Each execution has a clear start, one input set, one output email, and a state change at the end. That makes n8n execution logs a reliable audit trail of what each digest contained.
Step-by-Step Workflow Setup
1 Schedule the 7am digest run in n8n
A digest is a batch job, not a stream. An n8n Schedule Trigger fires once each morning at 7:00 so the compiled email is waiting before the team starts work. Everything that arrived overnight is gathered in one pass.
Add an n8n Schedule Trigger node and configure:
Node: Schedule Trigger
Trigger Interval: Days
Days Between Triggers: 1
Trigger at Hour: 7
Trigger at Minute: 0
The trigger output carries the current time, which the next steps use to build the "since yesterday" boundary. IMAP date searches use the format 1-Nov-2026, so the expression below produces exactly that.
Expression: {{ $now.minus({ days: 1 }).toFormat("d-LLL-yyyy") }}
{
"timestamp": "2026-11-05T07:00:00.000-08:00",
"since": "4-Nov-2026"
}
Choose a time after your busiest overnight senders (newsletters, alerts, vendors) have delivered, otherwise their mail will slip into tomorrow digest.
2 Fetch unread mail from the first account in n8n
The n8n IMAP Email node connects to a mailbox and returns messages that match search rules. Here it asks for unread messages received since yesterday, so a message you already read or an old backlog never enters the digest.
Add an n8n IMAP Email node and configure:
Node: IMAP Email (name it "IMAP Account A")
Credential: IMAP (host imap.gmail.com, port 993, SSL On, user, app password)
Mailbox Name: INBOX
Format: Simple
Download Attachments: Off
Custom Email Rules:
["UNSEEN", ["SINCE", "{{ $now.minus({ days: 1 }).toFormat('d-LLL-yyyy') }}"]]
Options > Mark as Read: Off
Leave Mark as Read off here. The workflow marks messages only after the digest has been sent successfully, so a failed run never loses mail. Use an app password in the n8n credential, not your real account password, for providers that enforce two factor login.
Expression: {{ $json.from.value[0].address }}
{
"uid": 18342,
"from": { "value": [{ "address": "billing@stripe.com", "name": "Stripe" }] },
"subject": "Your payout is on the way",
"date": "2026-11-04T22:41:10.000Z",
"textPlain": "Your payout of ..."
}
Turning attachment download off keeps memory low. A digest only needs sender, subject and date.
3 Add a second account and merge in n8n
Multi-account aggregation in n8n is just a second IMAP node feeding a Merge node. Duplicate the first node, point it at the other mailbox credential, tag each result with its account label, and append both streams into one list.
Add an n8n Merge node and configure:
Node: IMAP Email (name it "IMAP Account B")
Credential: IMAP (support@yourcompany.com)
Mailbox Name: INBOX
Custom Email Rules: same as Account A
Node: Edit Fields (Set), one after each IMAP node
account: A (or B)
Node: Merge
Mode: Append
Number of Inputs: 2
Input 1: Account A tagged items
Input 2: Account B tagged items
Append mode simply concatenates the lists, so the later nodes see one flat set of messages. Add as many inputs as you have mailboxes in your n8n canvas. The account tag is what lets the final step mark messages as read in the correct mailbox.
Expression: {{ $json.account }}
{
"account": "B",
"uid": 7710,
"from": { "value": [{ "address": "alerts@datadoghq.com" }] },
"subject": "Monitor recovered: api latency"
}
If one mailbox is empty, the Merge node still passes the other branch through in Append mode, so a quiet account never blocks the digest.
4 Group senders by domain in n8n
An n8n Code node implements the sender grouping algorithm. It reads the domain from each address, collects subjects under that domain, counts messages, and sorts the busiest senders first. Grouping by domain, not full address, folds notification@ and billing@ from one company into a single line.
Add an n8n Code node and configure:
Node: Code (Group and count)
Mode: Run Once for All Items
Language: JavaScript
const groups = {};
const uids = [];
for (const item of $input.all()) {
const j = item.json;
const addr = (j.from.value[0].address || '').toLowerCase();
const domain = addr.split('@')[1] || 'unknown';
if (groups[domain] === undefined) groups[domain] = { domain, count: 0, subjects: [] };
groups[domain].count += 1;
groups[domain].subjects.push(j.subject || '(no subject)');
uids.push({ account: j.account, uid: j.uid });
}
const list = Object.values(groups).sort((a, b) => b.count - a.count);
return [{ json: { total: uids.length, senders: list, uids } }];
Using an object keyed by domain gives constant time lookups, so even a few thousand messages group instantly. Subdomains such as mail.stripe.com can be normalised with a small regex if they split a sender into two rows.
Expression: {{ $json.senders.length }} senders, {{ $json.total }} messages
{
"total": 41,
"senders": [
{ "domain": "github.com", "count": 12, "subjects": ["[acme/api] PR merged", "[acme/web] CI failed"] },
{ "domain": "stripe.com", "count": 3, "subjects": ["Your payout is on the way"] }
],
"uids": [{ "account": "A", "uid": 18342 }]
}
The uids array travels along with the summary, because the last step needs it to mark the right messages as read.
5 Generate the HTML digest in n8n
A second n8n Code node turns the grouped data into an HTML email body. Senders with three or more messages get a highlighted row, so repeated alerts stand out from one-off messages. The template is built with plain string concatenation, so there is nothing to install on your n8n server.
Add an n8n Code node and configure:
Node: Code (Build digest)
Mode: Run Once for All Items
Language: JavaScript
const d = $input.first().json;
const esc = s => String(s).replace(/&/g, '&').replace(/</g, '<');
let html = '<h2>Inbox digest: ' + d.total + ' unread messages</h2>';
for (const s of d.senders) {
const hot = s.count >= 3;
html += '<div style="padding:8px;margin:6px 0;' + (hot ? 'background:#fff3cd;' : '') + '">';
html += '<strong>' + esc(s.domain) + '</strong> (' + s.count + ')';
html += '<ul>' + s.subjects.slice(0, 5).map(x => '<li>' + esc(x) + '</li>').join('') + '</ul>';
if (s.subjects.length > 5) html += '<em>and ' + (s.subjects.length - 5) + ' more</em>';
html += '</div>';
}
return [{ json: { html, total: d.total, uids: d.uids } }];
Escaping angle brackets in subjects stops a subject line containing markup from breaking the layout. Inline styles are used because most mail clients ignore style blocks. Showing only the first five subjects per sender keeps the digest scannable.
Expression: {{ $json.html.length }} characters of HTML
{
"html": "<h2>Inbox digest: 41 unread messages</h2><div style=\"padding:8px;background:#fff3cd;\">...",
"total": 41
}
Preview the output by pasting the html value into any HTML viewer before you send it to the team.
6 Send the digest through SMTP in n8n
The n8n Send Email node delivers one message to your distribution list. Because the n8n Code node already produced finished HTML, this step is only addressing and a subject line.
Add an n8n Send Email node and configure:
Node: Send Email
Credential: SMTP (smtp.yourcompany.com, port 587, STARTTLS)
From Email: digest@yourcompany.com
To Email: team@yourcompany.com
Subject: Inbox digest for {{ $now.toFormat("cccc, LLL d") }}: {{ $json.total }} messages
Email Format: HTML
HTML: {{ $json.html }}
Send to a distribution list address rather than a long comma list so membership is managed in one place. Add an IF node before this one that stops the run when $json.total equals 0 if you do not want empty digests.
Expression: {{ $now.toFormat("cccc, LLL d") }}
{
"accepted": ["team@yourcompany.com"],
"rejected": [],
"messageId": "<f3c1@yourcompany.com>"
}
After this node succeeds, a final IMAP Email node marks every collected message as read. Set that node to Execute Once, choose the Mark as Read operation for the account, and pass the UIDs with {{ $("Build digest").first().json.uids.filter(u => u.account === "A").map(u => u.uid).join(",") }}. Duplicate it for account B with the filter changed. Because it runs only after the Send Email node in n8n, any earlier failure leaves messages unread and they are picked up again tomorrow.
Extending This n8n Workflow
There are several natural ways to grow this n8n workflow. Add a sender allow list in a Set node so that only chosen domains appear, or an ignore list that hides newsletters entirely. Add a second n8n schedule for a weekly rollup that reads the previous seven days. If your team prefers chat, swap the Send Email node for a Slack node and post the grouped counts as plain text instead of HTML.
To keep the digest short as volume grows, cap the sender list at the top twenty domains and add a final line that reports how many smaller senders were omitted. The n8n Code node makes this a one line change.
Troubleshooting
The digest is empty even though the inbox has unread mail
The date rule is the usual cause. IMAP SINCE compares dates without times and in the server timezone, so format it as d-LLL-yyyy and test with a wider window such as two days. Also check that the Mailbox Name matches the real folder, since Gmail uses INBOX but labels appear as separate folders.
The same messages appear in the digest two days in a row
The mark-as-read step did not run or used the wrong UIDs. UIDs are unique only within one mailbox, so each account needs its own mark-read node and its own filtered list. Confirm the account tag from the merge step is present on every item.
Gmail rejects the IMAP login
Enable IMAP in Gmail settings and use an app password created after turning on two step verification. A normal password fails even when it is correct. For Microsoft 365, use a mailbox that still allows IMAP basic or app password access.
Free n8n Workflow Resources
Get started with the free workflow pack on the LightCube Labs templates page.
Production-Ready Workflow Packs
Get this workflow and more, with setup guides, editable configurations, and a shared failure-alert system.
View the Starter ($29) and Pro ($99) PacksFrequently Asked Questions
How is a digest different from forwarding every email to Slack?
A digest collects messages in a batch and sends one summary on a schedule, which is the right fit when you want to reduce interruptions. Real-time forwarding sends one notification per message as it arrives. Use this workflow for reading calm, and a trigger based workflow for urgent mail.
How many mailboxes can the digest cover?
Add one IMAP Email node and one tag step per mailbox, then raise the Number of Inputs on the Merge node. There is no fixed limit, but each added account increases run time because IMAP connections open one after another.
Why mark messages as read at the end rather than during the fetch?
Marking early risks silent data loss. If the SMTP step fails, the messages would be flagged read and excluded from the next run. Marking last makes the whole workflow safe to retry.
Can I change the highlight threshold from three emails?
Yes. The threshold lives in the line const hot = s.count >= 3 in the digest Code node. Change the number, or read it from a Set node so non-developers can adjust it.