Calrows
Updated 7 October 2026. Written by the developer of Calrows; Apps Script limits link to Google's documentation.
You don't need an add-on to put Google Sheets™ rows on Google Calendar™. Apps Script, built into every Google Sheet, can create, update and read events for free. Here is a script that works, the limits it runs into, and when Calrows is worth installing instead.
Write the script if the job is small or one-off, you're comfortable with JavaScript, or you need something Calrows doesn't do, such as recurring events. Use an add-on if the sheet keeps changing, takes minutes to process, needs both directions, or must keep working for people who don't code. Calrows runs on Apps Script too, so Google's limits still apply; the code that handles them is already written.
Make a tab named Events with headers in row 1: A Title, B Start, C End (optional), D Event ID (leave it empty). Start and End must be full date-times, not text or a time on its own. Open Extensions › Apps Script, paste the code, save and click Run. The first run asks you to allow access to your spreadsheets and calendars.
/**
* Tab "Events", headers in row 1.
* A: Title B: Start C: End (optional) D: Event ID (leave empty)
*/
function syncRowsToCalendar() {
var sheet = SpreadsheetApp.getActive().getSheetByName('Events');
var calendar = CalendarApp.getDefaultCalendar(); // or CalendarApp.getCalendarById('...')
var rows = sheet.getDataRange().getValues();
var started = Date.now();
for (var i = 1; i < rows.length; i++) { // rows[0] is the header row
if (Date.now() - started > 5 * 60 * 1000) { // stop before Google's 6-minute limit
console.log('Stopped at row ' + (i + 1) + '. Run again to continue.');
return;
}
var title = String(rows[i][0]).trim();
var start = rows[i][1], end = rows[i][2], id = rows[i][3];
if (!title || !(start instanceof Date)) continue; // skip blank rows and text dates
if (start.getFullYear() < 1900) continue; // a time with no date reads as 30 Dec 1899
if (!(end instanceof Date)) end = new Date(start.getTime() + 60 * 60 * 1000); // 1 hour
if (id) { // sent before: update, never re-create
var event = calendar.getEventById(id);
if (!event) continue; // no such event in this calendar
if (event.getTitle() !== title) event.setTitle(title);
if (event.getStartTime().getTime() !== start.getTime() ||
event.getEndTime().getTime() !== end.getTime()) {
event.setTime(start, end);
}
continue;
}
var created = calendar.createEvent(title, start, end);
sheet.getRange(i + 1, 4).setValue(created.getId()); // sheet row i + 1, column D
SpreadsheetApp.flush(); // write the ID now, not at the end
}
}
To run it on a schedule, open Triggers (the clock icon) in the editor and add a time-driven trigger for syncRowsToCalendar.
It leaves out all-day events, guests, descriptions, deleting events for deleted rows, pulling Calendar edits back, spotting rows that share an ID after a copy, and reporting which row failed. Any error stops the run there.
Google stops any script after six minutes with "Exceeded maximum execution time". Older posts mention 30 minutes for Workspace, but Google's quotas page now lists 6 minutes per execution for personal and Workspace accounts alike. Each new event is a call to Google Calendar, so a few hundred new rows can run out of time. Continuing automatically means saving your place and scheduling a one-off trigger.
Apps Script can create 5,000 calendar events a day on a personal Google account and 10,000 on Google Workspace, per user. A short-term limit can bite first: rapid calls can fail with "Service invoked too many times in a short time: Calendar. Try Utilities.sleep(1000) between calls." That one-second pause also caps a five-minute run at fewer than 300 rows.
Deleting an event in Google Calendar marks it as cancelled, and Google's Calendar API reference says its get method always returns cancelled events. In our testing, getEventById() also returned deleted events, while getEvents() for the same time left them out. So the script above can't tell a deleted event from a live one; to catch it, check that getEvents() over the event's times still lists its ID.
Your own trigger can run as often as every minute; add-ons are limited to once an hour. But trigger runs share a daily allowance: 90 minutes on a personal account, 6 hours on Workspace. A five-minute run every hour needs 120 minutes a day, more than a personal account allows, and past the allowance runs fail with "Service using too much computer time for one day". And a scheduled run that overlaps a manual one can create the same events twice unless you add a LockService lock.
All-day events need createAllDayEvent(), whose end date is exclusive, and can land a day off if the script's time zone differs from the spreadsheet's. Then you maintain it. A column inserted in the wrong place, or a cleared column D, can make it create every event again. The platform changes too: Google deprecated the original Rhino runtime, so older scripts must move to V8. Easy for a developer; harder once the developer has moved on.
The reverse is simpler: calendar.getEvents(start, end) returns a range of events, and one setValues() call writes them out. All-day events are stored as ending the day after they finish, and long ranges can hit the six-minute limit. For a timesheet, skip all-day events and divide end - start by 3,600,000 for hours. Our export guide has a complete script.
In Calrows, Import from Calendar and Timesheet do this from the sidebar; timesheets include rates, amounts, totals and a summary by title. The free plan covers 50 events per run. Pro is unlimited, but these don't continue in a second run, so a range too big for the time limit is cut short and flagged.
| Your own Apps Script | Calrows | |
|---|---|---|
| Cost | Free | Free: 50 new events a month; imports and timesheets up to 50 events. Pro: $6 for a month or $48 for a year. Team: $190 for a year, up to 10 people. One-time payments, no auto-renewal |
| Setup | Adapt the code to your columns | Install, pick a calendar, confirm the column mapping it guesses |
| Running it again | No duplicates only if you store event IDs | Hidden _eventId column: changed rows update, unchanged rows are skipped |
| ID column deleted or cleared | Next run duplicates every event | Refuses to send |
| Over six minutes of work | Stops; run it again or write continuation code | Pauses at 4.5 minutes; a follow-up run continues from that row |
| Google rate limits | The run fails unless you catch the error | Stops at that row; the next run picks up the rest |
| Two runs at once | Can create the same events twice | A lock blocks the second run |
| Event deleted in Calendar | getEventById() may still return it | Detected; the row is logged as an error |
| Row deleted from the sheet | Its event stays unless you code deletion | "Remove events for deleted rows", after you confirm; only events Calrows created from that sheet |
| Calendar edits back to the sheet | More code | "Pull changes from Calendar into sheet", for linked rows |
| Calendar to Sheets, timesheets | Separate scripts | Built in, any date range |
| Schedule | A trigger, as often as every minute | Pro: hourly, every 6 hours or daily |
| Recurring events, Meet links, a calendar per row | Possible (createEventSeries(), the advanced Calendar service) | Not supported |
| Record of each run | The Executions page, plus your own logging | Sync Log tab: created, updated and failed rows, with reasons |
| Your data | Stays in your Google account | Stays in your Google account; only your email address is sent, to check your plan |
| Maintenance | You | The developer |
Yes. createEvent() turns a row into an event, and storing the ID from getId() in the sheet lets later runs update that event instead of duplicating it. Two-way updates, deletions, all-day events and long sheets take more code.
Because createEvent() always makes a new event. Save each event's ID in a column as soon as it is created, and on later runs update the event from getEventById() instead.
Apps Script stops every run after six minutes, on personal and Google Workspace accounts alike. Stop your loop at about five minutes and run again; with IDs stored, finished rows aren't duplicated. Calrows pauses at 4.5 minutes and continues from the same row in a follow-up run.
Google's quotas page lists 5,000 a day for a personal Google account and 10,000 for Google Workspace, per user. Creating events in quick succession can fail sooner, with "Service invoked too many times in a short time: Calendar".
For one-off jobs; when you write code and want what Calrows doesn't do, such as recurring events, Meet links or runs more often than hourly; when add-ons aren't allowed; or when you need more than 50 new events a month with no budget.
See also: three ways to create Google Calendar events from Google Sheets and how two-way sync works.