Spyridon Pikoulas · HeyHeyThere

Create Google Calendar™ events from a Google Sheet, without duplicates

People keep all kinds of schedules in spreadsheets (shifts, class timetables, content calendars, bookings) and want them on Google Calendar™. There are three ways to get them there. Watch out for one thing: a sheet-to-calendar script that doesn’t remember which events it made creates every event again each time it runs.

1. Import a CSV file (no setup, one time)

Put the events in a sheet under these headers, in English: Subject, Start Date, Start Time, End Date, End Time, All Day Event, Description, Location. Only the first two are required. Dates must read like 05/30/2026 and times like 10:00 AM. Then:

  1. In Sheets™: File > Download > Comma Separated Values (.csv). This saves the sheet you’re on.
  2. In Google Calendar™: the gear icon > Settings > Import & export. Choose the file and a calendar, then click Import.

This works once. The file has no event IDs, so importing it again adds every event a second time, and later edits to the sheet never reach the calendar.

2. An Apps Script that remembers its events (free, a few minutes)

This script makes one event per row and writes the event’s ID into the row. On the next run, a row that has an ID updates its event instead of creating another one.

Lay out the sheet with a header row and:

B and C have to be real dates (Format > Number > Date time), not text that only looks like one. The script skips a row whose start is text, and it treats a text end as no end.

Go to Extensions > Apps Script, replace everything in Code.gs with the code below, and save:

function onOpen() {
  SpreadsheetApp.getUi().createMenu('Calendar')
    .addItem('Sync events', 'syncEvents')
    .addToUi();
}

/** Title in A, start in B, end in C (empty for an all-day event), D left to the script. */
function syncEvents() {
  var sheet = SpreadsheetApp.getActiveSheet();
  var zone = SpreadsheetApp.getActive().getSpreadsheetTimeZone();
  var cal = CalendarApp.getDefaultCalendar();
  var rows = sheet.getDataRange().getValues();
  for (var i = 1; i < rows.length; i++) {
    var title = rows[i][0], start = rows[i][1], end = rows[i][2], id = rows[i][3];
    if (!title || !(start instanceof Date)) continue;
    var timed = end instanceof Date;
    // Calendar reads an all-day date in the script's time zone: use the date the sheet shows.
    var ymd = Utilities.formatDate(start, zone, 'yyyy-M-d').split('-');
    var day = new Date(ymd[0], ymd[1] - 1, ymd[2]);
    var event = id && cal.getEventById(id);
    if (event) {
      event.setTitle(title);
      if (timed) event.setTime(start, end);
      else event.setAllDayDate(day);
    } else {
      event = timed ? cal.createEvent(title, start, end) : cal.createAllDayEvent(title, day);
      sheet.getRange(i + 1, 4).setValue(event.getId());
    }
  }
}

Reload the spreadsheet and a Calendar menu appears; choose Calendar > Sync events. The first run asks you to authorize the script. It’s your own script, so if Google warns that it hasn’t verified the app, click Advanced > Go to (your project name) (unsafe).

What to know:

3. EventFill (free for 50 events a day)

EventFill is the add-on I make for this, if you’d rather not keep a script. Open it from Extensions > EventFill > Open EventFill. Pick the title and date columns (and times, end dates, description, location, guests and color if you have them), pick a calendar, and click Add to Calendar:

It reaches only the spreadsheet you open it in and your calendars’ events, never your calendars’ settings or your other files. 50 events a day are free, every day. Past that, EventFill Pro is $4.99 a month.

Which one

For a one-off list that won’t change, use the CSV import. For a sheet you keep editing, use the script or EventFill, since both update events instead of doubling them.