100 Advanced Google Apps Script Examples

https://github.com/lsvekis/100-Advanced-Google-Apps-Script-Examples

A) Sheets: Performance, Data Ops, Automation (1–25)

1) Batch update with RangeList (fast formatting)

Does: Applies formatting to many ranges in one go.

function ex01_rangeListFormat() {
  const sh = SpreadsheetApp.getActiveSheet();
  sh.getRangeList(['A1:D1','A3:D3','A5:D5']).setFontWeight('bold').setBackground('#f3f4f6');
}

2) Write values + formulas in one pass

Does: Sets values and formulas efficiently.

function ex02_valuesAndFormulas() {
  const sh = SpreadsheetApp.getActiveSheet();
  sh.getRange('A1:B1').setValues([['Item','Total']]);
  sh.getRange('B2:B').setFormulaR1C1('=IF(RC[-1]="","",LEN(RC[-1]))');
}

3) Build an index of unique keys → row numbers

Does: Creates a lookup map for fast searches.

function ex03_buildIndexMap() {
  const sh = SpreadsheetApp.getActiveSheet();
  const vals = sh.getRange(2,1,sh.getLastRow()-1,1).getValues().flat();
  const map = {};
  vals.forEach((v,i)=> { const k=String(v).trim(); if(k) map[k]=i+2; });
  Logger.log(map);
}

4) Upsert rows by key (update or append)

Does: Updates existing row if key exists, else appends.

function ex04_upsertByKey() {
  const sh = SpreadsheetApp.getActiveSheet();
  const keyCol = 1;
  const key = 'ABC-123';
  const payload = ['ABC-123','New Name', new Date()];
  const data = sh.getDataRange().getValues();
  const idx = data.findIndex((r,i)=> i>0 && String(r[keyCol-1]).trim()===key);
  if (idx > -1) sh.getRange(idx+1,1,1,payload.length).setValues([payload]);
  else sh.appendRow(payload);
}

5) “Soft delete” rows (flag instead of deleting)

Does: Marks row as deleted for auditability.

function ex05_softDelete() {
  const sh = SpreadsheetApp.getActiveSheet();
  const row = 5;
  sh.getRange(row, sh.getLastColumn()+1).setValue('DELETED').setNote('Soft deleted ' + new Date());
}

6) Atomic log writer (prevents collisions)

Does: Uses LockService to avoid simultaneous writes.

function ex06_atomicLog() {
  const lock = LockService.getScriptLock();
  lock.waitLock(20000);
  try {
    const sh = SpreadsheetApp.getActive().getSheetByName('Logs') || SpreadsheetApp.getActive().insertSheet('Logs');
    sh.appendRow([new Date(), Session.getActiveUser().getEmail(), 'Event']);
  } finally {
    lock.releaseLock();
  }
}

7) Incremental processing with cursor (resume later)

Does: Processes 200 rows per run, saves cursor to properties.

function ex07_incrementalProcess() {
  const props = PropertiesService.getScriptProperties();
  const startRow = Number(props.getProperty('cursor') || 2);
  const sh = SpreadsheetApp.getActiveSheet();
  const last = sh.getLastRow();
  const batchSize = 200;
  const endRow = Math.min(last, startRow + batchSize - 1);
  if (startRow > last) { props.deleteProperty('cursor'); return; }

  const data = sh.getRange(startRow,1,endRow-startRow+1,sh.getLastColumn()).getValues();
  // do work...
  Logger.log(`Processed rows ${startRow}-${endRow}`);

  props.setProperty('cursor', String(endRow + 1));
}

8) De-duplicate sheet by composite key (keep first)

Does: Removes duplicates using key from multiple columns.

function ex08_dedupeComposite() {
  const sh = SpreadsheetApp.getActiveSheet();
  const data = sh.getDataRange().getValues();
  const header = data.shift();
  const seen = new Set();
  const out = [header];

  data.forEach(r=>{
    const key = [r[0],r[1],r[2]].map(v=>String(v).trim()).join('|');
    if (!key.replace(/\|/g,'')) return;
    if (!seen.has(key)) { seen.add(key); out.push(r); }
  });

  sh.clear();
  sh.getRange(1,1,out.length,out[0].length).setValues(out);
}

9) Add a “change log” note on edit (installable recommended)

Does: Appends notes with edit history.

function ex09_onEditNotes(e) {
  const r = e.range;
  if (r.getRow() === 1) return;
  const prev = r.getNote() || '';
  const msg = `${new Date().toISOString()} | ${e.oldValue ?? ''} → ${e.value ?? ''}`;
  r.setNote((prev ? prev + '\n' : '') + msg);
}

10) Create a protected “admin” column

Does: Protects a column while leaving others editable.

function ex10_protectAdminColumn() {
  const sh = SpreadsheetApp.getActiveSheet();
  const p = sh.getRange('H:H').protect().setDescription('Admin only');
  p.removeEditors(p.getEditors());
}

11) Generate a data dictionary tab

Does: Lists headers + column index + sample values.

function ex11_dataDictionary() {
  const ss = SpreadsheetApp.getActive();
  const sh = ss.getActiveSheet();
  const dict = ss.getSheetByName('Data Dictionary') || ss.insertSheet('Data Dictionary');
  const headers = sh.getRange(1,1,1,sh.getLastColumn()).getValues()[0];
  const samples = sh.getRange(2,1,Math.min(50, sh.getLastRow()-1), sh.getLastColumn()).getValues();

  const out = [['Column','Index','Sample Values']];
  headers.forEach((h,i)=>{
    const sample = samples.map(r=>r[i]).filter(v=>v!=='' && v!=null).slice(0,5).join(', ');
    out.push([h, i+1, sample]);
  });

  dict.clear();
  dict.getRange(1,1,out.length,out[0].length).setValues(out);
}

