Make decisions with functions and loops
Write Apps Script helper functions that take parameters, return a decision, branch with if, and stop a while loop after a fixed number of passes.
Call helpers with explicit inputs, follow their returned decisions, and explain which variables each function can use and why its loop stops.
Turn a request into a decision
Decide in memory whether a training session fits in a room. You need the Apps Script editor and log from Work with JavaScript values and objects. planTrainingRoom calls helpers to check the request and calculate the table count. Each helper has its own variables, and the table-count loop runs at most three times.
The room has three tables, each seating two people. The request asks for five seats with approved: true. The script checks approval and the seat count, tries one, two, and three tables until it finds enough capacity, and logs the decision.
A function declaration names a block of instructions, and a call executes them. This project defines an editor entrypoint, planTrainingRoom, and two helper functions: decideRoomRequest chooses the decision string, and findTableCount calculates how many tables are needed.
Install the standalone project
Create a standalone project that uses JavaScript values and Logger.
const MAX_TABLES = 3;
/**
* @typedef {{title: string, approved: boolean, seats: number}} RoomRequest_
*/
function planTrainingRoom() {
const request = {
title: 'Training session',
approved: true,
seats: 5,
};
const seatsPerTable = 2;
const decision = decideRoomRequest(request, seatsPerTable);
Logger.log('Request: ' + request.title);
Logger.log('Decision: ' + decision);
Logger.log('Decision again: ' + decideRoomRequest(request, seatsPerTable));
Logger.log('Seat count after planning: ' + request.seats);
}
/**
* @param {RoomRequest_} roomRequest
* @param {number} seatsPerTable
* @returns {string}
*/
function decideRoomRequest(roomRequest, seatsPerTable) {
if (roomRequest.approved !== true) {
return 'Needs approval';
}
if (Number.isInteger(roomRequest.seats) !== true) {
return 'Use a whole-number seat count';
}
if (roomRequest.seats < 1) {
return 'Request at least one seat';
}
const tableCount = findTableCount(roomRequest.seats, seatsPerTable);
if (tableCount === null) {
const capacity = MAX_TABLES * seatsPerTable;
return 'Too large; room capacity: ' + capacity + ' seats';
} else {
return 'Ready; tables needed: ' + tableCount;
}
}
/**
* @param {number} seatCount
* @param {number} seatsPerTable
* @returns {number | null}
*/
function findTableCount(seatCount, seatsPerTable) {
let tableCount = 0;
while (tableCount < MAX_TABLES) {
tableCount = tableCount + 1;
const capacity = tableCount * seatsPerTable;
if (capacity >= seatCount) {
return tableCount;
}
}
return null;
}
The project manifest sets the V8 runtime and UTC time zone:
{
"timeZone": "Etc/UTC",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8"
}
At
script.google.com, click New project and name itRequest decisions.Replace the default
Code.gscontents with the complete example file, includingMAX_TABLESand all three function declarations.In Project Settings, select Show "appsscript.json" manifest file in editor. Return to Editor, replace
appsscript.jsonwith the included manifest, and save both files.Keep
MAX_TABLESat3,seatsPerTableat2,approved: true, andseats: 5for the first run.Select
planTrainingRoomin the function menu, click Run, and open Execution log. Do not run a helper directly, because the editor passes it no arguments.
The manifest selects the V8 runtime, which supports const and let declarations. See Google's V8 guide.
The execution runs as the signed-in user at the keyboard and requires no Google service consent. Google's authorization guide explains which account an editor run uses and how Apps Script detects service scopes from project code.
Recognize the result
Expect these four log messages:
Request: Training session
Decision: Ready; tables needed: 3
Decision again: Ready; tables needed: 3
Seat count after planning: 5 Two tables hold four people, so five seats require all three tables. Calling decideRoomRequest twice with the same input returns the same answer. The helper does not change request, so request.seats stays at 5.
Pass arguments into named parameters
The declaration function decideRoomRequest(roomRequest, seatsPerTable) defines a function with two parameters, the local variable names for the values it receives. The braces enclose the function body, which runs when the function is called.
In decideRoomRequest(request, seatsPerTable), the expressions inside the parentheses are arguments: the values supplied for this call. Arguments match parameters by position, not by variable name. The first argument is assigned to roomRequest and the second to seatsPerTable. MDN's functions guide describes function declarations and calls.
The caller names its variable request and the helper names its parameter roomRequest, but both refer to the same object during the call. Changing roomRequest.seats would change the caller's object as well, so decideRoomRequest only reads its properties. The second argument (2) is a primitive number. Reassigning seatsPerTable inside the helper would not affect the caller's variable.
The next call, findTableCount(roomRequest.seats, seatsPerTable), passes two numbers, because findTableCount needs only the requested seat count and the seats per table.
planTrainingRoom() takes no parameters, so the editor can run it directly. It calls decideRoomRequest and findTableCount even though their declarations appear lower in Code.gs. MDN explains this behavior under function hoisting. Variables declared with const or let are not hoisted in the same way and must be declared before they are read.
Return a result to the caller
A return value is the result a function call sends back to its caller. When findTableCount reaches return tableCount, the call evaluates to a number. decideRoomRequest stores that number in its local tableCount, formats a decision string, and returns it. planTrainingRoom stores that string in decision.
planTrainingRoom supplies the request and 2 seats per table
decideRoomRequest checks approval and the requested 5 seats
findTableCount returns the number 3
decideRoomRequest returns "Ready; tables needed: 3"
planTrainingRoom stores that string in decision and logs it return exits the current function immediately and hands control back to the caller. Inside the loop in findTableCount, return tableCount ends findTableCount while decideRoomRequest continues running. See MDN's return reference.
Logger.log() writes text to the execution log. It does not return that text to a caller. A function that reaches its closing brace without a return statement returns undefined, so replacing a helper's return with Logger.log() breaks any caller that uses its result.
Keep the returned expression on the same line as return. A newline immediately after return triggers automatic semicolon insertion and returns undefined.
Choose a branch before doing more work
Control flow is the order in which statements execute. An if statement evaluates the condition in parentheses and runs its block when the condition is true. An optional else block runs when the condition is false. See MDN's control-flow guide.
The first check, roomRequest.approved !== true, tests whether approved is anything other than boolean true. When that condition is true, the helper exits immediately with Needs approval. This early return rejects false, undefined, and the string 'true' before running any seat math. A check that returns early like this is called a guard.
The next two checks validate the seat count. Number.isInteger(roomRequest.seats) returns true for whole numbers and false for strings such as '5', fractions, NaN, and Infinity. If the check fails, the helper returns Use a whole-number seat count.
If the value is an integer, roomRequest.seats < 1 checks for zero and negative numbers. For those values, the helper returns Request at least one seat.
Only a request that passes all three checks reaches findTableCount. Each guard returns from the function when its condition is met. As a result, the helper returns Needs approval for an unapproved request even if its seat count is invalid, and none of the guards require else blocks.
After findTableCount runs, tableCount === null means no table count within MAX_TABLES can seat the group, so that branch returns Too large; room capacity: 6 seats. Otherwise, the else branch returns the Ready decision with the required table count.
Make sure the loop ends
A loop repeats a block of statements. findTableCount uses a while loop, which tests tableCount < MAX_TABLES before each iteration (MDN while guide).
The counter starts at 0. Each pass assigns tableCount + 1 to tableCount, so the counter takes the values 1, 2, and 3. The pass then stores tableCount * seatsPerTable in capacity and checks whether capacity >= seatCount.
For the five-seat request:
| Table count after the increment | Capacity at two seats per table | Is capacity at least five? | Next action |
|---|---|---|---|
| 1 | 2 | No | Try the next count. |
| 2 | 4 | No | Try the next count. |
| 3 | 6 | Yes | Return 3 from the helper. |
Because tableCount increases by 1 on each pass, the first count that fits is the smallest valid table count. A two-seat request returns 1 on the first pass.
If capacity >= seatCount, the function returns tableCount immediately. If no pass finds enough capacity, tableCount reaches 3, the condition 3 < 3 is false, and the function reaches return null. With MAX_TABLES = 3, each call executes the loop body at most three times regardless of the requested seat count.
Every while loop must update state toward its exit condition. Writing tableCount + 1 without assigning the result back to tableCount leaves the counter at 0, so the loop repeats until Apps Script terminates the run.
Know which names each function can use
Scope determines where code can use a variable name. It depends on where the variable is declared. Lifetime is how long the variable exists while the script runs.
MAX_TABLES is declared at the top level outside any function, so it has global scope and both helpers can read it. Apps Script loads all server .gs files in a project into a single shared global scope (V8 module limitations), so top-level identifiers must be unique across files.
request, seatsPerTable, and decision inside planTrainingRoom are local to that function. decideRoomRequest cannot read request by name. It receives the object through its roomRequest parameter. See MDN's function-scope guide.
Both findTableCount and decideRoomRequest declare a local variable named tableCount. Those are independent variables in separate scopes. findTableCount increments its let tableCount during the loop and returns the resulting number, which decideRoomRequest stores in its own const tableCount.
A block is a section of code enclosed in braces, such as a loop body or an if branch. Variables declared with const or let inside a block are scoped to that block (MDN let reference). The two variables in findTableCount have different scopes:
const capacityis inside thewhileblock. It exists only during that pass and is unavailable after the loop.let tableCount = 0is before the loop. Its value persists between passes. Declaring it inside the loop body would reset it to0on every iteration.
Local variables disappear when their function call returns, and top-level variables reinitialize on every script execution. To keep data from one execution to the next, you need a storage service such as the Properties service.
Separate calculations from effects
A pure helper computes its return value from its inputs without mutating external state or calling external services. Modifying the local counter tableCount inside findTableCount does not affect purity because that variable is created fresh for each call. Both helpers also leave the passed roomRequest object unchanged.
planTrainingRoom writes to the execution log with Logger.log(). That write is a side effect. Keeping decision logic in pure helpers and logging in the entrypoint lets you call decideRoomRequest multiple times or across an array of records without producing extra log output on each call.
Request more seats and predict the result
Change
seats: 5toseats: 7inrequest. Leaveapproved: true,seatsPerTable = 2,MAX_TABLES = 3, and the helpers unchanged.Trace the loop before running: capacities
2,4, and6are all less than7, so the loop finishes three passes andfindTableCountreturnsnull.Save and run
planTrainingRoom. Expect:
Request: Training session
Decision: Too large; room capacity: 6 seats
Decision again: Too large; room capacity: 6 seats
Seat count after planning: 7 Restore
seats: 5, save, and rerun to confirm theReady; tables needed: 3output.
To test the guard order, set seats to 7 and approved to false. Both decision lines log Needs approval because the approval guard returns before findTableCount is called. Restore approved: true and seats: 5 afterward.
Recover from an editing mistake and rerun
A returned string such as Needs approval or Use a whole-number seat count is an ordinary return value. The run finishes without an error. For an approved request, seats: '5' returns Use a whole-number seat count and seats: 0 returns Request at least one seat.
Running decideRoomRequest directly from the editor toolbar passes undefined for roomRequest and fails when reading roomRequest.approved. Select planTrainingRoom instead. Calling a misspelled helper name or reading a variable outside its scope stops the run with ReferenceError: X is not defined. If a logged decision shows undefined after an edit, verify that the helper executes a return statement with its expression on the same line.
Continue to Work with JavaScript arrays to apply helper functions to collections of records.
Project files
Complete source files for this chapter's examples.
Plan a training room with functions
Code.gs
Download fileconst MAX_TABLES = 3;
/**
* @typedef {{title: string, approved: boolean, seats: number}} RoomRequest_
*/
function planTrainingRoom() {
const request = {
title: 'Training session',
approved: true,
seats: 5,
};
const seatsPerTable = 2;
const decision = decideRoomRequest(request, seatsPerTable);
Logger.log('Request: ' + request.title);
Logger.log('Decision: ' + decision);
Logger.log('Decision again: ' + decideRoomRequest(request, seatsPerTable));
Logger.log('Seat count after planning: ' + request.seats);
}
/**
* @param {RoomRequest_} roomRequest
* @param {number} seatsPerTable
* @returns {string}
*/
function decideRoomRequest(roomRequest, seatsPerTable) {
if (roomRequest.approved !== true) {
return 'Needs approval';
}
if (Number.isInteger(roomRequest.seats) !== true) {
return 'Use a whole-number seat count';
}
if (roomRequest.seats < 1) {
return 'Request at least one seat';
}
const tableCount = findTableCount(roomRequest.seats, seatsPerTable);
if (tableCount === null) {
const capacity = MAX_TABLES * seatsPerTable;
return 'Too large; room capacity: ' + capacity + ' seats';
} else {
return 'Ready; tables needed: ' + tableCount;
}
}
/**
* @param {number} seatCount
* @param {number} seatsPerTable
* @returns {number | null}
*/
function findTableCount(seatCount, seatsPerTable) {
let tableCount = 0;
while (tableCount < MAX_TABLES) {
tableCount = tableCount + 1;
const capacity = tableCount * seatsPerTable;
if (capacity >= seatCount) {
return tableCount;
}
}
return null;
}
appsscript.json
Download file{
"timeZone": "Etc/UTC",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8"
}