Sync Google Sheets Task Deadlines to a Google Calendar
How can I sync Google Sheets task deadlines to Google Calendar without creating duplicate events on each run?
Use Google Sheets with Apps Script to create one all-day Google Calendar event per stable task ID and save its event ID beside the task. Subsequent runs use that saved ID to update the event's title or deadline, while a time-driven trigger schedules the one-way sync.
What you need
- A Google Sheets spreadsheet with a Tasks worksheet and the four specified headers
- An executing account with sheet-editing and calendar-modification permissions
- Matching spreadsheet, Apps Script, and calendar time zones
- Script Properties containing SPREADSHEET_ID, CALENDAR_ID, and SHEET_NAME
How this script runs
- You run the script
- It reads tasks and stable task IDs from the sheet
- It looks for an event ID saved beside each task
- It updates that event, or creates an all-day event
- It saves the event ID beside the task
How should the Tasks sheet store deadlines and event IDs?
The Google Sheets deadline sync requires a worksheet named Tasks with these exact headers in columns A through D.
| Task ID | Event ID | Task | Due Date |
|---|---|---|---|
| TASK-001 | Draft Moonpickle Labs kickoff | 10/05/2026 | |
| TASK-002 | Review Zorbington Studio handoff | 10/06/2026 | |
| TASK-003 | Send Fluffwidget Co launch brief | 10/09/2026 |
The sample organizations are fictional. Set the spreadsheet locale to United States for these dates, format column D as Date, and enter real dates rather than text.
Leave column B empty initially; Apps Script fills it with Calendar event IDs. Assign each Task ID once and never recycle it. Changing a stable ID breaks event ownership matching.
Sort the entire table, including column B, to preserve task-to-event associations.
How do you configure and schedule the Google Sheets deadline sync?
Configure the Google Sheets deadline sync with matching time zones, Script Properties, and a time-driven Apps Script trigger.
-
Choose a calendar. Create a dedicated deadline calendar or select one where the executing account can create and edit events.
-
Copy its ID. Find Calendar ID under the calendar’s settings > Integrate calendar. Use the ID, not the display name, and keep the target unchanged after syncing.
-
Align time zones. Set the spreadsheet time zone under File > Settings and match the calendar’s time zone, such as
America/New_York. -
Open Apps Script. Choose Extensions > Apps Script, paste the complete script below, and save. In Project Settings, match the script time zone to the spreadsheet and calendar.
-
Add Script Properties. In Project Settings, enter these values. The spreadsheet ID is between
/d/and/editin its address; no API key is required.Property Value SPREADSHEET_IDYour spreadsheet ID CALENDAR_IDYour target calendar ID SHEET_NAMETasks -
Authorize manually. Select
syncTaskDeadlines, click Run, and review Google’s authorization prompts. The executing account needs sheet-editing and calendar-modification permissions. -
Schedule runs. After checking output, open Triggers and add a time-driven trigger for
syncTaskDeadlines, such as hourly. Its creator must retain access; edits appear after successful runs, not immediately.
Which Apps Script creates and updates the all-day deadline events?
The following Apps Script validates sheet input, updates events through saved IDs, and uses description markers for limited interrupted-write recovery.
The script lock blocks overlapping executions within this project. Input validation precedes event changes, but later Calendar errors can leave a partially completed sync.
function syncTaskDeadlines() {
const lock = LockService.getScriptLock();
lock.waitLock(30000);
try {
const props = PropertiesService.getScriptProperties();
const spreadsheetId = props.getProperty("SPREADSHEET_ID");
const calendarId = props.getProperty("CALENDAR_ID");
const sheetName = props.getProperty("SHEET_NAME");
if (!spreadsheetId || !calendarId || !sheetName) {
throw new Error(
"Set SPREADSHEET_ID, CALENDAR_ID, and SHEET_NAME in Script Properties."
);
}
const ss = SpreadsheetApp.openById(spreadsheetId);
const sheet = ss.getSheetByName(sheetName);
const calendar = CalendarApp.getCalendarById(calendarId);
if (!sheet) {
throw new Error("Worksheet not found: " + sheetName);
}
if (!calendar) {
throw new Error("Calendar not found or not accessible.");
}
const zone = ss.getSpreadsheetTimeZone();
if (
zone !== Session.getScriptTimeZone() ||
zone !== calendar.getTimeZone()
) {
throw new Error(
"Spreadsheet, script, and calendar time zones must match exactly."
);
}
const headers = sheet.getRange(1, 1, 1, 4).getDisplayValues()[0];
const expected = ["Task ID", "Event ID", "Task", "Due Date"];
if (headers.some((header, i) => header !== expected[i])) {
throw new Error("Headers must be: Task ID, Event ID, Task, Due Date.");
}
const lastRow = sheet.getLastRow();
if (lastRow < 2) return;
const values = sheet.getRange(2, 1, lastRow - 1, 4).getValues();
const idFormulas = sheet.getRange(2, 2, lastRow - 1, 1).getFormulas();
const taskIds = new Set();
const eventIds = new Set();
const tasks = [];
values.forEach((row, index) => {
const rowNumber = index + 2;
if (idFormulas[index][0]) {
throw new Error("Row " + rowNumber + ": Event ID cannot be a formula.");
}
if (row.every(value => value === "")) return;
const taskId = String(row[0]).trim();
const eventId = String(row[1]).trim();
const title = String(row[2]).trim();
const due = row[3];
if (!/^[A-Za-z0-9_-]+$/.test(taskId)) {
throw new Error(
"Row " + rowNumber + ": use letters, numbers, _ or - for Task ID."
);
}
if (taskIds.has(taskId)) {
throw new Error("Row " + rowNumber + ": duplicate Task ID.");
}
if (eventId && eventIds.has(eventId)) {
throw new Error("Row " + rowNumber + ": duplicate Event ID.");
}
if (!title || !(due instanceof Date) || isNaN(due.getTime())) {
throw new Error(
"Row " + rowNumber + ": enter a task title and a real Sheets date."
);
}
taskIds.add(taskId);
if (eventId) eventIds.add(eventId);
tasks.push({
rowNumber,
taskId,
eventId,
title,
day: new Date(due.getFullYear(), due.getMonth(), due.getDate())
});
});
for (const task of tasks) {
const marker =
"Managed Sheets task: " + task.taskId +
"\nSource: " + ss.getId() + "/" + sheet.getSheetId();
let event = task.eventId
? calendar.getEventById(task.eventId)
: null;
if (!event) {
const matches = calendar.getEventsForDay(task.day).filter(
candidate => candidate.getDescription() === marker
);
if (matches.length > 1) {
throw new Error(
"Row " + task.rowNumber + ": multiple matching events on due date."
);
}
event = matches[0] || null;
}
if (event) {
if (event.isRecurringEvent() || event.getDescription() !== marker) {
throw new Error(
"Row " + task.rowNumber +
": event ownership mismatch or recurring event."
);
}
event.setTitle(task.title);
event.setAllDayDate(task.day);
} else {
event = calendar.createAllDayEvent(task.title, task.day, {
description: marker
});
}
const savedId = event.getId();
if (savedId !== task.eventId) {
sheet.getRange(task.rowNumber, 2).setValue(savedId);
SpreadsheetApp.flush();
}
}
console.log("Synced " + tasks.length + " task deadlines.");
} finally {
lock.releaseLock();
}
}
What Calendar events should the sample task rows produce?
The three sample rows should produce three single-day, all-day Calendar events and save their generated IDs in column B.
| Task ID | Calendar event title | All-day date | Sheet result |
|---|---|---|---|
| TASK-001 | Draft Moonpickle Labs kickoff | October 5, 2026 | B2 contains its event ID |
| TASK-002 | Review Zorbington Studio handoff | October 6, 2026 | B3 contains its event ID |
| TASK-003 | Send Fluffwidget Co launch brief | October 9, 2026 | B4 contains its event ID |
IDs come from CalendarApp, not browser event addresses. Changing TASK-002’s deadline to 10/08/2026 moves its event to October 8 on the next successful run; B3 retains its ID. Renaming the task updates its event title.
Events have no guests. Sheets controls titles and dates, overwriting manual Calendar edits; preserve each managed description.
Removing a row leaves its event behind. For permanent removal, delete both the event and its sheet row before another run. Otherwise, missing events are recreated; clearing required fields causes validation errors.
Why does syncTaskDeadlines update existing Calendar events instead of creating new ones each run?
syncTaskDeadlines reuses saved event IDs and checks task-specific description markers, updating identified events rather than always creating replacements.
First, getScriptLock and waitLock serialize this project’s executions. Script Properties select the spreadsheet, worksheet, and calendar by configuration rather than relying on the active sheet. The script checks time zones and headers, then reads rows with getValues, preserving date objects instead of using displayed date strings. It validates all rows before Calendar changes, using sets to track uniqueness. Each deadline becomes a date at local midnight, expressing a calendar day rather than a timed appointment.
For each task, the description marker combines its stable Task ID with its spreadsheet and worksheet IDs. getEventById looks up the saved reference first; otherwise, getEventsForDay searches the deadline day for an exact marker match rather than matching titles. After ownership and recurrence checks, setTitle and setAllDayDate update an existing event; createAllDayEvent handles a new one.
Finally, changed event IDs are written to column B and flush applies pending spreadsheet changes immediately rather than leaving them buffered. After logging the task count, the finally block releases the lock.
How do you fix rejected dates and broken event associations?
Google Sheets deadline sync errors require correcting the rejected date, time-zone setting, or task-to-event association before rerunning.
A populated deadline is rejected
Symptom: “enter a task title and a real Sheets date.”
Exact fix: Supply any missing title, remove any leading apostrophe from the deadline, confirm United States locale, and re-enter 10/05/2026. Date formatting alone may leave text unchanged. =ISNUMBER(D2) should return TRUE for a valid stored date.
Time zones do not match
Symptom: Spreadsheet, script, and calendar time zones must match exactly.
Exact fix: Set all three to the same named zone and rerun. Do not substitute a fixed GMT offset for a regional zone with daylight saving rules.
Event ownership does not match
Symptom: Updates stop after rows are rearranged or an event description changes.
Exact fix: Restore row associations through Sheets version history if column B was sorted separately. Restore the original managed description if only that text changed. Do not clear all event IDs; losing references can leave old events behind.
An interruption leaves the Event ID blank
Symptom: Calendar contains the event, but its sheet ID is missing.
Exact fix: Keep the original deadline and rerun to recover the ID through the description marker. If the deadline changed, restore the original date, recover the ID, then apply the new deadline. Recovery searches that day only, not the entire calendar.
When should you avoid this scheduled Sheets-to-Calendar sync?
This Apps Script is unsuitable for large deadline lists, two-way editing, or workflows requiring dependable assignment alerts.
- Large lists: Per-task Calendar operations can exceed Apps Script execution limits. Larger integrations need batching and durable checkpoints.
- Two-way editing: Calendar-to-Sheets updates require conflict rules and change tracking, not another trigger for this function. Choose an integration with documented two-way sync.
- Critical alerts: This script configures no reminders. Notifications depend on calendar and account settings; use explicit assignment notifications when missed alerts have operational consequences.
Related questions
Do Calendar edits sync back to Google Sheets?
This Apps Script only syncs from Google Sheets to Google Calendar. Manual changes to a managed event's title or date are overwritten on the next successful sync.
How do I permanently remove a synced deadline?
Delete the Calendar event and remove its corresponding Sheets row before the next run. Removing only the row leaves the event behind, while deleting only the event causes the script to recreate it.
Does the script configure deadline reminders?
The script creates all-day events but does not configure reminders. Notifications depend on separate calendar and account settings.
Can the script recover an event ID after an interrupted run?
The script can recover an event by matching its managed description on the task's current due date. If the deadline changed after the interruption, restore the original date, rerun to recover the ID, and then apply the new deadline.