Alternate Runtimes for Google Workspace Add-ons is coming soon. Learn more.

Quickstart: Cats Google Workspace add-on

Google Workspace add-on sidebar

Complete the steps described in the rest of this page, and in about ten minutes you can create a Google Workspace add-on. This example add-on demonstrates some of the features of Google Workspace add-ons, such as homepages, contextual triggers, and connecting to third-party APIs.

This add-on creates contextual and non-contextual sidebars in Gmail, Calendar, and Drive. The sidebar presents a random image of a cat with text overlaying it. The text is either static (for homepages) or taken from the host application context (for contextual triggers).

The following steps show you how to create this add-on yourself:




Step 1: Create the script project

First create a new script project and fill it with the add-on code:

  1. Create a new standalone Apps Script project.
  2. In the left-nav, click the icon next to the Code.gs entry, then select Rename.
  3. In the next dialog, rename the script project to Cats, then click OK.
  4. After the script project rename completes, the Rename File dialog opens. Rename the Code file to Common, then click OK.
  5. Click File > New > Script file. Enter the new file name Gmail, then click OK. This creates a second code file in the script project named Gmail.gs. You can see it in the left nav under Common.gs.
  6. Repeat the above step to create another script file named Calendar, and then again to create one named Drive. When you are done you should have four separate script files listed in the left-nav.
  7. Replace the content of each of those files with the following content, respectively:

Common.gs

/**
 * This simple Google Workspace Add-on shows a random image of a cat in the
 * sidebar. When opened manually (the homepage card), some static text is
 * overlayed on the image, but when contextual cards are opened a new cat image
 * is shown with the text taken from that context (such as a message's subject
 * line) overlaying the image. There is also a button that updates the card with
 * a new random cat image.
 *
 * Click "File > Make a copy..." to copy the script, and "Publish > Deploy from
 * manifest > Install add-on" to install it.
 */

/**
 * The maximum number of characters that can fit in the cat image.
 */
var MAX_MESSAGE_LENGTH = 40;

/**
 * Callback for rendering the homepage card.
 * @return {CardService.Card} The card to show to the user.
 */
function onHomepage(e) {
  console.log(e);
  var hour = Number(Utilities.formatDate(new Date(), e.userTimezone.id, 'H'));
  var message;
  if (hour >= 6 && hour < 12) {
    message = 'Good morning';
  } else if (hour >= 12 && hour < 18) {
    message = 'Good afternoon';
  } else {
    message = 'Good night';
  }
  message += ' ' + e.hostApp;
  return createCatCard(message, true);
}

/**
 * Creates a card with an image of a cat, overlayed with the text.
 * @param {String} text The text to overlay on the image.
 * @param {Boolean} isHomepage True if the card created here is a homepage;
 *      false otherwise. Defaults to false.
 * @return {CardService.Card} The assembled card.
 */
