The duplicate events problem

Why your Apps Script makes duplicate events every run

The script worked. You ran it again after fixing two times in the sheet, and now every event exists twice. Nothing in your code is wrong in the way you're looking for. The function you called only knows how to create, and the piece that would let it recognize its own work is something the script has to save for itself.

A hand-typed calendar with an event at the wrong hour beside the same events synced correctly from a spreadsheet
Calendar Sync for Sheets™: one row, one event, however many times you sync.

The shortcut · from Marinette

Calendar Sync for Sheets™

Install free from the Marketplace →

No account to create · No website login · Your data stays in your Google account

createEvent only ever creates

Google's own CalendarApp reference describes createEvent() in three words: it creates a new event. That's the whole contract. There's no method anywhere in that reference that finds an event by title, by date, or by description and updates it instead.

So a loop like this isn't buggy. It's doing exactly what it says, twice.

The shape almost every tutorial gives you

rows.forEach(function (row) {
  cal.createEvent(row.title, row.start, row.end);   // always a new event
});

The only way to reach an event you already made is getEventById(), and it wants the event's iCal id. Not the title. Not the date. The id, which existed only for a moment inside the run that created it, and which vanished when that run ended.

That's the missing half of the job: not creating events, but remembering which row made which one.

What the second run does

Loop the rows, call createEvent

How duplicates arrive

Run 1

Forty rows, forty createEvent calls, forty events. Correct.

Edit

Two times move. You fix those two rows in the sheet.

Run 2

Forty more createEvent calls. The script has no record of run one, so it makes forty more events.

Eighty events. Thirty-eight duplicates and two corrections you now have to find by hand.

Give every row an id

What the script actually needs

Run 1

Create the event, call getId() on it, write that id back into a column on the same row.

Edit

Two times move. The ids stay where they are.

Run 2

Read the id first. Empty means create and save the id. Filled means getEventById() and update in place.

Forty events, two of them moved. The sheet is now the record of who owns what.
Or skip the bookkeeping: install Calendar Sync free →

Writing that yourself is a reasonable afternoon. Keeping it honest afterwards is the part that grows: what happens when somebody deletes the event in Google Calendar and the id now points at nothing, when a row is deleted but its event isn't, when two people run the script at once, when the id column gets sorted away from its row.

The three walls after that one

These are published in Google's Apps Script quota documentation, and a row-by-row calendar loop meets all three sooner than you would guess.

Apps Script quotas · Script runtime

"6 min / execution"

The same ceiling for consumer and Google Workspace accounts. A loop that creates an event and writes back to the sheet on every row can reach it within a few hundred rows, and the run simply stops partway.

Apps Script quotas · Calendar events created

"5,000 / day"

On a consumer account, 10,000 on Workspace. Easy to ignore until a re-run doubles a season of shifts.

Apps Script quotas · Triggers total runtime

"90 min / day"

On a consumer account, 6 hours on Workspace. This is the one that quietly kills the nightly sync you set up and forgot about.

None of this makes a script the wrong choice. If the job runs once, or the rules are strange enough that no add-on will ever fit them, a script is the right tool and you should write it. It's worth knowing what you've signed up for before the calendar has eighty events in it.

Your own script vs. Calendar Sync for Sheets™

A script you maintain Calendar Sync for Sheets™
Running it twice Duplicates everything unless you store ids yourself One row, one event, every time
Knowing what will happen Whatever the code does, found out afterwards A preview first: created, updated, and who gets emailed
Someone edits the event in Calendar The next run overwrites it, or fails on a dead id The row says so instead of silently overwriting
The 6 minute execution ceiling Yours to batch around Handled, with progress you can watch
Handing it to a colleague They inherit your code and its authorization prompts They install it from the Marketplace
Cost Free, plus your time forever Free. No caps, no card, no account

Script behavior above reflects Google's own documentation, the CalendarApp reference and Apps Script quotas, viewed July 24, 2026.

Let the sheet remember which event is which.

Calendar Sync for Sheets™ is free. No card, no account to create.

Install Calendar Sync for Sheets™ free →

Common questions

Because createEvent() only ever creates. Google's CalendarApp reference describes it as creating a new event, and no method in that reference finds an existing event by title, date, or description and updates it. A script that loops rows and calls createEvent() has no memory of what it made last time, so the second run makes a second copy of everything.

The only way back to an event you already created is getEventById(), which needs the event's iCal id, so the script has to have saved that id on the first run.

Store the id. When the script creates an event, call getId() on it and write that value into a column on the same row. On later runs, read that cell first: empty means create the event and save the new id, filled means call getEventById() and update that event in place.

The sheet becomes the record of which row owns which event, which is the part most tutorial scripts leave out.

Apps Script caps a single execution at 6 minutes, for consumer and Google Workspace accounts alike. A loop that creates an event and writes back to the sheet on every row can reach that in a few hundred rows, and the run stops wherever it got to.

The daily quotas are worth knowing too: 5,000 calendar events created per day on a consumer account and 10,000 on Workspace, plus 90 minutes of total trigger runtime per day on consumer accounts and 6 hours on Workspace.

A script is a good answer when the job runs once, the rules are unusual, and you're happy maintaining it. An add-on is the better answer when the same sheet is edited again and again by people who should not have to think about ids, quotas, or authorization prompts.

Calendar Sync for Sheets™ does the id bookkeeping, previews every change before it happens, and says what was created, what was updated, and what someone changed in Calendar.

Yes, but not on the same rows at the same time. Calendar Sync keeps its own sync identity per row, and a separate script creating events from those same rows would produce a second set that Calendar Sync didn't make and doesn't track. Point them at different sheets, or let one of them own the schedule.

Different door, same room. A CSV row carries no event id either, so a second import adds a second copy rather than updating the first. Why uploading a CSV to Google Calendar duplicates your events →

Method behavior and quota numbers on this page come from Google's own developer documentation, the CalendarApp reference and Apps Script quotas, viewed July 24, 2026. Google may change quotas at any time.

Still have a question? I'm happy to help.

Email support@getmarinette.com