Skip to content
// Calendar · Sheets · Apps Script

Export calendar events to a sheet in Google Calendar.

A working Apps Script that pulls Google Calendar events into a Sheets spreadsheet using getEvents with explicit date bounds and a single setValues call for performance.

I want to pull all my Google Calendar events into a spreadsheet so I can filter, sort, and report on them without clicking through the calendar UI one week at a time.

The script

copy · paste · trigger
exportCalendarEvents.gs
Apps Script
// Export events from a named calendar to the active sheet
// Adjust CALENDAR_NAME, START_DATE, and END_DATE before running

function exportCalendarEvents() {
  var CALENDAR_NAME = 'My Work Calendar';
  var START_DATE = new Date('2024-01-01');
  var END_DATE   = new Date('2024-12-31');

  var cal    = CalendarApp.getCalendarsByName(CALENDAR_NAME)[0];
  var events = cal.getEvents(START_DATE, END_DATE);

  var rows = [['Title', 'Start', 'End', 'Location', 'Description']];
  for (var i = 0; i < events.length; i++) {
    var e = events[i];
    rows.push([e.getTitle(), e.getStartTime(), e.getEndTime(),
               e.getLocation(), e.getDescription()]);
  }

  var sheet = SpreadsheetApp.getActiveSheet();
  sheet.clearContents();
  sheet.getRange(1, 1, rows.length, rows[0].length).setValues(rows);
}

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

Walkthrough

Why getEvents requires a date range

There is no CalendarApp method that returns all events unconditionally. getEvents always takes two Date objects as its first two arguments — a start (inclusive) and an end (exclusive). Pass JavaScript Date objects, not strings; the API will silently return nothing if you hand it a string, which is a frustrating first hour of debugging.

The practical consequence is that you must decide your window upfront. For a full-year export, new Date('2024-01-01') and new Date('2024-12-31') work fine. For a rolling 90-day window you can compute them: var end = new Date(); var start = new Date(end - 90 * 24 * 60 * 60 * 1000). The calendar quota is 500 events returned per call, so for dense calendars you will need to page by breaking the range into monthly chunks and concatenating the arrays.

getCalendarsByName returns an array, even when there is exactly one match, so index [0] is required. If the name does not match precisely (case and punctuation count), the array is empty and cal will be undefined — add a guard in production: if (!cal) { throw new Error('Calendar not found: ' + CALENDAR_NAME); }.

One setValues call instead of appendRow in a loop

The first time I ported this from a naive loop script, a 400-event export took over three minutes because appendRow is a separate Sheets API call per row. Each call has round-trip latency. At 400 events you are making 400 individual network calls inside the Apps Script runtime.

The fix is to build all rows into a plain JavaScript 2D array first, then write the whole thing in one shot with sheet.getRange(1, 1, rows.length, rows[0].length).setValues(rows). For 400 events this drops the write time from three minutes to under two seconds. The clearContents() before it ensures stale rows from a previous run do not persist below the new data.

One subtlety: setValues requires every inner array to be the same length. The header row in the snippet locks that length to 5. If you add a column to the push() call, add it to the header too, or setValues will throw a dimensions mismatch error that can be confusing because the error points at the range, not the data.

Scheduling automatic refreshes with a time-based trigger

Once the function works manually, wiring it to a trigger takes about 30 seconds: in the Apps Script editor, open Triggers (the clock icon), add a new trigger, pick exportCalendarEvents, set event source to Time-driven, and choose your interval. Daily at midnight is common for reporting dashboards.

One thing worth knowing: triggers run under the account that created them, not whoever has the spreadsheet open. If your calendar is on a work Google account and you set the trigger from a personal account, the trigger will see only the personal account's calendars. Match the account you use to open the script editor to the account that owns the calendar.

The Apps Script daily quota for Calendar reads is 500,000 read calls per day per project, which is effectively unlimited for a single-user export job. The realistic constraint is the 500-event-per-getEvents-call limit mentioned above, not quota.

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
Why does getEvents return an empty array even though events exist?
The two most common causes: the calendar name passed to getCalendarsByName does not match exactly (check for trailing spaces or capitalization differences), or the date range does not overlap with where the events actually sit. Log cal.getName() and the event count before building rows to confirm both resolved correctly.
Can I export events from a shared or team calendar I did not create?
Yes, as long as the Google account running the script has at least view access to that calendar. getCalendarsByName will find it. If the calendar was shared with you by email invite, it should appear in your calendar list and the name will match what you see in the Calendar UI sidebar.
How do I export recurring events without duplicates?
getEvents expands recurring events into individual occurrences within your date range, so a weekly meeting across 52 weeks will produce 52 rows. That is usually what you want for a flat export. If you need to deduplicate to just the series, filter on getTitle() plus e.isRecurringEvent() and track seen titles, though you lose per-occurrence time data.
The script hits the 500-event limit. How do I get more?
Break the date range into monthly chunks in a loop, call getEvents on each chunk, and concatenate the results into one rows array before the single setValues write. Splitting by calendar month keeps each chunk well under 500 for most calendars and avoids pagination complexity.
// one good script a week

Get a working Apps Script snippet in your inbox, weekly.