Cellbrook

Google Sheets guide · Calendars

How to create Google Calendar events from Google Sheets

Google Sheets™ and Google Calendar™ don’t talk to each other on their own, so a schedule in a sheet stays a schedule in a sheet. Here are four ways to turn the rows into events, and the one problem every method has to solve: running it twice without getting every event twice.

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

Updated · 13 min read · By the Cellbrook team

Set up the sheet

Every method below reads the same thing: one event per row, under a header row. This layout works for all of them:

TitleStartEndLocationDescription
Team standup11/2/2026 9:00:0011/2/2026 9:15:00OnlineWeekly check-in
Site visit11/10/2026 10:00:0011/10/2026 11:30:00400 Broad St, SeattleBring hard hats
  • Make Start and End real dates. Select the columns and choose Format → Number → Date time. A date stored as text is the number one reason events land on the wrong day or don’t appear at all. Quick test: =ISNUMBER(B2) returns TRUE for a real date.
  • Check the spreadsheet’s time zone in File → Settings. Scripts and add-ons read times in that zone, so a sheet set to New York time sends 9:00 AM New York time, even if you are in Los Angeles.
  • Keep one row per event. A weekly meeting is either one row per week or a repeat rule (see the FAQ), not a row with “every Monday” typed in a cell.

The four methods at a glance

MethodCostUpdates existing eventsBest for
CSV importFreeNo: importing again makes duplicatesA one-time list you will never change
Apps Script (below)FreeYes, with an Event ID columnPeople happy to paste and run a script
Zapier and similar toolsFree tier, then paidNot in the basic “new row” setupAdding an event whenever someone adds a row
CalendarRows add-onFree for 50 changes a dayYes, and deletes tooA schedule you keep editing in the sheet

1. One-off: CSV import

Google Calendar can import a CSV file, but only with its own English column names: Subject, Start Date, Start Time, End Date, End Time, All Day Event, Description, Location and Private. Only Subject and Start Date are required. Rename or rebuild your columns to match, then File → Download → Comma-separated values, and in Google Calendar on a computer go to Settings → Import & export → Import.

It works, with three catches: there is no column for guests, colours or repeats; each import adds new events, so importing a corrected file gives you every event twice; and there is no undo button. Our step-by-step CSV import guide has the exact formats, formulas that build the columns for you, and a safe way to test an import first.

2. Free: an Apps Script that doesn’t duplicate

Most scripts you find online call createEvent for every row, every time. Run them twice and you get two of everything. The fix is simple: write each new event’s ID back into the sheet, and next time update that event instead of making a new one. This script does that.

  1. Lay out your sheet as A Title, B Start, C End, D Location, E Description, and add the header Event ID in F1. Leave column F empty below it.
  2. Choose Extensions → Apps Script, delete the sample code and paste:
// Sheet layout, header in row 1:
// A Title | B Start | C End | D Location | E Description | F Event ID (the script fills this in)
// Start and End must be real date-time cells (Format > Number > Date time), not text.
const CALENDAR_ID = 'primary';   // or a Calendar ID from the calendar's Settings > Integrate calendar

function onOpen() {
  SpreadsheetApp.getUi().createMenu('Calendar')
    .addItem('Send rows to Calendar', 'sendRowsToCalendar')
    .addToUi();
}

