r/GoogleAppsScript

Top 10 practices to follow when building scripts in Apps ScriptTop 10 practices to follow while building scripts in Apps Script

Hi - I am a common Apps Script user. I would like to know what you think are the top 10 best practices or key things to know about Apps Script.

reddit.com
u/Fast-Conflict2847 — 17 hours ago
▲ 6 r/GoogleAppsScript+3 crossposts

Does anyone know how to help with this web application veification?

I received an email from Google after trying to verify my app, and because it has restricted scopes, (i knew i would run into this issue), i have to pay to get it verified. However, this is my first application I have built, and can someone tell me why "AL1 (formerly Tier 2) CASA Assessment" costs $600? Is there any way to get around this or any cheaper alternatives, please let me know!

reddit.com
u/ShifuPowered — 3 days ago

Any GAS Expert here

How are modern AI & workflow automations typically structured using Google Apps Script? (Looking for architectural patterns)

Hi everyone,
I am modernizing the operations for a traditional real estate agency by replacing manual paper processes with Google Workspace.

So far, I’ve built a central Google Master Calendar, a Google Docs/Drive office portal for form downloads, and structured Google Sheets for lead tracking. I am now looking to expand into deeper workflow and AI automations using Google Apps Script (GAS).

Coming from an operations background rather than computer science, I’d love to learn how experienced developers structure end-to-end automation pipelines using GAS.

Thx

reddit.com
u/Sumphy — 4 days ago

I CAN'T UNDERSTAND WHY THIS SCRIPT DOESN'T SEND OUT EMAILS

I'm developing a website for my cultural organization in Framer and I have designed a Form to request the subscription, where users need to input their data.
I've linked this form via web hook to my scripts in GoogleAppsScripts, linked to a google sheet.

The main code responsible for getting the datas is working, it gets and sorts the data out in the sheet, it also generates a pdf from the datas, but then it fails to send the confirmation email, while it does send an email to my organization address, which shows as recipient the email captured in the form.

Down here, you can find the code. Anyone has any recommendations?

CODE

/** 
 * Webhook Principale e Router - Nuova Alba APS 
 */
const CONFIG = {
  STRIPE_LINK_MAGGIORENNI: "https://buy.stripe.com/8x214meyr0DT2lq6Svgw000",
  STRIPE_LINK_MINORI: "https://buy.stripe.com/8x214meyr0DT2lq6Svgw000",
  NOME_APS: "Nuova Alba APS",
  COLOR_HEX: "#FC5408",
  EMAIL_STAFF: "info@nuovaalba.org"
};


function doPost(e) {
  const lock = LockService.getScriptLock();
  lock.tryLock(10000);


  try {
    let data = {};
    if (e && e.postData && e.postData.contents) {
      try {
        data = JSON.parse(e.postData.contents);
      } catch (err) {
        data = e.parameter || {};
      }
    } else if (e && e.parameter) {
      data = e.parameter;
    }


    // --- ROUTER DI SMISTAMENTO PARAMETRO 'tipo' ---
    const tipo = (e && e.parameter && e.parameter.tipo) 
      ? String(e.parameter.tipo).toLowerCase().trim() 
      : (data.tipo ? String(data.tipo).toLowerCase().trim() : "");


    if (tipo === "corsi") {
      return gestisciIscrizioneCorso(data);
    } else if (tipo === "minori") {
      return gestisciMinori(data);
    } else {
      return gestisciMaggiorenni(data);
    }


  } catch (error) {
    return ContentService
      .createTextOutput(JSON.stringify({ result: "error", error: error.toString() }))
      .setMimeType(ContentService.MimeType.JSON);
  } finally {
    lock.releaseLock();
  }
}


