For nearly 30 years, professionals have spent countless hours replying to emails—a repetitive task that consumes valuable time. This article presents a simple, cheap, and effective way to generate email reply drafts, helping you save up to 90% of your keystrokes.
At our company, email is managed through Google Workspace. While AI features like Gemini’s smart reply tools are helpful they are limited to tiny responses. In 2025 we still can’t rely on Google to fully analyze our inbox, understand our workflows, and automatically draft relevant replies for every message.
The good news? You don’t have to wait. With Google Apps Script calling OpenAI API, you can already automate Gmail draft reply and streamline your email management today. It costs around a one to two USD cents per draft edited.
The Google Apps Script calling OpenAI API
Below is the skeleton of the Google Apps Script we use. TODO markers indicate where you should customize the code. Additional remarks follow after the script.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
// ChatGPT API Key obtained by creating an app key at https://platform.openai.com/api-keys const OPENAI_API_KEY = "sk-proj-...TODO..."; function autoReplyDraft() { // TODO edit the selector to your need // For example add has:blue-star ==> l:^ss_sb has:green-check ==> l:^ss_cg // See more here: https://stackoverflow.com/a/31158059/27194 const selector = 'in:inbox is:unread -in:drafts'; var threads = GmailApp.search(selector); Logger.log(` ${threads.length} thread(s) matched by "${selector}"`); if (threads.length === 0) { return; } const drafts = GmailApp.getDraftMessages(); const urls = []; for (const thread of threads) { if(processThread(thread, drafts)) { const url = `https://mail.google.com/mail/u/0/#inbox/${thread.getId()}`; urls.push(url); } } // urls contain the list of threads with a draft edited if needed Logger.log(`#Draft edited: ${urls.length}`); } function processThread(thread, drafts) { // Don't process a thread that has a draft response already, if(hasDraftInThread(thread, drafts)) { return false; } // Most recent message first thanks to reverse() !! const msgs = thread.getMessages().reverse(); // TODO Find the most suited message in thread to answer to. // It should be the most recent one most of the time. var msg = msgs[0]; // Don't write draft for some threads (like don't answer to newsletter) if(!canReplyTo(msg)) { return false; } var subject = msg.getSubject(); var bodyAsHTML = msg.getBody(); const msgDate = msg.getDate(); const from = msg.getFrom(); var to = msg.getTo(); var prompt = `You are a senior professional sales, marketing and support engineer at TODO. Write a professional reply to the email found at the end of this prompt. - The reply must be polite, clear and helpful. - Most importantly keep your reply consise. - The reply must be formatted with HTML. - Line feed <br/> can only be inserted at the end of sentences. - VERY IMPORTANT: Make the reply easy to read by separating the greeting, each logical section and the signature with an extra line feed <br/>. - Dont repeat the mail subject anywhere in the email. - In answer only use ASCII characters. - TODO YOUR EXTRA PROMPT `; // TODO determine proper "to", make sure you don't draft a reply to yourself // TODO polish the prompt based on some keyword in subject and mail body... prompt += ` Finally here is the email: From: ${from} Subject: ${subject} Here is the email body as HTML in a CDATA tag: <![CDATA[${bodyAsHTML}]]>`; var htmlReply = callOpenAI(prompt); if (htmlReply === false) { return false; } // TODO determine if the original email should be quoted var quoteTheOriginalMail = true; if(quoteTheOriginalMail) { htmlReply += getMsgAsHTMLQuoted(msg); } // With createThreadedReplyDraft() w // we can have different subject if needed and remain in the same thread, // unlike : thread.createDraftReply('', {htmlBody: fullHtmlReply }); createThreadedReplyDraft(thread.getId(), htmlReply, subject, to); return true; } function canReplyTo(msg) { const from = msg.getFrom(); const subject = msg.getSubject(); // Block by senders const fromLowerCase = from.toLowerCase(); const blockedSenders = ["@xyz.com", "@abc.com" , /*..TODO..*/]; if (blockedSenders.some(s => fromLowerCase.includes(s))) { return false; } // Block by subjects const subjectLowerCase = subject.toLowerCase(); const blockedSubjects = ["subject1", "subject2", /*..TODO..*/]; if (blockedSubjects.some(s => subjectLowerCase.includes(s))) { return false; } return true; } function getMsgAsHTMLQuoted(msg) { const bodyHTML = msg.getBody(); const date = msg.getDate(); //const from = msg.getFrom(); const raw = msg.getRawContent(); const fromHeader = raw.match(/^From: (.+)$/m); const from = fromHeader ? fromHeader[1] : msg.getFrom(); // fallback // Build quoted original message const quoted = `<br/><br/>On ${formatDateAsInGmail(date)}, ${getFormattedFrom(msg)} wrote:\n <blockquote style="border-left:1px solid #ccc; margin-left:0.5em; padding-left:0.5em;"> ${bodyHTML} </blockquote>`; return quoted; } function hasDraftInThread(thread, drafts) { const threadId = thread.getId(); return drafts.some(draft => draft.getThread().getId() === threadId); } // // createThreadedReplyDraft(threadId, htmlBody, subject, to) // Use Gmail API to draft in the same thread with eventually a different email subject and 'to emails' // function createThreadedReplyDraft(threadId, htmlBody, subject, to) { const thread = GmailApp.getThreadById(threadId); const message = thread.getMessages().pop(); // get the last message // You need to get the Message-ID header (not available directly in GmailApp) const messageIdHeader = getMessageIdHeader(message.getId()); // Build MIME message const mimeMessage = `To: ${to}\r\n` + `Subject: ${subject}\r\n` + `In-Reply-To: ${messageIdHeader}\r\n` + `References: ${messageIdHeader}\r\n` + `Content-Type: text/html; charset="UTF-8"\r\n` + `MIME-Version: 1.0\r\n\r\n` + `${htmlBody}`; // Use UTF8 to get rid of complex char that would ge interpreted with a '?' in gmail draft response const utf8Bytes = Utilities.newBlob(mimeMessage).getBytes(); // Base64 encode and make it URL-safe const encodedEmail = Utilities.base64EncodeWebSafe(utf8Bytes); // Create draft using Gmail API Gmail.Users.Drafts.create({ message: { raw: encodedEmail, threadId: threadId } }, 'me'); } // // callOpenAI(prompt) // function callOpenAI(prompt) { for (var i = 0; i < 3; i++) { var htmlReply = callOpenAIEx(prompt); if (!htmlReply.includes('Empty answer')) { return htmlReply; // Success } } Logger.log("callOpenAI(): Failed to get a valid HTML reply after 3 attempts."); return false; // Failed after 3 attempts } function callOpenAIEx(prompt) { const payload = { // GPT‑3.5-Turbo ( 16k tokens): ~ 65K characters // GPT‑4 ( 8k tokens): ~ 32K characters // GPT‑4 Turbo (128k tokens): ~ 512K characters // GPT‑4.1 ( 1M tokens): ~ 4M characters model: "gpt-4-turbo", messages: [ { role: "system", content: "You are a professional sales, marketing and support engineer at TODO YourSite.com. " }, { role: "user", content: prompt } ], temperature: 0.5 }; const response = UrlFetchApp.fetch("https://api.openai.com/v1/chat/completions", { method: "post", contentType: "application/json", headers: { Authorization: "Bearer " + OPENAI_API_KEY }, payload: JSON.stringify(payload), muteHttpExceptions: true }); console.log(response.getResponseCode()); console.log(response.getContentText()); const json = JSON.parse(response.getContentText()); return json.choices?.[0]?.message?.content ?? "Empty answer"; } |
Remarks
Script features: We’ve developed common helper functions such as callOpenAI(prompt)
, createThreadedReplyDraft(threadId, htmlBody, subject, to)
, and hasDraftInThread(thread, drafts)
to help you get up and running quickly.
Edit Draft vs Sending it: This approach focuses on drafting replies to save you time and keystrokes. While Google Apps Script can automatically send the edited draft, we believe it’s best practice to review and confirm that the response is exactly what you intend to send. Else, edit the response and re-adjust your prompts.
Determine the scenarios somehow and write a prompt per scenario: We found that building one large script to cover every possible scenario (pre-sales, leads, support, etc.) is not efficient. A better approach is to first identify which process or stage the email thread belongs to. This can be done by matching keywords in the subject or body of the email, or by making an initial call to the ChatGPT API with a dedicated determine-scenario-prompt followed by the email subject and body, or by combining both methods (keywords + API call).
You’ll need to spend some time fine-tuning your prompts. since the prompt will evolved, you will need to test for regressions. To do so create a Gmail label called autdrafttest
and apply it to a set of incoming mock emails. To verify that all your prompts behave as expected, mark these threads as unread, update the line const selector = 'in:inbox is:unread -in:drafts label:autdrafttest'
in your script, and then run it and check that drafted replies are correct.
Add Services: You will need to add some Services that your script can use like Gmail
or Drive
. For example, we use the excellent Gmail extension Simple Gmail Notes (SGN), which provides a thread-based note system. The notes are stored in our Google Drive, and our script can read and edit each note seamlessly. Doing so both add context to edit the reply and save some keystrokes. (Disclaimer: We are not affiliated with SGN).
Conclusion
With well-crafted refined prompts, our experience shows a significant time savings, allowing to handle over 90% of emails much faster—for example, increasing productivity from around 20 replies per hour to 60. The key is to iteratively test and refine your scripts and prompts whenever it’s beneficial to do so.
While there are many other approaches using AI agents and orchestrator solutions, like Zapier or Make we found this way to be particularly simple, cheap, effective and highly customizable.
We’re constantly hearing about new AI systems that outperform professionals in various fields. While we haven’t reached full replacement of human expertise—and hopefully won’t for some time—the real opportunity lies in combining AI capabilities with human skills to boost productivity.
Perhaps one day Google Gmail or Microsoft Outlook will offer an out-of-the-box solution that generates accurate draft replies based on your company’s email activity over the past X months. But will it be cheaper and more customizable than this approach? That remains to be seen.
I thought this was a .NET blog?
Yes it is 99% .NET Blog, and sometime we post about more exotic findings worth sharing 🙂