Cellbrook

Google Sheets guide · Calendars

How to export Google Calendar to Google Sheets

Google Calendar™ has no “export to Google Sheets™” button. Its own export gives you an .ics file of every event you’ve ever had. If you want last month’s events as tidy rows (for billing, a timesheet or a report), here are three ways that work, and how to total the hours once they’re in.

Add CalendarRows to Google SheetsFree to install from the Google Workspace Marketplace

Updated · 8 min read · By the Cellbrook team

Three ways, compared

MethodDate rangeClean columnsEffortCost
Apps ScriptYesYesPaste and run, change two datesFree
CalendarRows add-onYes, or quick rangesYes, you choose themA few clicksFree up to 100 events per import
Calendar’s .ics exportNo, whole historyNo, needs convertingHighFree

Automation tools such as Zapier can also add a row for each new event from now on, but they don’t fetch the events you already have.

1. Free: Apps Script for a date range

This script copies one calendar’s events between two dates into a new tab, with one row per event.

  1. Open the spreadsheet you want the events in and choose Extensions → Apps Script.
  2. Delete the sample code and paste:
// Copies one calendar's events for a date range into a new tab.
const CALENDAR_ID = 'primary';   // or a Calendar ID from the calendar's Settings > Integrate calendar
const FROM = '2026-10-01';       // first day to include
const TO = '2026-10-31';         // last day to include

function exportCalendarToSheet() {
  const cal = CALENDAR_ID === 'primary' ? CalendarApp.getDefaultCalendar() : CalendarApp.getCalendarById(CALENDAR_ID);
  const day = (s, add) => { const p = s.split('-').map(Number); return new Date(p[0], p[1] - 1, p[2] + (add || 0)); };
  const events = cal.getEvents(day(FROM), day(TO, 1));   // the end is exclusive, so use the day after TO
  const rows = [['Title', 'Start', 'End', 'All day', 'Hours', 'Location', 'Description', 'Guests', 'Event ID']];
  events.forEach(function (e) {
    const allDay = e.isAllDayEvent();
    const start = allDay ? e.getAllDayStartDate() : e.getStartTime();
    // Google returns the day AFTER an all-day event ends, so step back one day for the real last day.
    const end = allDay ? new Date(e.getAllDayEndDate().getTime() - 86400000) : e.getEndTime();
    const hours = allDay ? '' : Math.round((end - start) / 36e5 * 100) / 100;
    const guests = e.getGuestList().map(function (g) { return g.getEmail(); }).join(', ');
    rows.push([e.getTitle(), start, end, allDay, hours, e.getLocation(), e.getDescription(), guests, e.getId()]);
  });
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const tab = ss.insertSheet('Calendar ' + FROM + ' to ' + TO);
  tab.getRange(1, 1, rows.length, rows[0].length).setValues(rows);
  if (rows.length > 1) tab.getRange(2, 2, rows.length - 1, 2).setNumberFormat('yyyy-mm-dd hh:mm');
  tab.setFrozenRows(1);
}
  1. Change FROM and TO to the first and last day you want. To export a calendar other than your main one, replace 'primary' with its ID from Settings and sharing → Integrate calendar.
  2. Click Save, then Run. Authorise the script the first time (it’s your own script, in your account).
  3. Switch back to the spreadsheet. A new tab such as “Calendar 2026-10-01 to 2026-10-31” holds the events.

Two details it gets right that many copies online don’t: all-day events show their real last day (Google reports the day after an all-day event ends, so the script steps back one day), and the date range includes the whole of the last day. Each occurrence of a repeating event becomes its own row. We tested the script against stand-ins for the Calendar and Sheets services before publishing.

The tab is a snapshot. Changing a row doesn’t change the calendar, and new events don’t appear until you run it again (into a new tab, because the tab name already exists).

2. An add-on: CalendarRows

CalendarRows is our add-on that sends rows to Google Calendar and also imports a calendar into your sheet:

  1. Install CalendarRows from the Google Workspace Marketplace (the button below).
  2. In your spreadsheet, choose Extensions → CalendarRows → Open CalendarRows and open the From Calendar tab.
  3. Pick a calendar and a date range, or a quick range such as This month or Next 30 days. Optionally type a word to import only events that contain it, such as a client’s name.
  4. Choose the columns. Title, Start, End, All day, Duration (hours) and Location are ticked by default; Description, Guests, Color, Event status and Calendar link are optional.
  5. Click Import events. The events go into a new tab with their event IDs. Your other tabs aren’t touched.

Unlike the snapshot methods, the new tab can go back the other way: edit it and use the To Calendar tab with the same calendar, and the matching events are updated.

3. Calendar’s own export (.ics)