function gestisciMaggiorenni(data) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Iscritti");


  function getTesto(val) {
    if (val === undefined || val === null) return "";
    if (Array.isArray(val)) return String(val[0] || "").trim();
    return String(val).trim();
  }


  const nome = getTesto(data.Nome || data.nome);
  const cognome = getTesto(data.Cognome || data.cognome);
  const email = getTesto(data.Email || data.email);
  const telefono = getTesto(data.Telefono || data.telefono);
  const cf = getTesto(data.CodiceFiscale || data.codice_fiscale || data["Codice Fiscale"]).toUpperCase();
  const luogoNascita = getTesto(data.LuogoDiNascita || data.luogo_nascita || data["Luogo di Nascita"] || data["Luogo Nascita"]);
  const dataNascita = getTesto(data.DataDiNascita || data["Data di Nascita"] || data.data_nascita || data["Data Nascita"]);
  const residenza = getTesto(data.Residenza || data.residenza);
  let privacy = getTesto(data.Privacy || data.privacy || "Accettato");
  if (privacy === "on" || privacy === "true") privacy = "Accettato";


  const lastRow = sheet.getLastRow();
  const oraAttuale = new Date();


  // --- 1. BLOCCO ANTI-SPAM ISTANTANEO (Meno di 60 secondi dall'ultimo invio) ---
  if (lastRow > 1) {
    const lastCF = String(sheet.getRange(lastRow, 7).getValue()).trim().toUpperCase();
    const lastDate = new Date(sheet.getRange(lastRow, 2).getValue());
    const diffSecondi = (oraAttuale - lastDate) / 1000;


    if (lastCF === cf && diffSecondi < 60) {
      return ContentService
        .createTextOutput(JSON.stringify({ result: "success", note: "doppio invio istantaneo bloccato" }))
        .setMimeType(ContentService.MimeType.JSON);
    }
  }


  // --- 2. CONTROLLO UTENTE GIÀ REGISTRATO ---
  if (lastRow > 1) {
    const elenchiCF = sheet.getRange(2, 7, lastRow - 1, 1).getValues();
    const elenchiMatricole = sheet.getRange(2, 1, lastRow - 1, 1).getValues();
    const elenchiStato = sheet.getRange(2, 12, lastRow - 1, 1).getValues();


    for (let i = 0; i < elenchiCF.length; i++) {
      const cfEsistente = String(elenchiCF[i][0]).trim().toUpperCase();
      if (cfEsistente === cf && cf !== "") {
        const matricolaEsistente = String(elenchiMatricole[i][0]);
        const statoPagamento = String(elenchiStato[i][0]).trim();
        
        inviaEmailGiaIscritto(email, nome, matricolaEsistente, CONFIG.STRIPE_LINK_MAGGIORENNI, statoPagamento);
        
        return ContentService
          .createTextOutput(JSON.stringify({ result: "success", note: "utente gia registrato" }))
          .setMimeType(ContentService.MimeType.JSON);
      }
    }
  }


  // --- 3. REGISTRAZIONE NUOVO SOCIO ---
  const initNome = nome ? nome.charAt(0).toUpperCase() : "X";
  const initCognome = cognome ? cognome.charAt(0).toUpperCase() : "X";
  const ultime3CF = cf.length >= 3 ? cf.slice(-3) : "000";
  const matricola = `NA-${initNome}${initCognome}${ultime3CF}`;


  sheet.appendRow([
    matricola,
    oraAttuale,
    nome,
    cognome,
    email,
    telefono,
    cf,
    dataNascita,
    luogoNascita,
    residenza,
    privacy,
    "In attesa"
  ]);


  // --- 4. GENERAZIONE TESSERA DIGITALE PDF ---
  let pdfTessera = null;
  try {
    pdfTessera = generaTesseraPDF(matricola, nome + " " + cognome);
  } catch (errTessera) {
    Logger.log("⚠️ Errore creazione PDF Tessera per " + matricola + ": " + errTessera.message);
  }


  // --- 5. INVIO EMAIL ---
  inviaEmailBenvenutoMaggiorenni(email, nome, matricola, pdfTessera);
  inviaMailNotificaStaff(nome, cognome, matricola, email, telefono, oraAttuale, "Maggiorenne");


  return ContentService
    .createTextOutput(JSON.stringify({ result: "success", matricola: matricola }))
    .setMimeType(ContentService.MimeType.JSON);
}


