Pull event RSVPs from emails into a Sheet
Parse yes/no replies to event invites and tally attendance automatically.
Published Jun 2, 2026
Every quarter Northwind hosts a client lunch, and every quarter the head count
is a guess. The invite goes out to a shared [email protected] address,
replies trickle back as plain emails — “yes, see you there”, “sorry, can’t make
it this time” — and someone ends up reading the inbox and keeping a tally on a
notepad. Numbers drift, names get missed, and the caterer gets a rough
estimate.
This script turns that mailbox into a clean attendance list. It scans replies
to the RSVP address, works out whether each one is a yes, a no, or a maybe from
the wording, and appends a row to an RSVPs sheet. Each respondent is counted
once, and a quick formula on the sheet gives you the live head count.
What you’ll need
- A Google Sheet with an
RSVPstab. Row 1 must be a header with the columnsemail,name,status,respondedAt— the script appends rows beneath it. - A Gmail account that receives the RSVP replies — that is, the account behind
[email protected]. - Nothing else. The script creates the
rsvp/countedlabel itself on first run.
The script
// The spreadsheet that holds the RSVP list.
const RSVPS_SHEET_ID = '1abcRsvpsSheetId';
// The address invitees reply to.
const RSVP_ADDRESS = '[email protected]';
// The label applied to threads already counted, so they are skipped.
const COUNTED_LABEL = 'rsvp/counted';
/**
* Scans recent replies to the RSVP address, classifies each as
* yes / no / maybe, and appends one row per new respondent.
*/
function tallyRsvps() {
// 1. Find replies sent to the RSVP address in the last 30 days that
// have not already been counted.
const threads = GmailApp.search(
'to:' + RSVP_ADDRESS + ' newer_than:30d -label:' + COUNTED_LABEL
);
if (!threads.length) {
Logger.log('No new RSVP emails to process.');
return;
}
// 2. Get (or create) the label used to mark counted threads.
const counted = GmailApp.getUserLabelByName(COUNTED_LABEL)
|| GmailApp.createLabel(COUNTED_LABEL);
// 3. Read the addresses already on the sheet so each person is
// counted only once, even if they reply twice.
const sheet = SpreadsheetApp.openById(RSVPS_SHEET_ID).getSheets()[0];
const known = new Set(sheet.getRange('A2:A').getValues().flat());
// 4. Build one row per new respondent whose reply we can classify.
const rows = [];
for (const thread of threads) {
const msg = thread.getMessages()[0];
const from = msg.getFrom();
// Extract the bare address from a "Name <addr>" From header.
const email = (from.match(/<(.+?)>/) || [, from])[1].toLowerCase();
if (known.has(email)) continue;
// Work out yes / no / maybe from the wording; skip if unclear.
const status = parseStatus(msg.getPlainBody());
if (!status) continue;
// Pull the display name out of the From header.
const name = from.split('<')[0].replace(/"/g, '').trim();
rows.push([email, name, status, new Date()]);
thread.addLabel(counted);
}
// 5. Append all the new rows in a single write.
if (rows.length) {
sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, 4).setValues(rows);
Logger.log('Recorded ' + rows.length + ' RSVP(s).');
} else {
Logger.log('No new classifiable RSVPs in this batch.');
}
}
/**
* Classifies an email body as "yes", "no" or "maybe" by looking for
* common reply phrasings. Returns null if nothing matches.
*/
function parseStatus(body) {
if (/\b(yes|coming|count me in|i'?ll be there)\b/i.test(body)) return 'yes';
if (/\b(no|can'?t make it|sorry)\b/i.test(body)) return 'no';
if (/\b(maybe|tentative|might)\b/i.test(body)) return 'maybe';
return null;
}
How it works
tallyRsvpssearches Gmail for messages sent to the RSVP address in the last 30 days, excluding anything already labelledrsvp/counted. If there is nothing new, it stops.- It fetches the
rsvp/countedlabel, creating it on the first run. - It reads column A of the sheet into a
Setof addresses already recorded. This is the deduplication guard — a second reply from the same person is skipped. - For each thread it parses the bare email address out of the
Fromheader (which usually looks likeJordan Lee <[email protected]>). If that address is already known, it moves on. parseStatusreads the body and matches it against three sets of phrases. A clear yes, no, or maybe produces a row; anything ambiguous returnsnulland the thread is left unlabelled for a human to read.- Classified replies become four-field rows — address, name, status, timestamp — and the thread is labelled so the next run skips it.
- All new rows are appended to the sheet in a single
setValuescall.
Example run
Two replies land in the RSVP mailbox:
From: Jordan Lee <[email protected]>
Subject: Re: Northwind client lunch
Yes! Count me in, looking forward to it.
From: Sam Cole <[email protected]>
Subject: Re: Northwind client lunch
Sorry, can't make it this quarter.
After the run, the RSVPs sheet has gained two rows:
| name | status | respondedAt | |
|---|---|---|---|
| [email protected] | Jordan Lee | yes | 2026-05-25 10:14 |
| [email protected] | Sam Cole | no | 2026-05-25 10:14 |
A formula like =COUNTIF(C2:C, "yes") then gives the live head count for the
caterer.
Trigger it
Run this on a time-driven trigger during the RSVP window:
- In the Apps Script editor, open Triggers (the clock icon).
- Click Add trigger.
- Choose function
tallyRsvps, event source Time-driven, type Hour timer, every hour.
Hourly is plenty — RSVPs are not time-critical, and an hourly poll keeps the sheet current without burning quota. Turn the trigger off once the event has passed.
Watch out for
- Wording is messy. “No problem, I’ll be there” contains the word “no” before
“I’ll be there” — the regex checks
yespatterns first, so this is read correctly, but unusual phrasings will be missed. Leave thersvp/countedlabel off unclassified threads so a human can catch them. - Only the first message is read. If someone replies “maybe” then later “yes” in the same thread, the thread is already counted as maybe. A late change of mind needs a fresh email or a manual edit.
- Quoted text counts. A reply that quotes the original invite (“…let us know
if you can make it”) may match a phrase you did not intend. If false matches
appear, strip quoted lines (those beginning with
>) before callingparseStatus. - One row per address forever. Once someone is on the sheet, later replies are
ignored. Clear the sheet and the
rsvp/countedlabel before the next quarter’s event so the count starts fresh. - The
From-header parsing assumes a standard format. A header with no display name still works, but an unusual one could leave a slightly oddnamevalue — harmless, but worth knowing.
Related
Convert long email threads into a summary note
Collapse a thread's history into a Doc for handover — perfect for client transitions or vacation cover.
Updated Jun 6, 2026
Turn forwarded emails into project tasks
Forward to [email protected] and a row lands in the Projects sheet under the right client.
Updated May 30, 2026
Turn starred emails into a task list
Sync every starred thread into the Northwind Tasks sheet automatically.
Updated May 26, 2026
Alert when a label hits a backlog threshold
Warn the Northwind team in Slack when a Gmail label has more than N unread threads.
Updated Mar 31, 2026
Rank email templates by reply rate
Log every templated send and compute which subject lines and bodies actually earn replies.
Updated Mar 24, 2026