BLUEBETA

Google Sheets Integration Guide

How to save to Google Sheets and send email from apps you build with Blue, with sample code

1. Overview

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.

2. Creating the WebApp (basic steps)

Step 1. Prepare a spreadsheet

  1. Create a new spreadsheet in Google Drive
  2. Put column names in row 1 (e.g. Timestamp / Name / Email / Message)
  3. Leave the sharing setting as "Restricted (only you)" — no change needed

Step 2. Create the GAS script

  1. From the sheet menu, open "Extensions" → "Apps Script"
  2. Paste the matching pattern from "3. Sample code" below into Code.gs
  3. Save (Ctrl / Cmd + S)

Step 3. Deploy as a WebApp

  1. Top right "Deploy" → "New deployment"
  2. Gear icon → choose "Web app"
  3. Settings —— Execute as: "Me" / Who has access: "Anyone" (public form). For internal use, choose "Anyone within (your domain)"
  4. "Deploy" → first time, approve permissions ("Advanced" → "Go to (project name)" → "Allow". This is you authorizing your own script — it is safe)
  5. Copy the WebApp URL (https://script.google.com/macros/s/AKfycbz.../exec) and paste it into your Blue app's code

Step 4. Verify

Open the WebApp URL directly in a browser; if it returns a response without an error (JSON when doGet is defined), you're set.

3. Sample code (GAS side)

Pattern 1: form submission → save to Sheet (most common)

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);
  }
}

Pattern 2: read Sheet data

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);
  }
}

Pattern 3: save to Sheet + send confirmation email

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).

Pattern 4: Workspace domain-restricted

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.

4. App-side code (running on Blue)

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.

5. Data sensitivity and recommended mode

Data typeRecommended mode
Contacts / inquiries / bookingsURL-as-token (default)
Internal-only business dataURL-as-token, or Workspace domain restriction
Confidential business dataWorkspace domain restriction
Third-party sensitive personal data (health, finance)OAuth + Sheets API directly
Payment info / passwordsDon't use GAS — use a dedicated SaaS like Stripe
A GAS "Anyone" WebApp URL is treated as a "semi-public token" by Google's design. It can leak via browser history, extensions, the Referer header, screenshots, etc. — so limit it to data whose exposure you can tolerate, and don't paste the URL into Git, social media, or public docs. For higher-sensitivity data, switch to the domain-restricted or OAuth route.

6. Constraints (GAS quotas)

ItemPersonal GmailWorkspace
Script runtime / day90 min6 hours
Emails / day1001500
URL Fetch calls / day20,000100,000
Cold starta 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.

7. Notice for end users

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>
Back to Support Open AI Build Glossary