function inviaEmailBenvenutoMaggiorenni(email, nome, matricola, pdfTessera) {
  const hex = CONFIG.COLOR_HEX;
  const corpoHtml = `
    <div style="font-family: Arial, sans-serif; color: #333; max-width: 600px; margin: 0 auto; border: 1px solid #eee; padding: 25px; border-radius: 8px;">
      <h2 style="color: ${hex}; margin-top: 0;">Ciao ${nome}, benvenuto/a!</h2>
      <p>Abbiamo ricevuto la tua richiesta di iscrizione all'associazione <strong>${CONFIG.NOME_APS}</strong>.</p>
      
      <p>La tua registrazione è avvenuta con successo. Ti è stato assegnato il seguente codice socio:</p>
      
      <div style="background-color: #fff7f2; border-left: 4px solid ${hex}; padding: 15px; margin: 20px 0; font-size: 18px; font-weight: bold;">
        Matricola Socio: <span style="color: ${hex};">${matricola}</span>
      </div>


      ${pdfTessera ? `<p>In allegato trovi la tua <strong>tessera digitale associativa</strong>.</p>` : ""}


      <h3 style="color: ${hex};">Completa la tua iscrizione</h3>
      <p>Per attivare ufficialmente la tessera associativa è necessario effettuare il versamento della quota annua. Puoi scegliere tra due opzioni:</p>
      
      <ol style="line-height: 1.6;">
        <li>
          <strong>Pagamento Online (Consigliato):</strong><br>
          Puoi pagare subito tramite carta cliccando sul pulsante sottostante:<br><br>
          <a href="${CONFIG.STRIPE_LINK_MAGGIORENNI}" style="background-color: ${hex}; color: white; padding: 12px 22px; text-decoration: none; border-radius: 5px; display: inline-block; font-weight: bold;">Paga la quota associativa con Stripe</a>
          <br><br>
        </li>
        <li>
          <strong>Pagamento in Sede:</strong><br>
          Puoi saldare la quota direttamente in contanti o POS presso la nostra sede.
        </li>
      </ol>


      <div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 25px 0; font-size: 14px;">
        ⚠️ <strong>Attenzione:</strong> la tessera sarà valida a tutti gli effetti solo dopo il pagamento della quota associativa. Fino ad allora ha valore puramente identificativo.
      </div>


      <hr style="border: 0; border-top: 1px solid #eee; margin: 30px 0 20px 0;">
      
      <div style="font-size: 14px; color: #555; line-height: 1.6;">
        <strong style="color: #222; font-size: 16px;">Nuova Alba APS</strong><br>
        Via Giamaica 6, Pomezia, RM<br>
        ✉️ <a href="mailto:info@nuovaalba.org" style="color: ${hex}; text-decoration: none;">info@nuovaalba.org</a><br>
        🌐 <a href="https://nuovaalba.org" style="color: ${hex}; text-decoration: none;" target="_blank">nuovaalba.org</a><br>
        📸 <a href="https://www.instagram.com/nuovaalba_/" style="color: ${hex}; text-decoration: none;" target="_blank">Instagram</a>
      </div>
    </div>`;


  const opzioniMail = {
    to: email,
    subject: `Benvenuto/a in ${CONFIG.NOME_APS}! La tua richiesta di iscrizione`,
    htmlBody: corpoHtml,
    name: CONFIG.NOME_APS,
    replyTo: CONFIG.EMAIL_STAFF
  };


  if (pdfTessera && typeof pdfTessera.getBlob === 'function') {
    try {
      opzioniMail.attachments = [pdfTessera.getBlob()];
    } catch (e) {
      Logger.log("⚠️ Impossibile allegare PDF: " + e.message);
    }
  }


  MailAppailApp.sendEmail(opzioniMail);
}