function sendRowsToCalendar() {
  const sheet = SpreadsheetApp.getActiveSheet();
  const last = sheet.getLastRow();
  if (last < 2) return;
  const cal = CALENDAR_ID === 'primary' ? CalendarApp.getDefaultCalendar() : CalendarApp.getCalendarById(CALENDAR_ID);
  const rows = sheet.getRange(2, 1, last - 1, 6).getValues();
  const isDate = (v) => v && typeof v.getTime === 'function' && !isNaN(v.getTime());
  let created = 0, updated = 0, skipped = 0;
  const ids = rows.map(function (r) {
    const [title, start, end, location, description, id] = r;
    if (!title || !isDate(start)) { skipped++; return [id]; }
    const stop = isDate(end) ? end : new Date(start.getTime() + 60 * 60 * 1000);   // no end: 1 hour
    let ev = id ? cal.getEventById(String(id)) : null;
    if (ev) {
      ev.setTitle(title).setTime(start, stop).setLocation(location).setDescription(description);
      updated++;
    } else {
      ev = cal.createEvent(title, start, stop, { location: location, description: description });
      created++;
    }
    return [ev.getId()];
  });
  sheet.getRange(2, 6, ids.length, 1).setValues(ids);
  SpreadsheetApp.getUi().alert(created + ' created, ' + updated + ' updated, ' + skipped + ' skipped.');
}
  1. Click Save, then reload the spreadsheet. A Calendar menu appears.
  2. Choose Calendar → Send rows to Calendar. The first time, Google asks you to authorise the script. It is your own script, running in your account.
  3. Edit a row and run it again. The event moves or renames; it doesn’t duplicate. A row with no title or no real start date is skipped.

To use a calendar other than your main one, replace 'primary' with the calendar’s ID from Settings and sharing → Integrate calendar in Google Calendar. We tested the script against stand-ins for the Sheets and Calendar services before publishing.

What it doesn’t do, so you know before you rely on it: all-day events, guests, colours and repeats need more code; deleting a row doesn’t delete its event; and if you delete an event in Google Calendar, clear that row’s Event ID so the next run can create it again. It also updates every row on every run, which is fine for hundreds of rows but slow for thousands, and Google limits how many events a script can create per day (5,000 for a personal account).

The script with all-day events, guests, colours and reminders

This longer version of the script handles the four things the basic one leaves out. It keeps the same Event ID trick, so running it again still updates instead of duplicating. Add these headers to the same tab, now named Events:

ColumnHeaderWhat to put in it
GAll dayA tick box (Insert → Checkbox). Ticked rows become all-day events; End can be empty for a one-day event or a later date for several days.
HGuestsEmail addresses separated by commas. Guests are added when the event is created, without an invitation email.
IColorA number from 1 to 11: 1 Lavender, 2 Sage, 3 Grape, 4 Flamingo, 5 Banana, 6 Tangerine, 7 Peacock, 8 Graphite, 9 Blueberry, 10 Basil, 11 Tomato.
JReminderMinutes before the event for a pop-up reminder, from 5 to 40320 (four weeks). Empty keeps your calendar’s default.
// Tab named Events, header in row 1:
// A Title | B Start | C End | D Location | E Description | F Event ID (the script fills this in)
// G All day (checkbox) | H Guests (emails, comma-separated) | I Color (1-11) | J Reminder (minutes)
const CALENDAR_ID = 'primary';   // or a Calendar ID from the calendar's Settings > Integrate calendar
const SHEET_NAME = 'Events';

function onOpen() {
  SpreadsheetApp.getUi().createMenu('Calendar')
    .addItem('Send rows to Calendar', 'sendRowsToCalendar')
    .addToUi();
}

