How to save to Google Sheets and send email from apps you build with Blue, with sample code
Apps built with Blue can connect to Google Sheets and Gmail using the WebApp feature of Google Apps Script (GAS). Blue does not proxy the call — your app's frontend sends data directly to the GAS URL. No new account or extra billing is needed; your existing Google account is enough.
Common use cases: contact form → save to a Sheet / signup form → save + confirmation email / display Sheet data in the app / survey collection.
Open the WebApp URL directly in a browser; if it returns a response without an error (JSON when doGet is defined), you're set.
function doPost(e) {
try {
const data = JSON.parse(e.postData.contents);
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
sheet.appendRow([new Date(), data.name || "", data.email || "", data.message || ""]);
return ContentService
.createTextOutput(JSON.stringify({ ok: true }))
.setMimeType(ContentService.MimeType.JSON);
} catch (err) {
return ContentService
.createTextOutput(JSON.stringify({ ok: false, error: String(err) }))
.setMimeType(ContentService.MimeType.JSON);
}
}
function doGet(e) {
try {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const rows = sheet.getDataRange().getValues();
const headers = rows[0];
const items = rows.slice(1).map(row => {
const obj = {};
headers.forEach((h, i) => { obj[h] = row[i]; });
return obj;
});
return ContentService
.createTextOutput(JSON.stringify({ ok: true, items: items }))
.setMimeType(ContentService.MimeType.JSON);
} catch (err) {
return ContentService
.createTextOutput(JSON.stringify({ ok: false, error: String(err) }))
.setMimeType(ContentService.MimeType.JSON);
}
}
function doPost(e) {
try {
const data = JSON.parse(e.postData.contents);
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
sheet.appendRow([new Date(), data.name || "", data.email || "", data.message || ""]);
if (data.email) {
const ownerEmail = Session.getActiveUser().getEmail();
MailApp.sendEmail({ to: ownerEmail, subject: "New inquiry", body: `Name: ${data.name}\nEmail: ${data.email}\n\n${data.message}` });
MailApp.sendEmail({ to: data.email, subject: "We received your inquiry", body: `Dear ${data.name},\n\nThank you for reaching out. We'll get back to you shortly.` });
}
return ContentService.createTextOutput(JSON.stringify({ ok: true })).setMimeType(ContentService.MimeType.JSON);
} catch (err) {
return ContentService.createTextOutput(JSON.stringify({ ok: false, error: String(err) })).setMimeType(ContentService.MimeType.JSON);
}
}
Email quota: personal Gmail is 100/day, Workspace is 1500/day (as of 2026).
The code is the same as patterns 1–3. The only difference is setting "Who has access" to "Anyone within (your domain)" at deploy time. It's the middle ground when you don't want external access but want colleagues to use it without a Google login prompt — and the sheet ACL is respected.
To avoid CORS, send the POST as Content-Type: text/plain (application/json triggers a preflight that fails).
const GAS_URL = "https://script.google.com/macros/s/AKfycbz.../exec";
document.getElementById("contact-form").addEventListener("submit", async (e) => {
e.preventDefault();
const formData = {
name: document.getElementById("name").value,
email: document.getElementById("email").value,
message: document.getElementById("message").value,
};
try {
const res = await fetch(GAS_URL, {
method: "POST",
headers: { "Content-Type": "text/plain;charset=utf-8" },
body: JSON.stringify(formData),
});
const data = await res.json();
alert(data.ok ? "Sent" : "Failed: " + data.error);
} catch (err) {
alert("Network error: " + err.message);
}
});
In a static app (HTML + JS), write the URL inline. In a backend app (Python / Node.js / PHP), reference a name you saved under "Generic Secrets" in Blue Settings as an environment variable.
| Data type | Recommended mode |
|---|---|
| Contacts / inquiries / bookings | URL-as-token (default) |
| Internal-only business data | URL-as-token, or Workspace domain restriction |
| Confidential business data | Workspace domain restriction |
| Third-party sensitive personal data (health, finance) | OAuth + Sheets API directly |
| Payment info / passwords | Don't use GAS — use a dedicated SaaS like Stripe |
| Item | Personal Gmail | Workspace |
|---|---|---|
| Script runtime / day | 90 min | 6 hours |
| Emails / day | 100 | 1500 |
| URL Fetch calls / day | 20,000 | 100,000 |
| Cold start | a few seconds on the first call | |
These constraints are managed on the user side. If they become a bottleneck at scale, consider moving to Cloud Run / Cloudflare Workers.
Operators often have a notification obligation under privacy laws (APPI / GDPR), so we recommend including a notice below the form like this.
<p class="privacy-notice">
Information submitted via this form is sent to Google services and managed by the operator of this page.
Google's handling of data follows the Google Privacy Policy.
</p>