function inviaEmailGiaIscritto(email, nome, matricola, stripeLink, statoPagamento) {
  const hex = CONFIG.COLOR_HEX;
  
  let bloccoPagamento = "";
  if (statoPagamento.toLowerCase() === "in attesa") {
    bloccoPagamento = `
      <p>Risulta che la tua quota associativa è ancora <strong>in attesa di pagamento</strong>. Puoi saldarla direttamente online tramite il pulsante sottostante:</p>
      <p style="margin: 20px 0;"><a href="${stripeLink}" style="background-color: ${hex}; color: white; padding: 12px 22px; text-decoration: none; border-radius: 5px; display: inline-block; font-weight: bold;">Paga la quota con Stripe</a></p>
    `;
  } else {
    bloccoPagamento = `
      <p>Ti confermiamo che la tua posizione associativa è <strong>in regola</strong> (Stato: <strong style="color: green;">${statoPagamento}</strong>). Non devi effettuare alcun pagamento aggiuntivo.</p>
    `;
  }


  const corpoHtml = `
    <div style="font-family: Arial, sans-serif; color: #333; max-width: 600px; margin: 0 auto; border: 1px solid #eee; padding: 25px; border-radius: 8px;">
      <h2 style="color: ${hex}; margin-top: 0;">Ciao ${nome}, sei già iscritto/a!</h2>
      <p>Risulti già presente nel registro soci dell'associazione <strong>${CONFIG.NOME_APS}</strong>.</p>
      
      <div style="background-color: #fff7f2; border-left: 4px solid ${hex}; padding: 15px; margin: 20px 0; font-size: 18px; font-weight: bold;">
        La tua Matricola Socio è: <span style="color: ${hex};">${matricola}</span>
      </div>
      ${bloccoPagamento}
      <hr style="border: 0; border-top: 1px solid #eee; margin: 30px 0 20px 0;">
      
      <div style="font-size: 14px; color: #555; line-height: 1.6;">
        <strong style="color: #222; font-size: 16px;">Nuova Alba APS</strong><br>
        Via Giamaica 6, Pomezia, RM<br>
        ✉️ <a href="mailto:info@nuovaalba.org" style="color: ${hex}; text-decoration: none;">info@nuovaalba.org</a><br>
        🌐 <a href="https://nuovaalba.org" style="color: ${hex}; text-decoration: none;" target="_blank">nuovaalba.org</a><br>
        📸 <a href="https://www.instagram.com/nuovaalba_/" style="color: ${hex}; text-decoration: none;" target="_blank">Instagram</a>
      </div>
    </div>`;


  MailApp.sendEmail({ to: email, subject: `Sei già iscritto/a a ${CONFIG.NOME_APS}`, htmlBody: corpoHtml });
}
reddit.com
u/leotyeahbaby — 6 days ago

Newbie here. Is Clasp still used?

I've just found out about GAS and now Clasp. It's incredible all you can get done with these tools! Bunch of companies already depend tremendously in manual tasks made on googles platform and automating with GAS is just amazing, and using Clasp to develop locally (+git) and then deploy is just amazing.

But is it actively maintained? Is it of common usage? Is GAS and Clasp worth learning? I've been thinking about offering freelance work with this techs but I don't know if I'm being silly...

reddit.com
u/Informal_Witness3869 — 7 days ago

After 2 years of silence, I finally rebuilt Google Apps Script Copilot. Sorry for disappearing.

Hey everyone,

Some of you might remember GS Copilot (Google Apps Script Copilot) — the Chrome extension that adds an AI sidebar directly into the Apps Script editor. I launched it, got some early traction, and then... life happened. Work, other commitments, the usual stuff that makes side projects quietly die. I went almost silent for close to 2 years. No updates, barely any support replies. If you installed it back then and it just sat there half-broken, I'm sorry — that's on me.

What kept nagging at me is that over 20,000 people actually installed this thing. That's not nothing. People kept using it, kept emailing me, kept leaving reviews asking if it was still alive. So a few months ago I sat down and basically rebuilt the whole thing from scratch.

Here's what's new:

  • Agent mode — describe what you want and it writes, edits, and applies the code for you across your project, not just one file at a time
  • Plan mode — for bigger changes, it lays out the plan before touching anything, so you're not surprised by a wall of edits
  • Quick edit + diff view — inline edits with an actual diff so you can see exactly what changed before accepting it
  • Context-aware file reading — it actually understands your whole Apps Script project structure, not just the file you have open
  • MCP connectors for Google Workspace — it can hook into Sheets/Docs/Drive as MCP tools when you need it to act on that context
  • Execution log integration — when your script throws an error, it reads the actual execution log and helps you fix it instead of guessing
  • Skills system — reusable sub-agents/snippets for stuff you do often

I'm going to be actively working on this now — not disappearing again. Demo video of it in action is attached below so you can see it working instead of just taking my word for it.

