# Geavanceerde mail merge met Apps Script en Docs [[TOC]] ## Wat is mail merge met Apps Script? Mail merge combineert een sjabloondocument met een datalijst om gepersonaliseerde documenten te genereren. Met Google Apps Script automatiseer je dit proces volledig: het script leest de data uit Sheets, maakt voor elke rij een kopie van het Docs-sjabloon, vervangt de placeholders door echte data en exporteert het resultaat als PDF. Veelvoorkomende toepassingen: - Gepersonaliseerde contracten voor klanten - Certificaten voor deelnemers aan een training - Facturen of offertes op basis van spreadsheet-data - Individuele rapportages per medewerker :::info title="Verder dan een kant-en-klare add-on" Dit voorbeeld gaat verder dan eenvoudige mail merge add-ons. Met Apps Script heb je volledige controle: tabellen toevoegen, conditionele inhoud, automatisch e-mailen en archiveren in Drive. ::: ## Stap 1: Het Docs-sjabloon aanmaken Maak een Google Docs-document met placeholders voor variabele inhoud. Gebruik dubbele accolades als scheidingsteken: ``` {{NAAM}} {{BEDRIJF}} {{DATUM}} {{BEDRAG}} ``` Noteer de document-ID uit de URL. Dat is het lange deel met letters en cijfers na `/d/`. ## Stap 2: De Sheets-datalijst aanmaken Maak een Google Sheets-bestand met de data. De eerste rij bevat kolomhoofden die exact overeenkomen met de placeholders in het sjabloon (zonder de accolades): | NAAM | BEDRIJF | DATUM | BEDRAG | EMAIL | STATUS | |---|---|---|---|---|---| | Jan Jansen | Bedrijf A | 01-06-2026 | 1250,00 | jan@bedrijfa.nl | | | Eva de Vries | Bedrijf B | 01-06-2026 | 2400,00 | eva@bedrijfb.nl | | De kolom STATUS wordt later door het script bijgewerkt, zodat een rij niet dubbel wordt verwerkt. ## Stap 3: Het Apps Script schrijven ```javascript var SJABLOON_ID = 'jouw-docs-sjabloon-id-hier'; var SHEETS_ID = 'jouw-sheets-id-hier'; var EXPORT_MAP_NAAM = 'Gegenereerde Documenten'; function voerMailMergeUit() { var ss = SpreadsheetApp.openById(SHEETS_ID); var sheet = ss.getActiveSheet(); var data = sheet.getDataRange().getValues(); var headers = data[0]; var statusIndex = headers.indexOf('STATUS'); var naamIndex = headers.indexOf('NAAM'); var emailIndex = headers.indexOf('EMAIL'); var exportMap = getOfMaakMap(EXPORT_MAP_NAAM); for (var i = 1; i < data.length; i++) { var rij = data[i]; if (rij[statusIndex] === 'Verzonden') { continue; } var plaatsvervangingen = {}; headers.forEach(function(header, index) { plaatsvervangingen['{{' + header + '}}'] = rij[index] || ''; }); var pdfBlob = genereerDocument(plaatsvervangingen, rij[naamIndex]); var bestand = exportMap.createFile(pdfBlob); if (rij[emailIndex]) { verstuurEmail(rij[emailIndex], rij[naamIndex], bestand.getUrl()); } sheet.getRange(i + 1, statusIndex + 1).setValue('Verzonden'); Utilities.sleep(500); } console.log('Mail merge voltooid.'); } function genereerDocument(plaatsvervangingen, naam) { var sjabloon = DriveApp.getFileById(SJABLOON_ID); var kopie = sjabloon.makeCopy('Temp - ' + naam); var doc = DocumentApp.openById(kopie.getId()); var body = doc.getBody(); Object.keys(plaatsvervangingen).forEach(function(placeholder) { body.replaceText(placeholder, String(plaatsvervangingen[placeholder])); }); doc.saveAndClose(); var pdfBlob = kopie.getAs('application/pdf'); pdfBlob.setName(naam + ' - Document.pdf'); kopie.setTrashed(true); return pdfBlob; } function getOfMaakMap(mapNaam) { var mappen = DriveApp.getFoldersByName(mapNaam); if (mappen.hasNext()) { return mappen.next(); } return DriveApp.createFolder(mapNaam); } function verstuurEmail(emailAdres, naam, driveUrl) { var onderwerp = 'Uw document is beschikbaar'; var bericht = 'Beste ' + naam + ', ' + 'Uw document is gegenereerd en beschikbaar via de volgende link: ' + driveUrl + ' ' + 'Met vriendelijke groet, ' + 'Cloud Captains'; GmailApp.sendEmail(emailAdres, onderwerp, bericht); } ``` Let op de regel `body.replaceText(placeholder, ...)`: het eerste argument van `replaceText` wordt door Apps Script als reguliere expressie behandeld. Accolades zijn daarin veilig, maar gebruik je tekens als `(`, `)`, `[`, `.` of `*` in een placeholder, escape die dan. ## Conditionele inhoud toevoegen Soms moet een sectie alleen verschijnen voor bepaalde rijen, bijvoorbeeld een premium-blok. Markeer de sectie in het sjabloon met een begin- en eind-placeholder en verwijder die naargelang de data: ```javascript function verwerkConditioneleInhoud(body, data) { if (data['{{PREMIUM}}'] === 'Ja') { body.replaceText('\{\{PREMIUM_SECTIE_BEGIN\}\}', ''); body.replaceText('\{\{PREMIUM_SECTIE_EIND\}\}', ''); } else { body.replaceText( '\{\{PREMIUM_SECTIE_BEGIN\}\}[\s\S]*?\{\{PREMIUM_SECTIE_EIND\}\}', '' ); } } ``` In de `Ja`-tak verwijder je alleen de twee markeringen, zodat de inhoud blijft staan. In de `else`-tak verwijder je de markeringen plus alles ertussen in een keer met een reguliere expressie. :::warn title="Test eerst op een paar rijen" Test het script altijd eerst op een kleine subset van de data, bijvoorbeeld alleen rij 2, voordat je het op de volledige lijst loslaat. Voeg tijdelijk een `break`-statement onderaan de loop toe om na de eerste rij te stoppen, en verwijder dat weer als alles klopt. ::: ## Foutafhandeling toevoegen Vang fouten af en stuur jezelf een melding, zodat een mislukte run niet onopgemerkt blijft: ```javascript function voerMailMergeUitMetFoutafhandeling() { try { voerMailMergeUit(); } catch (e) { GmailApp.sendEmail( Session.getActiveUser().getEmail(), 'Mail merge fout', 'Er is een fout opgetreden: ' + e.message + ' Stack: ' + e.stack ); console.error('Mail merge fout: ' + e.message); } } ``` ## Het script starten vanuit Sheets Wil je het script met een knop in je spreadsheet starten in plaats van vanuit de editor? Voeg dan een eigen menu toe. :::howto title="Een eigen menu toevoegen" 1. Open het Apps Script-project via **Extensies** en dan **Apps Script**. 2. Voeg een `onOpen()`-functie toe die het menu opbouwt: ```javascript function onOpen() { SpreadsheetApp.getUi() .createMenu('Mail Merge') .addItem('Start', 'voerMailMergeUitMetFoutafhandeling') .addToUi(); } ``` 3. Sla op en herlaad het spreadsheet. Het menu **Mail Merge** verschijnt in de menubalk. 4. Klik op **Start** en doorloop de autorisatie de eerste keer. ::: :::tip title="Houd je IDs uit de code" Zet `SJABLOON_ID` en `SHEETS_ID` niet hardcoded in je script als je het deelt. Bewaar ze via `PropertiesService.getScriptProperties()` en lees ze in runtime uit. Zo lekken gevoelige verwijzingen niet mee in een gedeelde kopie. ::: :::faq ### Kan ik ook Word-documenten genereren? Nee, Apps Script werkt native met Google Docs. De PDF-export werkt goed voor de meeste distributiedoeleinden. Wil je per se een bewerkbaar Word-bestand, exporteer dan via de Drive API met het MIME-type voor docx in plaats van pdf. ### Is er een limiet aan het aantal documenten dat ik kan genereren? Ja. Een script-uitvoering mag maximaal 6 minuten duren (geldt zowel voor gratis Gmail- als Workspace-accounts, situatie juni 2026). Afhankelijk van de complexiteit haal je daarmee grofweg 20 tot 100 documenten per run. Voor grotere batches verdeel je het werk over meerdere runs of een tijdtrigger. ### Hoe zit het met de opmaak van getallen en datums? Getallen en datums uit Sheets worden automatisch naar tekst omgezet, soms anders dan je verwacht. Formatteer een datum voor vervanging met `Utilities.formatDate(new Date(datum), 'Europe/Amsterdam', 'dd-MM-yyyy')` en gebruik voor bedragen bijvoorbeeld `Utilities.formatString`. ### Waarom zie ik na een time-out maar de helft van mijn documenten? Omdat het script per rij de kolom STATUS op `Verzonden` zet, worden al verwerkte rijen bij een volgende run overgeslagen. Start het script gewoon opnieuw; het pakt de resterende rijen op. ### Hoe beveilig ik gevoelige data in het script? Gebruik `PropertiesService` voor opslag van gevoelige configuratie zoals IDs of API-sleutels in plaats van ze in de code te zetten. Beperk de toegang tot het spreadsheet tot bevoegde personen en deel het sjabloon niet breder dan nodig. ### Worden de tijdelijke kopieen netjes opgeruimd? Ja. De functie `genereerDocument` zet elke tijdelijke kopie na de PDF-export in de prullenbak via `setTrashed(true)`. Leeg af en toe je prullenbak in Drive om opslagruimte vrij te maken. :::