Stop drowning in endless email threads and reclaim your focus. Learn how to build a custom AI assistant with Google’s Gemini Pro to automatically tame your chaotic inbox.
Let’s be honest: your inbox is a battlefield. It’s a relentless stream of notifications, newsletters, urgent requests, and sprawling conversation threads where the original point was lost fifty replies ago. We’ve all been there—scrolling endlessly through a single email chain just to piece together the context, or staring at a wall of unread messages, feeling the creeping dread of information fatigue. The modern inbox isn’t just a communication tool; it’s a major source of cognitive load.
This tutorial is about fighting back. It’s about reclaiming your focus and your time by building a smart, automated assistant that lives right inside your Gmail account. We’re going to leverage the power of Google’s Gemini Pro AI model and the flexibility of [AI Powered Cover Letter [Automated Job Creation in Real Time Jobber and Google Sheets Integration from Gmail](https://votuduc.com/Automated-Job-Creation-in-Jobber-from-Gmail-p115606) Engine](https://votuduc.com/AI-Powered-Cover-Letter-Automated Quote Generation and Delivery System for Jobber-Engine-p111092) to build a system that tames the chaos, one summary at a time.
The core issue with email isn’t just the volume; it’s the inefficiency of information retrieval. Critical decisions, action items, and key updates are often buried deep within lengthy threads. Manually sifting through this digital deluge is a time-consuming and error-prone process.
Consider these common scenarios:
Joining a Project Mid-Stream: You’re added to a long-running email thread and have to spend 30 minutes reading the entire history just to understand the current status.
The Daily Digest: You return from a meeting to find 20+ new emails. Which ones are just FYI, and which ones require immediate action?
Complex Customer Support: A support ticket has been passed between multiple team members, and you need to quickly grasp the customer’s issue and the steps already taken.
In each case, the challenge is the same: extracting the signal from the noise. The time spent on this “digital archaeology” is time you’re not spending on deep work, creative problem-solving, or strategic planning.
What if, instead of manually reading every word, you could get a concise, accurate summary of any email thread on demand? This is precisely where Large Language Models (LLMs) like Gemini Pro shine. These models are trained on vast amounts of text and have a sophisticated understanding of language, nuance, and context. They can read a mountain of text and distill it into its essential points, identifying key decisions, action items, and the overall sentiment.
To bring this intelligence into our inbox, we’ll use Genesis Engine AI Powered Content to Video Production Pipeline. Think of it as the connective tissue for the [Automatically create new folders in Google Drive, generate templates in new folders, fill out text automatically in new files, and save info in [Automated Web Scraping with [Multilingual Text-to-Speech Tool with SocialSheet Streamline Your Social Media Posting 123](https://votuduc.com/Multilingual-Text-to-Speech-Tool-with-Google-Workspace-p809282)](https://votuduc.com/Automated-Web-Scraping-with-Google-Sheets-p292968)](https://workspace.google.com/marketplace/app/auto_create_folder_and_files/430076014869) ecosystem. It’s a cloud-based JavaScript platform that lets us write code that interacts directly with services like Gmail, Sheets, and Docs. In our case, it will act as the orchestrator:
It will watch our Gmail account for specific triggers.
It will securely grab the email content we want to summarize.
It will communicate with the Gemini Pro API, sending the content for processing.
It will take the AI-generated summary and place it right back into our inbox where we need it most.
The result is a powerful, personalized Automated Work Order Processing for UPS that works seamlessly within your existing workflow, turning a reactive chore into a proactive tool for clarity.
By the end of this guide, you will have built a fully functional, serverless email summarization engine. You won’t just be copying and pasting code; you’ll understand each component and how they fit together.
Specifically, you will construct a system that:
Triggers automatically whenever you apply a specific label (e.g., “Summarize”) to an email thread in Gmail.
Collects the entire conversation from the labeled thread, preparing it for the AI.
Makes a secure API call to the Gemini Pro model, authenticating with your own API key.
Uses a carefully crafted prompt to instruct the AI to generate a high-quality, structured summary.
Posts the summary back as a new reply in the original thread, so the context is preserved and easily accessible for you and your team.
Ready to transform your inbox from a source of stress into a streamlined source of information? Let’s get started.
Before we dive into the code that will power our automated summarizer, we need to lay the groundwork. This involves setting up our Google environment, enabling the necessary APIs, and securely storing our credentials. Think of this as building the launchpad before we assemble the rocket. Getting these steps right is crucial for a smooth development process.
At the core of this project are two fundamental components of the Google ecosystem.
First, you’ll need a Google Account. The good news is that a standard, free @gmail.com account is all you need. You don’t need a paid Automated Client Onboarding with Google Forms and Google Drive. account to follow along with this tutorial.
Second, you need a Google Cloud Platform (GCP) Project. This is your central hub for managing Google’s powerful cloud services. While it sounds intimidating, it’s straightforward to set up. The GCP project is where the Gemini API is officially managed and where you would handle billing if you were to scale beyond the generous free tiers.
If you don’t already have a GCP project, head over to the Google Cloud Console, sign in, and create a new project. Give it a memorable name like gmail-summarizer-project.
With your Google account ready, it’s time to create the actual file where our code will live. Google Apps Script is a cloud-based JavaScript platform that lets you build integrations with Automated Discount Code Management System applications.
The easiest way to get started is by creating a standalone script:
Navigate to script.google.com.
In the top-left corner, click + New project.
The Apps Script editor will open. The first thing you should do is give your project a name. Click on Untitled project and rename it to something descriptive, like Gmail Gemini Summarizer.
You’ll be greeted with a file named Code.gs. This is where we’ll be writing all our JavaScript code. This editor is your integrated development environment (IDE) in the cloud.
By default, an Apps Script project doesn’t have permission to access other Google services like Gmail or Google Drive. We need to explicitly enable these connections. These are called “Advanced Google Services” within the Apps Script environment.
Inside your new Apps Script project editor:
On the left-hand sidebar, find the Services section and click the + icon to add a service.
A dialog box will appear with a list of available Google APIs.
Scroll down to find and select Gmail API. Click Add.
Repeat the process: Click the + icon next to Services again.
This time, find and select Google Drive API. Click Add.
After adding them, you’ll see Gmail and Drive listed under the Services menu. This action tells Google that your script intends to use these services, and it will prompt you for the necessary permissions the first time you run the script. We need these for two key reasons:
Gmail API: To read the content of the emails you want to summarize.
Drive API: To create a new Google Doc where we will store the generated summary.
The final and most critical piece of setup is obtaining and securing your API key for Gemini Pro. This key is your secret credential that authenticates your script’s requests to the AI model.
1. Get Your API Key
First, you need to generate a key from Google AI Studio:
Go to Google AI Studio.
Sign in with the same Google account associated with your GCP project.
Click on Create API key in new project.
Your new API key will be generated and displayed. Copy this key immediately and store it somewhere safe temporarily.
2. Securely Store Your API Key
Warning: Never, ever paste your API key directly into your Code.gs file. This is called “hardcoding,” and it’s a major security risk. If you share your code or accidentally expose it, your key could be stolen and used by others, potentially incurring costs on your account.
The best practice is to use Script Properties, a built-in feature of Google Apps Script for storing secrets.
In the Apps Script editor, click on the Project Settings (gear icon ⚙️) in the left sidebar.
Scroll down until you see the Script Properties section.
Click Add script property.
In the Property field, enter a name for your secret. Let’s use GEMINI_API_KEY.
In the Value field, paste the API key you copied from Google AI Studio.
Click Save script properties.
Now, your key is stored securely and is associated with this specific script project. We can access it in our code safely using a simple command, which we’ll see later:
// How we'll retrieve the key in our code
const GEMINI_API_KEY = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
With our environment configured, services enabled, and API key securely stored, our launchpad is officially ready. Now, we can move on to the exciting part: writing the code.
Before we can ask Gemini to summarize anything, we need to programmatically access and structure the email content. This is where the power of Google Apps Script and its native integration with G Suite services shines. Our first task is to pinpoint the exact emails we want to process and then deconstruct them into a format that an AI model can understand.
The cornerstone of our script is the GmailApp service, a built-in Apps Script library that gives us robust control over your Gmail account. Instead of pulling your entire inbox (which would be inefficient and costly), we can use GmailApp.search() to retrieve only the threads that meet specific criteria.
The beauty of this method is that it uses the same powerful search syntax you already use in the Gmail search bar. This makes it incredibly intuitive to target emails by sender, label, subject, time frame, or status (like is:unread).
Let’s start with a function to fetch threads. We’ll target unread threads that also have a specific label, which is a common workflow for organizing important emails that need action.
/**
* Fetches Gmail threads based on a specific search query.
*
* @returns {GoogleAppsScript.Gmail.GmailThread[]} An array of GmailThread objects.
*/
function getTargetThreads() {
// Define the search query. This finds all unread threads with the label "Project-Summaries".
// Customize this query to fit your needs.
// Examples: 'is:important from:[email protected]', 'label:invoices is:unread'
const query = 'label:Project-Summaries is:unread';
try {
const threads = GmailApp.search(query);
Logger.log(`Found ${threads.length} threads to process.`);
return threads;
} catch (e) {
Logger.log(`Error fetching Gmail threads: ${e.toString()}`);
return []; // Return an empty array on error
}
}
In this snippet, GmailApp.search(query) returns an array of GmailThread objects. Each object represents a full conversation, which could contain one or many individual messages.
A GmailThread is a container for one or more GmailMessage objects. To understand the full context of a conversation, we must iterate through these messages and extract the relevant details in chronological order. The getMessages() method on a thread object returns all messages sorted from oldest to newest, which is perfect for our needs.
For each message, we need to capture three key pieces of information:
Sender (getFrom()): Who sent the message?
Date (getDate()): When was it sent?
Body (getPlainBody()): What did it say?
We specifically use getPlainBody() instead of getBody() to strip out all HTML formatting (styles, images, signatures). This provides clean, raw text, which is the ideal input for a language model and helps us stay within token limits.
Here’s how we can expand our script to loop through each thread and its messages:
/**
* Processes an array of Gmail threads to extract and structure their content.
*
* @param {GoogleAppsScript.Gmail.GmailThread[]} threads The threads to process.
* @returns {string} A formatted string representing the conversation.
*/
function parseThreadContent(thread) {
let conversation = '';
const messages = thread.getMessages();
for (const message of messages) {
const sender = message.getFrom();
const date = message.getDate().toISOString(); // Use ISO format for consistency
const body = message.getPlainBody();
// We will format this properly in a later step.
// For now, let's just log the extracted data.
Logger.log(`From: ${sender}, Date: ${date}, Body Length: ${body.length}`);
}
// In the next steps, we'll build the 'conversation' string here.
return conversation;
}
Language models like Gemini Pro have a “context window,” which is the maximum amount of text (measured in tokens) they can consider at one time. A very long email thread with dozens of replies and quoted text can easily exceed this limit, leading to errors or incomplete summaries. We must be proactive in managing the input size.
Character count is a decent proxy for token count (a rough rule of thumb is ~4 characters per token). Let’s set a conservative character limit, say 25,000 characters, to stay safely within Gemini’s typical 32k token limit.
A smart strategy for long threads is to prioritize the most important messages. Often, this means keeping the first message (which sets the initial context) and the last few messages (which contain the most recent developments). However, a simpler and often effective approach is to simply process messages chronologically and stop once we approach our character limit.
Let’s implement this safety check:
/**
* Parses a single Gmail thread into a formatted string, respecting a character limit.
*
* @param {GoogleAppsScript.Gmail.GmailThread} thread The thread to parse.
* @param {number} [charLimit=25000] The maximum number of characters for the final output.
* @returns {string} The formatted conversation string.
*/
function parseThreadContentWithLimit(thread, charLimit = 25000) {
let conversation = '';
const messages = thread.getMessages();
for (const message of messages) {
// We'll define the formatting in the next section.
// For now, let's create a temporary formatted block for this message.
const messageBlock = `From: ${message.getFrom()}\nDate: ${message.getDate().toISOString()}\nBody:\n${message.getPlainBody()}\n\n`;
// Check if adding the next message would exceed our limit.
if (conversation.length + messageBlock.length > charLimit) {
Logger.log(`Character limit reached for thread: ${thread.getFirstMessageSubject()}. Halting processing.`);
break; // Stop adding messages to avoid exceeding the context window
}
conversation += messageBlock;
}
return conversation;
}
This function now gracefully handles long threads by truncating them, ensuring our API calls to Gemini will not fail due to excessive input length.
The final step in this stage is to format the extracted text into a single, structured string. The model needs clear cues to distinguish between different messages, senders, and timestamps. A poorly formatted input will lead to a confusing and inaccurate summary.
We’ll create a clean, human-readable format using simple delimiters. This structure helps the AI recognize the conversational turn-taking and attribute points to the correct participants.
Here is the final, complete function for fetching and formatting a single thread. It combines all the concepts we’ve discussed.
/**
* Parses a single Gmail thread into a clearly formatted string for an AI model,
* respecting a character limit.
*
* @param {GoogleAppsScript.Gmail.GmailThread} thread The thread to parse.
* @param {number} [charLimit=25000] The maximum number of characters for the final output.
* @returns {string} The formatted conversation string ready for the AI.
*/
function formatThreadForAI(thread, charLimit = 25000) {
let fullConversation = "";
const messages = thread.getMessages();
for (const message of messages) {
const sender = message.getFrom();
const date = message.getDate().toISOString();
// Clean the body: trim whitespace and remove excessive blank lines
const body = message.getPlainBody().trim().replace(/(\r\n|\n|\r){3,}/g, "\n\n");
const messageBlock = `--- MESSAGE START ---
From: ${sender}
Date: ${date}
Body:
${body}
--- MESSAGE END ---\n\n`;
// Check if adding the next message would exceed our limit
if (fullConversation.length + messageBlock.length > charLimit) {
Logger.log(`Character limit reached for thread: "${thread.getFirstMessageSubject()}". Truncating.`);
break;
}
fullConversation += messageBlock;
}
// Add a final check in case the thread is empty or couldn't be processed.
if (!fullConversation) {
Logger.log(`Warning: Thread "${thread.getFirstMessageSubject()}" resulted in empty content.`);
return null;
}
return fullConversation.trim();
}
With this function, we now have a robust mechanism to take a GmailThread object and transform it into a perfectly structured, size-controlled string. This output is the payload we’ll send to the Gemini Pro API in the next step.
With our email content neatly extracted, it’s time for the main event: sending this data to the Gemini Pro API and asking it to work its magic. This process involves three key parts: telling the model exactly what we want (the prompt), making the actual web request to Google’s servers, and then carefully unpacking the response to get our summary.
The quality of your summary is directly proportional to the quality of your prompt. Simply throwing the email text at the API with a “summarize this” command will work, but we can do so much better. A well-structured prompt guides the model, ensuring the output is consistent, useful, and formatted exactly how we need it.
A powerful prompt for this use case should include:
A Role: Instruct the model to act as a specific persona. “You are an expert executive assistant” sets a professional tone.
A Clear Task: State the primary objective. “Your task is to summarize the following email thread concisely.”
Context: Provide the raw data, clearly labeling each part (From, Subject, Body). This helps the model differentiate between metadata and the core message.
Constraints and Formatting Instructions: This is where you enforce your desired output. You can specify the length, format (e.g., paragraph vs. bullet points), and even ask for additional insights.
Let’s combine these into a robust prompt template within our Apps Script code. We’ll use a function to build this prompt dynamically for each email.
/**
* Creates a detailed prompt for the Gemini API to summarize an email.
* @param {string} from - The sender of the email.
* @param {string} subject - The subject of the email.
* @param {string} body - The plain text body of the email.
* @return {string} The fully constructed prompt.
*/
function createSummarizationPrompt(from, subject, body) {
// Using template literals (backticks) for easy multiline strings and variable injection.
const prompt = `
You are an expert executive assistant. Your task is to summarize the following email thread concisely.
Analyze the content and provide the following in your response:
1. **One-Paragraph Summary:** A brief, easy-to-read summary of the entire email.
2. **Action Items:** A bulleted list of any specific tasks, questions, or actions required from the recipient. If there are no action items, state "None."
3. **Sentiment:** Classify the overall sentiment of the email as Positive, Negative, or Neutral.
Here is the email content:
**From:** ${from}
**Subject:** ${subject}
**Body:**
${body}
`;
return prompt;
}
This prompt is explicit. It tells the model its role, its goal, and precisely how to structure the output, which will make parsing the response much more reliable later on.
Google Apps Script can’t directly import a Gemini Pro library. Instead, we communicate with the API the way most web services do: by making an HTTP request. Apps Script provides a built-in service called UrlFetchApp for exactly this purpose.
We’ll construct a POST request containing our prompt in a JSON payload and send it to the Gemini API endpoint.
Here’s the breakdown of the request:
Endpoint URL: The specific URL for the Gemini Pro model. It includes our API key for authentication.
Headers: We specify the Content-Type as application/json to tell the server what kind of data we’re sending.
Payload: This is the JSON body of our request. It follows a specific structure required by the Gemini API, containing our prompt and any configuration settings (like temperature, which controls randomness).
Options: We’ll bundle the headers, method (POST), and payload into an options object for the UrlFetchApp.fetch() call. A crucial option is muteHttpExceptions: true, which prevents our script from crashing on an API error (e.g., a server timeout) and allows us to handle it gracefully in our code.
Let’s see what this looks like in code:
function callGeminiApi(prompt) {
const API_KEY = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
if (!API_KEY) {
throw new Error("GEMINI_API_KEY not found in Script Properties.");
}
const API_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${API_KEY}`;
// Structure the payload according to the Gemini API documentation.
const payload = {
"contents": [{
"parts": [{
"text": prompt
}]
}],
"generationConfig": {
"temperature": 0.2, // Lower temperature for more deterministic, less "creative" summaries.
"maxOutputTokens": 500
}
};
const options = {
'method': 'post',
'contentType': 'application/json',
'payload': JSON.stringify(payload),
'muteHttpExceptions': true // IMPORTANT: This lets us handle errors gracefully.
};
// Make the API call
const response = UrlFetchApp.fetch(API_URL, options);
return response;
}
When UrlFetchApp gets a response, it doesn’t automatically give us the summary. It gives us an HTTPResponse object containing the status code (e.g., 200 for OK), headers, and the raw response body as a string of JSON. Our job is to parse this response and extract the text we need.
First, we must always check the response code. If it’s not 200, something went wrong, and we should log the error instead of trying to parse the body.
If the call was successful, the response body will be a JSON string that looks something like this:
{
"candidates": [
{
"content": {
"parts": [
{
"text": "Here is the summary you requested..."
}
],
"role": "model"
},
// ... other fields
}
],
// ... other fields
}
As you can see, the summary text is nested deep inside: candidates[0].content.parts[0].text. We need to navigate this structure carefully. Using optional chaining (?.) is a modern and safe way to do this, as it prevents errors if any intermediate property is missing.
Let’s create a final function that ties it all together—making the call and safely extracting the result.
/**
* Gets a summary for an email by calling the Gemini API.
* @param {string} from - The sender of the email.
* @param {string} subject - The subject of the email.
* @param {string} body - The plain text body of the email.
* @return {string|null} The summary text, or null if an error occurred.
*/
function getEmailSummary(from, subject, body) {
try {
const prompt = createSummarizationPrompt(from, subject, body);
const response = callGeminiApi(prompt);
const responseCode = response.getResponseCode();
const responseBody = response.getContentText();
if (responseCode === 200) {
const parsedData = JSON.parse(responseBody);
// Safely access the nested text using optional chaining (?.)
const summaryText = parsedData.candidates?.[0]?.content?.parts?.[0]?.text;
if (summaryText) {
return summaryText.trim();
} else {
Logger.log("Could not find summary text in API response: " + responseBody);
return null;
}
} else {
Logger.log(`Error calling Gemini API. Status: ${responseCode}. Response: ${responseBody}`);
return null;
}
} catch (e) {
Logger.log("Failed to get email summary. Error: " + e.message);
return null;
}
}
With this robust getEmailSummary function, we now have a reliable way to pass in email details and get back a clean, well-formatted summary. We’re ready to move on to the final step: putting these summaries where we can see them.
Now that we have our shiny, AI-generated summary, it’s not much use just sitting in a variable. We need to persist it somewhere useful. Rather than creating a new document for every single summary (which would quickly clutter your Drive), we’ll create a single Google Doc that acts as a running log. This approach keeps everything neat, chronological, and easily searchable.
We’ll write a function that intelligently finds this log document or creates it if it’s the first time the script is run. Then, we’ll append our new summary along with some crucial metadata.
The first task is to get a handle on our target Google Doc. We’ll use the DriveApp service, a powerful part of Apps Script for interacting with files and folders in your Google Drive.
Our logic is simple:
Search for a file with a specific, predefined name (e.g., “Gmail AI Summaries”).
If it exists, open it.
If it doesn’t exist, create it.
This “get or create” pattern is incredibly common and useful in automation scripts. It makes our code robust and idempotent—meaning you can run it multiple times without creating duplicate files.
Here’s a helper function that encapsulates this logic:
/**
* Finds the summary document by name, or creates it if it doesn't exist.
* @returns {GoogleAppsScript.Document.Document} The summary Google Doc.
*/
function getOrCreateSummaryDoc() {
const docName = "Gmail AI Summaries";
const files = DriveApp.getFilesByName(docName);
if (files.hasNext()) {
// If the file exists, open and return it.
const file = files.next();
Logger.log(`Found existing summary document: ${file.getName()}`);
return DocumentApp.openById(file.getId());
} else {
// If the file doesn't exist, create it, add a title, and return it.
const doc = DocumentApp.create(docName);
doc.getBody().appendParagraph("Gmail AI Summaries Log").setHeading(DocumentApp.ParagraphHeading.TITLE);
doc.saveAndClose(); // Save the initial changes
Logger.log(`Created new summary document: ${doc.getName()}`);
return DocumentApp.openById(doc.getId()); // Re-open to append content
}
}
In this snippet:
DriveApp.getFilesByName(docName) returns an Iterator of all files matching the name.
files.hasNext() checks if the iterator contains at least one file.
If a file is found, we grab it with files.next() and use DocumentApp.openById() to get the document object we can work with.
If no file is found, DocumentApp.create(docName) builds a brand new Google Doc for us. We even add a nice title to the new document for good measure.
With our document object in hand, we can now add content. This is handled by the DocumentApp service, which lets us manipulate the internals of the document—paragraphs, tables, images, and more.
For our purpose, we want to append each new summary to the end of the document. To keep things clean and readable, we’ll add a horizontal rule to visually separate each entry.
Let’s create a new function, storeSummary, that takes the summary text and the original Gmail thread as arguments. This function will be the main entry point for this step.
function storeSummary(summaryText, thread) {
const summaryDoc = getOrCreateSummaryDoc();
const body = summaryDoc.getBody();
// Add a separator for a new entry
body.appendHorizontalRule();
// ... We will add metadata here in the next section ...
// Append the actual summary from Gemini
body.appendParagraph(summaryText);
summaryDoc.saveAndClose();
Logger.log(`Appended summary to "${summaryDoc.getName()}"`);
}
summaryDoc.getBody() gives us the main body section of the document where all the content lives.
body.appendHorizontalRule() inserts a simple line—perfect for separating log entries.
body.appendParagraph(summaryText) adds our AI-generated summary as a new paragraph.
summaryDoc.saveAndClose() is crucial. It commits all our changes to the document.
A summary is only useful if you have context. “When was this generated?” and “Where is the original email?” are the two most important questions we can answer. Adding this metadata makes our summary log infinitely more practical.
We’ll add two key pieces of information before the summary text:
Timestamp: A simple new Date() will tell us exactly when the script processed the thread.
Source Link: The GmailThread object has a fantastic method, getPermalink(), which gives us a direct, permanent URL to that specific thread in the Gmail interface.
Let’s update our storeSummary function to include this rich context.
/**
* Appends a summary and its metadata to the master Google Doc.
* @param {string} summaryText The summary generated by the AI.
* @param {GoogleAppsScript.Gmail.GmailThread} thread The original Gmail thread.
*/
function storeSummary(summaryText, thread) {
const summaryDoc = getOrCreateSummaryDoc();
const body = summaryDoc.getBody();
const subject = thread.getFirstMessageSubject();
// Add a separator for a new entry
body.appendHorizontalRule();
// 1. Add the Timestamp as a heading
const timestamp = new Date().toLocaleString();
body.appendParagraph(`Summary for "${subject}"`)
.setHeading(DocumentApp.ParagraphHeading.HEADING2);
// 2. Add the source link and generation time
const permalink = thread.getPermalink();
const metadataParagraph = body.appendParagraph(''); // Create an empty paragraph
metadataParagraph.appendText(`Generated: ${timestamp} | `);
metadataParagraph.appendText('View Original Thread')
.setLinkUrl(permalink);
// 3. Add the AI-Generated Summary
body.appendParagraph(summaryText).setItalic(true);
summaryDoc.saveAndClose();
Logger.log(`Successfully appended summary for thread "${subject}" to the document.`);
}
Look at how we’ve enhanced the output:
We grab the thread’s subject using thread.getFirstMessageSubject() to create a clear, contextual heading for each entry.
We use setHeading() to style the subject line, making the document easy to scan.
We create a single paragraph for the metadata. The key here is appendText(), which allows us to add multiple text runs to the same paragraph.
Crucially, .setLinkUrl(permalink) turns the “View Original Thread” text into a clickable hyperlink that takes you straight to the email in Gmail.
We even italicize the AI summary text with .setItalic(true) to visually distinguish it from our metadata.
With this function complete, we now have a robust system for logging every summary in a single, well-organized, and highly useful Google Doc.
With our setup complete, it’s time to bring everything together in Google Apps Script. This script will act as the central nervous system of our operation, fetching emails, communicating with the Gemini API, and logging the results in our Google Sheet.
Below is the complete, commented script. Copy and paste this into your Google Apps Script editor, replacing the placeholder values at the top with your own information.
// -----------------------------------------------------------------------------
// CONFIGURATION
// -----------------------------------------------------------------------------
// IMPORTANT: Replace with your actual Gemini API Key
const GEMINI_API_KEY = "YOUR_GEMINI_API_KEY";
// The name of the Google Sheet tab where summaries will be stored
const SHEET_NAME = "Email Summaries";
// The Gmail search query to find emails you want to summarize.
// Examples:
// "is:unread label:newsletter" - Unread emails with the 'newsletter' label
// "is:unread from:[email protected]" - Unread emails from a specific sender
// "is:unread" - All unread emails (use with caution, can be a lot!)
const GMAIL_QUERY = "is:unread label:summarize-me";
// The prompt you want to send to Gemini. The script will append the email body to this.
const PROMPT = `
Please summarize the key points and any action items from the following email.
Provide the summary as a concise paragraph. If there are specific action items, list them below the summary using bullet points.
Email Content:
`;
// -----------------------------------------------------------------------------
// MAIN FUNCTION
// -----------------------------------------------------------------------------
/**
* Main function to orchestrate the email processing and summarization.
* This is the function you will set the trigger to run.
*/
function processAndSummarizeEmails() {
const threads = fetchUnreadEmails(GMAIL_QUERY);
if (threads.length === 0) {
Logger.log("No new emails found matching the query. Exiting.");
return;
}
Logger.log(`Found ${threads.length} email threads to process.`);
for (const thread of threads) {
const message = thread.getMessages()[0]; // Process the first message in the thread
const emailData = {
date: message.getDate(),
from: message.getFrom(),
subject: message.getSubject(),
body: message.getPlainBody().substring(0, 15000) // Truncate body to respect API limits
};
try {
Logger.log(`Generating summary for email with subject: "${emailData.subject}"`);
const summary = callGeminiAPI(emailData.body);
const dataToWrite = [
emailData.date,
emailData.from,
emailData.subject,
summary
];
writeToSheet(dataToWrite);
Logger.log("Successfully wrote summary to Google Sheet.");
// Mark the entire thread as read to avoid reprocessing
thread.markRead();
Logger.log("Marked email thread as read.");
} catch (e) {
Logger.log(`Error processing email: ${emailData.subject}. Error: ${e.toString()}`);
}
}
}
// -----------------------------------------------------------------------------
// HELPER FUNCTIONS
// -----------------------------------------------------------------------------
/**
* Fetches unread email threads from Gmail based on a search query.
* @param {string} query - The Gmail search query.
* @returns {GoogleAppsScript.Gmail.GmailThread[]} An array of Gmail threads.
*/
function fetchUnreadEmails(query) {
return GmailApp.search(query);
}
/**
* Calls the Gemini Pro API to generate a summary for the given text.
* @param {string} emailBody - The plain text body of the email.
* @returns {string} The summary generated by the Gemini API.
*/
function callGeminiAPI(emailBody) {
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${GEMINI_API_KEY}`;
const requestBody = {
"contents": [{
"parts": [{
"text": PROMPT + emailBody
}]
}],
"generationConfig": {
"temperature": 0.4,
"topK": 32,
"topP": 1,
"maxOutputTokens": 1024,
"stopSequences": []
},
"safetySettings": [
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}
]
};
const options = {
'method': 'post',
'contentType': 'application/json',
'payload': JSON.stringify(requestBody),
'muteHttpExceptions': true // Prevents script from stopping on HTTP errors
};
const response = UrlFetchApp.fetch(url, options);
const responseCode = response.getResponseCode();
const responseBody = response.getContentText();
if (responseCode === 200) {
const jsonResponse = JSON.parse(responseBody);
// Navigate through the nested JSON to get the actual text content
return jsonResponse.candidates[0].content.parts[0].text;
} else {
throw new Error(`API Error: ${responseCode} - ${responseBody}`);
}
}
/**
* Appends a row of data to the specified Google Sheet.
* @param {Array<any>} data - An array of values to write to the row.
*/
function writeToSheet(data) {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheet = spreadsheet.getSheetByName(SHEET_NAME);
if (!sheet) {
throw new Error(`Sheet with name "${SHEET_NAME}" not found.`);
}
// Append the data as a new row
sheet.appendRow(data);
}
Let’s dissect the script to understand how each part contributes to the automation.
At the top of the script, we define several constants. This makes it easy to change key parameters without digging through the code.
GEMINI_API_KEY: The most important variable. This is your secret key for accessing the Gemini API.
SHEET_NAME: Must exactly match the name of the tab in your Google Sheet where you want the summaries to go.
GMAIL_QUERY: This is a powerful Gmail search string. We’ve set it to find unread emails with a specific label (summarize-me), but you can customize this to fit your workflow perfectly.
PROMPT: This is where the magic of [Prompt Engineering for Reliable Autonomous Workspace Agents for Reliable Autonomous Workspace Agents](https://votuduc.com/prompt-engineering-for-reliable-autonomous-workspace-agents-p-20260319404106) happens. We provide clear instructions to Gemini on how we want the email summarized. The script will append the actual email content to this prompt before sending the request.
processAndSummarizeEmails()This is the main orchestrator function. When it runs, it executes the entire workflow in a logical sequence:
It calls fetchUnreadEmails() to get a list of email threads matching your query.
It loops through each thread.
For each email, it extracts the necessary data (date, sender, subject, body). Note that we truncate the body (substring(0, 15000)) as a safeguard against excessively long emails that might exceed API token limits.
It calls callGeminiAPI() with the email body to get the summary.
It calls writeToSheet() to log the original email info and the new summary.
Crucially, it marks the email thread as read (thread.markRead()). This prevents the script from processing the same email again on its next run.
callGeminiAPI(emailBody)This function handles all communication with the Gemini API.
URL Construction: It builds the API endpoint URL, embedding your GEMINI_API_KEY as a query parameter.
Request Body: It constructs the JSON payload exactly as the Gemini API expects. The contents.parts.text field combines our predefined PROMPT with the emailBody. We also include some basic generationConfig parameters like temperature (for creativity) and maxOutputTokens (to limit summary length).
API Call: It uses Google Apps Script’s built-in UrlFetchApp.fetch() service to send the POST request.
Response Handling: It parses the JSON response from the API. The actual summary text is nested deep inside the response object, so we navigate to candidates[0].content.parts[0].text to extract it. It also includes error handling to catch non-200 responses from the API.
writeToSheet(data)A straightforward helper function that uses the SpreadsheetApp service to find your active spreadsheet, get the correct sheet by name, and then use .appendRow() to add the summary data to the next available empty row.
A script is only truly automated if it runs on its own. We’ll use a time-driven trigger to execute our processAndSummarizeEmails function on a schedule.
In the Google Apps Script editor, click on the Triggers icon (it looks like a clock) in the left-hand sidebar.
Click the Add Trigger button in the bottom-right corner.
Configure the trigger settings in the pop-up window:
Choose which function to run: Select processAndSummarizeEmails.
Choose which deployment should run: Leave as Head.
Select event source: Change this to Time-driven.
Select type of time-based trigger: You have many options. Hour timer is a good choice to check for new emails frequently. You could choose Day timer to get a daily digest.
Select time interval/time of day: Based on your previous selection, choose how often you want it to run (e.g., “Every hour” or “8am to 9am”).
You’ve done it. You’ve successfully bridged the gap between your Gmail inbox and the powerful generative AI of Gemini Pro. What you’ve built isn’t just a clever script; it’s a foundational piece of a personalized productivity engine. You’ve taken a chaotic stream of information and imposed order, transforming raw data into actionable intelligence. Let’s recap what this means and explore where you can go from here.
The traditional goal of “Inbox Zero” is about clearing out emails. We’ve taken that a step further into “Inbox Intelligence.” Instead of just archiving messages, you’re now programmatically understanding them. With Google Apps Script as the diligent orchestrator and Gemini Pro as the cognitive engine, you’ve created a system that:
Automates Triage: The script sifts through the noise, identifying emails that match your specific criteria.
Distills Knowledge: Gemini Pro reads and synthesizes the content, saving you precious time and mental energy.
Creates Clarity: You receive a clean, concise summary delivered directly to you, allowing you to grasp the essence of your communications at a glance.
This workflow is a testament to the power of modern APIs and simple scripting. You’ve effectively built a custom AI assistant tailored to your exact needs, running silently and efficiently in the background.
The script we built is a powerful proof-of-concept, but it’s just the beginning. To turn this into a robust, day-to-day tool, consider these enhancements:
Advanced Error Handling: Production-level scripts need to be resilient.
API Failures: Wrap the UrlFetchApp call in a try...catch block to gracefully handle network issues or temporary Gemini API outages. You could implement a retry mechanism with exponential backoff.
Input Validation: What if an email has no body or is formatted unexpectedly? Add checks to ensure you’re only processing valid content.
Logging: Use Logger.log() or, even better, log errors and successful runs to a dedicated Google Sheet. This creates an audit trail and makes debugging infinitely easier.
A More Sophisticated User Interface (UI):
Custom Menu: Use SpreadsheetApp.getUi().createMenu() to add a custom menu to your Google Sheet, allowing you to trigger the summary process manually or configure settings without digging into the script editor.
HTML Service: Build a simple web app interface using Apps Script’s HtmlService. This could allow a non-technical user to select date ranges, specify search queries, or even tweak the AI prompt.
Gmail Add-on: For the ultimate integration, use Card Service to build a sidebar right inside Gmail. Imagine selecting a few email threads and clicking a button to summarize them instantly in a pop-up card.
Smarter Workflows:
Dynamic Labeling: Modify the prompt to ask Gemini to categorize each email (e.g., “Invoice,” “Project Update,” “Urgent Request”). Then, use the GmailApp service to automatically apply the corresponding labels in your inbox.
Action Item Extraction: Prompt Gemini to identify and extract specific action items or deadlines. From there, you could use the Google Calendar API or Google Tasks API to automatically create events and to-do items.
Building custom solutions like this is incredibly rewarding and powerful. It gives you unparalleled control over your digital workflows. However, as you’ve seen, moving from a script to a polished, reliable application requires significant effort in UI design, error handling, and maintenance.
If you’re inspired by the potential of this automation but prefer a ready-made, enterprise-grade solution, we invite you to explore the ContentDrive app ecosystem.
ContentDrive takes the core concepts we’ve explored—automating, summarizing, and acting on your content—and packages them into a suite of powerful, intuitive, and no-code applications for Automated Email Journey with Google Sheets and Google Analytics. Instead of writing the code, you connect the blocks.
With ContentDrive, you can:
Deploy sophisticated document and email processing workflows in minutes, not hours.
Leverage pre-built integrations with services like Slack, Asana, and more.
Benefit from a polished user interface, robust error handling, and dedicated support.
You’ve now experienced firsthand the magic of automating your inbox with AI. Take the next step and see how ContentDrive can amplify that power across your entire organization.
**Explore the ContentDrive Apps Today & Supercharge Your Productivity**Whether you choose the path of the custom script developer or the streamlined efficiency of a ready-made platform, the principle remains the same: you are now in control. You’ve taken a significant step from being managed by your inbox to actively managing your information flow. The journey into intelligent automation is an exciting one, and you’ve just built your first vehicle. Happy building
[EDITOR’S NOTE: AI failed to complete this section automatically.]
Quick Links
Legal Stuff
