id field to each form submission to uniquely identify each entry.Start a new spreadsheet with the Blank template.| A | B | C | ... | |
|---|---|---|---|---|
| 1 | id | timestamp |
[!TIP]
To learn how to add additional input fields, checkout section 7 below.
Extensions > Apps Script which should open a new tab.form-script.js and paste it into the Code.gs tab in the Script Editor.File > Save:[!TIP]
If you want to better understand what this script is doing, checkout theform-script-commented.jsfile in the repo for a detailed explanation.
If you want to receive email notifications on new submissions, make sure to replace the placeholder values with your actual email and name.
[!NOTE]
Thanks to @LandonMoss for the email notification feature
Code.gs file:function sendNewSubmissionEmailNotification(subject, body) {
const recipient = 'INSERT_YOUR_EMAIL_HERE'
const senderName = 'INSERT_YOUR_NAME_HERE'
MailApp.sendEmail({
to: recipient,
subject: subject,
htmlBody: body,
name: senderName,
})
}
try block of the doPost function before the return:const emailSubject = 'New Form Submission'
const emailBody = 'A new submission has been added to row ' + nextRow
sendNewSubmissionEmailNotification(emailSubject, emailBody)
const errorSubject = 'Error in Form Submission'
const errorBody = 'Form submission error:\n' + e.toString()
sendNewSubmissionEmailNotification(errorSubject, errorBody)
Run > Run Function > initialSetup to run this function.[!IMPORTANT]
If you're getting the "Google hasn’t verified this app screen", you can click onAdvancedand thenGo to Submit Form to Google Sheets (unsafe). This is because the script isn't verified by Google, but since you're the only one using it, I think it's safe to proceed.
Authorization Required dialog, click on Review Permissions.Hi {Your Name}, {APP_NAME} wants to...AllowTriggers in the sidebar.
= Then click on + Add Trigger in the bottom right corner.Choose which function to run dropdown select doPostSelect event source to From spreadsheetSelect event type to On form submitSaveDeploy > New deployment in the top right corner.Select type dropdown, choose Web app.Execute as: to Me(your@address.com).Who has access select Anyone.Deploy.Web app URL from the dialog.Done.[!IMPORTANT]
If you have a custom domain with Gmail, you might need to clickOK, refresh the page, and then go toPublish > Deploy as web app…again to get the proper web app URL. It should look something likehttps://script.google.com/a/yourdomain.com/macros/s/XXXX….
Open index.html.
name attributeSCRIPT_URL with your script url:const form = document.forms['YOUR_FORM_NAME'] to match the name attribute of your form[!NOTE]
Thanks to @lacabra for the sheet name feature
<form name="YOUR_FORM_NAME">
<input type="hidden" name="sheet_name" value="YOUR_SHEET_NAME" />
<input name="email" type="email" placeholder="Email" required />
<button type="submit">Send</button>
</form>
<script>
const scriptURL = 'YOUR_SCRIPT_URL'
const form = document.forms['YOUR_FORM_NAME']
form.addEventListener('submit', e => {
e.preventDefault()
fetch(scriptURL, { method: 'POST', body: new FormData(form) })
.then(response => response.json())
.then(response => console.log('Success!', response))
.catch(error => console.error('Error!', error.message))
})
</script>
[!TIP]
Fun fact! The<html>,<head>, andbodytags are actually among a handful of optional tags, but since the rules around how the browser parses a page are kinda complicated, you'd probably not want to omit them on real websites.
To capture additional data, you'll just need to create new columns with titles matching exactly the name values from your form inputs. For example, if you want to add first and last name inputs, you'd give them name values like so:
<form name="newsletter_form">
<input name="email" type="email" placeholder="Email" required />
<input name="firstName" type="text" placeholder="First Name" />
<input name="lastName" type="text" placeholder="Last Name" />
<button type="submit">Send</button>
</form>
Then create new headers with the exact, case-sensitive name values:
| A | B | C | D | E | ... | |
|---|---|---|---|---|---|---|
| 1 | id | timestamp | firstName | lastName |
[!IMPORTANT]
If you are using checkboxes for your forms, then use a uniquenameattribute for every checkbox option and add these unique names to the sheet to collect all responses. Thanks to @ashwinbalaji0811!
Check out examples/honeypot.html for an example of how to implement a honeypot field to help prevent spam submissions.
The default honeypot field is named mobile_number.
<label>If you are human, leave this blank:</label>
<input type="text" name="mobile_number" />
In the form-script.js the parameter is checked here:
// Changing the input name will require updating this check
// so if it's `name="my_new_name"`, these `e.parameter.my_new_name`
if (e.parameter.mobile_number && e.parameter.mobile_number !== '') {
return ContentService.createTextOutput(
JSON.stringify({ result: 'success', message: 'Bot detected' }),
).setMimeType(ContentService.MimeType.JSON)
}
You can also add basic input validation and restrictions directly in your HTML form using attributes like maxlength and pattern. This helps prevent most accidental or low-level malicious attempts before anything gets sent to your Google Apps Script. For example, to restrict the first name input to a maximum of 50 characters and prevent it from starting with symbols like =, +, -, or @, you can use the following HTML:
<input
name="firstName"
type="text"
placeholder="First Name"
maxlength="50"
pattern="[^=+\-@].*"
title="Names cannot start with symbols like =, +, -, or @" />
The google script sanitizes (by prepending a single quotation mark) and formats all inputs as plain text before inserting them into the spreadsheet. This ensures that any potentially harmful characters are neutralized and that the data is stored consistently.
[!NOTE]
This means that any submissions that start with=,+,-, or@will be prepended with a single quote. This will not be visible in the cell but will be shown in the formula bar when the cell is selected.
Please create a new issue. PRs are definitely welcome, but please run your ideas by me before putting in a lot of work. Thanks!
$ claude mcp add form-to-google-sheets \
-- python -m otcore.mcp_server <graph>