Gmail Email Scheduler
An automated Google Apps Script that monitors important emails and intelligently creates calendar events using Google's Gemini AI to extract dates and details.
How It Works
Trigger: User marks an email as "Important."
Process: System scans inbox hourly, aggregates thread history, and queries the Gemini API.
Action: Updates Google Calendar (Create/Edit/Cancel) and provides a direct link to the email source.
// ==========================================
// CONFIGURATION
// ==========================================
// ๐ด IMPORTANT: Replace this with your NEW key. Do not publish real keys!
var GEMINI_API_KEY = "PASTE_YOUR_KEY_HERE"; // <--- DOUBLE CHECK THIS!
var CALENDAR_ID = "primary";
// ==========================================
// MAIN FUNCTION (Run this Hourly)
// ==========================================
function processImportantEmails() {
// 1. Garbage Collection (Self-Healing DB)
cleanupOldMemory();
var threads = GmailApp.search('is:important', 0, 5);
if (threads.length === 0) {
Logger.log("No important emails found. Standing by.");
return;
}
Logger.log("Found " + threads.length + " important emails...");
for (var i = 0; i < threads.length; i++) {
var thread = threads[i];
var messages = thread.getMessages(); // Get FULL history
var subject = messages[0].getSubject();
// 2. Stitch Full Conversation (Solving the "Agreement Wall")
var fullConversation = "";
for (var m = 0; m < messages.length; m++) {
var msg = messages[m];
// Simple cleanup to remove excess whitespace
fullConversation += `\n--- Message ${m+1} (From: ${msg.getFrom()}) ---\n`;
fullConversation += msg.getPlainBody().replace(/\s+/g, " ").trim() + "\n";
}
// 3. AI Analysis
var aiResult = callGeminiAPI(subject, fullConversation);
if (aiResult && aiResult.isValid) { // <--- FIXED: Matches prompt JSON key
// SCENARIO: CANCELLATION
if (aiResult.action === "cancel") {
handleCancellation(thread.getId());
Logger.log("โ Event Cancelled: " + subject);
}
// SCENARIO: CREATE or UPDATE
else {
// If updating, we first delete the old event (Update = Delete + Create)
handleCancellation(thread.getId());
createCalendarEvent(aiResult, thread.getPermalink(), thread.getId());
Logger.log("โ
Scheduled/Updated: " + aiResult.title);
}
// Cleanup: Remove Important marker
thread.markUnimportant();
} else {
// SCENARIO: AMBIGUOUS / FAILURE
thread.markUnimportant();
// Star the LATEST message so you see it at the top of your inbox
messages[messages.length - 1].star();
Logger.log("โ ๏ธ Ambiguous. Starred for review: " + subject);
}
}
}
// ==========================================
// DATABASE & CALENDAR LOGIC
// ==========================================
function createCalendarEvent(data, emailLink, threadId) {
var calendar = CalendarApp.getCalendarById(CALENDAR_ID);
var event = calendar.createEvent(data.title, new Date(data.startTime), new Date(data.endTime), {
location: data.location,
description: data.description + "\n\n๐ Source Email: " + emailLink
});
// SAVE TO MEMORY (Persistence)
var props = PropertiesService.getScriptProperties();
var payload = JSON.stringify({
eventId: event.getId(),
eventDate: data.startTime
});
props.setProperty(threadId, payload);
}
function handleCancellation(threadId) {
var props = PropertiesService.getScriptProperties();
var record = props.getProperty(threadId);
if (record) {
try {
var data = JSON.parse(record);
var calendar = CalendarApp.getCalendarById(CALENDAR_ID);
var event = calendar.getEventById(data.eventId);
if (event) {
event.deleteEvent();
}
props.deleteProperty(threadId); // Remove from DB
} catch (e) {
Logger.log("Error cancelling event: " + e);
}
}
}
function cleanupOldMemory() {
var props = PropertiesService.getScriptProperties();
var data = props.getProperties();
var now = new Date();
var keysToDelete = [];
for (var key in data) {
try {
var record = JSON.parse(data[key]);
var eventDate = new Date(record.eventDate);
var diffDays = (now - eventDate) / (1000 * 60 * 60 * 24);
if (diffDays > 7) keysToDelete.push(key); // Delete if older than 7 days
} catch (e) {
keysToDelete.push(key);
}
}
if (keysToDelete.length > 0) {
for (var k = 0; k < keysToDelete.length; k++) {
props.deleteProperty(keysToDelete[k]);
}
Logger.log("๐งน Cleaned " + keysToDelete.length + " old records.");
}
}
// ==========================================
// GEMINI API HELPER (Version 4.2: Timezone Fix)
// ==========================================
function callGeminiAPI(subject, conversation) {
var url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-latest:generateContent?key=" + GEMINI_API_KEY;
var cleanText = conversation.substring(0, 30000);
// 1. Get User's Timezone dynamically (e.g., "America/Los_Angeles")
var userTimeZone = Session.getScriptTimeZone();
var prompt = `
You are a scheduling assistant. Analyze this email thread history.
Subject: ${subject}
Conversation History: ${cleanText}
CONTEXT:
- User's Timezone: ${userTimeZone}
- Today's Date: ${new Date().toString()}
CRITICAL INSTRUCTIONS:
1. TIMESTAMPS: Return all 'startTime' and 'endTime' in ISO 8601 format WITH THE CORRECT OFFSET for the User's Timezone (e.g., "2026-01-25T16:00:00-08:00"). Do NOT use UTC (Z) unless the user specifically asks for UTC.
2. LATEST DATA: The conversation is chronological. The LAST message is the source of truth.
3. IGNORE QUOTES: If the last message contains quoted history (e.g. "On Jan 24..."), IGNORE the history. Only read the new text typed by the sender.
Determine the FINAL agreed-upon intent.
Return RAW JSON:
- isValid: boolean (true if a meeting is confirmed OR cancelled)
- action: string ("create" or "cancel")
- title: string (event title)
- startTime: string (ISO 8601 with offset)
- endTime: string (ISO 8601 with offset)
- location: string (or "TBD")
- description: string (summary)
`;
var payload = { "contents": [{ "parts": [{"text": prompt}] }] };
var options = {
"method": "post",
"contentType": "application/json",
"payload": JSON.stringify(payload),
"muteHttpExceptions": true
};
try {
var response = UrlFetchApp.fetch(url, options);
var json = JSON.parse(response.getContentText());
if (json.error || !json.candidates) return null;
var rawText = json.candidates[0].content.parts[0].text;
//Logger.log("๐ค RAW AI RESPONSE: " + rawText); // <--- Add this line!
rawText = rawText.replace(/```json/g, "").replace(/```/g, "").trim();
return JSON.parse(rawText);
} catch (e) {
//Logger.log("API Error: " + e.toString());
return null;
}
}Key Features
- Context-Aware Analysis: Aggregates full email threads (not just the last message) to resolve complex references and "agreement walls."
- CRUD Lifecycle Management: Distinct logic for Creating, Reading, Updating, and Deleting events based on changing email context.
- Self-Healing Architecture: Includes a garbage collector that automatically purges database records older than 7 days to ensure long-term performance.
- State Persistence: Leverages PropertiesService to map Gmail threads to Calendar IDs, preventing duplicate entries during rescheduling.
- Smart Fallback: Automatically "Stars" ambiguous emails for human review rather than guessing or failing silently.
- Source of Truth Linking: Embeds a "View Email" link directly into every calendar event for instant context retrieval.
Technologies: Google Apps Script, Gmail API, Google Calendar API, Gemini AI, PropertiesService
Status: Deployed for Personal Use