12) Create a “validated import” pipeline

Does: Validates required columns before accepting import.

function ex12_validateImport() {
  const sh = SpreadsheetApp.getActiveSheet();
  const headers = sh.getRange(1,1,1,sh.getLastColumn()).getValues()[0].map(String);
  const required = ['Email','Name','Status'];
  const missing = required.filter(r=>!headers.includes(r));
  if (missing.length) throw new Error('Missing columns: ' + missing.join(', '));
  Logger.log('Import schema OK');
}

13) Apply smart filters + freeze + styling (setup table)

function ex13_tableSetup() {
  const sh = SpreadsheetApp.getActiveSheet();
  sh.setFrozenRows(1);
  const r = sh.getDataRange();
  r.createFilter();
  sh.getRange(1,1,1,sh.getLastColumn()).setFontWeight('bold');
}

14) Split sheet into multiple sheets by category

function ex14_splitByCategory() {
  const ss = SpreadsheetApp.getActive();
  const sh = ss.getActiveSheet();
  const data = sh.getDataRange().getValues();
  const header = data.shift();
  const catCol = 3; // column C

  const buckets = {};
  data.forEach(r=>{
    const k = String(r[catCol-1]).trim() || 'Uncategorized';
    (buckets[k] ||= []).push(r);
  });

  Object.entries(buckets).forEach(([name,rows])=>{
    const tab = ss.getSheetByName(name) || ss.insertSheet(name);
    tab.clear();
    tab.getRange(1,1,1,header.length).setValues([header]);
    tab.getRange(2,1,rows.length,header.length).setValues(rows);
  });
}

15) Create an audit snapshot (copy values only)

function ex15_snapshotValuesOnly() {
  const ss = SpreadsheetApp.getActive();
  const src = ss.getActiveSheet();
  const snap = ss.insertSheet(`Snapshot ${Utilities.formatDate(new Date(), ss.getSpreadsheetTimeZone(), 'yyyy-MM-dd HH:mm')}`);
  const values = src.getDataRange().getValues();
  snap.getRange(1,1,values.length,values[0].length).setValues(values);
}

16) Export a range to CSV and save to Drive

function ex16_exportRangeToCsv() {
  const sh = SpreadsheetApp.getActiveSheet();
  const values = sh.getRange('A1:D20').getValues();
  const csv = values.map(r=>r.map(v=>`"${String(v).replace(/"/g,'""')}"`).join(',')).join('\n');
  const file = DriveApp.createFile('export.csv', csv, MimeType.CSV);
  Logger.log(file.getUrl());
}

17) Custom function with caching

function EX17_CACHED_LEN(input) {
  const cache = CacheService.getScriptCache();
  const key = 'len:' + input;
  const hit = cache.get(key);
  if (hit) return Number(hit);
  const val = String(input ?? '').length;
  cache.put(key, String(val), 300);
  return val;
}

18) Named range “schema” enforcer

function ex18_enforceNamedRanges() {
  const ss = SpreadsheetApp.getActive();
  const required = ['INPUTS','OUTPUTS'];
  required.forEach(n=>{
    if (!ss.getRangeByName(n)) throw new Error(`Missing named range: ${n}`);
  });
  Logger.log('All named ranges present');
}

19) Auto-create weekly tabs

function ex19_createWeeklyTabs() {
  const ss = SpreadsheetApp.getActive();
  const tz = ss.getSpreadsheetTimeZone();
  for (let i=0;i<4;i++){
    const d = new Date(Date.now() + i*7*24*3600*1000);
    const name = 'Week ' + Utilities.formatDate(d, tz, 'YYYY-ww');
    if (!ss.getSheetByName(name)) ss.insertSheet(name);
  }
}

20) Conditional formatting rule builder (dynamic range)

function ex20_conditionalFormattingDynamic() {
  const sh = SpreadsheetApp.getActiveSheet();
  const rng = sh.getRange(2,4,Math.max(1,sh.getLastRow()-1),1); // column D from row 2
  const rule = SpreadsheetApp.newConditionalFormatRule()
    .whenTextContains('ERROR')
    .setBackground('#ffe4e6')
    .setRanges([rng]).build();
  sh.setConditionalFormatRules([...sh.getConditionalFormatRules(), rule]);
}

21) Import JSON into sheet (flatten basic objects)

function ex21_importJsonToSheet() {
  const url = 'https://httpbin.org/json';
  const json = JSON.parse(UrlFetchApp.fetch(url).getContentText());
  const sh = SpreadsheetApp.getActiveSheet();
  const rows = Object.entries(json).map(([k,v])=>[k, typeof v === 'object' ? JSON.stringify(v) : v]);
  sh.getRange(1,1,rows.length,2).setValues(rows);
}

22) Sheet “safe write” (verify headers match)

function ex22_safeWriteByHeaders() {
  const sh = SpreadsheetApp.getActiveSheet();
  const headers = sh.getRange(1,1,1,sh.getLastColumn()).getValues()[0];
  const expected = ['ID','Name','Status'];
  if (expected.join('|') !== headers.slice(0,3).join('|')) throw new Error('Header mismatch.');
  sh.getRange(2,1,1,3).setValues([[1,'Test','OK']]);
}

23) Batch delete rows by predicate (bottom-up)

function ex23_deleteRowsIfBlankColB() {
  const sh = SpreadsheetApp.getActiveSheet();
  const last = sh.getLastRow();
  const vals = sh.getRange(2,2,last-1,1).getValues().flat();
  for (let i=vals.length-1;i>=0;i--) if (!String(vals[i]).trim()) sh.deleteRow(i+2);
}

24) Build a mini dashboard sheet (KPIs)

function ex24_dashboardKpis() {
  const ss = SpreadsheetApp.getActive();
  const dataSh = ss.getActiveSheet();
  const dash = ss.getSheetByName('Dashboard') || ss.insertSheet('Dashboard');
  const last = dataSh.getLastRow();
  const total = Math.max(0, last-1);
  const unique = new Set(dataSh.getRange(2,1,total,1).getValues().flat().map(v=>String(v).trim()).filter(Boolean)).size;
  const out = [['Metric','Value'],['Total rows', total],['Unique IDs (col A)', unique],['Updated', new Date()]];
  dash.clear();
  dash.getRange(1,1,out.length,2).setValues(out);
  dash.getRange('A1:B1').setFontWeight('bold');
}

25) Create and refresh an Apps Script “config” sheet

function ex25_configSheet() {
  const ss = SpreadsheetApp.getActive();
  const sh = ss.getSheetByName('Config') || ss.insertSheet('Config');
  const out = [['Key','Value'],['ENV','dev'],['MAX_BATCH','200'],['LAST_RUN', new Date().toISOString()]];
  sh.clear(); sh.getRange(1,1,out.length,2).setValues(out);
}

B) Drive: File Ops, Search, Structure (26–45)

26) Find files by query and list URLs

function ex26_driveSearch() {
  const it = DriveApp.searchFiles('mimeType="application/vnd.google-apps.document" and trashed=false');
  let n=0;
  while (it.hasNext() && n<10) { const f=it.next(); Logger.log(f.getName() + ' ' + f.getUrl()); n++; }
}

27) Deep clone folder (copies files + subfolders)

function ex27_cloneFolder() {
  const sourceId = 'PASTE_SOURCE_FOLDER_ID';
  const targetParentId = 'PASTE_TARGET_PARENT_FOLDER_ID';
  const src = DriveApp.getFolderById(sourceId);
  const parent = DriveApp.getFolderById(targetParentId);
  const dst = parent.createFolder(src.getName() + ' (Copy)');
  cloneFolder_(src, dst);
}
function cloneFolder_(src, dst) {
  const files = src.getFiles();
  while (files.hasNext()) dst.addFile(files.next().makeCopy());
  const folders = src.getFolders();
  while (folders.hasNext()) {
    const sub = folders.next();
    const newSub = dst.createFolder(sub.getName());
    cloneFolder_(sub, newSub);
  }
}

28) Create a “year/month” folder path if missing

function ex28_yearMonthFolders() {
  const rootId = 'PASTE_ROOT_FOLDER_ID';
  const root = DriveApp.getFolderById(rootId);
  const now = new Date();
  const year = String(now.getFullYear());
  const month = Utilities.formatDate(now, Session.getScriptTimeZone(), 'MM');
  const y = getOrCreateSub_(root, year);
  const m = getOrCreateSub_(y, month);
  Logger.log(m.getUrl());
}
function getOrCreateSub_(parent, name) {
  const it = parent.getFoldersByName(name);
  return it.hasNext() ? it.next() : parent.createFolder(name);
}

29) Convert a PDF/image to Google Doc (Drive OCR requires API—not built-in)

Does: Notes limitation; Apps Script alone can’t OCR without external service.

function ex29_noteOcrLimit() {
  Logger.log('Apps Script DriveApp cannot OCR by itself. Use Drive API + "ocr" parameter or external OCR service.');
}

30) Move all files older than X days to “Archive”

function ex30_archiveOldFiles() {
  const folderId = 'PASTE_FOLDER_ID';
  const archiveId = 'PASTE_ARCHIVE_FOLDER_ID';
  const days = 30;
  const cutoff = Date.now() - days*24*3600*1000;

  const src = DriveApp.getFolderById(folderId);
  const archive = DriveApp.getFolderById(archiveId);
  const files = src.getFiles();
  while (files.hasNext()) {
    const f = files.next();
    if (f.getLastUpdated().getTime() < cutoff) archive.addFile(f), src.removeFile(f);
  }
}

31) Rename files with a consistent prefix

function ex31_prefixRename() {
  const folderId='PASTE_FOLDER_ID';
  const prefix='2026-';
  const folder=DriveApp.getFolderById(folderId);
  const files=folder.getFiles();
  while(files.hasNext()){
    const f=files.next();
    if(!f.getName().startsWith(prefix)) f.setName(prefix + f.getName());
  }
}

32) Create share links (view-only) for a list of files

function ex32_shareLinks() {
  const ids = ['PASTE_FILE_ID_1','PASTE_FILE_ID_2'];
  ids.forEach(id=>{
    const f = DriveApp.getFileById(id);
    f.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
    Logger.log(f.getUrl());
  });
}

33) Export Google Doc as PDF into a folder

function ex33_exportDocPdfToFolder() {
  const docId='PASTE_DOC_ID', folderId='PASTE_FOLDER_ID';
  const file=DriveApp.getFileById(docId);
  const pdf=file.getBlob().getAs(MimeType.PDF).setName(file.getName()+'.pdf');
  DriveApp.getFolderById(folderId).createFile(pdf);
}

34) Build a Drive inventory sheet (name, type, size, url)

function ex34_driveInventoryToSheet() {
  const folderId='PASTE_FOLDER_ID';
  const sh=SpreadsheetApp.getActiveSheet();
  const out=[['Name','MimeType','Size','URL','Last Updated']];
  const files=DriveApp.getFolderById(folderId).getFiles();
  while(files.hasNext()){
    const f=files.next();
    out.push([f.getName(), f.getMimeType(), f.getSize(), f.getUrl(), f.getLastUpdated()]);
  }
  sh.clear();
  sh.getRange(1,1,out.length,out[0].length).setValues(out);
}

35) Find duplicate files by name in a folder

function ex35_duplicateNamesInFolder() {
  const folderId='PASTE_FOLDER_ID';
  const files=DriveApp.getFolderById(folderId).getFiles();
  const map = {};
  while(files.hasNext()){
    const f=files.next();
    (map[f.getName()] ||= []).push(f.getId());
  }
  const dupes = Object.entries(map).filter(([,ids])=>ids.length>1);
  Logger.log(dupes);
}

36) Trash files matching a pattern (careful!)

function ex36_trashMatching() {
  const folderId='PASTE_FOLDER_ID';
  const pattern=/^tmp_/i;
  const files=DriveApp.getFolderById(folderId).getFiles();
  while(files.hasNext()){
    const f=files.next();
    if(pattern.test(f.getName())) f.setTrashed(true);
  }
}

37) Copy a file and keep it in the same folder

function ex37_copyInPlace() {
  const fileId='PASTE_FILE_ID';
  const f=DriveApp.getFileById(fileId);
  const parents=f.getParents();
  if(!parents.hasNext()) throw new Error('No parent folder.');
  const folder=parents.next();
  const copy=f.makeCopy(f.getName() + ' (Copy)', folder);
  Logger.log(copy.getUrl());
}

38) Create “safe” unique filenames

function ex38_uniqueName(baseName, folderId) {
  const folder = DriveApp.getFolderById(folderId);
  let name = baseName, i=1;
  while (folder.getFilesByName(name).hasNext()) name = `${baseName} (${i++})`;
  return name;
}

39) Bulk convert XLSX to Google Sheets (Drive API recommended)

Needs: Advanced Drive Service (Drive) enabled.

function ex39_convertXlsxToSheet() {
  const fileId='PASTE_XLSX_FILE_ID';
  const resource = { title: 'Converted Sheet', mimeType: MimeType.GOOGLE_SHEETS };
  const newFile = Drive.Files.copy(resource, fileId);
  Logger.log(newFile.alternateLink);
}

40) Set file description + star it

function ex40_fileMeta() {
  const fileId='PASTE_FILE_ID';
  const f=DriveApp.getFileById(fileId);
  f.setDescription('Updated by Apps Script on ' + new Date());
  f.setStarred(true);
}

41) Find large files (>50MB) in Drive root

function ex41_largeFiles() {
  const it = DriveApp.searchFiles('trashed=false and "root" in parents and quotaBytesUsed > 50000000');
  while(it.hasNext()) {
    const f=it.next();
    Logger.log(`${f.getName()} ${f.getSize()} ${f.getUrl()}`);
  }
}

42) Build a folder tree report (recursive)

function ex42_folderTree() {
  const rootId='PASTE_FOLDER_ID';
  const root=DriveApp.getFolderById(rootId);
  printTree_(root, 0);
}
function printTree_(folder, depth) {
  Logger.log('  '.repeat(depth) + folder.getName());
  const sub=folder.getFolders();
  while(sub.hasNext()) printTree_(sub.next(), depth+1);
}

43) Replace a file in a folder (version-style)

function ex43_replaceFileInFolder() {
  const folderId='PASTE_FOLDER_ID';
  const folder=DriveApp.getFolderById(folderId);
  const name='current-report.pdf';
  const existing=folder.getFilesByName(name);
  while(existing.hasNext()) existing.next().setTrashed(true);
  folder.createFile(Utilities.newBlob('new content','text/plain',name));
}

44) Create a public “download” link (view only)

function ex44_publicLink() {
  const fileId='PASTE_FILE_ID';
  const f=DriveApp.getFileById(fileId);
  f.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
  Logger.log(f.getUrl());
}

45) Drive permissions audit (requires Drive API)

Needs: Advanced Drive Service (Drive)

function ex45_permissionsAudit() {
  const fileId='PASTE_FILE_ID';
  const perms = Drive.Permissions.list(fileId).items || [];
  perms.forEach(p=>Logger.log(`${p.role} ${p.type} ${p.emailAddress || ''}`));
}

C) Docs: Template Systems, Cleanup, Generation (46–65)

46) Generate a report doc from sheet rows

function ex46_sheetToDocReport() {
  const sh=SpreadsheetApp.getActiveSheet();
  const data=sh.getDataRange().getValues();
  const doc=DocumentApp.create('Sheet Report');
  const body=doc.getBody();
  body.appendParagraph('Report').setHeading(DocumentApp.ParagraphHeading.HEADING1);
  data.slice(1).forEach(r=> body.appendParagraph(`• ${r[0]} — ${r[1]}`));
  Logger.log(doc.getUrl());
}

47) Replace placeholders in a Doc (multiple fields)

function ex47_replacePlaceholders() {
  const doc=DocumentApp.getActiveDocument();
  const body=doc.getBody();
  const map = { '{{NAME}}':'Lars', '{{DATE}}': new Date().toDateString() };
  Object.entries(map).forEach(([k,v])=>body.replaceText(k, String(v)));
}

48) Insert table from array

function ex48_insertTable() {
  const doc=DocumentApp.getActiveDocument();
  const body=doc.getBody();
  const rows=[['Name','Score'],['Ava','95'],['Noah','88']];
  body.appendTable(rows);
}

49) Convert selected text to heading styles

function ex49_styleHeadings() {
  const doc=DocumentApp.getActiveDocument();
  const pars=doc.getBody().getParagraphs();
  pars.forEach(p=>{
    const t=p.getText();
    if(t.startsWith('## ')) { p.setHeading(DocumentApp.ParagraphHeading.HEADING2); p.setText(t.replace(/^##\s+/,''));
    } else if(t.startsWith('# ')) { p.setHeading(DocumentApp.ParagraphHeading.HEADING1); p.setText(t.replace(/^#\s+/,''));
    }
  });
}

50) Remove double blank lines

function ex50_removeDoubleBlankLines() {
  const body=DocumentApp.getActiveDocument().getBody();
  const paras=body.getParagraphs();
  for(let i=paras.length-2;i>=0;i--){
    if(!paras[i].getText().trim() && !paras[i+1].getText().trim()) paras[i+1].removeFromParent();
  }
}

51) Normalize bullet indentation (flatten)

function ex51_flattenBullets() {
  const body=DocumentApp.getActiveDocument().getBody();
  const paras=body.getParagraphs();
  paras.forEach(p=>{
    const li=p.editAsText(); // safe call; indentation set on paragraph
    if (p.getListId()) p.setIndentStart(0).setIndentFirstLine(0);
  });
}

52) Insert page breaks before headings

function ex52_pageBreakBeforeH1() {
  const body=DocumentApp.getActiveDocument().getBody();
  const pars=body.getParagraphs();
  for(let i=1;i<pars.length;i++){
    if(pars[i].getHeading()===DocumentApp.ParagraphHeading.HEADING1){
      body.insertParagraph(body.getChildIndex(pars[i]), '').appendPageBreak();
    }
  }
}

53) Build a Table of Contents automatically

function ex53_addTOC() {
  const body=DocumentApp.getActiveDocument().getBody();
  body.insertTableOfContents(0);
}

54) Convert markdown-style bullets to real bullets

function ex54_markdownBulletsToList() {
  const doc=DocumentApp.getActiveDocument();
  const body=doc.getBody();
  const pars=body.getParagraphs();
  pars.forEach(p=>{
    const t=p.getText();
    if(/^-\s+/.test(t)) { p.setText(t.replace(/^-+\s+/,'')); p.setBullet(true); }
  });
}

55) Add footer with date + page number

function ex55_footer() {
  const doc=DocumentApp.getActiveDocument();
  const footer=doc.addFooter();
  footer.appendParagraph('Generated: ' + new Date().toDateString());
}

56) Merge multiple docs into one

function ex56_mergeDocs() {
  const target=DocumentApp.create('Merged Doc');
  const body=target.getBody();
  const ids=['PASTE_DOC_ID_1','PASTE_DOC_ID_2'];
  ids.forEach(id=>{
    const src=DocumentApp.openById(id).getBody();
    src.getParagraphs().forEach(p=> body.appendParagraph(p.getText()));
    body.appendPageBreak();
  });
  Logger.log(target.getUrl());
}

57) Extract all links from a doc

function ex57_extractLinks() {
  const body=DocumentApp.getActiveDocument().getBody();
  const found=[];
  body.getParagraphs().forEach(p=>{
    const text=p.editAsText();
    for(let i=0;i<text.getText().length;i++){
      const url=text.getLinkUrl(i);
      if(url) found.push(url);
    }
  });
  Logger.log([...new Set(found)]);
}

58) Replace smart quotes with straight quotes

function ex58_normalizeQuotes() {
  const body=DocumentApp.getActiveDocument().getBody();
  const t=body.editAsText();
  t.replaceText('[“”]', '"');
  t.replaceText('[‘’]', "'");
}

59) Highlight placeholder tokens like {{…}}

function ex59_highlightPlaceholders() {
  const body=DocumentApp.getActiveDocument().getBody().editAsText();
  const text=body.getText();
  const re=/\{\{[^}]+\}\}/g;
  let m;
  while((m=re.exec(text))){
    body.setBackgroundColor(m.index, m.index+m[0].length-1, '#fff2cc');
  }
}

60) Create a “chapter” doc per heading

function ex60_splitDocByH1() {
  const doc=DocumentApp.getActiveDocument();
  const pars=doc.getBody().getParagraphs();
  let current=null;

  pars.forEach(p=>{
    if(p.getHeading()===DocumentApp.ParagraphHeading.HEADING1){
      current=DocumentApp.create(p.getText());
      current.getBody().appendParagraph(p.getText()).setHeading(DocumentApp.ParagraphHeading.HEADING1);
    } else if(current) {
      current.getBody().appendParagraph(p.getText());
    }
  });
}

61) Add code block styling (monospace + background)

function ex61_styleCodeBlocks() {
  const body=DocumentApp.getActiveDocument().getBody();
  body.getParagraphs().forEach(p=>{
    const t=p.getText();
    if(t.startsWith('```') || t.startsWith('    ')){
      p.setBackgroundColor('#f3f4f6');
      p.setFontFamily('Courier New');
    }
  });
}

62) Replace multiple whitespace with single spaces

function ex62_collapseSpaces() {
  const t=DocumentApp.getActiveDocument().getBody().editAsText();
  t.replaceText('\\s{2,}', ' ');
}

63) Generate a doc from JSON data

function ex63_docFromJson() {
  const data = { title:'Status', items:[{name:'Ava',score:95},{name:'Noah',score:88}] };
  const doc = DocumentApp.create(data.title);
  const body = doc.getBody();
  body.appendParagraph(data.title).setHeading(DocumentApp.ParagraphHeading.HEADING1);
  data.items.forEach(i=> body.appendParagraph(`${i.name}: ${i.score}`));
  Logger.log(doc.getUrl());
}

64) Mail-merge to PDFs (Doc template + sheet rows)

function ex64_mailMergeToPdfs() {
  const templateId='PASTE_TEMPLATE_DOC_ID';
  const folderId='PASTE_OUTPUT_FOLDER_ID';
  const sh=SpreadsheetApp.getActiveSheet();
  const rows=sh.getDataRange().getValues();
  const header=rows.shift();

  rows.forEach(r=>{
    const map={}; header.forEach((h,i)=>map[`{{${h}}}`]=String(r[i] ?? ''));
    const copy=DriveApp.getFileById(templateId).makeCopy('Letter - ' + (r[0]||'Recipient'));
    const doc=DocumentApp.openById(copy.getId());
    const body=doc.getBody();
    Object.entries(map).forEach(([k,v])=>body.replaceText(k,v));
    doc.saveAndClose();

    const pdf=DriveApp.getFileById(copy.getId()).getBlob().getAs(MimeType.PDF).setName(copy.getName()+'.pdf');
    DriveApp.getFolderById(folderId).createFile(pdf);
    copy.setTrashed(true);
  });
}

65) Convert a doc to plain text and save to Drive

function ex65_docToTxt() {
  const doc=DocumentApp.getActiveDocument();
  const text=doc.getBody().getText();
  const file=DriveApp.createFile(doc.getName()+'.txt', text, MimeType.PLAIN_TEXT);
  Logger.log(file.getUrl());
}

D) Gmail: Search, Threads, Automation (66–80)

66) Advanced Gmail search + summarize subjects

function ex66_gmailSearch() {
  const threads = GmailApp.search('newer_than:7d in:inbox', 0, 20);
  threads.forEach(t=>Logger.log(t.getFirstMessageSubject()));
}

67) Label messages by rule (e.g., invoices)

function ex67_labelInvoices() {
  const label = GmailApp.getUserLabelByName('Invoices') || GmailApp.createLabel('Invoices');
  const threads = GmailApp.search('subject:(invoice OR receipt) newer_than:30d');
  threads.forEach(t=>label.addToThread(t));
}

68) Auto-archive newsletters (by sender)

function ex68_archiveSender() {
  const threads = GmailApp.search('from:newsletter@example.com in:inbox');
  threads.forEach(t=>t.moveToArchive());
}

69) Send an email with HTML + inline image

function ex69_emailInlineImage() {
  const blob = UrlFetchApp.fetch('https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png').getBlob();
  GmailApp.sendEmail('someone@example.com','HTML Email','Your client does not support HTML.',{
    htmlBody:'<h2>Hello</h2><p>Inline image:</p><img src="cid:logo"/>',
    inlineImages:{ logo: blob }
  });
}

70) Daily digest: send yourself a summary of matching emails

function ex70_dailyDigest() {
  const threads = GmailApp.search('newer_than:1d in:inbox', 0, 50);
  const lines = threads.map(t=>'- ' + t.getFirstMessageSubject()).join('\n');
  GmailApp.sendEmail(Session.getActiveUser().getEmail(), 'Daily Digest', lines || 'No messages found.');
}

71) Save attachments to Drive folder

function ex71_saveAttachments() {
  const folderId='PASTE_FOLDER_ID';
  const folder=DriveApp.getFolderById(folderId);
  const threads=GmailApp.search('has:attachment newer_than:7d',0,20);
  threads.forEach(t=>{
    t.getMessages().forEach(m=>{
      m.getAttachments().forEach(a=>folder.createFile(a));
    });
  });
}

72) Convert CSV attachments to Sheets (Drive API helpful)

function ex72_csvAttachmentToSheet() {
  const threads=GmailApp.search('filename:csv newer_than:7d',0,5);
  if(!threads.length) return;
  const att=threads[0].getMessages()[0].getAttachments().find(a=>a.getContentType().includes('csv'));
  const csv=att.getDataAsString();
  const rows=Utilities.parseCsv(csv);
  const ss=SpreadsheetApp.create('Imported CSV ' + new Date().toISOString());
  ss.getSheets()[0].getRange(1,1,rows.length,rows[0].length).setValues(rows);
  Logger.log(ss.getUrl());
}

73) Auto-reply to specific threads (basic)

function ex73_autoReply() {
  const threads=GmailApp.search('subject:"Request Info" newer_than:1d',0,10);
  threads.forEach(t=>{
    const msg=t.getMessages().pop();
    msg.reply('Thanks! We received your request and will respond soon.');
  });
}

74) Unsubscribe helper: list emails with “unsubscribe” in body

function ex74_findUnsubscribe() {
  const threads=GmailApp.search('newer_than:30d "unsubscribe"',0,50);
  threads.forEach(t=>Logger.log(t.getFirstMessageSubject()));
}

75) Extract sender domains frequency (top 20)

function ex75_senderDomains() {
  const threads=GmailApp.search('newer_than:14d',0,100);
  const freq={};
  threads.forEach(t=>{
    const from=t.getMessages()[0].getFrom();
    const email=(from.match(/<(.+?)>/)?.[1]||from).trim();
    const domain=email.split('@')[1]||'';
    if(domain) freq[domain]=(freq[domain]||0)+1;
  });
  const top=Object.entries(freq).sort((a,b)=>b[1]-a[1]).slice(0,20);
  Logger.log(top);
}

76) Create a “Support” label + apply to unread from specific alias

function ex76_labelSupportAlias() {
  const label=GmailApp.getUserLabelByName('Support')||GmailApp.createLabel('Support');
  GmailApp.search('to:support@yourdomain.com is:unread',0,50).forEach(t=>label.addToThread(t));
}

77) Send email from a draft template (replace tokens)

function ex77_sendFromDraft() {
  const subject='TEMPLATE: Welcome';
  const drafts=GmailApp.getDrafts().filter(d=>d.getMessage().getSubject()===subject);
  if(!drafts.length) throw new Error('Draft not found: ' + subject);
  const msg=drafts[0].getMessage();
  const html=msg.getBody().replace('{{NAME}}','Friend');
  GmailApp.sendEmail('someone@example.com','Welcome!','', { htmlBody: html });
}

78) Clean up old labels (danger: removes labels!)

function ex78_deleteEmptyLabels() {
  const labels=GmailApp.getUserLabels();
  labels.forEach(l=>{
    if(l.getThreads(0,1).length===0 && l.getUnreadCount()===0) {
      // l.deleteLabel(); // uncomment only if you really want it
      Logger.log('Empty label: ' + l.getName());
    }
  });
}

79) Thread summary to Google Doc

function ex79_threadToDoc() {
  const thread=GmailApp.search('newer_than:7d',0,1)[0];
  if(!thread) return;
  const doc=DocumentApp.create('Email Thread Summary');
  const body=doc.getBody();
  thread.getMessages().forEach(m=>{
    body.appendParagraph(m.getFrom()).setHeading(DocumentApp.ParagraphHeading.HEADING2);
    body.appendParagraph(m.getDate().toString());
    body.appendParagraph(m.getPlainBody().slice(0,1000));
    body.appendHorizontalRule();
  });
  Logger.log(doc.getUrl());
}

80) Schedule a daily Gmail cleanup trigger

function ex80_createDailyTrigger() {
  ScriptApp.newTrigger('ex68_archiveSender').timeBased().everyDays(1).atHour(7).create();
}

E) Calendar, Forms, Web Apps, Advanced Services (81–100)

81) Create events from sheet rows

function ex81_sheetToCalendar() {
  const cal=CalendarApp.getDefaultCalendar();
  const sh=SpreadsheetApp.getActiveSheet();
  const rows=sh.getRange(2,1,sh.getLastRow()-1,3).getValues(); // Title, Start, End
  rows.forEach(r=>{
    const [title,start,end]=r;
    if(title && start && end) cal.createEvent(String(title), new Date(start), new Date(end));
  });
}

82) Avoid duplicates: only create event if not already there

function ex82_calendarNoDuplicates() {
  const cal=CalendarApp.getDefaultCalendar();
  const title='Standup';
  const start=new Date(); const end=new Date(start.getTime()+30*60000);
  const existing=cal.getEvents(start,end,{search:title});
  if(existing.length===0) cal.createEvent(title,start,end);
}

83) Add guests + conference (Meet link)

function ex83_eventWithGuests() {
  const cal=CalendarApp.getDefaultCalendar();
  const start=new Date(Date.now()+3600*1000), end=new Date(start.getTime()+3600*1000);
  const ev=cal.createEvent('Planning',start,end,{guests:'a@example.com,b@example.com'});
  ev.addEmailReminder(30);
}

84) Form submit trigger → write to “processed” sheet

function ex84_onFormSubmit(e) {
  const ss=SpreadsheetApp.getActive();
  const sh=ss.getSheetByName('Processed')||ss.insertSheet('Processed');
  sh.appendRow([new Date(), JSON.stringify(e.namedValues)]);
}

85) Web app: JSON API with API key check

function doGet(e) {
  const key = e.parameter.key;
  const expected = PropertiesService.getScriptProperties().getProperty('API_KEY');
  if (!expected || key !== expected) {
    return ContentService.createTextOutput(JSON.stringify({ok:false,error:'unauthorized'}))
      .setMimeType(ContentService.MimeType.JSON);
  }
  return ContentService.createTextOutput(JSON.stringify({ok:true,time:new Date().toISOString()}))
    .setMimeType(ContentService.MimeType.JSON);
}

86) Web app: write POST body to sheet

function doPost(e) {
  const ss=SpreadsheetApp.openById('PASTE_SHEET_ID');
  const sh=ss.getSheetByName('WebAppLog')||ss.insertSheet('WebAppLog');
  sh.appendRow([new Date(), e.postData.type, e.postData.contents]);
  return ContentService.createTextOutput('OK');
}

87) OAuth2 to external APIs (pattern)

Does: Apps Script needs OAuth2 library; this is the setup pattern.

function ex87_oauth2Pattern_note() {
  Logger.log('Use the OAuth2 library: https://github.com/googleworkspace/apps-script-oauth2');
}

88) Use Advanced Drive API to create file in shared drive folder

Needs: Advanced Drive Service (Drive)

function ex88_driveApiCreateInFolder() {
  const folderId='PASTE_FOLDER_ID';
  const resource={ title:'hello.txt', mimeType:'text/plain', parents:[{id:folderId}] };
  const file=Drive.Files.insert(resource, Utilities.newBlob('Hello'));
  Logger.log(file.alternateLink);
}

89) Admin SDK: list users (domain only)

Needs: Advanced Admin Directory Service (AdminDirectory) + admin rights.

function ex89_listUsers() {
  const users = AdminDirectory.Users.list({customer:'my_customer', maxResults: 10}).users || [];
  users.forEach(u=>Logger.log(u.primaryEmail));
}

90) BigQuery job from Apps Script (advanced)

Needs: Advanced BigQuery service enabled.

function ex90_bigqueryQuery() {
  const projectId='PASTE_PROJECT_ID';
  const request={ query:'SELECT CURRENT_TIMESTAMP() AS now', useLegacySql:false };
  const res=BigQuery.Jobs.query(request, projectId);
  Logger.log(JSON.stringify(res.rows));
}

91) Pub/Sub publish (advanced)

Needs: Advanced Pub/Sub service (or UrlFetch with token).

function ex91_pubsubNote() {
  Logger.log('Pub/Sub from Apps Script typically uses UrlFetch + OAuth token; enable API in GCP.');
}

92) Create Slides deck from sheet data

function ex92_slidesFromSheet() {
  const sh=SpreadsheetApp.getActiveSheet();
  const rows=sh.getRange(2,1,Math.max(1,sh.getLastRow()-1),2).getValues(); // title, bullet
  const pres=SlidesApp.create('Auto Deck');
  rows.forEach(r=>{
    const [title,bullet]=r;
    const slide=pres.appendSlide(SlidesApp.PredefinedLayout.TITLE_AND_BODY);
    slide.getPlaceholder(SlidesApp.PlaceholderType.TITLE).asShape().getText().setText(String(title||''));
    slide.getPlaceholder(SlidesApp.PlaceholderType.BODY).asShape().getText().setText('• ' + String(bullet||''));
  });
  pres.getSlides()[0].remove(); // remove default title slide
  Logger.log(pres.getUrl());
}

93) Generate PDF from Slides and email it

function ex93_emailSlidesPdf() {
  const presId='PASTE_SLIDES_ID';
  const file=DriveApp.getFileById(presId);
  const pdf=file.getBlob().getAs(MimeType.PDF).setName(file.getName()+'.pdf');
  GmailApp.sendEmail('someone@example.com','Slides PDF','Attached.',{attachments:[pdf]});
}

94) HTMLService UI + server call (google.script.run)

function ex94_showUi() {
  const html = HtmlService.createHtmlOutput(`
    <button onclick="go()">Run</button>
    <pre id="out"></pre>
    <script>
      function go(){
        google.script.run.withSuccessHandler(r=>out.textContent=r).serverHello();
      }
      const out=document.getElementById('out');
    </script>
  `).setWidth(300).setHeight(200);
  SpreadsheetApp.getUi().showModalDialog(html,'UI Demo');
}
function serverHello(){ return 'Hello from server at ' + new Date().toISOString(); }

95) HTML template file pattern

function ex95_templateHtml() {
  const t = HtmlService.createTemplate('<h2>Hello <?= name ?></h2>');
  t.name = 'Lars';
  SpreadsheetApp.getUi().showSidebar(t.evaluate().setTitle('Template Demo'));
}

96) Robust error wrapper with logging + email alert

function ex96_safeRun() {
  try {
    risky_();
  } catch (err) {
    Logger.log(err.stack || err);
    GmailApp.sendEmail(Session.getActiveUser().getEmail(),'Script error', String(err.stack || err));
    throw err;
  }
}
function risky_(){ throw new Error('Boom'); }

97) Feature flags via PropertiesService

function ex97_featureFlag() {
  const props=PropertiesService.getScriptProperties();
  const enabled = props.getProperty('FEATURE_X') === 'true';
  Logger.log('FEATURE_X enabled? ' + enabled);
}

98) Rate-limited worker (sleep between API calls)

function ex98_rateLimitedCalls() {
  const urls=['https://example.com','https://example.com'];
  urls.forEach((u,i)=>{
    const res=UrlFetchApp.fetch(u,{muteHttpExceptions:true});
    Logger.log(i + ' ' + res.getResponseCode());
    Utilities.sleep(500); // throttle
  });
}

99) Centralized config loader (sheet → object)

function ex99_loadConfigFromSheet() {
  const ss=SpreadsheetApp.getActive();
  const sh=ss.getSheetByName('Config');
  if(!sh) throw new Error('Missing Config sheet');
  const rows=sh.getRange(2,1,sh.getLastRow()-1,2).getValues();
  const cfg={};
  rows.forEach(([k,v])=>{ if(k) cfg[String(k).trim()] = String(v ?? '').trim(); });
  Logger.log(cfg);
  return cfg;
}

100) End-to-end: fetch → store → report → email

Does: Fetches JSON, writes to sheet, generates summary, emails results.

function ex100_pipeline() {
  const ss=SpreadsheetApp.getActive();
  const sh=ss.getSheetByName('API Data')||ss.insertSheet('API Data');
  const json=JSON.parse(UrlFetchApp.fetch('https://httpbin.org/json').getContentText());

  const rows=Object.entries(json).map(([k,v])=>[k, typeof v==='object'?JSON.stringify(v):v]);
  sh.clear();
  sh.getRange(1,1,rows.length,2).setValues(rows);

  const summary = `Loaded ${rows.length} keys into "${sh.getName()}" at ${new Date().toISOString()}`;
  GmailApp.sendEmail(Session.getActiveUser().getEmail(),'Pipeline Complete', summary);
}