A Practical Guide to Full Attribution Tracking - From Click to Your CRM
Discover how to build a complete attribution and user journey tracking system using GA4, Google Tag Manager, and HubSpot without expensive SaaS tools.

Managing multiple Google Calendars can be a hassle, especially when juggling clients, businesses, or teams that need visibility into your availability. While paid tools exist, there’s a free and easy way to sync your calendars automatically using Google Apps Script. In this guide, I'll walk you through setting up a two-way calendar sync that updates events in real time—without the need for third-party software.
If you manage multiple calendars—whether for different clients, projects, or businesses—you’ve likely faced the frustration of keeping them in sync.
Many professionals use multiple Google Calendars, but when a client provides you with a company email, their team often needs visibility into your availability. The challenge? Google Calendar doesn’t offer a built-in way to sync events across accounts—and manually updating them is time-consuming.
Several paid tools help sync calendars, including:
While these tools work well, they all charge a fee for what seems like a basic function: copying events between calendars.
For those who prefer a free and customizable solution, Google Apps Script provides an alternative—allowing you to automatically sync events both ways between multiple calendars.
With Google Apps Script, you can:
✔️ Sync events between multiple Google Calendars
✔️ Reflect changes automatically (so updates in one calendar appear in another)
✔️ Remove outdated events if they’ve changed after syncing
This method requires no third-party software—just a simple script running within your Google account.
In your primary calendar, you must first request permissions to access / view the secondary calendar.
You will need to:
Once you grant the permissions you can run the script -- see below for the code
Here’s a basic Google Apps Script you can use to sync events between two Google Calendars:
function sync() {
// Array of secondary calendar IDs – these are the calendars you’re pulling free/busy info from
var secondaryCalendarIds = ["secondary_cal1@domain.com", "secondary_cal2@domain.com"]; // these are the calendars you pull events from
var today = new Date();
var enddate = new Date();
enddate.setDate(today.getDate() + 1); // one day outlook for debugging
Logger.log("=== Sync Started ===");
Logger.log("Time range: " + today + " to " + enddate);
// Use PropertiesService to track which events (by their unique time key) have been processed.
var props = PropertiesService.getScriptProperties();
var processedJSON = props.getProperty('processedEvents');
var processed = processedJSON ? JSON.parse(processedJSON) : {};
// Combine free/busy (secondary) events from all secondary calendars into one array
var secondaryEvents = [];
for (var i = 0; i < secondaryCalendarIds.length; i++) {
var calId = secondaryCalendarIds[i];
Logger.log("Fetching events from secondary calendar: " + calId);
var cal = CalendarApp.getCalendarById(calId);
if (cal) {
try {
var events = cal.getEvents(today, enddate);
Logger.log("Fetched " + events.length + " events from " + calId);
secondaryEvents = secondaryEvents.concat(events);
} catch (e) {
Logger.log("Error fetching events from calendar " + calId + ": " + e);
}
} else {
Logger.log("Calendar with ID " + calId + " not found.");
}
}
// Get events from primary calendar (the calendar on the account where the script runs)
var primaryCal = CalendarApp.getDefaultCalendar();
var primaryEvents = [];
try {
primaryEvents = primaryCal.getEvents(today, enddate);
Logger.log("Fetched " + primaryEvents.length + " events from primary calendar");
} catch (e) {
Logger.log("Error fetching events from primary calendar: " + e);
}
// Process each event from secondary calendars
for (var i = 0; i < secondaryEvents.length; i++) {
var secEvent = secondaryEvents[i];
// Create a unique key based solely on the start and end times.
// (Note: this assumes that no two distinct events share exactly the same times.)
var key = secEvent.getStartTime().getTime() + "_" + secEvent.getEndTime().getTime();
// Skip if we've already processed an event for this time slot.
if (processed[key]) {
Logger.log("Skipping event (already processed): " + key);
continue;
}
// If the event is an all-day event or falls on a weekend, skip it.
if (secEvent.isAllDayEvent()) {
Logger.log("Skipping all-day event for " + secEvent.getStartTime());
continue;
}
var day = secEvent.getStartTime().getDay();
if (day < 1 || day > 5) {
Logger.log("Skipping non-weekday event for " + secEvent.getStartTime());
continue;
}
// Check if a matching event already exists in the primary calendar (by time).
var exists = false;
for (var j = 0; j < primaryEvents.length; j++) {
var primEvent = primaryEvents[j];
// We use start/end times for matching, since titles/details may be unavailable.
if (
primEvent.getStartTime().getTime() === secEvent.getStartTime().getTime() &&
primEvent.getEndTime().getTime() === secEvent.getEndTime().getTime()
) {
exists = true;
Logger.log("Found matching primary event for time slot: " + key);
break;
}
}
if (exists) {
// Mark it as processed so we don't process it again later.
processed[key] = true;
continue;
}
// Create a new event in the primary calendar marked as "Booked".
try {
var newEvent = primaryCal.createEvent('Booked', secEvent.getStartTime(), secEvent.getEndTime());
// Optionally, add a marker in the description to indicate it was synced (for debugging)
newEvent.setDescription("sync: booked; key: " + key);
Logger.log("Created event 'Booked' in primary calendar for time slot: " + key);
// Mark this time slot as processed.
processed[key] = true;
} catch (e) {
Logger.log("Error creating event for time slot " + key + ": " + e);
}
}
// Optionally: Remove primary events that no longer exist in the secondary free/busy data.
// Since free/busy doesn't supply details, you could loop through primary events marked with your sync marker
// and delete any whose time slot is not present in the current secondary events.
for (var i = 0; i < primaryEvents.length; i++) {
var primEvent = primaryEvents[i];
// Only consider events that were created by this sync process
if (primEvent.getDescription().toLowerCase().indexOf("sync: booked") === -1) {
continue;
}
var primKey = primEvent.getStartTime().getTime() + "_" + primEvent.getEndTime().getTime();
var stillPresent = false;
for (var j = 0; j < secondaryEvents.length; j++) {
var secEvent = secondaryEvents[j];
var secKey = secEvent.getStartTime().getTime() + "_" + secEvent.getEndTime().getTime();
if (primKey === secKey) {
stillPresent = true;
break;
}
}
if (!stillPresent) {
try {
primEvent.deleteEvent();
Logger.log("Deleted primary event 'Booked' for time slot: " + primKey);
// Also remove it from our processed store.
delete processed[primKey];
} catch (e) {
Logger.log("Error deleting event for time slot " + primKey + ": " + e);
}
}
}
// Save the updated processed events record
props.setProperty('processedEvents', JSON.stringify(processed));
Logger.log("=== Sync Completed ===");
}
This free and customizable approach eliminates the need for paid tools, giving you full control over how your calendars sync.
While third-party tools offer additional features, this script is an excellent solution for:
✅ Freelancers working with multiple clients
✅ Consultants needing cross-calendar visibility
✅ Business owners managing multiple ventures
Discover how to build a complete attribution and user journey tracking system using GA4, Google Tag Manager, and HubSpot without expensive SaaS tools.

If you need to create an intelligent auto-reply with specific conditions—like restricting replies to certain domains, or controlling when the auto-responder fires—this script will do the job perfectly.

Keeping customers with recurring subscriptions or product renewals is quite critical for sustainable revenue growth. One tactic is to offer targeted pricing discounts that can nudge customers into renewing their products—without sacrificing your overall revenue.

AI-driven search engines like ChatGPT and Google Gemini are changing how content ranks in 2025. To optimize for AI-powered search, content must be conversational, structured in a Q&A format, and utilize long-tail keywords. Implementing FAQ schema and structured data improves discoverability, while authority and freshness enhance ranking. As AI search evolves, businesses must adapt their content strategies to stay competitive.

Managing multiple Google Calendars can be a hassle, especially when juggling clients, businesses, or teams that need visibility into your availability. While paid tools exist, there’s a free and easy way to sync your calendars automatically using Google Apps Script. In this guide, I'll walk you through setting up a two-way calendar sync that updates events in real time—without the need for third-party software.