Understand the runtime and permissions
Separate JavaScript execution, project context, Apps Script services, manifest configuration, authorization scopes, and execution identity.
Explain why a project can calculate locally yet lack a document or permission, and change one harmless manifest setting without broadening access.
Explain what a run is allowed to do
The Customer arrays project counts records in memory without requesting spreadsheet access, while the first Sheets project requires both a bound spreadsheet context and user authorization. Both run JavaScript on the V8 runtime, but runtime environment, document context, service enablement, manifest configuration, OAuth scopes, and execution identity are separate boundaries.
Use those two existing projects to compare each boundary, then change and restore the timeZone field in Customer arrays.
The editor is in a browser; the script runs on Google's servers
A runtime is the environment that executes code and provides global objects. Apps Script's V8 runtime executes .gs files on Google's servers rather than in the browser tab showing the editor.
Standard JavaScript built-ins such as Array, String, and Math come from the V8 engine. Host globals such as Logger and SpreadsheetApp are provided by Apps Script; see Google's built-in-services guide.
Different JavaScript environments expose different host APIs:
| Code environment | Objects it can use | What that means here |
|---|---|---|
Apps Script server code in .gs files | JavaScript built-ins and Apps Script services | summarizeActiveCustomers can use array methods and Logger.log. |
| JavaScript inside a browser page | Browser objects such as window and document | Browser globals do not exist in .gs server files. |
| Node.js on a computer or server | Node APIs such as process and file-system modules | Node built-in modules cannot be imported into Code.gs. |
Apps Script server code has no browser DOM (window, document), browser fetch, Node require, or ES module import/export. To make HTTP requests from .gs code, use UrlFetchApp. See Google's V8 runtime limitations.
V8 is the standard Apps Script runtime. Older manifests may specify "runtimeVersion": "STABLE" or "DEPRECATED_ES5", which refer to the legacy Rhino runtime (manifest reference). Google deprecated Rhino on January 31, 2026; if you inherit a Rhino script, follow the V8 migration guide and test it on a copy. Every example in this book uses "runtimeVersion": "V8".
When an Apps Script project later serves an HTML dialog, sidebar, or web app, the browser JavaScript inside that HTML runs in the user's browser while .gs files run on Google's servers. Neither environment shares the other's global objects.
A bound project has a parent document
A bound project is attached to a specific spreadsheet, document, presentation, or form and is opened from that file via Extensions > Apps Script. A standalone project, such as Customer arrays, lives as its own file in Drive and has no parent document. Both project types can call Apps Script services; see the bound-project guide and standalone-project guide.
Project shape determines how code locates a file. In a bound spreadsheet script run from the editor, SpreadsheetApp.getActiveSpreadsheet() returns the parent spreadsheet. In a standalone project, getActiveSpreadsheet() returns null even if you have a spreadsheet open in another browser tab.
Compare the read-only Sheets script from Run your first Sheets script:
function readAllCustomerRows() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Customers');
if (!sheet) throw new Error('Add a Sheet tab named Customers before running this function.');
const values = sheet.getDataRange().getValues();
Logger.log(values);
}
Its first line chains SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Customers'). If getActiveSpreadsheet() returns null in a standalone project, calling getSheetByName throws a TypeError before the missing-tab check runs. To open a spreadsheet from a standalone project, pass a file ID to SpreadsheetApp.openById() instead.
Even in a bound project, active-document and UI methods depend on how the script starts: Google's bound-script guide notes that getActiveSpreadsheet() and UI methods are unavailable when the project runs as a web app or via the Apps Script API.
Choose a service for the required operation
SpreadsheetApp is the built-in service for Google Sheets. Built-in services are exposed as globals without imports or Services configuration. On a Range object, getValues() reads cell values into a JavaScript array, whereas setValues() overwrites cells in the sheet.
An advanced Google service is a thin wrapper around a public Google REST API: for example, the advanced Sheets service wraps the Google Sheets API v4, whereas SpreadsheetApp is the built-in service. When a task requires an advanced service, enable it under Services in the editor, which records its identifier, service ID, and version under dependencies.enabledAdvancedServices in appsscript.json. For default Google Cloud projects, Apps Script enables the underlying Cloud API automatically; for standard Google Cloud projects, you must enable the API in the Cloud console yourself (advanced-services guide).
Read configuration separately from code
Every Apps Script project has an appsscript.json manifest that stores project settings (manifest guide). Reveal it in Project Settings by checking Show "appsscript.json" manifest file in editor. Because appsscript.json is strict JSON, keys and strings require double quotes, and comments or trailing commas prevent the file from saving.
Inspect the Customer arrays project alongside the first Sheets project:
function summarizeActiveCustomers() {
const customerRows = [
['Name', 'Email', 'Status'],
['Avery Quinn', 'avery@example.com', 'Active'],
['Morgan Lee', 'morgan@example.com', 'Paused'],
['Riley Park', 'riley@example.com', 'Active']
];
const headers = customerRows[0];
const nameIndex = headers.indexOf('Name');
const emailIndex = headers.indexOf('Email');
const statusIndex = headers.indexOf('Status');
if (statusIndex === -1) {
throw new Error('Add a Status header before running this function.');
}
if (nameIndex === -1) {
throw new Error('Add a Name header before running this function.');
}
if (emailIndex === -1) {
throw new Error('Add an Email header before running this function.');
}
const dataRows = customerRows.slice(1);
const customers = dataRows.map((row) => {
return {
Name: row[nameIndex],
Email: row[emailIndex],
Status: row[statusIndex]
};
});
const activeCustomers = customers.filter(isActiveCustomer);
const activeNames = activeCustomers.map((customer) => String(customer.Name));
Logger.log('Active customers: ' + activeNames.length);
Logger.log(activeNames.join(', '));
}
/**
* @param {{Name: string, Email: string, Status: string}} customer
* @returns {boolean}
*/
function isActiveCustomer(customer) {
return customer.Status === 'Active';
}
Compare the two manifests:
{
"timeZone": "Etc/UTC",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/spreadsheets.currentonly"
]
}
{
"timeZone": "Etc/UTC",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8"
}
Compare the fields in their appsscript.json manifests (manifest structure reference):
| Field | Meaning in these projects |
|---|---|
runtimeVersion: "V8" | Selects the V8 JavaScript runtime. |
timeZone: "Etc/UTC" | Sets the project time zone used by Session.getScriptTimeZone() and time-driven triggers. |
dependencies: {} | Declares enabled advanced services and libraries. Built-in services do not appear here. |
exceptionLogging: "STACKDRIVER" | Sends uncaught exception traces to Cloud Logging. |
oauthScopes | Present in the Sheets project; lists the exact OAuth scopes the project requests. |
A scope request is not a permission grant
An OAuth scope identifies a category of Google service access. When oauthScopes is omitted from appsscript.json, Apps Script scans all code files in the project, not only the function selected in the toolbar, and infers scopes from the service calls it finds. When oauthScopes is present in appsscript.json, Apps Script requests only that explicit list, so you must update oauthScopes whenever you add service calls that require new permissions (scope guide).
The first Sheets project declares https://www.googleapis.com/auth/spreadsheets.currentonly. That scope restricts access to the bound spreadsheet rather than all spreadsheets in Drive, but it is not read-only: both getValues() and setValues() use spreadsheets.currentonly. The first script is read-only because its code calls getValues() and never calls a write method.
Consent is the user's approval of the requested scopes on the OAuth prompt (authorization guide). OAuth consent also does not override Google Drive file permissions: the executing user must have view or edit access to the target file for a service call to succeed.
Expect consent to some scopes and not others
Google's consent screen can list each requested scope with its own checkbox, allowing a user to approve some scopes and leave others unchecked (granular OAuth consent). Your script receives only the approved scopes, and any later call requiring an unapproved scope throws an authorization error mid-run.
ScriptApp provides two methods to verify consent before running any steps:
ScriptApp.requireScopes(authMode, scopes)checks the listed scopes and halts execution with an authorization prompt if any are missing.ScriptApp.requireAllScopes(authMode)checks every scope declared or inferred for the project.
Call the check at the start of an entrypoint before reading or writing data:
function updateCustomerStatuses() {
ScriptApp.requireScopes(ScriptApp.AuthMode.FULL, [
'https://www.googleapis.com/auth/spreadsheets.currentonly',
]);
// The spreadsheet work starts here, after consent is confirmed.
} To inspect consent without halting immediately, call ScriptApp.getAuthorizationInfo(authMode, scopes). Its getAuthorizationStatus() reports whether additional consent is needed, getAuthorizedScopes() returns the approved scope URLs, and getAuthorizationUrl() returns a URL the user can open to grant missing scopes (ScriptApp reference).
Because an installable trigger runs in the background with no user at the keyboard, it cannot display a consent prompt and fails immediately if a required scope is missing. Call requireScopes inside the function that installs the trigger so the user grants consent during setup.
Name the user behind the execution
The execution identity is the account whose permissions a run uses. When you click Run in the editor for a bound or standalone script, the code executes as your signed-in account.
Do not confuse the last person who edited Code.gs with the execution identity: an installable trigger always runs as the user who created the trigger, and a web app runs as either the visitor or the script owner depending on its deployment configuration (authorization identity table).
Inspect, change and restore one manifest field
Use the standalone Customer arrays project to test a manifest change:
In
Customer arrays, verify thatsummarizeActiveCustomersis selected andappsscript.jsonhas"runtimeVersion": "V8","timeZone": "Etc/UTC","dependencies": {}, and nooauthScopesfield.Run
summarizeActiveCustomersand confirm the log showsActive customers: 2andAvery Quinn, Riley Parkwithout prompting for OAuth consent.In
appsscript.json, change"timeZone"from"Etc/UTC"to"America/New_York"and save.Run
summarizeActiveCustomersagain. Because the function compares static strings in memory and never reads the project time zone, the two logged lines remain identical.Restore
"timeZone"to"Etc/UTC", save, and rerun to confirm the baseline.
Fix the boundary that actually failed
Match each error to the boundary that produced it:
Manifest will not save: Check
appsscript.jsonfor missing double quotes or trailing commas.TypeError: Cannot call method "getSheetByName" of null:getActiveSpreadsheet()returnednullbecause the code ran outside a bound spreadsheet context. Open the bound project from the spreadsheet via Extensions > Apps Script.ReferenceError: window is not defined(ordocument,require,fetch): The code uses browser or Node globals that do not exist in the V8 server runtime.Authorization or permission error: Check the project's
oauthScopes, the executing user's granted consent, and that user's Drive sharing permissions on the target file (authorization troubleshooting).
Continue to Read and write Sheets in batches to read a table once, compute updates in JavaScript arrays, and write the updated column back in a single call.
Project files
Complete source files for this chapter’s examples.
Read all customer rows
Code.gs
Download filefunction readAllCustomerRows() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Customers');
if (!sheet) throw new Error('Add a Sheet tab named Customers before running this function.');
const values = sheet.getDataRange().getValues();
Logger.log(values);
}
appsscript.json
Download file{
"timeZone": "Etc/UTC",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/spreadsheets.currentonly"
]
}
sample.csv
Download fileName,Email,Status
Avery Quinn,avery@example.com,Active
Morgan Lee,morgan@example.com,Paused
Riley Park,riley@example.com,Active
Filter active customers in a two-dimensional array
Code.gs
Download filefunction summarizeActiveCustomers() {
const customerRows = [
['Name', 'Email', 'Status'],
['Avery Quinn', 'avery@example.com', 'Active'],
['Morgan Lee', 'morgan@example.com', 'Paused'],
['Riley Park', 'riley@example.com', 'Active']
];
const headers = customerRows[0];
const nameIndex = headers.indexOf('Name');
const emailIndex = headers.indexOf('Email');
const statusIndex = headers.indexOf('Status');
if (statusIndex === -1) {
throw new Error('Add a Status header before running this function.');
}
if (nameIndex === -1) {
throw new Error('Add a Name header before running this function.');
}
if (emailIndex === -1) {
throw new Error('Add an Email header before running this function.');
}
const dataRows = customerRows.slice(1);
const customers = dataRows.map((row) => {
return {
Name: row[nameIndex],
Email: row[emailIndex],
Status: row[statusIndex]
};
});
const activeCustomers = customers.filter(isActiveCustomer);
const activeNames = activeCustomers.map((customer) => String(customer.Name));
Logger.log('Active customers: ' + activeNames.length);
Logger.log(activeNames.join(', '));
}
/**
* @param {{Name: string, Email: string, Status: string}} customer
* @returns {boolean}
*/
function isActiveCustomer(customer) {
return customer.Status === 'Active';
}
appsscript.json
Download file{
"timeZone": "Etc/UTC",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8"
}