If you try it, I'd genuinely appreciate honest feedback — bugs, rough edges, missing features, whatever. This subreddit has more Apps Script experience than almost anywhere else, so if something's broken or annoying, I want to know.

Link: gscopilot.com

Thanks for sticking around this long, even the ones who just complained in reviews. Fair enough.

u/Razah786 — 9 days ago
▲ 9 r/GoogleAppsScript+3 crossposts

Zendesk Bulk Ops Tool: Mailroom

I built an open-source “mailroom” for Zendesk. bulk updates, mail merge & Slack reply routing

I work in support operations and kept running into the same annoying problem:

“We need to contact/update hundreds of Zendesk tickets at once, but we still need everything to remain traceable to the original ticket.”

So I built Zendesk Mailroom.

It runs from Google Sheets + Google Apps Script and lets you:
📧 Send personalized updates to hundreds of Zendesk ticket requesters
🏷️ Bulk add/remove tags
📝 Bulk add internal notes
🔄 Bulk change ticket status
🌍 Translate templates using Gemini
🔒 Handle closed tickets by automatically creating a new ticket when necessary
💬 Route guest replies back into a specific Slack thread
📊 Maintain an audit trail for every bulk operation
⚡ Process jobs in chunks so Apps Script’s execution limits don’t kill the workflow
🚦 Use Zendesk’s update_many endpoint for efficient bulk operations

One thing I specifically wanted to avoid was building a giant backend just to solve an internal operations problem.

So there’s no server, no hosting and no build step it’s essentially Google Sheets acting as the operational interface and Apps Script handling the orchestration.

GitHub: github.com/GVyom/zendesk-mailroom

I’d genuinely love feedback from people who work with Zendesk:

What would you add/change to make something like this actually useful for a support team?

Especially curious about workflows around bulk ticket operations that Zendesk doesn’t handle particularly well out of the box.

u/OccasionSuper2536 — 7 days ago
▲ 3 r/GoogleAppsScript+1 crossposts

Can I find work with these skills ?

I actually started using Google Sheets about a week and a half ago, and two days ago I switched from using Sheets directly to an application that uses it as a database. It all stemmed from a personal need: I began by manually logging raw data into Sheets, but I wanted to optimize the process. It started with automated tables and evolved into an app developed in C# that records entries directly into a Sheets spreadsheet. Right now, I’m transforming it into a multi-tab application—meaning the app I’ve built so far will become just one "branch" of the larger system. I’d like to know if there’s a potential career path for me in this field; I’d love to make a living from my passion, and this tool has become exactly that...

reddit.com
u/TearPuzzleheaded5498 — 6 days ago

Help with a set up sheets to Google Calendar

Hi,

So I inputed a script that turns my rows in my sheet into Google Calendar dates. I was able to get it to upload drive pdfs when needed but when I try uploading a Google doc it doesn’t work does anyone have any advice?

reddit.com
u/Sayzar1 — 8 days ago

Do you use typescript and create tests?

Do you use typescript and create tests for your app scripts? Or you do everything through the web interface, I'm curious which method is most common.

reddit.com
u/IllustriousPut442 — 9 days ago

I automated warehouse transfers between 18 stores and our warehouse with Google Apps Script

Our ERP was a mess so I built a workaround with Google Apps Script.

We had 18 outlets submitting Item Requisitions in one Sheet. The warehouse team had to manually copy those quantities into a separate Inventory Movement Sheet. Double entry = errors + delays.

What I built:
A Google Apps Script that:

  1. Watches the "Item Requisition" sheet for new submissions
  2. Automatically syncs the quantity sent to each location
  3. Deducts it from the "Warehouse Inventory Movement" sheet in real time

Result:
No more double entry. Warehouse now has real-time visibility across all 18 locations. Transfer accuracy way up.

Happy to share the code/snippet if anyone wants it. Also open to feedback on making the sync more robust for concurrent edits.

Did anyone else here use Apps Script to patch gaps in their ERP?

reddit.com
u/One_Tooth5185 — 12 days ago

Workaround Available?

Is there a workaround available for a web app page only opening in incognito mode? To open in a regular browser window, I have to completely log out. Will users also have to do this?

reddit.com
u/ARkieGirl501 — 12 days ago