One big “open items” email to the whole team gets ignored. One short email per person, listing only their rows, gets read. sendDigestPerOwner(rows, ownerKey, renderFn, opts) groups row objects by an owner field (an email address), calls your renderFn(owner, rows) to build each message, and sends one email per owner. dryRun logs instead of sending, and maxSends caps how many go out in one run.
When to use
- “Your open tickets / tasks / invoices” emails from a shared sheet
- Weekly reminders where each person only cares about their own rows
- Any digest that currently goes to everyone and helps no one
How to use this snippet
/**
* Group rows by owner and send each owner one email.
*
* @param {Object[]} rows row objects (e.g. from rowsToObjects)
* @param {string} ownerKey field holding the owner's email
* @param {function(string, Object[]): {subject: string, body: string, htmlBody: (string|undefined)}} renderFn
* @param {Object=} opts { dryRun: false, maxSends: 50 }
* @return {{sent: string[], skipped: string[], dryRun: boolean}}
*/
function sendDigestPerOwner(rows, ownerKey, renderFn, opts) {
opts = opts || {};
var maxSends = opts.maxSends || 50;
var groups = {};
rows.forEach(function (r) {
var owner = String(r[ownerKey] || '').trim().toLowerCase();
if (!owner) return;
(groups[owner] = groups[owner] || []).push(r);
});
var result = { sent: [], skipped: [], dryRun: !!opts.dryRun };
Object.keys(groups).sort().forEach(function (owner) {
if (result.sent.length >= maxSends) { result.skipped.push(owner); return; }
var msg = renderFn(owner, groups[owner]);
if (opts.dryRun) {
console.log('[dry run] to ' + owner + ': ' + msg.subject + ' (' + groups[owner].length + ' rows)');
} else {
MailApp.sendEmail({ to: owner, subject: msg.subject, body: msg.body, htmlBody: msg.htmlBody });
}
result.sent.push(owner);
});
if (result.skipped.length) console.log('maxSends reached; skipped: ' + result.skipped.join(', '));
return result;
}
Example
function weeklyOpenItems() {
var sh = SpreadsheetApp.getActive().getSheetByName('Tasks');
var rows = rowsToObjects(sh.getDataRange().getValues())
.filter(function (r) { return r.Status !== 'Done'; });
var res = sendDigestPerOwner(rows, 'Owner', function (owner, mine) {
return {
subject: 'You have ' + mine.length + ' open task' + (mine.length === 1 ? '' : 's'),
body: mine.map(function (r) { return '- ' + r.Task + ' (due ' + r.Due + ')'; }).join('\n') +
'\n\nSheet: ' + SpreadsheetApp.getActive().getUrl()
};
}, { dryRun: true, maxSends: 30 });
console.log(JSON.stringify(res));
}
Tips:
- Run with
dryRun: truefirst and read the Log before anyone gets an email. - Owner emails are trimmed and lower-cased, so
Ana@Example.comandana@example.comget one email, not two. - Rows with a blank owner are ignored. Count them separately if they matter.
- Keep
maxSendsunder your daily quota, or pair with sendEmailWithBudget.
Tip: NitroGAS Co-Pilot can write the renderFn for your columns. Paste your header row and what each person should see.
Happy Coding!
