How to build your own ChatGPT Email bot with Gmail and Google Apps Script

Working with the OpenAI API is some of the most fun I’ve had in ages. It makes building AI powered workflows magically easy. Combined with the Google Apps Script Editor; An easy to use web based integrated development environment. You just need a browser to get started!

We can build a fully functional email bot in just a few hours. It will respond to our emails in the same way as ChatGPT. This guide explains how to build a chatbot using the OpenAI Chat Completion API, Google Apps Script, and a Gmail Address. Follow the steps to make your own chatbot!

Overview of the build goals

The flow of our gmail GPT bot

The build is pretty straightforward. As a user, I want to send an email with my prompt and receive a reply from GPT. To accomplish this, our solution roughly needs to do the following:

  • Receive email from user

  • Map email conversation to messages for the Chat Completion API

  • Receive the completion response

  • Reply to the thread

  • Mark read and archive

Because we are building something for ourselves and not a consumer application. There are some key shortcuts that simplify our build. Using Google Apps Script allows us to easily extend google services with custom functions.

Setting up Gmail, API keys, and Google Apps Script properties

Before we dig into the script, there are a few things we need to set up first.

1. Set up a new Gmail account for the bot

The key piece is a Google Apps Script that responds to emails to a Gmail account. So we need to set up a Gmail account for our bot. Sign up for a new account here. The following steps should be configured from the Bot account.

2. Set up an OpenAI account and get an API key and your Org ID

We can’t access the completion API without an API key. If you don’t have an OpenAI account, register for one here.

Once you have an account, set up billing and usage limits and alerts. API keys can be created in User > API Keys. Org ID can be found in Organization > Settings.

3. Set up a Gmail filter and label for emails you want the bot to respond to

We don’t want the bot to respond to every email, just the ones that are from “approved” senders. I set up a label called "bot response". I also created a filter to label emails from my personal email with that label.

Test this step by sending the bot account an email and see if the filter and labeling works as expected. The incoming email is automatically labeled but remains unread.

4. Create a new Google Apps Script in the bot Gmail account.

Go to script.google.com in the bot account and create a new project. You should be greeted with an IDE with “Untitled Project” and a boilerplate function `myFunction`

5. Go to Project Settings and add API keys to the script properties

We don’t want to hardcode the API keys into the script, so we save it in script properties. This way, when we share our code we won’t accidentally expose our secrets. The properties we need to set are.

BOT_EMAIL_ACCOUNT: the account we just created. yourbotname@gmail.com
OPENAI_API_KEY: the API key you created in step 2.
OPENAI_ORG_ID: your OpenAI Org ID

Scripting the message flow

Now that everything is set up, we can move on to building the script.

6. Configure variables

const properties = PropertiesService.getScriptProperties()
const OPENAI_API_KEY = properties.getProperty('OPENAI_API_KEY')
const OPENAI_ORG_ID = properties.getProperty('OPENAI_ORG_ID')
const BOT_EMAIL_ACCOUNT = properties.getProperty('BOT_EMAIL_ACCOUNT')
const EMAIL_LABEL = 'bot-response'
const SYSTEM_MESSAGE = { "role": "system", "content": "You are a helpful assistant." }

We use the Properties Service to retrieve the script properties we set in step 5.

The email label is the one we set up in step 3

According to the OpenAI docs, “The system message helps set the behavior of the assistant.” I kept it simple here. However, it's fun to experiment with different scenarios. For example, "you are a pirate" or "you are a lead actor that only responds in Shakespearean sonnets".

async function getOpenAIChatResponse(messages = [{
  'role': 'user',
  'content': 'Say this is a test!'
}]) {
  try {
    const response = await UrlFetchApp.fetch('https://api.openai.com/v1/chat/completions', {
      'method': 'post',
      'headers': {
        'Authorization': `Bearer $`,
        'OpenAI-Organization': OPENAI_ORG_ID,
        'Content-Type': 'application/json'
      },
      'payload': JSON.stringify({
        'model': 'gpt-3.5-turbo',
        messages,
        // I like it spicy but not too hot
        'temperature': 0.7
      })
    })
    const data = JSON.parse(response.getContentText())
    // gets the first choice response
    const textResponse = data['choices'][0]['message']['content']
    return textResponse
  } catch (error) {
    // We're not going to deal with the token limit here
    return `ERROR: $. Please start a new Email Thread`
  }
}

We write a function here that we will use later. The function calls the chat completion endpoint with an array of messages. We use a default value as a test.

Later, when we use this function, we will pass in an array of messages. This will happen each time we use the function.

Using the Apps Script IDE, we can debug this function to ensure it runs correctly. The test should return “this is a test”.

The header values come from the script properties we retrieved in step 6. The organization ID is optional if you don’t need to specify which org is paying for your calls. We use `['choices'][0]['message']['content']` to get the bare response.

Note, I am not dealing with the token limit in this script (yet). There will be a limit to how long a thread you can have with the bot.

8. Retrieve unread email threads and reply

Using the function we wrote in step 7, we can now respond to the email threads.

async function respondToLabeledEmails() {
  // Get the threads with the label 'bot response' and that are unread
  const threads = GmailApp.search(`label:$`);
  const unreads = threads.filter(t => t.isUnread())
  await unreads.forEach(async (thread) => {
    // Get the message history of the thread
    const m = [SYSTEM_MESSAGE]
    const messages = thread.getMessages().map((message) => {
      return {
        // if the sender is the bot, it's the assistant, otherwise assume it's the user.
        'role': isBot(message.getFrom()) ? 'assistant' : 'user',
        'content': message.getPlainBody()
      };
    });
    const response = await getOpenAIChatResponse(m.concat(messages))
    thread.reply(response);
    thread.markRead();
    thread.moveToArchive();
  });
}
function isBot(sender) {
  return sender === BOT_EMAIL_ACCOUNT ? true : false
}

In the first part of the function, we get all the unread email threads that are labeled for response. This is what we set up in step 3.

We get the messages for each unread email. We then map them into the format required by the chat completion API. We use `.getFrom()` to set the role for each message in the thread.

We feed the messages into the function we wrote previously. This will give us a response from the API.

Once we get the response, reply with the message. Then the email is marked read and moved to the archive. This way the message will not be responded to again until the user responds in the thread, restarting the chain.

9. Set up the script to run on a timer

Google Apps Script comes with several kinds of triggers. One of them is a time based trigger like a cron job. Set this up by going to the left bar and selecting Triggers > Add a Trigger.

Choose the function we wrote in step 8, `respondToLabeledEmails`. Set the event source to “Time-Driven” and set an interval.

I use “Minutes Timer” and “Every 5 Minutes. Each time the function runs, any unread emails will be responded to. From the user perspective, the maximum response time of the Bot will be 5mins. When you set up the trigger, you will be asked to grant the script permissions from the google account. Make sure you do this, or the script will not run properly.

10. Send the bot an email

With everything set, we can now send our bot an email prompt. If we’ve followed the steps properly, the bot should respond to our email with a chat completion response. Now we have a Gmail GPT Bot!

It’s aliiiivveeeeeeee.

Extending the Script

If you’ve made it this far and got it to work, congratulations! If you didn't, you can copy and paste the script from this gist after you complete step 5.

Google Apps Script has APIs for other G Suite or Google Workplace applications. We can "add AI" to our google docs, google sheets, and slides. The same pattern can be used in other scripting extension environments where you can call external services.