function sendRowsToCalendar() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
  if (!sheet) throw new Error('There is no tab named ' + SHEET_NAME);
  const last = sheet.getLastRow();
  if (last < 2) return;
  const cal = CALENDAR_ID === 'primary' ? CalendarApp.getDefaultCalendar() : CalendarApp.getCalendarById(CALENDAR_ID);
  if (!cal) throw new Error('Calendar not found, or you cannot edit it: ' + CALENDAR_ID);
  const rows = sheet.getRange(2, 1, last - 1, 10).getValues();
  const isDate = (v) => v && typeof v.getTime === 'function' && !isNaN(v.getTime());
  const day = (d, plus) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + (plus || 0));
  let created = 0, updated = 0, skipped = 0;
  const ids = rows.map(function (r) {
    const [title, start, end, location, description, id, allDay, guests, color, reminder] = r;
    if (!title || !isDate(start)) { skipped++; return [id]; }
    const options = { location: String(location), description: String(description),
                      guests: String(guests || '').replace(/\s+/g, ''), sendInvites: false };
    let ev = id ? cal.getEventById(String(id)) : null;
    if (allDay === true) {
      const first = day(start);
      const after = day(isDate(end) && end > start ? end : start, 1);   // the end date is exclusive
      if (ev) { ev.setAllDayDates(first, after); updated++; }
      else { ev = cal.createAllDayEvent(String(title), first, after, options); created++; }
    } else {
      const stop = isDate(end) ? end : new Date(start.getTime() + 60 * 60 * 1000);   // no end: 1 hour
      if (ev) { ev.setTime(start, stop); updated++; }
      else { ev = cal.createEvent(String(title), start, stop, options); created++; }
    }
    ev.setTitle(String(title)).setLocation(options.location).setDescription(options.description);
    const c = String(color).trim();
    if (/^([1-9]|1[01])$/.test(c)) ev.setColor(c);                  // 1 Lavender ... 11 Tomato
    const mins = Number(reminder);
    if (reminder !== '' && mins >= 5 && mins <= 40320) {          // Calendar allows 5 min to 4 weeks
      ev.removeAllReminders();
      ev.addPopupReminder(mins);
    }
    return [ev.getId()];
  });
  sheet.getRange(2, 6, ids.length, 1).setValues(ids);
  const msg = created + ' created, ' + updated + ' updated, ' + skipped + ' skipped.';
  try {
    SpreadsheetApp.getUi().alert(msg);
  } catch (e) {
    console.log(msg);   // run by a trigger: there is no screen to show an alert on
  }
}

Replace the basic script with this one, save, reload the spreadsheet and use Calendar → Send rows to Calendar as before. When it finishes it tells you how many events it created, updated and skipped. A row with no End gets a one-hour event; an all-day row from November 10 to November 12 covers all three days (Calendar stores the day after the last one, and the script adds it for you). A colour or reminder outside the allowed range is ignored rather than stopping the run.

Guests are only set when the event is first created, so to change the guest list later, edit the event in Google Calendar. We tested this version against stand-ins for the Sheets and Calendar services, including re-runs, all-day ranges and bad colour values, before publishing.

Run it automatically

You don’t have to click the menu. Apps Script can run the function on a timer or whenever a Google Form adds a row:

  1. In the Apps Script editor, click the clock icon (Triggers) on the left, then Add Trigger.
  2. Choose the function sendRowsToCalendar and the event source:
    • Time-driven, then an hour or day timer, for a sheet people edit by hand.
    • From spreadsheet and On form submit, for a sheet that collects Google Form responses.
  3. Click Save and authorise again if asked.

A trigger has no screen, so the summary goes to the script’s log (Executions on the left) instead of a pop-up. Triggers share a daily allowance of run time (90 minutes a day on a personal account), and every run updates every row, so an hourly timer on a very long sheet uses more of it than you might expect. For Form responses, put the formulas that build Title, Start and End on a separate tab, not in the response tab itself, because new responses are inserted as new rows.

3. Automation tools

Zapier, Make and similar services can watch a sheet and create an event whenever a new row appears. That suits forms and sign-ups that land in a sheet one row at a time. They are less suited to a schedule you plan in bulk and then edit: the basic setup reacts to new rows, so changing a date in the sheet doesn’t move the event unless you build a second, more complex flow. You also need an account with the service, and larger volumes move you onto a paid plan.

4. An add-on: CalendarRows

CalendarRows is our add-on for this job. It does what the script does, plus the parts the script leaves out:

  1. Install CalendarRows from the Google Workspace Marketplace (the button at the end of this section).
  2. In your sheet, choose Extensions → CalendarRows → Open CalendarRows. On the To Calendar tab, pick the sheet, the header row and the calendar.
  3. Check the columns it suggests from your headers. Only Title and Start date are required; Location, Description, Guests, Color, All day and an Action column are optional. Start and end can be in one date-time cell or separate date and time columns.
  4. Click Preview to see how many events will be created, updated or deleted, and which rows have problems. Nothing changes yet.
  5. Click Send to Calendar. CalendarRows adds an event ID column and a status column (CREATED, UPDATED, DELETED, or an error with the reason).

