Finalize and distribute meeting minutes
Lock the notes Doc and email it to attendees once a meeting ends.
Published Dec 21, 2025
The hour after a Northwind meeting is when the notes are most fragile. Someone keeps typing into the Doc, an attendee starts editing what was said, the “final” version drifts away from what was actually agreed. Then a week later two people quote different lines at each other and the argument starts over. Locking the Doc the moment the meeting ends solves it — but nobody remembers to do it.
This script runs hourly, picks up calendar events that finished recently, and for each one flips the linked notes Doc to read-only and emails the link to every guest. It uses event tags to record which docs are already finalised, so it never sends the same email twice. The Doc ID is stored on the event as a tag, set when the agenda was created.
What you’ll need
- Calendar events that include a
notes-doctag holding the Doc ID of the meeting notes. Set this when you create the agenda — see Watch out for if you want to add it manually. - The script account on each notes Doc as an editor, so it can change sharing.
- Nothing else — Gmail and Calendar work straight from the built-in services.
The script
// How far back to look for events that have just ended. Two hours covers
// an hourly trigger comfortably, with overlap for missed runs.
const LOOKBACK_MS = 2 * 60 * 60 * 1000;
// Tag names stored on the calendar event itself.
const NOTES_TAG = 'notes-doc';
const DONE_TAG = 'finalised';
/**
* Walks the recent calendar window, locks each ended meeting's notes Doc,
* emails the link to every guest, and tags the event so it does not get
* processed twice.
*/
function finaliseMeetingNotes() {
const now = new Date();
const windowStart = new Date(now.getTime() - LOOKBACK_MS);
const events = CalendarApp.getDefaultCalendar().getEvents(windowStart, now);
if (!events.length) {
Logger.log('No events in the lookback window.');
return;
}
let processed = 0;
for (const event of events) {
// 1. Only act on meetings that have actually ended.
if (event.getEndTime() > now) continue;
// 2. Skip anything already finalised on a previous run.
if (event.getTag(DONE_TAG) === 'yes') continue;
// 3. Need a Doc ID to do anything useful. Agendas without one
// are calendar holds, lunches, etc.
const docId = event.getTag(NOTES_TAG);
if (!docId) continue;
try {
const file = DriveApp.getFileById(docId);
// 4. Lock the Doc — anyone with the link can read, nobody can edit.
file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
// 5. Build a clean guest list. Filter blanks in case the event
// has resource rooms or external invites without addresses.
const guests = event.getGuestList()
.map((g) => g.getEmail())
.filter(Boolean);
if (guests.length) {
GmailApp.sendEmail(
guests.join(','),
'Notes — ' + event.getTitle(),
'Final notes: ' + file.getUrl() + '\n\n— Northwind'
);
}
// 6. Mark the event so the next run skips it.
event.setTag(DONE_TAG, 'yes');
processed++;
} catch (err) {
Logger.log('Skipping "' + event.getTitle() + '": ' + err.message);
}
}
Logger.log('Finalised ' + processed + ' meeting(s).');
}
How it works
finaliseMeetingNotesworks out a window from two hours ago to now and asks the default calendar for every event inside it. The lookback is generous on purpose: even if one hourly trigger fails, the next one will still see the same events.- It iterates the events and skips anything that has not actually ended — a long meeting starting in the window but still running gets ignored.
- Events already tagged
finalised: yesare skipped, which is what makes the script safe to run again and again. - The Doc ID lives on the event as the
notes-doctag. If there is no tag, it is a meeting without a notes Doc (a lunch, a focus block) and the script moves on. setSharingflips the Doc to view-only for anyone with the link. The meeting’s actual edit history stays intact — only future edits are blocked.- Guest emails are pulled off the event, filtered for blanks, and sent the link in a one-line email. If the event has no guests (a private meeting), no email is sent, but the Doc still gets locked.
- The event is tagged
finalised: yesso the next run skips it. The whole block is wrapped intry/catchso one broken event (deleted Doc, missing permission) does not kill the whole pass.
Example run
The calendar at 14:00 contains:
| Event | End time | notes-doc tag | finalised tag |
|---|---|---|---|
| Acme weekly sync | 13:30 | 1abcAcmeNotes | (none) |
| Studio standup | 13:45 | 1abcStandupNotes | yes |
| Pricing review | 15:00 (ongoing) | 1abcPricingNotes | (none) |
| Lunch | 13:00 | (none) | (none) |
The 14:00 trigger fires. Acme weekly sync gets its Doc locked, an email
goes to its three guests, and the event is tagged finalised: yes. Studio
standup is skipped (already done). Pricing review is skipped (not yet
over). Lunch is skipped (no Doc tag). The log reads
Finalised 1 meeting(s).
Trigger it
This is a clock-driven job, not a button.
- Open Triggers in the Apps Script editor.
- Add Trigger for
finaliseMeetingNotes, event source Time-driven, type Hour timer, interval Every hour. - Approve the Calendar, Drive, and Gmail scopes on the first run.
Hourly is the sweet spot — short enough that the notes lock within an hour of the meeting ending, long enough that the script does not churn through the calendar every few minutes.
Watch out for
- The
notes-doctag does not appear in the Calendar UI. Set it from a separate script that creates agendas, or by hand withevent.setTag('notes-doc', docId)in a one-off Apps Script function. setSharing(ANYONE_WITH_LINK, VIEW)makes the Doc readable by anyone who has the URL — including people forwarded the email by a guest. For tighter meetings, change it toDOMAIN_WITH_LINKso only your Workspace domain can open the file.- Tags are calendar metadata and are not visible on the event itself, but they survive event edits. If you delete and re-create the event, the tag is gone and the script will treat it as new.
getEvents(start, end)returns recurring instances individually, which is what you want — each weekly Acme sync is finalised on its own.- If a Doc was already manually locked,
setSharingis a no-op. The email still gets sent and the event still gets tagged, which is the safer default.
Related
Generate personalized study guides from notes
Reformat raw notes into structured study guides — for Northwind's internal training programme.
Updated Feb 8, 2026
Build a contract-clause assembly system
Construct Northwind agreements from a library of approved clauses — drag-drop in code.
Updated Feb 1, 2026
Translate and resolve Doc comments
Localise reviewer feedback on a shared Doc so multilingual teams can collaborate.
Updated Jan 25, 2026
Auto-archive finalized Docs to dated folders
File completed Northwind Docs by month so the active folder stays focused on in-flight work.
Updated Jan 18, 2026
Build a fillable intake form inside a Doc
Create structured intake forms with placeholder fields readers can fill — for client briefs.
Updated Jan 11, 2026