This quickstart creates a Google Forms Editor add-on that uses triggers to send an email when a user responds to the form.
Objectives
- Set up the script.
- Run the script.
Prerequisites
To use this sample, you need the following prerequisites:
- A Google Account (Google Workspace accounts might require administrator approval).
- A web browser with access to the internet.
Set up the script
- Create a Google Forms form at forms.new .
- Click More more_vert > Script editor.
- Click Untitled project.
- Rename the Apps Script project Forms notificationsand click Rename.
- Click Add a file add
>
HTML. Name the file
sidebar
. - Repeat step 5 to create 4 more HTML files named
about
,authorizationEmail
,creatorNotification
, andrespondentNotification
. When you're done you should have 1 script file and 5 HTML files. -
Replace the contents of each file with the following corresponding code, then click Save
.
code.gs
forms/notifications/notification.gs/** * @OnlyCurrentDoc * * The above comment directs Apps Script to limit the scope of file * access for this add-on. It specifies that this add-on will only * attempt to read or modify the files in which the add-on is used, * and not all of the user's files. The authorization request message * presented to users will reflect this limited scope. */ /** * A global constant String holding the title of the add-on. This is * used to identify the add-on in the notification emails. */ const ADDON_TITLE = 'Form Notifications' ; /** * A global constant 'notice' text to include with each email * notification. */ const NOTICE = 'Form Notifications was created as an sample add-on, and is' + ' meant for' + 'demonstration purposes only. It should not be used for complex or important' + 'workflows. The number of notifications this add-on produces are limited by the' + 'owner\'s available email quota; it will not send email notifications if the' + 'owner\'s daily email quota has been exceeded. Collaborators using this add-on on' + 'the same form will be able to adjust the notification settings, but will not be' + 'able to disable the notification triggers set by other collaborators.' ; /** * Adds a custom menu to the active form to show the add-on sidebar. * * @param {object} e The event parameter for a simple onOpen trigger. To * determine which authorization mode (ScriptApp.AuthMode) the trigger is * running in, inspect e.authMode. */ function onOpen ( e ) { try { FormApp . getUi () . createAddonMenu () . addItem ( 'Configure notifications' , 'showSidebar' ) . addItem ( 'About' , 'showAbout' ) . addToUi (); } catch ( e ) { // TODO (Developer) - Handle exception console . log ( 'Failed with error: %s' , e . error ); } } /** * Runs when the add-on is installed. * * @param {object} e The event parameter for a simple onInstall trigger. To * determine which authorization mode (ScriptApp.AuthMode) the trigger is * running in, inspect e.authMode. (In practice, onInstall triggers always * run in AuthMode.FULL, but onOpen triggers may be AuthMode.LIMITED or * AuthMode.NONE). */ function onInstall ( e ) { onOpen ( e ); } /** * Opens a sidebar in the form containing the add-on's user interface for * configuring the notifications this add-on will produce. */ function showSidebar () { try { const ui = HtmlService . createHtmlOutputFromFile ( 'sidebar' ) . setTitle ( 'Form Notifications' ); FormApp . getUi (). showSidebar ( ui ); } catch ( e ) { // TODO (Developer) - Handle exception console . log ( 'Failed with error: %s' , e . error ); } } /** * Opens a purely-informational dialog in the form explaining details about * this add-on. */ function showAbout () { try { const ui = HtmlService . createHtmlOutputFromFile ( 'about' ) . setWidth ( 420 ) . setHeight ( 270 ); FormApp . getUi (). showModalDialog ( ui , 'About Form Notifications' ); } catch ( e ) { // TODO (Developer) - Handle exception console . log ( 'Failed with error: %s' , e . error ); } } /** * Save sidebar settings to this form's Properties, and update the onFormSubmit * trigger as needed. * * @param {Object} settings An Object containing key-value * pairs to store. */ function saveSettings ( settings ) { try { PropertiesService . getDocumentProperties (). setProperties ( settings ); adjustFormSubmitTrigger (); } catch ( e ) { // TODO (Developer) - Handle exception console . log ( 'Failed with error: %s' , e . error ); } } /** * Queries the User Properties and adds additional data required to populate * the sidebar UI elements. * * @return {Object} A collection of Property values and * related data used to fill the configuration sidebar. */ function getSettings () { try { const settings = PropertiesService . getDocumentProperties (). getProperties (); // Use a default email if the creator email hasn't been provided yet. if ( ! settings . creatorEmail ) { settings . creatorEmail = Session . getEffectiveUser (). getEmail (); } // Get text field items in the form and compile a list // of their titles and IDs. const form = FormApp . getActiveForm (); const textItems = form . getItems ( FormApp . ItemType . TEXT ); settings . textItems = []; for ( let i = 0 ; i < textItems . length ; i ++ ) { settings . textItems . push ({ title : textItems [ i ]. getTitle (), id : textItems [ i ]. getId () }); } return settings ; } catch ( e ) { // TODO (Developer) - Handle exception console . log ( 'Failed with error: %s' , e . error ); } } /** * Adjust the onFormSubmit trigger based on user's requests. */ function adjustFormSubmitTrigger () { try { const form = FormApp . getActiveForm (); const triggers = ScriptApp . getUserTriggers ( form ); const settings = PropertiesService . getDocumentProperties (); const triggerNeeded = settings . getProperty ( 'creatorNotify' ) === 'true' || settings . getProperty ( 'respondentNotify' ) === 'true' ; // Create a new trigger if required; delete existing trigger // if it is not needed. let existingTrigger = null ; for ( let i = 0 ; i < triggers . length ; i ++ ) { if ( triggers [ i ]. getEventType () === ScriptApp . EventType . ON_FORM_SUBMIT ) { existingTrigger = triggers [ i ]; break ; } } if ( triggerNeeded && ! existingTrigger ) { const trigger = ScriptApp . newTrigger ( 'respondToFormSubmit' ) . forForm ( form ) . onFormSubmit () . create (); } else if ( ! triggerNeeded && existingTrigger ) { ScriptApp . deleteTrigger ( existingTrigger ); } } catch ( e ) { // TODO (Developer) - Handle exception console . log ( 'Failed with error: %s' , e . error ); } } /** * Responds to a form submission event if an onFormSubmit trigger has been * enabled. * * @param {Object} e The event parameter created by a form * submission; see * https://developers.google.com/apps-script/understanding_events */ function respondToFormSubmit ( e ) { try { const settings = PropertiesService . getDocumentProperties (); const authInfo = ScriptApp . getAuthorizationInfo ( ScriptApp . AuthMode . FULL ); // Check if the actions of the trigger require authorizations that have not // been supplied yet -- if so, warn the active user via email (if possible). // This check is required when using triggers with add-ons to maintain // functional triggers. if ( authInfo . getAuthorizationStatus () === ScriptApp . AuthorizationStatus . REQUIRED ) { // Re-authorization is required. In this case, the user needs to be alerted // that they need to reauthorize; the normal trigger action is not // conducted, since authorization needs to be provided first. Send at // most one 'Authorization Required' email a day, to avoid spamming users // of the add-on. sendReauthorizationRequest (); } else { // All required authorizations have been granted, so continue to respond to // the trigger event. // Check if the form creator needs to be notified; if so, construct and // send the notification. if ( settings . getProperty ( 'creatorNotify' ) === 'true' ) { sendCreatorNotification (); } // Check if the form respondent needs to be notified; if so, construct and // send the notification. Be sure to respect the remaining email quota. if ( settings . getProperty ( 'respondentNotify' ) === 'true' && MailApp . getRemainingDailyQuota () > 0 ) { sendRespondentNotification ( e . response ); } } } catch ( e ) { // TODO (Developer) - Handle exception console . log ( 'Failed with error: %s' , e . error ); } } /** * Called when the user needs to reauthorize. Sends the user of the * add-on an email explaining the need to reauthorize and provides * a link for the user to do so. Capped to send at most one email * a day to prevent spamming the users of the add-on. */ function sendReauthorizationRequest () { try { const settings = PropertiesService . getDocumentProperties (); const authInfo = ScriptApp . getAuthorizationInfo ( ScriptApp . AuthMode . FULL ); const lastAuthEmailDate = settings . getProperty ( 'lastAuthEmailDate' ); const today = new Date (). toDateString (); if ( lastAuthEmailDate !== today ) { if ( MailApp . getRemainingDailyQuota () > 0 ) { const template = HtmlService . createTemplateFromFile ( 'authorizationEmail' ); template . url = authInfo . getAuthorizationUrl (); template . notice = NOTICE ; const message = template . evaluate (); MailApp . sendEmail ( Session . getEffectiveUser (). getEmail (), 'Authorization Required' , message . getContent (), { name : ADDON_TITLE , htmlBody : message . getContent () }); } settings . setProperty ( 'lastAuthEmailDate' , today ); } } catch ( e ) { // TODO (Developer) - Handle exception console . log ( 'Failed with error: %s' , e . error ); } } /** * Sends out creator notification email(s) if the current number * of form responses is an even multiple of the response step * setting. */ function sendCreatorNotification () { try { const form = FormApp . getActiveForm (); const settings = PropertiesService . getDocumentProperties (); let responseStep = settings . getProperty ( 'responseStep' ); responseStep = responseStep ? parseInt ( responseStep ) : 10 ; // If the total number of form responses is an even multiple of the // response step setting, send a notification email(s) to the form // creator(s). For example, if the response step is 10, notifications // will be sent when there are 10, 20, 30, etc. total form responses // received. if ( form . getResponses (). length % responseStep === 0 ) { const addresses = settings . getProperty ( 'creatorEmail' ). split ( ',' ); if ( MailApp . getRemainingDailyQuota () > addresses . length ) { const template = HtmlService . createTemplateFromFile ( 'creatorNotification' ); template . summary = form . getSummaryUrl (); template . responses = form . getResponses (). length ; template . title = form . getTitle (); template . responseStep = responseStep ; template . formUrl = form . getEditUrl (); template . notice = NOTICE ; const message = template . evaluate (); MailApp . sendEmail ( settings . getProperty ( 'creatorEmail' ), form . getTitle () + ': Form submissions detected' , message . getContent (), { name : ADDON_TITLE , htmlBody : message . getContent () }); } } } catch ( e ) { // TODO (Developer) - Handle exception console . log ( 'Failed with error: %s' , e . error ); } } /** * Sends out respondent notification emails. * * @param {FormResponse} response FormResponse object of the event * that triggered this notification */ function sendRespondentNotification ( response ) { try { const form = FormApp . getActiveForm (); const settings = PropertiesService . getDocumentProperties (); const emailId = settings . getProperty ( 'respondentEmailItemId' ); const emailItem = form . getItemById ( parseInt ( emailId )); const respondentEmail = response . getResponseForItem ( emailItem ) . getResponse (); if ( respondentEmail ) { const template = HtmlService . createTemplateFromFile ( 'respondentNotification' ); template . paragraphs = settings . getProperty ( 'responseText' ). split ( '\n' ); template . notice = NOTICE ; const message = template . evaluate (); MailApp . sendEmail ( respondentEmail , settings . getProperty ( 'responseSubject' ), message . getContent (), { name : form . getTitle (), htmlBody : message . getContent () }); } } catch ( e ) { // TODO (Developer) - Handle exception console . log ( 'Failed with error: %s' , e . error ); } }
sidebar.html
forms/notifications/sidebar.html<!DOCTYPE html> <html> <head> <base target="_top"> <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css"> <!-- The CSS package above applies Google styling to buttons and other elements. --> <style> .branding-below { bottom: 54px; top: 0; } .branding-text { left: 7px; position: relative; top: 3px; } .logo { vertical-align: middle; } .width-100 { width: 100%; box-sizing: border-box; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; } label { font-weight: bold; } #creator-options, #respondent-options { background-color: #eee; border-color: #eee; border-width: 5px; border-style: solid; display: none; } #creator-email, #respondent-email, #button-bar, #submit-subject { margin-bottom: 10px; } #response-step { display: inline; } </style> </head> <body> <div class="sidebar branding-below"> <form> <div class="block"> <input type="checkbox" id="creator-notify"> <label for="creator-notify">Notify me</label> </div> <div class="block form-group" id="creator-options"> <label for="creator-email"> My email addresses (comma-separated) </label> <input type="text" class="width-100" id="creator-email"> <label for="response-step">Send notifications after every</label> <input type="number" id="response-step" value="10" min="1" max="99999"> responses (default 10) </div> <div class="block"> <input type="checkbox" id="respondent-notify"> <label for="respondent-notify">Notify respondents</label> </div> <div class="block form-group" id="respondent-options"> <label for="respondent-email"> Which question asks for their email? </label> <select class="width-100" id="respondent-email"></select> <label for="submit-subject"> Notification email subject: </label> <input type="text" class="width-100" id="submit-subject"> <label for="submit-notice">Notification email body:</label> <textarea rows="8" cols="40" id="submit-notice" class="width-100"></textarea> </div> <div class="block" id="button-bar"> <button class="action" id="save-settings">Save</button> </div> </form> </div> <div class="sidebar bottom"> <img alt="Add-on logo" class="logo" width="25" src="https://g-suite-documentation-images.firebaseapp.com/images/newFormNotificationsicon.png"> <span class="gray branding-text">Form Notifications by Google</span> </div> <script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"> </script> <script> /** * On document load, assign required handlers to each element, * and attempt to load any saved settings. */ $(function() { $('#save-settings').click(saveSettingsToServer); $('#creator-notify').click(toggleCreatorNotify); $('#respondent-notify').click(toggleRespondentNotify); $('#response-step').change(validateNumber); google.script.run .withSuccessHandler(loadSettings) .withFailureHandler(showStatus) .withUserObject($('#button-bar').get()) .getSettings(); }); /** * Callback function that populates the notification options using * previously saved values. * * @param {Object} settings The saved settings from the client. */ function loadSettings(settings) { $('#creator-email').val(settings.creatorEmail); $('#response-step').val(!settings.responseStep ? 10 : settings.responseStep); $('#submit-subject').val(!settings.responseSubject ? 'Thank you for filling out our form!' : settings.responseSubject); $('#submit-notice').val(!settings.responseText ? 'Thank you for responding to our form!' : settings.responseText); if (settings.creatorNotify === 'true') { $('#creator-notify').prop('checked', true); $('#creator-options').show(); } if (settings.respondentNotify === 'true') { $('#respondent-notify').prop('checked', true); $('#respondent-options').show(); } // Fill the respondent email select box with the // titles given to the form's text Items. Also include // the form Item IDs as values so that they can be // easily recovered during the Save operation. for (var i = 0; i < settings.textItems.length; i++) { var option = $('<option>').attr('value', settings.textItems[i]['id']) .text(settings.textItems[i]['title']); $('#respondent-email').append(option); } $('#respondent-email').val(settings.respondentEmailItemId); } /** * Toggles the visibility of the form creator notification options. */ function toggleCreatorNotify() { $('#status').remove(); if ($('#creator-notify').is(':checked')) { $('#creator-options').show(); } else { $('#creator-options').hide(); } } /** * Toggles the visibility of the form sumbitter notification options. */ function toggleRespondentNotify() { $('#status').remove(); if($('#respondent-notify').is(':checked')) { $('#respondent-options').show(); } else { $('#respondent-options').hide(); } } /** * Ensures that the entered step is a number between 1 * and 99999, inclusive. */ function validateNumber() { var value = $('#response-step').val(); if (!value) { $('#response-step').val(10); } else if (value < 1) { $('#response-step').val(1); } else if (value > 99999) { $('#response-step').val(99999); } } /** * Collects the options specified in the add-on sidebar and sends them to * be saved as Properties on the server. */ function saveSettingsToServer() { this.disabled = true; $('#status').remove(); var creatorNotify = $('#creator-notify').is(':checked'); var respondentNotify = $('#respondent-notify').is(':checked'); var settings = { 'creatorNotify': creatorNotify, 'respondentNotify': respondentNotify }; // Only save creator options if notify is turned on if (creatorNotify) { settings.responseStep = $('#response-step').val(); settings.creatorEmail = $('#creator-email').val().trim(); // Abort save if entered email is blank if (!settings.creatorEmail) { showStatus('Enter an owner email', $('#button-bar')); this.disabled = false; return; } } // Only save respondent options if notify is turned on if (respondentNotify) { settings.respondentEmailItemId = $('#respondent-email').val(); settings.responseSubject = $('#submit-subject').val(); settings.responseText = $('#submit-notice').val(); } // Save the settings on the server google.script.run .withSuccessHandler( function(msg, element) { showStatus('Saved settings', $('#button-bar')); element.disabled = false; }) .withFailureHandler( function(msg, element) { showStatus(msg, $('#button-bar')); element.disabled = false; }) .withUserObject(this) .saveSettings(settings); } /** * Inserts a div that contains an status message after a given element. * * @param {String} msg The status message to display. * @param {Object} element The element after which to display the Status. */ function showStatus(msg, element) { var div = $('<div>') .attr('id', 'status') .attr('class','error') .text(msg); $(element).after(div); } </script> </body> </html>
about.html
forms/notifications/about.html<!DOCTYPE html> <html> <head> <base target="_top"> <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css"> <!-- The CSS package above applies Google styling to buttons and other elements. --> </head> <body> <div> <p> <i>Form Notifications</i> was created as an sample add-on, and is meant for demonstration purposes only. It should not be used for complex or important workflows. </p> <p> The number of notifications this add-on produces are limited by the owner's available email quota; it will not send email notifications if the owner's daily email quota has been exceeded. Collaborators using this add-on on the same form will be able to adjust the notification settings, but will not be able to disable the notification triggers set by other collaborators. </p> </div> </body> </html>
authorizationEmail.html
forms/notifications/authorizationEmail.html<p>The Google Forms add-on <i>Form Notifications</i> is set to run automatically whenever a form is submitted. The add-on was recently updated and it needs you to re-authorize it to run on your behalf.</p> <p>The add-on's automatic functions are temporarily disabled until you re-authorize the add-on. You can accomplish this by opening one of the forms using the add-on and running the add-on through the menu. Alternatively, you can click this link to approve authorization directly:</p> <p><a href="<?= url ?>">Click here</a> to re-authorize the add-on.</p> <p>This notification email will be sent to you at most once per day until the add-on is re-authorized.</p> <hr> <p style="font-size:80%">This automatic message was sent to you via the <i>Form Notifications</i> add-on for Google Forms. <?= notice ?></p>
creatorNotification.html
forms/notifications/creatorNotification.html<p><i>Form Notifications</i> (a Google Forms add-on) has detected that the form titled <a href="<?= formUrl?>"><b><?= title ?></b></a> has received <?= responses ?> responses so far.</p> <p><a href="<?= summary ?>">Summary of form responses</a></p> <p>You are receiving this email because an editor of this form configured <i>Form Notifications</i> to alert you every time this form receives <b><?= responseStep ?></b> responses.</p> <p>To change this setting, or to stop receiving these notifications, have the form owner or editors open the form and adjust the <i>Form Notifications</i> add-on configuration via the "Configure notifications" menu item.</p> <hr> <p style="font-size:80%">This automatic message was sent to you via the <i>Form Notifications</i> add-on for Google Forms. <?= notice ?></p>
respondentNotification.html
forms/notifications/respondentNotification.html<? for (var i = 0; i < paragraphs.length; i++) { ?> <p><?= paragraphs[i] ?></p> <? } ?> <hr> <p style="font-size:80%">This automatic message was sent to you via the <i>Form Notifications</i> add-on for Google Forms. <?= notice ?></p>
Run the script
- Switch back to your form and refresh the page.
- Add a short answer text question to your form. In Untitled question, enter Email Address. Optionally, you can create other form questions.
- Click add-ons extension > Form notifications. It might take several seconds for add-ons extension to appear.
- In the dialog, click Configure notifications.
- When prompted, authorize the add-on.
- Again, click add-ons extension > Form notifications > Configure notifications.
- In the add-on, check the Notify mebox and enter your email address.
- For Send notifications after every, enter 1.
- Click Save.
- To submit a response, click Preview visibility
- Fill out the form and click Submit.
- Check your email for a notification.