Build a contract-renewal calendar
Track Northwind's recurring-revenue renewal dates as calendar events for proactive sales.
Published Oct 8, 2025
Northwind’s retainer income depends on contracts being renewed on time, but the renewal dates live in a spreadsheet nobody opens between quarters. By the time someone notices a contract is up, the conversation is rushed — or worse, the renewal has already lapsed. Recurring revenue quietly leaks away because the dates were never anywhere the team actually looks.
This script puts those dates where the team will see them. It reads a
Contracts sheet and creates an all-day calendar event for each renewal, so the
date shows up alongside everyone’s normal schedule. The sales team gets a
natural prompt to start the renewal conversation early, instead of finding out
after the fact.
What you’ll need
- A
Contractssheet with a header row and columnsclient,renewalDateandeventId(leaveeventIdblank; the script fills it in). - A Google Calendar to hold the renewal events — a dedicated one keeps them separate from personal schedules.
- The
Contractsspreadsheet ID and the renewal calendar’s ID, both copied from their respective URLs.
The script
// The calendar that holds the renewal events.
const RENEWAL_CALENDAR_ID = '1abcRenewalCalId';
// The spreadsheet that holds the contract records.
const CONTRACTS_SHEET_ID = '1abcContractsId';
/**
* Reads the Contracts sheet and creates an all-day calendar event for every
* contract that has a renewal date but no event yet. The new event's ID is
* written back so the contract is never double-booked on a later run.
*/
function syncRenewals() {
const sheet = SpreadsheetApp.openById(CONTRACTS_SHEET_ID).getSheets()[0];
// 1. Read the whole sheet in one call — header plus data rows.
const values = sheet.getDataRange().getValues();
if (values.length < 2) {
Logger.log('No contracts to sync — nothing to do.');
return;
}
// 2. Split off the header and build a name -> column-index lookup.
const [header, ...rows] = values;
const col = Object.fromEntries(header.map((name, i) => [name, i]));
// 3. Open the renewal calendar once, up front.
const cal = CalendarApp.getCalendarById(RENEWAL_CALENDAR_ID);
if (!cal) {
Logger.log('Renewal calendar not found — check RENEWAL_CALENDAR_ID.');
return;
}
// 4. Create an event for each contract that needs one.
let created = 0;
rows.forEach((row, i) => {
// Skip rows without a real renewal date.
if (!(row[col.renewalDate] instanceof Date)) return;
// Skip rows that already have an event — keeps the script re-runnable.
if (row[col.eventId]) return;
// Create an all-day event and record its ID back on the row.
const event = cal.createAllDayEvent(
'Renewal: ' + row[col.client],
row[col.renewalDate]
);
values[i + 1][col.eventId] = event.getId();
created++;
});
// 5. Write the sheet back so the new event IDs are saved.
sheet.getDataRange().setValues(values);
Logger.log('Created ' + created + ' renewal event(s).');
}
How it works
syncRenewalsopens theContractsspreadsheet and reads the whole data range in one call.- If the sheet has only a header it logs a message and stops.
- It splits off the header and builds a
collookup, so the script does not depend on a fixed column order. - It opens the renewal calendar once and bails out with a clear message if the ID is wrong, rather than failing obscurely later.
- For each row it applies two guards: it skips any row whose
renewalDateis not a real date, and any row that already has aneventId. That second guard is what makes the script safe to run repeatedly — an already-synced contract is never given a duplicate event. - For each remaining row it creates an all-day event titled
Renewal:plus the client name on the renewal date, and stores the new event’s ID back in theeventIdcell. - After every row is processed it writes the sheet back in one call so the new IDs are saved.
Example run
Say the Contracts sheet looks like this before a run:
| client | renewalDate | eventId |
|---|---|---|
| Harbour Coffee | 2025-12-01 | |
| Meridian Books | 2026-01-15 | [email protected] |
| Glasshouse Studio | 2025-11-20 |
After the script runs, two new all-day events appear on the renewal calendar — Renewal: Harbour Coffee on 1 December and Renewal: Glasshouse Studio on 20 November — and the sheet records their IDs:
| client | renewalDate | eventId |
|---|---|---|
| Harbour Coffee | 2025-12-01 | [email protected] |
| Meridian Books | 2026-01-15 | [email protected] |
| Glasshouse Studio | 2025-11-20 | [email protected] |
Meridian Books already had an eventId, so it was left untouched — no duplicate
event was created.
Trigger it
Run this on a time-driven trigger so newly added contracts get an event without anyone remembering to sync:
- In the Apps Script editor, open Triggers (the clock icon).
- Click Add Trigger.
- Choose
syncRenewals, a Day timer, and an early-morning slot. - Save and approve the authorisation prompt.
A daily run is plenty — renewal dates are months away, so a one-day delay before an event appears makes no difference.
Watch out for
- The
renewalDatecolumn must hold real dates. A date typed as text fails theinstanceof Datecheck and that contract will never get an event — format the column as a date. - The script only ever creates events. If you change a contract’s
renewalDate, the old event is not moved — delete it and clear theeventIdcell so the next run creates a fresh one. - Likewise, deleting a contract row leaves its event on the calendar. Remove stale events by hand, or extend the script to reconcile deletions.
- All-day events have no built-in reminder. Add a default notification on the renewal calendar, or set reminders on the events, so the date actually prompts someone.
setValuesrewrites the whole sheet each run. Avoid editing theContractssheet while the script runs, or an in-flight edit could be overwritten — a daily off-hours trigger keeps that window small.
Related
Schedule personal habits and routines
Block recurring habits on Awadesh's calendar — gym, walks, deep-work mornings.
Updated Nov 5, 2025
Sync birthdays and anniversaries to Calendar
Populate recurring personal dates from a Sheet — the Northwind team rituals calendar.
Updated Nov 1, 2025
Generate recurring events with custom exceptions
Handle complex recurrence rules in code — every Tuesday except UK bank holidays.
Updated Oct 28, 2025
Auto-reschedule low-priority conflicts
Move flexible Northwind events around fixed ones — focus blocks bend, client calls don't.
Updated Oct 16, 2025
Apply attendee rules by event type
Auto-invite the right Northwind people from a Sheet — design reviews get design, client calls get account leads.
Updated Oct 4, 2025