After that, the sheet is the control panel: edit a row and send again to update its event, or type delete in the Action column to remove it. Only rows that changed are sent. A row with a date and no time becomes an all-day event. Guests are added to the event without CalendarRows sending them an invitation email.

Fix the common problems

  • Events are an hour or more off. The spreadsheet’s time zone (File → Settings) differs from your calendar’s. Change one to match.
  • Events land on the wrong day, or 1/2 becomes February 1. The dates are text, or typed in a different order from the spreadsheet’s locale. Re-enter them as real dates, or use =DATE(2026, 11, 2).
  • Every event appears twice. Something created them again instead of updating. With CSV, delete the duplicates (or the test calendar) and import once. With a script, keep the Event ID column.
  • “You don’t have permission” or the calendar isn’t listed. You can only add events to calendars where you have “Make changes to events” access.

When the script misbehaves

  • The Calendar menu doesn’t appear. The menu is added when the spreadsheet opens. Save the script, then reload the spreadsheet tab. If it still isn’t there, check that the script was opened from this spreadsheet with Extensions → Apps Script.
  • “Google hasn’t verified this app”. That screen appears for any script you write yourself. Click Advanced, then Go to your project’s name. Only do this for code you’ve read and trust.
  • Rows are counted as skipped. The Title is empty or the Start cell is text. Click a Start cell: if it’s left-aligned or =ISNUMBER(B2) says FALSE, Sheets didn’t recognise it as a date. Retype it in your spreadsheet’s date format, or build it with =DATE(2026, 11, 2)+TIME(9, 0, 0).
  • “Calendar not found, or you cannot edit it”. The Calendar ID is mistyped, or you can only see that calendar. Copy the ID again from Settings and sharing → Integrate calendar, and ask the owner for “Make changes to events”.
  • “You have been creating or deleting too many calendars or calendar events in a short time”. Google slows down accounts that create many events in a burst. Wait a while, then run again: rows that already have an Event ID are updated, not recreated, so nothing doubles.
  • An event was deleted in Calendar and the row now fails. Clear that row’s Event ID so the next run creates a new event.

Frequently asked questions

Can Google Sheets add events to Google Calendar automatically?

Not by itself. You need a CSV import (one-time), an Apps Script, an automation tool such as Zapier, or an add-on such as CalendarRows.

How do I stop duplicate events when I run my script again?

Save each event's ID in the sheet when you create it. On later runs, look the event up with getEventById and update it instead of calling createEvent again. The script in this guide does exactly that.

Can I create repeating events from a spreadsheet?

The Calendar CSV import has no repeat column. In Apps Script you can use createEventSeries. CalendarRows Pro reads a Repeat column such as weekly x 10 or monthly until 2026-12-31.

Will the guests get an email invitation?

CalendarRows adds guests without sending invitation emails, so nobody is emailed by surprise. The CSV import has no guest column at all.

Can I send rows to a shared calendar?

Yes, if you have permission to make changes to its events. In a script, use the calendar's ID; in CalendarRows, pick it from the list, which only shows calendars you can edit.

Can I create all-day events from Google Sheets?

Yes. The longer script in this guide adds an All day tick box column. Ticked rows become all-day events, and an End date makes them span several days.

Can the script run automatically when a Google Form is submitted?

Yes. In the Apps Script editor, add a trigger for the function with the event source From spreadsheet and the type On form submit. Keep your formula columns on a separate tab from the form responses.

Can I set a colour for each event?

Yes, with a number from 1 to 11 in the Color column of the longer script: 1 is Lavender and 11 is Tomato. The guide lists all eleven.

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.