// Calendar · Sheets

Sync Google Calendar events into a spreadsheet for reporting.

Pull every event from your primary calendar for the next 30 days into a Sheet — start, end, title, location, attendees. Idempotent: re-running gives a fresh snapshot, not duplicates.

I want my next month of meetings as rows in a sheet so I can pivot, sort, and report on them.

The script

copy · paste · trigger
syncCalendarToSheet.gs
Apps Script
// Pull events from the user's primary calendar for the next 30 days
// into the active sheet. Clears prior data rows so re-running is a
// fresh snapshot, not an append.
function syncCalendarToSheet() {
  const sheet = SpreadsheetApp.getActiveSheet();
  const cal = CalendarApp.getDefaultCalendar();

  const now = new Date();
  const end = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
  const events = cal.getEvents(now, end);

  // Clear previous rows, leaving the header.
  if (sheet.getLastRow() > 1) {
    sheet.getRange(2, 1, sheet.getLastRow() - 1, 5).clearContent();
  }

  events.forEach(e => {
    sheet.appendRow([
      e.getStartTime(),
      e.getEndTime(),
      e.getTitle(),
      e.getLocation() || '',
      e.getGuestList().map(g => g.getEmail()).join(', '),
    ]);
  });
}

Need a variant? Gnaw writes a custom version from one sentence — fields, triggers, edge cases handled.

Walkthrough

How it works

CalendarApp.getDefaultCalendar() picks the user's primary calendar — the one tied to the Google account the script runs as. For shared/team calendars, swap to CalendarApp.getCalendarById('<calendar id>').

getEvents(start, end) returns every event whose start time falls in the range. The 30-day window here is the typical reporting horizon; lengthen by changing the multiplier.

The clearContent() step is what makes the script idempotent. Without it, every run would append duplicates. Header row stays — only data rows get wiped.

How to schedule it

Add a daily time-based trigger via the Triggers panel. Daily is the right cadence — calendar churn is faster than weekly but slower than hourly, and the snapshot is cheap.

If you want change-detection (alert me when a meeting moves), keep this script for the snapshot and add a second script that diffs the latest snapshot against the previous one (store previous in PropertiesService).

Common pitfalls

Quota: getEvents can return up to 2,500 events per call. A 30-day window is well under that for most calendars.

Time zones: Apps Script returns event times in the script's configured time zone (File → Project settings). Set it to match your calendar to avoid off-by-hours surprises.

Recurring events: each occurrence is returned as a separate event. To dedupe by series, group by e.getEventSeries().getId().

Want a custom version?

Describe your sheet and the rule you want. Gnaw writes the Apps Script — fields, triggers, edge cases — in one shot.

FAQ

4 questions
Can I sync from a shared team calendar?
Yes. Replace getDefaultCalendar() with getCalendarById('<id>'). Find the ID in Calendar settings → Integrate calendar → Calendar ID.
How do I include declined events?
By default getEvents returns everything you're invited to, including declined. Filter with `events.filter(e => e.getMyStatus() !== CalendarApp.GuestStatus.NO)`.
Why is the attendee column blank?
getGuestList() returns guests other than the script owner. Solo events have no guests, so the column is empty by design.
Can I sync to a sheet in a different workbook?
Yes. Replace getActiveSheet() with SpreadsheetApp.openById('<id>').getSheetByName('<name>'). The trigger needs to be installable (not simple) for cross-document writes.