function createCatCard(text, isHomepage) {
  // Explicitly set the value of isHomepage as false if null or undefined.
  if (!isHomepage) {
    isHomepage = false;
  }

  // Use the "Cat as a service" API to get the cat image. Add a "time" URL
  // parameter to act as a cache buster.
  var now = new Date();
  // Replace formward slashes in the text, as they break the CataaS API.
  var caption = text.replace(/\//g, ' ');
  var imageUrl =
      Utilities.formatString('https://cataas.com/cat/says/%s?time=%s',
          encodeURIComponent(caption), now.getTime());
  var image = CardService.newImage()
      .setImageUrl(imageUrl)
      .setAltText('Meow')

  // Create a button that changes the cat image when pressed.
  // Note: Action parameter keys and values must be strings.
  var action = CardService.newAction()
      .setFunctionName('onChangeCat')
      .setParameters({text: text, isHomepage: isHomepage.toString()});
  var button = CardService.newTextButton()
      .setText('Change cat')
      .setOnClickAction(action)
      .setTextButtonStyle(CardService.TextButtonStyle.FILLED);
  var buttonSet = CardService.newButtonSet()
      .addButton(button);

  // Create a footer to be shown at the bottom.
  var footer = CardService.newFixedFooter()
      .setPrimaryButton(CardService.newTextButton()
          .setText('Powered by cataas.com')
          .setOpenLink(CardService.newOpenLink()
              .setUrl('https://cataas.com')));

  // Assemble the widgets and return the card.
  var section = CardService.newCardSection()
      .addWidget(image)
      .addWidget(buttonSet);
  var card = CardService.newCardBuilder()
      .addSection(section)
      .setFixedFooter(footer);

  if (!isHomepage) {
    // Create the header shown when the card is minimized,
    // but only when this card is a contextual card. Peek headers
    // are never used by non-contexual cards like homepages.
    var peekHeader = CardService.newCardHeader()
      .setTitle('Contextual Cat')
      .setImageUrl('https://www.gstatic.com/images/icons/material/system/1x/pets_black_48dp.png')
      .setSubtitle(text);
    card.setPeekCardHeader(peekHeader)
  }

  return card.build();
}

/**
 * Callback for the "Change cat" button.
 * @param {Object} e The event object, documented {@link
 *     https://developers.google.com/gmail/add-ons/concepts/actions#action_event_objects
 *     here}.
 * @return {CardService.ActionResponse} The action response to apply.
 */
function onChangeCat(e) {
  console.log(e);
  // Get the text that was shown in the current cat image. This was passed as a
  // parameter on the Action set for the button.
  var text = e.parameters.text;

  // The isHomepage parameter is passed as a string, so convert to a Boolean.
  var isHomepage = e.parameters.isHomepage === 'true';

  // Create a new card with the same text.
  var card = createCatCard(text, isHomepage);

  // Create an action response that instructs the add-on to replace
  // the current card with the new one.
  var navigation = CardService.newNavigation()
      .updateCard(card);
  var actionResponse = CardService.newActionResponseBuilder()
      .setNavigation(navigation);
  return actionResponse.build();
}

/**
 * Truncate a message to fit in the cat image.
 * @param {string} message The message to truncate.
 * @return {string} The truncated message.
 */
function truncate(message) {
  if (message.length > MAX_MESSAGE_LENGTH) {
    message = message.slice(0, MAX_MESSAGE_LENGTH);
    message = message.slice(0, message.lastIndexOf(' ')) + '...';
  }
  return message;
}

Gmail.gs

/**
 * Callback for rendering the card for a specific Gmail message.
 * @param {Object} e The event object.
 * @return {CardService.Card} The card to show to the user.
 */
function onGmailMessage(e) {
  console.log(e);
  // Get the ID of the message the user has open.
  var messageId = e.gmail.messageId;

  // Get an access token scoped to the current message and use it for GmailApp
  // calls.
  var accessToken = e.gmail.accessToken;
  GmailApp.setCurrentMessageAccessToken(accessToken);

  // Get the subject of the email.
  var message = GmailApp.getMessageById(messageId);
  var subject = message.getThread().getFirstMessageSubject();

  // Remove labels and prefixes.
  subject = subject
      .replace(/^([rR][eE]|[fF][wW][dD])\:\s*/, '')
      .replace(/^\[.*?\]\s*/, '');

  // If neccessary, truncate the subject to fit in the image.
  subject = truncate(subject);

  return createCatCard(subject);
}

/**
 * Callback for rendering the card for the compose action dialog.
 * @param {Object} e The event object.
 * @return {CardService.Card} The card to show to the user.
 */
function onGmailCompose(e) {
  console.log(e);
  var header = CardService.newCardHeader()
      .setTitle('Insert cat')
      .setSubtitle('Add a custom cat image to your email message.');
  // Create text input for entering the cat's message.
  var input = CardService.newTextInput()
      .setFieldName('text')
      .setTitle('Caption')
      .setHint('What do you want the cat to say?');
  // Create a button that inserts the cat image when pressed.
  var action = CardService.newAction()
      .setFunctionName('onGmailInsertCat');
  var button = CardService.newTextButton()
      .setText('Insert cat')
      .setOnClickAction(action)
      .setTextButtonStyle(CardService.TextButtonStyle.FILLED);
  var buttonSet = CardService.newButtonSet()
      .addButton(button);
  // Assemble the widgets and return the card.
  var section = CardService.newCardSection()
      .addWidget(input)
      .addWidget(buttonSet);
  var card = CardService.newCardBuilder()
      .setHeader(header)
      .addSection(section);
  return card.build();
}

/**
 * Callback for inserting a cat into the Gmail draft.
 * @param {Object} e The event object.
 * @return {CardService.UpdateDraftActionResponse} The draft update response.
 */
function onGmailInsertCat(e) {
  console.log(e);
  // Get the text that was entered by the user.
  var text = e.formInput.text;
  // Use the "Cat as a service" API to get the cat image. Add a "time" URL
  // parameter to act as a cache buster.
  var now = new Date();
  var imageUrl = 'https://cataas.com/cat';
  if (text) {
    // Replace formward slashes in the text, as they break the CataaS API.
    var caption = text.replace(/\//g, ' ');
    imageUrl += Utilities.formatString('/says/%s?time=%s',
        encodeURIComponent(caption), now.getTime());
  }
  var imageHtmlContent = '<img style="display: block; max-height: 300px;" src="'
      + imageUrl + '"/>';
  var response = CardService.newUpdateDraftActionResponseBuilder()
      .setUpdateDraftBodyAction(CardService.newUpdateDraftBodyAction()
          .addUpdateContent(imageHtmlContent,CardService.ContentType.MUTABLE_HTML)
          .setUpdateType(CardService.UpdateDraftBodyType.IN_PLACE_INSERT))
      .build();
  return response;
}

Calendar.gs

/**
 * Callback for rendering the card for a specific Calendar event.
 * @param {Object} e The event object.
 * @return {CardService.Card} The card to show to the user.
 */
function onCalendarEventOpen(e) {
  console.log(e);
  var calendar = CalendarApp.getCalendarById(e.calendar.calendarId);
  // The event metadata doesn't include the event's title, so using the
  // calendar.readonly scope and fetching the event by it's ID.
  var event = calendar.getEventById(e.calendar.id);
  if (!event) {
    // This is a new event still being created.
    return createCatCard('A new event! Am I invited?');
  }
  var title = event.getTitle();
  // If neccessary, truncate the title to fit in the image.
  title = truncate(title);
  return createCatCard(title);
}

Drive.gs

/**
 * Callback for rendering the card for specific Drive items.
 * @param {Object} e The event object.
 * @return {CardService.Card} The card to show to the user.
 */
function onDriveItemsSelected(e) {
  console.log(e);
  var items = e.drive.selectedItems;
  // Include at most 5 items in the text.
  items = items.slice(0, 5);
  var text = items.map(function(item) {
    var title = item.title;
    // If neccessary, truncate the title to fit in the image.
    title = truncate(title);
    return title;
  }).join('\n');
  return createCatCard(text);
}

  1. Click File > Save and click OK to save your project changes.

By dividing the add-on code across four script files, you've created a logical organization division based on the add-on's functionality. This helps with long-term add-on maintenance.

Step 2: Update the script manifest

Now configure the add-on by updating its manifest file:

  1. In the script editor, select the View > Show manifest file menu item. This opens the manifest file (appsscript.json) in the editor.
  2. Delete the content of the manifest and replace it with the following:

    {
      "timeZone": "America/New_York",
      "dependencies": {
      },
      "exceptionLogging": "STACKDRIVER",
      "oauthScopes": [
        "https://www.googleapis.com/auth/calendar.addons.execute",
        "https://www.googleapis.com/auth/calendar.readonly",
        "https://www.googleapis.com/auth/drive.addons.metadata.readonly",
        "https://www.googleapis.com/auth/gmail.addons.current.action.compose",
        "https://www.googleapis.com/auth/gmail.addons.current.message.readonly",
        "https://www.googleapis.com/auth/gmail.addons.execute",
        "https://www.googleapis.com/auth/script.locale"],
      "runtimeVersion": "DEPRECATED_ES5",
      "addOns": {
        "common": {
          "name": "Cats",
          "logoUrl": "https://www.gstatic.com/images/icons/material/system/1x/pets_black_48dp.png",
          "useLocaleFromApp": true,
          "homepageTrigger": {
            "runFunction": "onHomepage",
            "enabled": true
          },
          "universalActions": [{
            "label": "Learn more about Cataas",
            "openLink": "https://cataas.com"
          }]
        },
        "gmail": {
          "contextualTriggers": [{
            "unconditional": {
            },
            "onTriggerFunction": "onGmailMessage"
          }],
          "composeTrigger": {
            "selectActions": [{
              "text": "Insert cat",
              "runFunction": "onGmailCompose"
            }],
            "draftAccess": "NONE"
          }
        },
        "drive": {
          "onItemsSelectedTrigger": {
            "runFunction": "onDriveItemsSelected"
          }
        },
        "calendar": {
          "eventOpenTrigger": {
            "runFunction": "onCalendarEventOpen"
          }
        }
      }
    }
    
    
  3. Select File > Save to save these changes to the manifest. This step configures your add-ons homepage and contextual triggers, and also sets other information like the add-on name and scopes.

Step 3: Install the unpublished add-on

The add-on is ready to test. Install it for testing by doing the following:

  1. Select Publish > Deploy from manifest... to open the Deployments dialog.
  2. In the Latest Version (Head) row, click Install add-on to install the currently saved version of the add-on in development-mode. If you install the Latest Version (Head) of the add-on, any changes you make to the add-on code are applied immediately without you needing to reinstall it.
  3. Click Close.

Step 4: Try it out

You can now use the add-on:

  1. Open your Gmail, or refresh your Gmail tab if you already have it open. The add-on icon appears in the right icon column of your Gmail inbox.
  2. Click the icon in the right icon column to open the add-on.
  3. Once opened, the add-on alerts you that it hasn't been authorized. Click Authorize Access.
  4. Choose an account to sign-in with.
  5. In the next dialog, read the authorization text and then click Allow to authorize the add-on.
  6. Once authorized, the add-on reloads and can be used.

The add-on presents a cat image and text. You can change the image by clicking the Change cat button. The menu presents items that let you refresh the page and learn more about the third-party API that is supplying the cat images (Cataas).

If you open a Gmail message while the add-on is open, the image refreshes and the text changes to the subject line of the message (truncated if it's too long). Moving back to the Gmail inbox returns you to the add-on homepage.

You can see similar operations in Calendar and Drive, for both homepages and contextual content. You won't need to reauthorize the add-on to use it in those hosts.

Publish

Since this is an example add-on, our tutorial ends here. If you were developing a real add-on, the last step would be to publish it for other people to find and install.

Learn more

To continue learning about how to extend Google Workspace with Apps Script, take a look at the following resources: