$npx -y skills add jezweb/claude-skills --skill google-apps-scriptBuild Google Apps Script automation for Sheets and Workspace. Custom menus, triggers (onEdit / time-driven / form submit), dialogs, sidebars, email batches, PDF export, external API. Use whenever the user wants to automate a Google Sheet, build a Sheets menu / sidebar / dialog, h
| 1 | # Google Apps Script |
| 2 | |
| 3 | Build automation scripts for Google Sheets and Workspace apps. Scripts run server-side on Google's infrastructure with a generous free tier. |
| 4 | |
| 5 | ## What You Produce |
| 6 | |
| 7 | - Apps Script code pasted into Extensions > Apps Script |
| 8 | - Custom menus, dialogs, sidebars |
| 9 | - Automated triggers (on edit, time-driven, form submit) |
| 10 | - Email notifications, PDF exports, API integrations |
| 11 | |
| 12 | ## Workflow |
| 13 | |
| 14 | ### Step 1: Understand the Automation |
| 15 | |
| 16 | Ask what the user wants automated. Common scenarios: |
| 17 | - Custom menu with actions (report generation, data processing) |
| 18 | - Auto-triggered behaviour (on edit, on form submit, scheduled) |
| 19 | - Sidebar app for data entry |
| 20 | - Email notifications from sheet data |
| 21 | - PDF export and distribution |
| 22 | |
| 23 | ### Step 2: Generate the Script |
| 24 | |
| 25 | Follow the structure template below. Every script needs a header comment, configuration constants at top, and `onOpen()` for menu setup. |
| 26 | |
| 27 | ### Step 3: Provide Installation Instructions |
| 28 | |
| 29 | All scripts install the same way: |
| 30 | 1. Open the Google Sheet |
| 31 | 2. **Extensions > Apps Script** |
| 32 | 3. Delete any existing code in the editor |
| 33 | 4. Paste the script |
| 34 | 5. Click **Save** |
| 35 | 6. Close the Apps Script tab |
| 36 | 7. **Reload the spreadsheet** (onOpen runs on page load) |
| 37 | |
| 38 | ### Step 4: First-Time Authorisation |
| 39 | |
| 40 | Each user gets a Google OAuth consent screen on first run. For unverified scripts (most internal scripts), users must click: |
| 41 | |
| 42 | **Advanced > Go to [Project Name] (unsafe) > Allow** |
| 43 | |
| 44 | This is a one-time step per user. Warn users about this in your output. |
| 45 | |
| 46 | --- |
| 47 | |
| 48 | ## Script Structure Template |
| 49 | |
| 50 | Every script should follow this pattern: |
| 51 | |
| 52 | ```javascript |
| 53 | /** |
| 54 | * [Project Name] - [Brief Description] |
| 55 | * |
| 56 | * [What it does, key features] |
| 57 | * |
| 58 | * INSTALL: Extensions > Apps Script > paste this > Save > Reload sheet |
| 59 | */ |
| 60 | |
| 61 | // --- CONFIGURATION --- |
| 62 | const SOME_SETTING = 'value'; |
| 63 | |
| 64 | // --- MENU SETUP --- |
| 65 | function onOpen() { |
| 66 | const ui = SpreadsheetApp.getUi(); |
| 67 | ui.createMenu('My Menu') |
| 68 | .addItem('Do Something', 'myFunction') |
| 69 | .addSeparator() |
| 70 | .addSubMenu(ui.createMenu('More Options') |
| 71 | .addItem('Option A', 'optionA')) |
| 72 | .addToUi(); |
| 73 | } |
| 74 | |
| 75 | // --- FUNCTIONS --- |
| 76 | function myFunction() { |
| 77 | // Implementation |
| 78 | } |
| 79 | ``` |
| 80 | |
| 81 | --- |
| 82 | |
| 83 | ## Critical Rules |
| 84 | |
| 85 | ### Public vs Private Functions |
| 86 | |
| 87 | Functions ending with `_` (underscore) are **private** and CANNOT be called from client-side HTML via `google.script.run`. This is a silent failure -- the call simply doesn't work with no error. |
| 88 | |
| 89 | ```javascript |
| 90 | // WRONG - dialog can't call this, fails silently |
| 91 | function doWork_() { return 'done'; } |
| 92 | |
| 93 | // RIGHT - dialog can call this |
| 94 | function doWork() { return 'done'; } |
| 95 | ``` |
| 96 | |
| 97 | **Also applies to**: Menu item function references must be public function names as strings. |
| 98 | |
| 99 | ### Batch Operations (Critical for Performance) |
| 100 | |
| 101 | Read/write data in bulk, never cell-by-cell. The difference is 70x. |
| 102 | |
| 103 | ```javascript |
| 104 | // SLOW (70 seconds on 100x100) - reads one cell at a time |
| 105 | for (let i = 1; i <= 100; i++) { |
| 106 | const val = sheet.getRange(i, 1).getValue(); |
| 107 | } |
| 108 | |
| 109 | // FAST (1 second) - reads all at once |
| 110 | const allData = sheet.getRange(1, 1, 100, 1).getValues(); |
| 111 | for (const row of allData) { |
| 112 | const val = row[0]; |
| 113 | } |
| 114 | ``` |
| 115 | |
| 116 | Always use `getRange().getValues()` / `setValues()` for bulk reads/writes. |
| 117 | |
| 118 | ### V8 Runtime |
| 119 | |
| 120 | V8 is the **only** runtime (Rhino was removed January 2026). Supports modern JavaScript: `const`, `let`, arrow functions, template literals, destructuring, classes, async/generators. |
| 121 | |
| 122 | **NOT available** (use Apps Script alternatives): |
| 123 | |
| 124 | | Missing API | Apps Script Alternative | |
| 125 | |-------------|------------------------| |
| 126 | | `setTimeout` / `setInterval` | `Utilities.sleep(ms)` (blocking) | |
| 127 | | `fetch` | `UrlFetchApp.fetch()` | |
| 128 | | `FormData` | Build payload manually | |
| 129 | | `URL` | String manipulation | |
| 130 | | `crypto` | `Utilities.computeDigest()` / `Utilities.getUuid()` | |
| 131 | |
| 132 | ### Flush Before Returning |
| 133 | |
| 134 | Call `SpreadsheetApp.flush()` before returning from functions that modify the sheet, especially when called from HTML dialogs. Without it, changes may not be visible when the dialog shows "Done." |
| 135 | |
| 136 | ### Simple vs Installable Triggers |
| 137 | |
| 138 | | Feature | Simple (`onEdit`) | Installable | |
| 139 | |---------|-------------------|-------------| |
| 140 | | Auth required | No | Yes | |
| 141 | | Send email | No | Yes | |
| 142 | | Access other files | No | Yes | |
| 143 | | URL fetch | No | Yes | |
| 144 | | Open dialogs | No | Yes | |
| 145 | | Runs as | Active user | Trigger creator | |
| 146 | |
| 147 | Use simple triggers for lightweight reactions. Use installable triggers (via `ScriptApp.newTrigger()`) when you need email, external APIs, or cross-file access. |
| 148 | |
| 149 | ### Custom Spreadsheet Functions |
| 150 | |
| 151 | Functions used as `=MY_FUNCTION()` in cells have s |