Export a printable monthly calendar PDF
Generate a Northwind schedule document each month — for the studio noticeboard.
Published Aug 21, 2025
Northwind keeps a paper schedule on the studio noticeboard — partly nostalgia, partly because a visitor walking in should be able to see what is on without asking. The handwritten version was a mess of crossings-out by week three of every month, and printing the calendar from the Google UI strips out all the context that makes the noticeboard worth reading.
This script renders the current month as a Google Doc grouped by day, then saves a PDF copy to Drive. Pin the PDF link in the studio Drive folder, send it to the printer on the first of every month, and the noticeboard is current without anybody typing dates by hand.
What you’ll need
- Edit access to a Google Calendar (the script reads your default calendar).
- Permission to create Docs and Drive files as the script owner.
- A printer reachable from Drive — or you can just open the saved PDF and print from your browser.
The script
// The Drive folder ID the PDF should land in. Leave as null to drop it
// in My Drive, or paste a folder ID to keep months together.
const OUTPUT_FOLDER_ID = null;
// Format strings used in the document.
const MONTH_FORMAT = 'MMMM yyyy';
const DAY_HEADING_FORMAT = 'EEEE d';
const TIME_FORMAT = 'HH:mm';
/**
* Reads the current month's events and produces a printable PDF
* grouped by day. The intermediate Google Doc is left in Drive so
* you can tweak the layout before reprinting.
*/
function monthlyPdf() {
const tz = Session.getScriptTimeZone();
const today = new Date();
// 1. Build the month window. Day 0 of next month is the last day of
// this month — a small JavaScript trick that beats manual
// "30 vs 31" arithmetic.
const start = new Date(today.getFullYear(), today.getMonth(), 1);
const end = new Date(today.getFullYear(), today.getMonth() + 1, 0);
end.setHours(23, 59, 59, 999);
const events = CalendarApp.getDefaultCalendar().getEvents(start, end);
// 2. Create a Doc titled "Calendar — June 2026". The doc URL is
// handy in the log for ad-hoc edits.
const monthLabel = Utilities.formatDate(today, tz, MONTH_FORMAT);
const doc = DocumentApp.create('Calendar — ' + monthLabel);
const body = doc.getBody();
// 3. Title at the top, big.
body.appendParagraph(monthLabel)
.setHeading(DocumentApp.ParagraphHeading.TITLE);
// 4. Group events by day. The events come back in chronological
// order, so a running "current day" string is enough to detect
// when to insert a new heading.
let day = '';
for (const e of events) {
const dayLabel = Utilities.formatDate(e.getStartTime(), tz, DAY_HEADING_FORMAT);
if (dayLabel !== day) {
body.appendParagraph(dayLabel)
.setHeading(DocumentApp.ParagraphHeading.HEADING2);
day = dayLabel;
}
const time = e.isAllDayEvent()
? 'all day'
: Utilities.formatDate(e.getStartTime(), tz, TIME_FORMAT);
body.appendParagraph(time + ' ' + e.getTitle());
}
if (!events.length) {
body.appendParagraph('No events scheduled this month.');
}
doc.saveAndClose();
// 5. Export the Doc to PDF and drop it in the chosen folder.
const pdfBlob = DriveApp.getFileById(doc.getId()).getAs('application/pdf');
pdfBlob.setName('Calendar — ' + monthLabel + '.pdf');
const folder = OUTPUT_FOLDER_ID
? DriveApp.getFolderById(OUTPUT_FOLDER_ID)
: DriveApp.getRootFolder();
const pdfFile = folder.createFile(pdfBlob);
Logger.log('Created ' + pdfFile.getUrl());
}
How it works
- The script anchors the current month with the first-of-month / day-zero
trick —
new Date(year, month + 1, 0)gives the last day of the current month without needing a switch on 28/30/31. getEventsreads every event whose interval touches the month, recurring instances expanded. Events come back in chronological order, which is what makes the grouping step simple.- A new Google Doc is created with a title like “Calendar — June 2026”. The document title sits at the top in the TITLE heading style.
- The loop tracks the current day-of-month label in a
dayvariable. When the next event’s day differs, a HEADING2 paragraph is appended; otherwise the event lines just stack under the heading already in place. - Each event becomes a paragraph of
time title. All-day events read “all day” instead of a clock time. - After
saveAndClose, the script fetches the Doc as a PDF blob viaDriveApp.getFileById(...).getAs('application/pdf')and drops the PDF into the configured folder (or My Drive by default). The Doc itself stays in Drive, so you can tweak and re-export if you spot a typo.
Example run
For a calendar with a handful of events in June 2026, the resulting PDF contains:
Calendar — June 2026
Monday 1 09:30 Standup 14:00 Client kickoff — Patel
Tuesday 2 10:00 Northwind weekly sync
Wednesday 3 all day Deep work day — no meetings
Thursday 4 14:00 Appointment: Priya Patel
Print that to A4 and it fits a normal noticeboard without further design work.
Trigger it
This is a once-per-month job, so a monthly time-driven trigger is the right fit:
- In the Apps Script editor, open Triggers.
- Add a trigger for
monthlyPdf, event source Time-driven, type Month timer, on the first of the month, time 6am to 7am.
If you prefer to print on the last working day of the previous month for the
month ahead, change today to new Date(today.getFullYear(), today.getMonth() + 1, 1) so the window covers the next month instead.
Watch out for
- Recurring events are expanded into one paragraph per instance. A weekly standup will appear four or five times in the month, which is what you want for a noticeboard — but it makes the document longer than the calendar UI suggests.
- The intermediate Doc lives in My Drive (or the configured folder) and is
not cleaned up. Over a year you accumulate twelve, plus their PDFs. Add a
setTrashed(true)call on the Doc if you only care about the PDF. - Formatting is plain — title, day headings, plain paragraphs. For a prettier noticeboard, edit the Doc once, save it as a template, then switch the script to clone the template and fill in paragraphs rather than creating a Doc from scratch.
- The script reads only your default calendar. Pull from
getAllOwnedCalendars()and merge if the noticeboard should reflect the whole studio’s schedule.
Related
Send a feedback survey after each event
Email attendees a survey link automatically after Northwind workshops or trainings.
Updated Oct 24, 2025
Build a team-capacity view from calendars
Show how booked the Northwind team is this week — meeting hours per person.
Updated Oct 20, 2025
Flag meetings that could have been emails
Detect short, agendaless, oversized meetings — the smell of bad calendar hygiene.
Updated Oct 12, 2025
Send tiered deadline countdown reminders
Email Northwind teammates at 7, 3, and 1 days out from a Sheet of upcoming deadlines.
Updated Sep 30, 2025
Archive past events to a log sheet
Keep a searchable Northwind meeting history — every event logged with title, attendees, duration.
Updated Sep 26, 2025