Agent skills, system prompts, and AI developer rules for googleworkspace/apps-script-samples
# Apps Script Sample Development Guide
This guide outlines best practices for developing Google Apps Script projects, focusing on type safety and modern JavaScript features.
## Important
* For new sample directories, ensure the top-level folder is included in the [`test.yaml`](.github/workflows/test.yaml) GitHub workflow's matrix configuration.
* Do not move or delete snippet tags: `[END apps_script_... ]` or `[END apps_script_... ]`.
* Keep code within snippet tags self-contained. Avoid depending on helper functions defined outside the snippet tags if the snippet is intended to be copied and pasted.
* Avoid function name collisions (e.g., multiple `onOpen` or `main` functions) by placing separate samples in their own directories or files. Do not append suffixes like `_2`, `_3` to function names. For variables, replace collisions with a more descriptive name.
## Tools
Lint and format code using [Biome](https://biomejs.dev/).
```bash
pnpm lint
pnpm format
```
## Apps Script Code Best Practices
Apps Script supports the V8 runtime, which enables modern ECMAScript syntax. Using these features makes your code cleaner, more readable, and less error-prone.
### `let` and `const`
Use `let` and `const` instead of `var` for block-scoped variables.
* **`const`**: Use for values that should not be reassigned.
* **`let`**: Use for values that will change.
```javascript
const PI = 3.14;
let count = 0;
if (true) {
let local = "I exist only in this block";
}
// local is not accessible here
```
### Arrow Functions
Use arrow functions for concise function expressions, especially for callbacks.
```javascript
const numbers = [1, 2, 3];
const squares = numbers.map(x => x * x); // [1, 4, 9]
```
### Destructuring
Unpack values from arrays or properties from objects into distinct variables.
```javascript
const user = { name: "Alice", age: 30 };
const { name, age } = user;
const coords = [10, 20];
const [x, y] = coords;
```
### Template Literals
Use template literals for string interpolation and multi-line strings.
```javascript
const name = "World";
const greeting = `Hello, ${name}!`;
const multiLine = `
This is a
multi-line string.
`;
```
### Default Parameters
Specify default values for function parameters.
```javascript
function greet(name = "Guest") {
console.log(`Hello, ${name}!`);
}
greet(); // "Hello, Guest!"
```
### Prefer `for...of` for Iteration
While `forEach` is convenient, `for...of` loops generally offer better performance and more control (e.g., `break`, `continue`) in Apps Script, especially when dealing with large arrays.
```javascript
const numbers = [1, 2, 3];
// Using forEach (less performant for large arrays)
numbers.forEach(num => {
console.log(num);
});
// Using for...of (preferred)
for (const num of numbers) {
console.log(num);
}
```
## Apps Script V8 Runtime
It's important to understand that the Apps Script V8 runtime is
not a standard Node.js or browser environment. This can lead to compatibility
issues when incorporating third-party libraries or adapting code examples
from other JavaScript environments.
### Unavailable APIs
The following standard JavaScript APIs are **NOT** available in the
Apps Script V8 runtime:
* **Timers**: `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval`
* **Streams**: `ReadableStream`, `WritableStream`, `TextEncoder`,
`TextDecoder`
* **Web APIs**: `fetch`, `FormData`, `File`, `Blob`, `URL`, `URLSearchParams`,
`DOMException`, `atob`, `btoa`
* **Crypto**: `crypto`, `SubtleCrypto`
* **Global Objects**: `window`, `navigator`, `performance`, `process`
(Node.js)
Instead of the unavailable APIs, you can use the following
Apps Script APIs as alternatives:
* **Timers**: Use
[`Utilities.sleep(milliseconds)`](https://developers.google.com/apps-script/reference/utilities/utilities#sleepmilliseconds)
for synchronous pauses. Asynchronous timers are not supported.
* **Fetch**: Use [`UrlFetchApp.fetch(url,
params)`](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app) to make HTTP(S)
requests.
* **atob**: Use
[`Utilities.base64Decode()`](https://developers.google.com/apps-script/reference/utilities/utilities#base64decodeencoded)
to decode Base64-encoded strings.
* **btoa**: Use
[`Utilities.base64Encode()`](https://developers.google.com/apps-script/reference/utilities/utilities#base64encodedata)
to encode strings in Base64.
* **Crypto**: Use [`Utilities`](https://developers.google.com/apps-script/reference/utilities/utilities)
for cryptographic functions like
[`computeDigest()`](https://developers.google.com/apps-script/reference/utilities/utilities#computedigestalgorithm,-value),
[`computeHmacSha256Signature()`](https://developers.google.com/apps-script/reference/utilities/utilities#computehmacsha256signaturevalue,-key),
and
[`computeRsaSha256Signature()`](https://developers.google.com/apps-script/reference/utilities/utilities#computersasha256signaturevalue,-key).
For some APIs, other workarounds might exist. For example, you might be able to
use a polyfill for `TextEncoder`.
### Asynchronous Limitations
The V8 runtime supports `async` and `await` syntax and the `Promise` object.
However, the Apps Script runtime environment is fundamentally
synchronous.
* **Microtasks (Supported)**: The runtime processes the microtask queue (where
`Promise.then()` callbacks and `await` resolutions occur) after the current
call stack clears.
* **Macrotasks (Not Supported)**: Apps Script does not have a
standard event loop for macrotasks. Functions like `setTimeout()` and
`setInterval()` are not available.
* **WebAssembly Exception**: The WebAssembly API is the only built-in
feature that operates in a non-blocking manner within the runtime, allowing
for specific asynchronous compilation patterns (WebAssembly.instantiate).
All I/O operations, such as
[`UrlFetchApp.fetch()`](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app), are
blocking. To achieve parallel network requests, use
[`UrlFetchApp.fetchAll()`](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app#fetchallrequests).
### Class Limitations
The V8 runtime has specific limitations regarding modern ES6+ class features:
* **Private Fields**: Private class fields (for example, `#field`) are not
supported and cause parsing errors. Consider using closures or `WeakMap` for
true encapsulation.
* **Static Fields**: Direct static field declarations within the class body
(for example, `static count = 0;`) are not supported. Assign static
properties to the class after its definition (for example, `MyClass.count =
0;`).
### Module Limitations
* **ES6 Modules**: The V8 runtime does not support ES6 modules (`import` /
`export`). To use libraries, you must either use the [
Apps Script library mechanism](https://developers.google.com/apps-script/guides/libraries)
or bundle your code and its dependencies into a single script file. ([Issue
Tracker](https://issuetracker.google.com/issues/134627726))
* **File Execution Order**: All script files in your project are executed in a
global scope. It's best to avoid top-level code with side effects and ensure
functions and classes are defined before being used across files. Explicitly
order your files in the editor if dependencies exist between them.
## Type Checking with JSDoc
This project uses a type checker to validate `.gs` files for errors. Since `.gs` files are technically JavaScript, we use JSDoc comments to provide type information. This ensures your code is type-safe and well-documented.
### Running Checks
You can run the type checker from the root of the repository.
**Check all projects:**
```bash
pnpm run check
```
**Check a specific path:**
To check only projects within a specific directory (e.g., `solutions/automations`), pass the path as an argument:
```bash
pnpm run check solutions/automations
```
### Core Concepts
#### 1. Basic Types
Use `@param` and `@return` to define function inputs and outputs.
```javascript
/**
* Adds two numbers.
* @param {number} a The first number.
* @param {number} b The second number.
* @return {number} The sum.
*/
function add(a, b) {
return a + b;
}
```
#### 2. Apps Script Types
You can reference global Apps Script types directly.
```javascript
/**
* Gets the active sheet name.
* @return {string} The name of the sheet.
*/
function getSheetName() {
// Types like SpreadsheetApp, Sheet, Range are available globally
const sheet = SpreadsheetApp.getActiveSheet();
return sheet.getName();
}
```
#### 3. Optional Parameters
Use `[]` or `=` to denote optional parameters.
```javascript
/**
* @param {string} name The name.
* @param {number=} age Optional age.
*/
function greet(name, age) {
if (age) { ... }
}
```
### Advanced Patterns
#### 1. Custom Objects (@typedef)
For complex objects, define a type using `@typedef`.
```javascript
/**
* @typedef {Object} UserConfig
* @property {string} username The user's name.
* @property {boolean} isAdmin Whether the user is an admin.
* @property {number} [retryCount] Optional retry attempts.
*/
/**
* Processes a user configuration.
* @param {UserConfig} config The configuration object.
*/
function processUser(config) {
console.log(config.username);
}
```
#### 2. Type Casting
Sometimes the type checker cannot infer the type correctly. Use inline `@type` to cast.
```javascript
const data = JSON.parse(jsonString);
/** @type {UserConfig} */
const config = data;
```
#### 3. Arrays and Generics
Specify array contents clearly.
```javascript
/**
* @param {string[]} names An array of strings.
* @return {Array<number>} An array of numbers.
*/
function lengths(names) {
return names.map(n => n.length);
}
```
#### 4. Handling `null` and `undefined`
Be explicit if a value can be null.
```javascript
/**
* @param {string|null} id The ID, or null if not found.
*/
function find(id) { ... }
```
### Common Issues & Fixes
- **TypeScript**: DO NOT REFERENCE GoogleAppsScript in JSDocs. Instead use a locally defined type definition and link to the appropriate reference documenation page if possible.
- **"Property 'x' does not exist on type 'Object'"**: This usually means you are accessing a property on a generic object. Define a `@typedef` for that object structure.
- **Implicit 'any'**: If you see "Parameter 'x' implicitly has an 'any' type", it means you forgot a JSDoc `@param` tag. Add it to fix the error.
- **Advanced Services**: To fix errors with these globals, check for existence. This helps TypeScript narrow the type and prevents runtime errors if the service is not enabled.
```js
if (!AdminDirectory) {
console.log('AdminDirectory Advanced Service must be enabled.');
return;
}
```
- **Optional Properties**: Use optional chaining (`?.`) when accessing properties that might be undefined in API responses. This is often the case when when using `fields` to limit the response.
```js
// Safe access
console.log(user.name?.fullName);
```
- **Error Handling**: Avoid wrapping code in `try/catch` blocks if you are only logging the error message. Let the runtime handle the error reporting for cleaner sample code.
```js
// Avoid this
try {
AdminDirectory.Users.list();
} catch (err) {
console.log(err.message);
}
// Prefer this
AdminDirectory.Users.list();
```