In Google Calendar on a computer: gear icon → Settings → Import & export → Export. You get a ZIP file with one .ics file per calendar, covering every event since the calendar was created. There is no date range, and Google Sheets can’t open .ics files, so you need a converter to turn it into CSV, and repeat rules, time zones and line breaks usually need cleaning afterwards.

It’s the right tool for a backup or a move to another calendar app. For a month of events in a spreadsheet, the first two methods are far less work. Be careful with online converters that ask for your calendar’s “secret address in iCal format”: anyone with that link can read your calendar.

Export last month automatically

For a timesheet or invoice every month, let the script work out the dates. Replace the two lines that set FROM and TO with these:

const now = new Date();
const fmt = (d) => Utilities.formatDate(d, Session.getScriptTimeZone(), 'yyyy-MM-dd');
const FROM = fmt(new Date(now.getFullYear(), now.getMonth() - 1, 1));   // first day of last month
const TO = fmt(new Date(now.getFullYear(), now.getMonth(), 0));         // last day of last month

Run in November 2026, that gives 2026-10-01 to 2026-10-31; run in January, it gives December of the year before; in March of a leap year it ends on February 29. We checked those cases. Then add a trigger: in the editor click Triggers (the clock) → Add Trigger, choose exportCalendarToSheet, Time-driven, Month timer, on the 1st. Each run makes a new tab named after the month, so earlier months are kept.

Several calendars. Run the script once per calendar, changing CALENDAR_ID, and rename the tabs (say Work and Personal). Stack them in one list with =FILTER({Work!A2:I; Personal!A2:I}, {Work!A2:A; Personal!A2:A}<>""); the second part drops the empty rows below each list.

Billing in 15-minute steps. To round each event up to the next quarter hour, add a column with =CEILING(E2, 0.25): 0.6 hours becomes 0.75, and 1.1 becomes 1.25.

Total the hours

Once the events are rows, the usual reason for exporting is a total: hours per client, per project or per month. Both methods above give you an hours column (Hours in the script, Duration (hours) in CalendarRows). Then:

  • Hours per event title: select the data and choose Insert → Pivot table. Add the title column under Rows and the hours column under Values, summarised by SUM.
  • Hours for one client: with titles in A and hours in E, =SUMIF(A2:A, "*Acme*", E2:E) totals every event whose title contains “Acme”.
  • Hours in one month: with start times in B, =SUMIFS(E2:E, B2:B, ">="&DATE(2026,10,1), B2:B, "<"&DATE(2026,11,1)).

All-day events count differently: the script leaves their Hours blank, but CalendarRows gives them 24 hours per day, so filter on the All day column before you total.

Things that trip people up

  • Times look shifted. Times are written in the spreadsheet’s time zone (File → Settings). Set it to the zone you work in before you export.
  • Declined meetings show up. Events you were invited to and declined can still be on your calendar. CalendarRows’ Event status column won’t flag them: it shows the event’s own status (confirmed, tentative or cancelled), not your reply, so remove them by hand.
  • Shared calendars. You can export any calendar you can see. Events marked private on someone else’s calendar may show only as busy, with no details.

Frequently asked questions

Can Google Calendar export directly to Google Sheets?

No. Its built-in export produces an .ics file of the whole calendar. To get a date range as rows, use an Apps Script (free) or an add-on such as CalendarRows.

How do I export only one month of events?

Use the Apps Script in this guide with FROM and TO set to the first and last day of the month, or CalendarRows' From Calendar tab with a date range or the This month quick range.

How are repeating events exported?

Both the script and CalendarRows write one row per occurrence, so a weekly meeting over a month becomes four or five rows. The .ics export keeps a single event with a repeat rule instead.

Can I export someone else's calendar?

Only calendars shared with you, and only the details they let you see. Private events may appear as busy without a title.

Is there a limit to how many events I can export?

The Apps Script has no fixed limit apart from Google's 6-minute run time per execution. CalendarRows imports up to 100 events at a time on Free and up to 5,000 on Pro.

Can I export Google Calendar to Sheets automatically every month?

Yes. Change the script's dates to last month as the guide shows, then add a time-driven trigger with a month timer. Each run adds a new tab for that month.

Can I export Google Calendar to Excel?

Export it to Google Sheets first with the script or CalendarRows, then choose File, then Download, then Microsoft Excel (.xlsx).

Disclosure: we make CalendarRows, the Google Sheets add-on mentioned in this guide. We describe the other methods as fairly as we can, including when they are the better choice.

Google Sheets and Google Calendar are trademarks of Google LLC. Microsoft and Excel are trademarks of the Microsoft group of companies. Zapier is a trademark of Zapier, Inc. CalendarRows and Cellbrook are not affiliated with or endorsed by Google, Microsoft or Zapier.