A searchable companion to all 50 interactive lessons. Every lesson includes an explanation, a coding task, a worked solution and a real-project transfer activity.
01 · Your very first script
1. What is Google Apps Script?
Apps Script combines JavaScript with services for Google Workspace.
An automation is a set of instructions a computer repeats. Apps Script lets you write those instructions in a browser editor and run them on Google’s servers. It can work with spreadsheets, documents and other services when authorized. The browser editor is where you write; it is not where your server-side script executes. This trainer runs ordinary JavaScript locally and models a small set of services, so you can begin without an account. A function is a named set of instructions. The word return gives a result back to the caller. You will make one function return a short text value; no Google data is needed.
Try it
Change welcome() so it returns the exact text Hello, Apps Script! Keep the quotation marks.
Find the quoted text after return.
Replace only the text, then select Run to see the console.
Run checks and compare the exact greeting.
Common trap
Leaving out the quotes makes JavaScript look for a variable instead of a text value.
Reveal a worked solution
function welcome() { return "Hello, Apps Script!"; }
Practice in Google
Open the Start here walkthrough. No Google account is required for this browser exercise.
Writing a function and calling it are separate actions.
In a disposable Google Sheet, choose Extensions → Apps Script to create a bound project. Replace the sample code with a small function, save, choose the function in the toolbar and select Run. A first run may request authorization if the code uses services that need it. Read the requested access; your organization may restrict it. The execution log shows messages from console.log. Defining a function stores instructions; adding parentheses calls it. Here a wrapper logs the result of firstRun. Tests call the function directly.
Try it
Return Practice started from firstRun(). The starter already calls and logs it.
Locate the empty string.
Enter the requested message inside the quotes.
Run, inspect the console, then run the checks.
Common trap
The function name in the editor must match the function you intend to run.
Reveal a worked solution
function firstRun(){return "Practice started";}
Practice in Google
Use resources/start-here.html for the exact first-run steps and a downloadable .gs file.
Braces contain instructions; return supplies the result.
function introduces a declaration. A name such as getLabel identifies it. Parentheses hold parameters, which will come later. Curly braces enclose the body. Each statement performs one step. A semicolon ends a statement explicitly. Keep pairs of braces and quotes balanced. Code after a return in the same path does not execute. Indentation is for readability; it makes the structure easier to see but does not replace braces.
Try it
Complete getLabel() so its returned value is Ready.
Write return inside the braces.
Add the quoted text and a semicolon.
Use the Syntax Builder lab to arrange the same parts.
Common trap
A missing closing quote or brace prevents the code from parsing.
Reveal a worked solution
function getLabel(){return "Ready";}
Practice in Google
In your Google project, add this function and a separate wrapper that logs getLabel().
Use const for a binding you will not reassign. Use let when you intend to replace that binding’s value. Both names are scoped to their block. Names are case-sensitive: total and Total differ. const does not make an object deeply immutable; that distinction becomes useful with arrays. In this lesson, store a fixed hourly rate and calculate a total. Arithmetic uses numbers, not quoted numeric text.
Try it
In fixedTotal(), set rate to 20 and hours to 3, then return their product.
Change the rate to the number 20.
Read the expression after return.
Predict the product before checking.
Common trap
"20" is text; 20 is a number. Do not depend on accidental type conversion.
Reveal a worked solution
function fixedTotal(){const rate=20;const hours=3;return rate*hours;}
Practice in Google
Use the execution tracer to see how a running total changes across steps.
The type of a value affects what an expression does.
A string is text in quotes, a number is a numeric value, and a boolean is true or false without quotes. The + operator adds numbers but can concatenate text. typeof reports common primitive types. Null is an intentional empty value; undefined often means a value was not supplied. This exercise distinguishes three common primitives and labels everything else other, avoiding an assumption that typeof alone explains all JavaScript values.
Try it
Implement describe(value): return text, number, boolean or other according to its type.
Use typeof value to inspect its type.
Compare the type to "string", "number" and "boolean".
Return the matching label; keep other as the fallback.
Common trap
false is a real value, not the same as a missing input.
Reveal a worked solution
function describe(value){if(typeof value==="string")return "text";if(typeof value==="number")return "number";if(typeof value==="boolean")return "boolean";return "other";}
Practice in Google
Log typeof for a number, text and boolean in Google’s editor and compare the results.
Logs show runtime evidence; comments explain intent.
A comment beginning // is ignored until the end of the line. It explains why code exists without executing. console.log displays diagnostic values; it does not return them to the caller. A function that only logs a value returns undefined unless it explicitly returns something. Record the smallest useful evidence. Avoid placing personal data, credentials or entire sensitive records into logs. The exercise computes a value, logs it for inspection and returns it for callers.
Try it
Complete addOne(n) so it logs the calculated result and returns n + 1.
Update the calculation to n + 1.
Keep the log and return statements separate.
Inspect the console and check the returned values.
Common trap
Seeing the right console text does not prove that your function returns the right value.
Reveal a worked solution
function addOne(n){const result=n+1;console.log(result);return result;}
Practice in Google
Run a function with console.log and inspect its execution record. Use only fictional values.
Parameters let one function work with different inputs.
A parameter is a named input in the function declaration. An argument is a value supplied at a call. greet("Ada") passes Ada into the parameter name. Use a template literal with backticks and ${name} to insert the value into a string. This lesson assumes the caller supplies text; later lessons add validation. Avoid hard-coding one example when the contract asks for any name.
Try it
Implement greet(name) so it returns Hello, followed by a space and the supplied name, then !.
Keep name as the parameter.
Build the message from fixed text and name.
Test with two different names.
Common trap
Double quotes around ${name} do not interpolate it.
Reveal a worked solution
function greet(name){return `Hello, ${name}!`;}
Practice in Google
Make a wrapper that logs greet with two different fictional names.
A small calculation can be tested independently of services.
Separate arithmetic from the code that reads or writes Google files. A pure calculation uses its arguments and returns a result without external changes. Multiplication and subtraction make a discounted price: price × (1 − percent / 100). Parentheses group the fraction before multiplication. The contract here accepts numeric values and does not round currency; real financial requirements need explicit rounding rules.
An if statement runs a block when its condition is truthy. else supplies the alternative. Comparison operators such as >= return booleans. Order boundary checks deliberately: 60 belongs in the passing branch when the rule says at least 60. Prefer precise rules over guesses from example values. This contract assumes a numeric score and returns one of two exact labels.
Try it
Implement resultLabel(score): Pass for scores at least 60, otherwise Practice.
Write the condition score >= 60.
Return Pass when true.
Return Practice for the remaining path.
Common trap
Using > instead of >= changes the behavior at 60.
Reveal a worked solution
function resultLabel(score){if(score>=60)return "Pass";return "Practice";}
Practice in Google
Use the code checks as a record of the intended boundary rule.
Strict equality compares without coercing different types.
The === operator checks equality without converting strings to numbers. A spreadsheet can contain numbers, text and blanks, so exact comparison matters. && means both conditions must hold; || means at least one. The ! operator negates a truth value. This exercise accepts only the exact status Open and the actual boolean true for enabled. Text such as "true" is not a boolean permission.
Try it
Implement shouldProcess(status, enabled): true only for status Open and enabled === true.
Compare status exactly to Open.
Compare enabled exactly to true.
Combine the two checks with &&.
Common trap
A nonempty string such as "false" is truthy; do not use it as an unchecked flag.
Reveal a worked solution
function shouldProcess(status,enabled){return status==="Open" && enabled===true;}
Practice in Google
Apply explicit status checks when filtering spreadsheet rows.
Arrays are ordered collections with zero-based indexes.
An array literal uses square brackets. The first element is at index 0; length is the number of elements. An empty array has length 0. Reading an absent index normally gives undefined, but a function can deliberately return a different signal. This contract returns null when the list is empty and preserves any actual first value, including 0, false or an empty string. Do not use a truthiness test to decide whether the value exists.
Try it
Implement firstItem(items): return the first item, or null if the array is empty.
Check items.length.
Read items[0] if an item exists.
Return null only for an empty array.
Common trap
items[0] || null would replace valid false and zero values.
Reveal a worked solution
function firstItem(items){return items.length?items[0]:null;}
Practice in Google
Compare a sheet’s first row number, 1, with its corresponding array index, 0.
for...of gives you each value of an iterable array in order. Start a running total at zero using let, then add each value. After the loop, return the total. Returning inside the loop would stop after the first item. This contract accepts arrays of numbers; explicit conversion of text comes later. The empty list correctly returns zero because there is nothing to add.
Try it
Implement sumNumbers(values) with a running total and a for...of loop.
Declare let total = 0.
For each value, add it to total.
Return total after the closing loop brace.
Common trap
A const total cannot be reassigned with +=.
Reveal a worked solution
function sumNumbers(values){let total=0;for(const value of values){total+=value;}return total;}
Practice in Google
Step through the Loop Tracer lab and predict each total before revealing it.
An object literal uses curly braces and key:value pairs. Dot notation reads a named property, such as task.title. An array can hold multiple task objects. When a function creates a result object, callers can use meaningful names rather than remembering positions. This exercise constructs a new object with title and minutes; no service is called. Names are case-sensitive, and the property spelling is part of the contract.
Try it
Implement makeTask(title, minutes), returning an object with those two properties.
Open an object literal after return.
Map title to the title argument and minutes to minutes.
Keep numeric minutes as a number.
Common trap
An array and an object are different shapes even when they contain similar values.
Reveal a worked solution
function makeTask(title,minutes){return {title:title,minutes:minutes};}
Practice in Google
Inspect an array of these records in the console before writing a sheet adapter.
map returns one result for each original element. filter returns only elements whose callback matches. Arrow functions such as task => task.done describe small callbacks. Neither operation needs a spreadsheet connection. In this contract, keep tasks whose done property is exactly true and return their titles, preserving order. An empty input yields an empty output. This is a small pipeline: select, then transform.
Try it
Implement completedTitles(tasks): return titles for tasks with done === true.
Filter with task.done === true.
Map the remaining tasks to task.title.
Return the resulting array.
Common trap
Returning the filtered objects does not meet a contract that asks for titles.
Reveal a worked solution
function completedTitles(tasks){return tasks.filter(task=>task.done===true).map(task=>task.title);}
Practice in Google
Use this pattern after reading rows and converting them into records.
Values entered by people are not always ready for arithmetic. Number("12") produces 12, but Number("") produces 0. That can hide missing data. Check whether the input is a number or a nonblank string before converting, then require a finite nonnegative value. This contract returns null for invalid values. Zero is allowed. Distinguish a validation result from an exception: null tells the caller to handle an expected invalid input.
A sheet is a grid, and a rectangular range becomes an array of row arrays. grid[0][1] means first row, second column. In Sheets, that position is B1 if the selected range starts at A1. Array indexes are relative to the range, not absolute sheet coordinates. A range beginning at B2 has B2 at values[0][0]. This exercise counts body rows by excluding one header row; it returns zero for an empty or header-only grid.
Try it
Implement bodyCount(grid): count rows after the first header row, never less than zero.
Start from grid.length.
Subtract one header row.
Use Math.max to prevent a negative count.
Common trap
The number of rows is not the number of cells.
Reveal a worked solution
function bodyCount(grid){return Math.max(0,grid.length-1);}
Practice in Google
Change the selected rectangle in the Sheet & Range Lab and inspect its nested arrays.
Use a service to get a spreadsheet, sheet and range.
SpreadsheetApp is a built-in Apps Script service. getActiveSpreadsheet returns the spreadsheet associated with the current context when one exists. getSheetByName("Tasks") selects a tab by its name; getRange("A2") selects a cell. getValue reads the top-left value. Separate these objects mentally: spreadsheet, sheet, range, value. A missing sheet returns null, so check it before calling methods. This trainer provides a fictional Tasks sheet whose A2 value begins as Ada. Each run starts with fresh fixtures.
Try it
Implement readFirstName(): read A2 from Tasks; return null if Tasks is missing.
Select Tasks from the spreadsheet.
Return null if the sheet is missing.
Read the range A2 with getValue.
Common trap
The browser simulator does not grant access to your Google Sheets.
Reveal a worked solution
function readFirstName(){const sheet=SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Tasks");if(!sheet)return null;return sheet.getRange("A2").getValue();}
Practice in Google
In a disposable bound spreadsheet, create a Tasks tab with Name in A1 and Ada in A2. Run a wrapper that logs readFirstName(). Do not copy Lab test helpers.
A write changes state; test the destination deliberately.
setValue places one value in a range. Select the destination explicitly and validate that the sheet exists before writing. A write is a side effect; unlike a pure calculation, its result includes changed data. In the trainer it changes a local simulated sheet only. In Google it changes the actual spreadsheet, so practice in a disposable file. Returning a boolean can communicate whether the write took place. Here you will update Tasks C2, then inspect the service panel to see the changed cell.
Try it
Implement writeStatus(status): write status to Tasks C2; return true on success or false if Tasks is missing.
Guard against a missing sheet.
Call sheet.getRange("C2").setValue(status).
Return true after the write.
Common trap
Selecting the wrong range can overwrite unrelated data.
Reveal a worked solution
function writeStatus(status){const sheet=SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Tasks");if(!sheet)return false;sheet.getRange("C2").setValue(status);return true;}
Practice in Google
Use the first-run practice file, put Status in C1, then run a wrapper writeStatus("Done"). Confirm only C2 changed.
An automation is a repeatable agreement about inputs, outputs and side effects.
Start by defining a pure summary function. The input contains task records; the output counts all records, counts records whose status is exactly Done, and sums only finite numeric minutes. Numeric-looking strings are not numbers under this contract. That choice prevents silent coercion from hiding bad sheet data. A pure function has no service calls, so you can explore it without changing a file. Later an adapter will read a sheet and pass normalized records to it. Apps Script runs JavaScript on Google’s servers. A bound project is attached to a file; a standalone project is independent. A browser page cannot directly call SpreadsheetApp. In a real project, open Extensions → Apps Script from a disposable spreadsheet, save your function, choose it in the editor and run it. Review the requested access and the execution record. Permissions, Workspace policies and the identity running the code affect what happens. This Studio models only selected service behavior; it does not request Google access.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
A truthy status is not the same as Done; zero minutes is valid.
Reveal a worked solution
function summarize(tasks) { return {total:tasks.length,done:tasks.filter(t=>t.status==="Done").length,minutes:tasks.reduce((n,t)=>n+(typeof t.minutes==="number"&&Number.isFinite(t.minutes)?t.minutes:0),0)}; }
Practice in Google
Create a disposable bound script and run a wrapper that logs summarize with artificial records. Inspect Executions and compare the log with the contract before connecting a real sheet.
Sheet coordinates begin at one; array indexes begin at zero.
A rectangular array is a snapshot of values, not a live range. Translate each coordinate once at the boundary and validate it before indexing. This exercise returns null for an invalid coordinate or a cell outside the supplied rows. A stored empty string remains an empty string. Null is the explicit missing-coordinate signal, so downstream logic can distinguish a blank cell from an invalid selection. Row lengths may differ in a JavaScript fixture even though getValues returns a rectangle. Apps Script runs JavaScript on Google’s servers. A bound project is attached to a file; a standalone project is independent. A browser page cannot directly call SpreadsheetApp. In a real project, open Extensions → Apps Script from a disposable spreadsheet, save your function, choose it in the editor and run it. Review the requested access and the execution record. Permissions, Workspace policies and the identity running the code affect what happens. This Studio models only selected service behavior; it does not request Google access.
Try it
Implement cellAt(rows,row,col) with one-based integer coordinates; return null outside the supplied data.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
Using row directly as an array index skips the first row.
Reveal a worked solution
function cellAt(rows,row,col) { if(!Number.isInteger(row)||!Number.isInteger(col)||row<1||col<1)return null;return rows[row-1]&&col<=rows[row-1].length?rows[row-1][col-1]:null; }
Practice in Google
Log range.getRow(), range.getColumn() and range.getValues() for B2:C3 in a test sheet. Compare absolute sheet coordinates with indexes inside that two-by-two snapshot.
Normalize at the boundary, then calculate with consistent types.
Spreadsheet users enter spaces, labels and numeric text. Decide what is acceptable rather than relying on JavaScript truthiness. Here a duration accepts a finite nonnegative number or a nonblank string that converts to one. Invalid, negative and missing values return null. The string 0 is valid; a blank string is not. This is a deliberately permissive numeric parser, so formats such as 1e2 are accepted. A production form might require a stricter decimal grammar. Apps Script runs JavaScript on Google’s servers. A bound project is attached to a file; a standalone project is independent. A browser page cannot directly call SpreadsheetApp. In a real project, open Extensions → Apps Script from a disposable spreadsheet, save your function, choose it in the editor and run it. Review the requested access and the execution record. Permissions, Workspace policies and the identity running the code affect what happens. This Studio models only selected service behavior; it does not request Google access.
Try it
Implement minutes(value) returning a finite nonnegative number, otherwise null; reject blanks, booleans and null.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
Number("") is zero, which can turn missing data into an apparently valid duration.
Reveal a worked solution
function minutes(value) {if(typeof value!=="number"&&typeof value!=="string")return null;if(typeof value==="string"&&!value.trim())return null;const n=Number(value);return Number.isFinite(n)&&n>=0?n:null;}
Practice in Google
Add test rows containing zero, a blank, numeric text and a date. Record which formats your automation supports. Use getDisplayValues only when you intentionally need formatted strings.
A result record makes success and failure inspectable.
A function that says finished tells an operator very little. Return counts that reflect what happened and classify every item exactly once. In this exercise only valid records are processed. A processed record with changed true counts as updated; other processed records count as unchanged. Invalid records count as skipped. The identity updated + unchanged + skipped = input length is an invariant you can check. Real execution logs should contain IDs and counts rather than full private records. Apps Script runs JavaScript on Google’s servers. A bound project is attached to a file; a standalone project is independent. A browser page cannot directly call SpreadsheetApp. In a real project, open Extensions → Apps Script from a disposable spreadsheet, save your function, choose it in the editor and run it. Review the requested access and the execution record. Permissions, Workspace policies and the identity running the code affect what happens. This Studio models only selected service behavior; it does not request Google access.
Try it
Implement report(items): {updated,unchanged,skipped}; valid must be true, changed must be true.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
Do not report attempted writes as successful writes before the service call returns.
Reveal a worked solution
function report(items){return items.reduce((r,x)=>{if(x.valid!==true)r.skipped++;else if(x.changed===true)r.updated++;else r.unchanged++;return r;},{updated:0,unchanged:0,skipped:0});}
Practice in Google
Wrap your real entry point in try/catch, log a safe summary, and rethrow unexpected failures so Executions records a failure. Do not log secrets or personal message bodies.
Read only the rows and columns your contract requires.
The Tasks sheet has a header and three data columns. Get the final used row, return an empty list if there is no body, and read the body in one call. Avoid requesting zero rows: getRange needs positive dimensions. Mapping each row to an object makes later code easier to read. getLastRow reflects content anywhere in the sheet, so production tables with notes below the data may need a more precise boundary. This fixture uses a clean table. A service call crosses from your JavaScript into a Google service. Read a rectangular block once, transform its two-dimensional array in memory, then write a rectangular result. Distinguish the sheet’s one-based coordinates from JavaScript’s zero-based indexes. A cell that looks empty can differ from a formula result, formatted value or date. Test against a small disposable spreadsheet before touching shared data. The simulator stores simple values only: it does not calculate formulas, preserve formatting or reproduce every spreadsheet behavior.
Try it
Implement readTasks() using SpreadsheetApp: read Tasks body A:C once and return {name,minutes,status} records.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
Calling getRange(2,1,0,3) is invalid for a header-only sheet.
Reveal a worked solution
function readTasks(){const s=SpreadsheetApp.getActive().getSheetByName("Tasks");const n=s.getLastRow();if(n<2)return [];return s.getRange(2,1,n-1,3).getValues().map(r=>({name:r[0],minutes:r[1],status:r[2]}));}
Practice in Google
Create Tasks with Name, Minutes, Status headers. Compare a three-row body with a header-only sheet. Add a note far below the table to understand how getLastRow changes.
setValues requires an array whose shape matches the target range.
Prepare all output rows in memory and make one write call. This exercise writes name and minutes beginning at A2 in Report and returns the count. An empty input is a no-op. The function does not clear old rows, so a shorter second report leaves stale content below the new block. That is intentional in this narrow exercise: the next design step is choosing a replace or append contract and implementing cleanup deliberately. Do not clear a whole shared sheet by accident. A service call crosses from your JavaScript into a Google service. Read a rectangular block once, transform its two-dimensional array in memory, then write a rectangular result. Distinguish the sheet’s one-based coordinates from JavaScript’s zero-based indexes. A cell that looks empty can differ from a formula result, formatted value or date. Test against a small disposable spreadsheet before touching shared data. The simulator stores simple values only: it does not calculate formulas, preserve formatting or reproduce every spreadsheet behavior.
Try it
Implement writeReport(items) to batch-write [name,minutes] rows at Report!A2; return count and do nothing for an empty list.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
A flat array is not a two-dimensional row matrix.
Reveal a worked solution
function writeReport(items){if(!items.length)return 0;SpreadsheetApp.getActive().getSheetByName("Report").getRange(2,1,items.length,2).setValues(items.map(x=>[x.name,x.minutes]));return items.length;}
Practice in Google
Use a Report tab you own. Decide whether old rows should be cleared and preserve headers or formulas outside the output area. Test a large report followed by a smaller one.
Names describe a schema more reliably than remembered column positions.
Build an index from trimmed, lowercased header names. In this contract the first occurrence wins, including an empty header. Returning a null-prototype object avoids collisions with inherited property names. A reordered sheet still works when downstream logic asks the index for status rather than assuming column three. In production, validate required names and reject duplicates when ambiguity would risk a wrong update. This exercise makes its duplicate rule explicit so you can test it. A service call crosses from your JavaScript into a Google service. Read a rectangular block once, transform its two-dimensional array in memory, then write a rectangular result. Distinguish the sheet’s one-based coordinates from JavaScript’s zero-based indexes. A cell that looks empty can differ from a formula result, formatted value or date. Test against a small disposable spreadsheet before touching shared data. The simulator stores simple values only: it does not calculate formulas, preserve formatting or reproduce every spreadsheet behavior.
Try it
Implement headerMap(headers): trimmed lowercase keys mapped to zero-based index; keep first occurrence.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
A truthy check loses a valid index of zero.
Reveal a worked solution
function headerMap(headers){const m=Object.create(null);headers.forEach((h,i)=>{const k=String(h).trim().toLowerCase();if(!Object.hasOwn(m,k))m[k]=i;});return m;}
Practice in Google
Move the Status column in a test sheet and adapt your reader using the header map. Throw a clear missing-column error before any write when a required header disappears.
Transform a source snapshot into a separate output matrix.
Treat the header as schema and the remaining rows as data. Select exact Open rows, retain their original order and project just name and minutes. Returning new arrays keeps your source snapshot intact for later validation or comparisons. Filtering an empty collection is fine; a missing header-only body should also be fine. This example uses fixed columns to focus on transformation; combine it with the previous header map to tolerate reordering. A service call crosses from your JavaScript into a Google service. Read a rectangular block once, transform its two-dimensional array in memory, then write a rectangular result. Distinguish the sheet’s one-based coordinates from JavaScript’s zero-based indexes. A cell that looks empty can differ from a formula result, formatted value or date. Test against a small disposable spreadsheet before touching shared data. The simulator stores simple values only: it does not calculate formulas, preserve formatting or reproduce every spreadsheet behavior.
Try it
Implement openRows(rows) to skip the header and return [name,minutes] for exact Open status; do not mutate rows.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
Array.sort mutates its input; a report should not unexpectedly reorder source data.
Reveal a worked solution
function openRows(rows){return rows.slice(1).filter(r=>r[2]==="Open").map(r=>[r[0],r[1]]);}
Practice in Google
Combine readTasks with a pure filter and writeReport. Log the planned rows before writing. Test missing tabs and a no-match result, then choose whether a no-match report should clear old output.
Check the event source and affected rectangle before doing work.
An edit handler should ignore unrelated cells and rows. Here accept only single-cell edits in Tasks column three below the header. A multi-cell paste is rejected by this specific contract. Missing e or range returns false, allowing manual execution to fail harmlessly. The guard uses range metadata instead of e.value, which may be absent for cleared cells or multi-cell edits. Keep the handler short and delegate the actual update to a separate function. An event object is supplied by the trigger system, not by clicking Run in the editor. Design the handler as a thin adapter: validate the event, obtain a bounded input, then call a function you can test directly. Simple triggers have authorization restrictions. An installable trigger runs with its creator’s authorization, and installing it is a deliberate setup step. Script or API edits do not invoke onEdit. Event payloads differ by source, so a spreadsheet edit, spreadsheet form submission and clock event need different assumptions.
Try it
Implement shouldHandle(e): true only for one cell in Tasks column 3, row greater than 1; otherwise false.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
Clicking Run does not supply an edit event.
Reveal a worked solution
function shouldHandle(e){const r=e&&e.range;return !!r&&r.getSheet().getName()==="Tasks"&&r.getRow()>1&&r.getColumn()===3&&r.getNumRows()===1&&r.getNumColumns()===1;}
Practice in Google
Attach a small onEdit(e) wrapper to a disposable sheet. Test a user edit, clearing a cell, editing the header and pasting a rectangle. Check logs rather than expecting an editor Run to mimic an event.
Compute the intersection between the edited rectangle and the target status column. If the rectangle spans column three, return the affected body row numbers, excluding row one. Otherwise return an empty list. This planning function does not read any values. A real handler can use the planned rows to read a bounded status block and process each relevant item. Avoid assuming every event has one old value and one new value. An event object is supplied by the trigger system, not by clicking Run in the editor. Design the handler as a thin adapter: validate the event, obtain a bounded input, then call a function you can test directly. Simple triggers have authorization restrictions. An installable trigger runs with its creator’s authorization, and installing it is a deliberate setup step. Script or API edits do not invoke onEdit. Event payloads differ by source, so a spreadsheet edit, spreadsheet form submission and clock event need different assumptions.
Try it
Implement affectedRows(e) for Tasks column 3; return intersecting row numbers greater than 1.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
e.value is not a complete representation of a multi-cell paste.
Reveal a worked solution
function affectedRows(e){const r=e&&e.range;if(!r||r.getSheet().getName()!=="Tasks"||r.getColumn()>3||r.getColumn()+r.getNumColumns()-1<3)return [];return Array.from({length:r.getNumRows()},(_,i)=>r.getRow()+i).filter(n=>n>1);}
Practice in Google
Paste a two-column block spanning B:C and inspect the range dimensions. Re-read the affected values explicitly. Decide how to handle rapid edits and overlapping runs before adding side effects.
Spreadsheet form submissions provide arrays under namedValues.
Use the event contract for the trigger you installed. A spreadsheet form-submit event can supply namedValues, mapping question labels to arrays of strings. This exercise extracts the first Name and Email value, trims both and lowercases the email. Missing values become empty strings. Renaming a form question can break this mapping, so production code should validate the expected schema. A Google Forms-bound submit event has a different response object and cannot be passed to this parser unchanged. An event object is supplied by the trigger system, not by clicking Run in the editor. Design the handler as a thin adapter: validate the event, obtain a bounded input, then call a function you can test directly. Simple triggers have authorization restrictions. An installable trigger runs with its creator’s authorization, and installing it is a deliberate setup step. Script or API edits do not invoke onEdit. Event payloads differ by source, so a spreadsheet edit, spreadsheet form submission and clock event need different assumptions.
Try it
Implement submission(e): return {name,email} from e.namedValues.Name and Email first values; safely default missing fields.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
Form-bound and spreadsheet-bound submission events are different.
Reveal a worked solution
function submission(e){const v=e&&e.namedValues||{};const first=k=>Array.isArray(v[k])?String(v[k][0]??"").trim():"";return {name:first("Name"),email:first("Email").toLowerCase()};}
Practice in Google
Connect a test form to a test spreadsheet and install a spreadsheet form-submit trigger. Submit artificial data once, inspect safe keys, and confirm that renamed questions fail validation clearly.
A clock event provides a schedule signal, not a selected range.
Make scheduling logic depend on explicit values. This exercise receives a UTC hour and a list of enabled integer hours; it returns true only for a valid hour present in the schedule. Passing time into a pure function makes tests repeatable. Real time-driven triggers can run within a window rather than at an exact second. Configure script and spreadsheet timezones deliberately and avoid interpreting a formatted date string as an unambiguous instant. An event object is supplied by the trigger system, not by clicking Run in the editor. Design the handler as a thin adapter: validate the event, obtain a bounded input, then call a function you can test directly. Simple triggers have authorization restrictions. An installable trigger runs with its creator’s authorization, and installing it is a deliberate setup step. Script or API edits do not invoke onEdit. Event payloads differ by source, so a spreadsheet edit, spreadsheet form submission and clock event need different assumptions.
Try it
Implement isScheduled(hour,hours): accept integer hour 0–23 and require membership in hours.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
A time-driven handler should not depend on whichever cell a user selected last.
Reveal a worked solution
function isScheduled(hour,hours){return Number.isInteger(hour)&&hour>=0&&hour<24&&hours.includes(hour);}
Practice in Google
Create a manually runnable scheduled entry point first. Then install one time-driven trigger under the intended owner and confirm timezone, notification settings and duplicate-trigger prevention.
PropertiesService stores strings; parse and validate configuration at the boundary.
A batch size controls how much work one run attempts. Read BATCH_SIZE from script properties and accept only a nonblank integer string from one through one hundred. Otherwise use ten. A safe fallback keeps an absent or malformed setting from accidentally causing an unbounded run. In a higher-risk operation, throwing a configuration error may be preferable to a fallback. Choose the behavior explicitly and document it. Cloud executions can overlap, stop early or be retried. A reliable workflow makes its progress observable and its repeat behavior explicit. Keep the amount of work bounded, use stable item identifiers and separate preparation from external side effects. Script properties are strings shared by the project; they are not a database transaction. A lock coordinates cooperating executions only. Test a second invocation, a missing setting, an interrupted batch and an unavailable lock. The Studio models these situations deterministically, without reproducing distributed execution.
Try it
Implement batchSize() reading BATCH_SIZE; return integer 1–100 or default 10.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
Script properties are accessible to project code and editors; do not treat them as an isolated secret vault.
Reveal a worked solution
function batchSize(){const s=PropertiesService.getScriptProperties().getProperty("BATCH_SIZE");if(s===null||!s.trim())return 10;const n=Number(s);return Number.isInteger(n)&&n>=1&&n<=100?n:10;}
Practice in Google
Set a project script property, run the reader, then test missing and invalid values. Decide whether your production action should stop or fall back when configuration is invalid.
Acquire before shared work and release in finally.
The function in this exercise returns busy if another execution holds the script lock. After successful acquisition it calls the supplied work function and returns its result. A finally block releases the lock whether work returns or throws. Do not swallow a failure and claim success. Keep the protected section small and ensure every cooperating writer uses the same locking strategy. A lock cannot roll back a message already sent or turn several independent services into a transaction. Cloud executions can overlap, stop early or be retried. A reliable workflow makes its progress observable and its repeat behavior explicit. Keep the amount of work bounded, use stable item identifiers and separate preparation from external side effects. Script properties are strings shared by the project; they are not a database transaction. A lock coordinates cooperating executions only. Test a second invocation, a missing setting, an interrupted batch and an unavailable lock. The Studio models these situations deterministically, without reproducing distributed execution.
Try it
Implement withLock(work): tryLock(1000); return "busy" if unavailable; otherwise return work() and release in finally.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
Releasing only on the happy path can leave the lock held until execution ends.
Reveal a worked solution
function withLock(work){const lock=LockService.getScriptLock();if(!lock.tryLock(1000))return "busy";try{return work();}finally{lock.releaseLock();}}
Practice in Google
Test two manual executions with a deliberate short pause in a disposable script. Verify busy handling and errors. Avoid holding locks across long network waits unless your consistency design requires it.
A stable item ID survives sorting and row insertion.
Given incoming records and a list of already-seen IDs, select the first occurrence of each unseen nonempty string ID. Do not mutate the input or the supplied history. This exercise only creates a plan; it does not mark anything delivered. In a real workflow, the point at which you record completion matters. Recording before a send can lose work; recording after a send can allow duplicates after a crash. Choose a recovery strategy appropriate to the service. Cloud executions can overlap, stop early or be retried. A reliable workflow makes its progress observable and its repeat behavior explicit. Keep the amount of work bounded, use stable item identifiers and separate preparation from external side effects. Script properties are strings shared by the project; they are not a database transaction. A lock coordinates cooperating executions only. Test a second invocation, a missing setting, an interrupted batch and an unavailable lock. The Studio models these situations deterministically, without reproducing distributed execution.
Try it
Implement unseen(items,seenIds): keep first records with nonempty string IDs not previously seen.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
Row numbers are positions, not stable identifiers.
Reveal a worked solution
function unseen(items,seenIds){const seen=new Set(seenIds);return items.filter(x=>{if(typeof x.id!=="string"||!x.id.trim()||seen.has(x.id))return false;seen.add(x.id);return true;});}
Practice in Google
Add a durable ID column to test data and sort the sheet between runs. Design an audit record that distinguishes planned, attempted and confirmed outcomes instead of one ambiguous processed flag.
A checkpoint should describe the next unit of work clearly.
For this frozen-list exercise, the cursor is a zero-based offset. Slice at most limit items and return the next cursor and a done flag. Clamp a valid cursor beyond the end to the length; reject negative or fractional cursors and nonpositive or fractional limits. In a changing dataset, an offset can skip or repeat records, so a stable ID or immutable snapshot may be necessary. Persist progress only after the batch’s intended work has completed according to your recovery contract. Cloud executions can overlap, stop early or be retried. A reliable workflow makes its progress observable and its repeat behavior explicit. Keep the amount of work bounded, use stable item identifiers and separate preparation from external side effects. Script properties are strings shared by the project; they are not a database transaction. A lock coordinates cooperating executions only. Test a second invocation, a missing setting, an interrupted batch and an unavailable lock. The Studio models these situations deterministically, without reproducing distributed execution.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
An offset checkpoint assumes the list remains in a stable order.
Reveal a worked solution
function nextBatch(items,cursor,limit){if(!Number.isInteger(cursor)||cursor<0||!Number.isInteger(limit)||limit<1)throw Error("Invalid batch");const start=Math.min(cursor,items.length),next=Math.min(start+limit,items.length);return {items:items.slice(start,next),next,done:next===items.length};}
Practice in Google
Store a cursor as a script property for a fixed test snapshot. Interrupt between batches, resume, and verify every ID appears once in your result. Plan how a changed source invalidates or migrates the checkpoint.
A response can be valid HTTP and still contain unusable data.
Use muteHttpExceptions to inspect non-success status codes intentionally. Accept only 2xx responses, parse JSON separately, and validate that items is an array before returning it. This exercise returns an empty list for status, parse, shape or transport failures. Production systems should also record a safe error category or throw for failures that need operator attention. Returning an empty collection without explanation can otherwise look like a legitimate no-data result. Integrations introduce several distinct failure layers: transport, HTTP status, decoding, response shape and the business result. Handle each deliberately and avoid logging credentials or personal content. Apps Script built-in service calls are synchronous; wrapping them in async functions does not magically parallelize requests. Limits and permitted services depend on the account and environment. Use Google’s current quota documentation when planning a real workload. The Studio’s fixtures never make network requests or create messages in your account.
Try it
Implement fetchItems(url): fetch with muteHttpExceptions:true; return body.items only for 2xx valid JSON with an items array; otherwise [].
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
Do not treat a 200 response as proof of the expected JSON schema.
Reveal a worked solution
function fetchItems(url){try{const r=UrlFetchApp.fetch(url,{muteHttpExceptions:true});if(r.getResponseCode()<200||r.getResponseCode()>=300)return [];const x=JSON.parse(r.getContentText());return Array.isArray(x.items)?x.items:[];}catch(e){return [];}}
Practice in Google
Use a public test endpoint you control or trust. Add a status category to the result, keep tokens in configuration and never log Authorization headers. Verify current UrlFetch quotas and endpoint rules.
A page is a partial result, not necessarily the whole collection.
For this exercise a continuation token must be a nonempty string that has not already been visited. Return null when pagination should stop. Tracking visited tokens prevents a broken API from causing an endless loop by repeating the same cursor. Real pagination code also needs a maximum page count and an execution budget. Persisting a continuation token may let a later run continue, but only if the API documents its lifetime and consistency guarantees. Integrations introduce several distinct failure layers: transport, HTTP status, decoding, response shape and the business result. Handle each deliberately and avoid logging credentials or personal content. Apps Script built-in service calls are synchronous; wrapping them in async functions does not magically parallelize requests. Limits and permitted services depend on the account and environment. Use Google’s current quota documentation when planning a real workload. The Studio’s fixtures never make network requests or create messages in your account.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
A repeated next-page token can create an infinite loop.
Reveal a worked solution
function nextToken(response,visited){const t=response&&response.nextPageToken;return typeof t==="string"&&t.trim()&&!visited.includes(t)?t:null;}
Practice in Google
Read the exact pagination contract of your chosen API. Test zero results, a final page, a repeated token and an expired token before adding the loop to a scheduled job.
Retry policy should be limited and appropriate to the failure.
Calculate an exponential delay from a zero-based attempt number, starting at one second and capped at thirty seconds. Invalid attempts return zero. This is a deterministic teaching calculation, not a complete retry system. Real code should classify retryable errors, respect documented Retry-After guidance, add jitter when suitable and limit total attempts and elapsed time. Retrying a non-idempotent action can duplicate its side effect. Sleeping still consumes execution time. Integrations introduce several distinct failure layers: transport, HTTP status, decoding, response shape and the business result. Handle each deliberately and avoid logging credentials or personal content. Apps Script built-in service calls are synchronous; wrapping them in async functions does not magically parallelize requests. Limits and permitted services depend on the account and environment. Use Google’s current quota documentation when planning a real workload. The Studio’s fixtures never make network requests or create messages in your account.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
Blindly retrying a message send may deliver duplicates.
Reveal a worked solution
function retryDelay(attempt){return Number.isInteger(attempt)&&attempt>=0?Math.min(30000,1000*2**attempt):0;}
Practice in Google
Build a retry wrapper around an idempotent test request. Simulate transient and permanent errors. Use Utilities.sleep only inside a bounded budget and record attempts without exposing request secrets.
Draft creation is an authorized side effect; preview the content first.
This lesson creates a simulated Gmail draft only when the recipient looks like a basic email address and the name is nonblank. Trim both values, generate a plain-text greeting and return the draft ID. The regex is intentionally a simple input check, not proof of deliverability or a full email grammar. A real createDraft call changes the user’s Gmail account and requires authorization; the Studio only records a local outbox entry. No send method is provided here. Integrations introduce several distinct failure layers: transport, HTTP status, decoding, response shape and the business result. Handle each deliberately and avoid logging credentials or personal content. Apps Script built-in service calls are synchronous; wrapping them in async functions does not magically parallelize requests. Limits and permitted services depend on the account and environment. Use Google’s current quota documentation when planning a real workload. The Studio’s fixtures never make network requests or create messages in your account.
Try it
Implement makeDraft(person): valid trimmed name/email → simulated Gmail draft subject "Welcome", body "Hello NAME!" and return ID; invalid → null.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
A draft preview is not a sent message, and validation is not delivery confirmation.
Reveal a worked solution
function makeDraft(person){const name=String(person.name??"").trim(),email=String(person.email??"").trim();if(!name||!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))return null;return GmailApp.createDraft(email,"Welcome","Hello "+name+"!").getId();}
Practice in Google
Run a dry-run content builder first. If you choose to create real drafts, use an approved test account, inspect requested scopes and review each draft manually. Do not add send logic until recipients, ownership and repeat behavior are designed.
A template renderer turns named fields into predictable plain text.
Replace tokens shaped like {{name}} using own properties of the supplied data object. Unknown tokens remain visible so missing data does not silently disappear. A replacement callback inserts values literally, including dollar signs that otherwise have special meaning in replacement strings. This pure renderer is not the same as DocumentApp.replaceText, which uses regular-expression matching and operates on document text structure. Use it to plan content before creating a document. Workspace automation is most useful when pure transformation logic is separated from service adapters. Compute the desired document, file selection or event proposal first, inspect it, and only then perform an authorized change. A preview is evidence of intent, not proof that the later service call will succeed. Access rights, shared-drive policies, timezone settings and existing files all matter in production. This chapter practices the planning layer; Docs, Drive and Calendar services are not implemented by the browser simulator.
Try it
Implement renderTemplate(text,data): replace {{word}} tokens from own properties, stringify values, leave unknown tokens unchanged.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
A replacement string containing $& can reinsert the matched text unless handled literally.
Reveal a worked solution
function renderTemplate(text,data){return text.replace(/\{\{(\w+)\}\}/g,(all,key)=>Object.hasOwn(data,key)?String(data[key]):all);}
Practice in Google
Use a test Docs template with a small placeholder set. DocumentApp.replaceText patterns are regex-based; escape literal patterns and test tokens across formatting boundaries. Store created document IDs if reruns should update rather than duplicate.
File selection should be a testable policy before it becomes a mutation.
Select untrashed records with the exact Google Docs MIME type and a nonempty string ID. Return unique IDs in first-seen order. Names are not unique identifiers, so the function never selects by name alone. The records in this exercise are plain fixtures, not live Drive objects. A real adapter should page through the intended folder or query scope and respect the caller’s access, shared-drive behavior and API version. Workspace automation is most useful when pure transformation logic is separated from service adapters. Compute the desired document, file selection or event proposal first, inspect it, and only then perform an authorized change. A preview is evidence of intent, not proof that the later service call will succeed. Access rights, shared-drive policies, timezone settings and existing files all matter in production. This chapter practices the planning layer; Docs, Drive and Calendar services are not implemented by the browser simulator.
Try it
Implement documentIds(files): unique nonempty IDs for untrashed application/vnd.google-apps.document records.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
Two files can share a name; moving or deleting by name alone can target the wrong file.
Reveal a worked solution
function documentIds(files){return [...new Set(files.filter(f=>f.trashed===false&&f.mimeType==="application/vnd.google-apps.document"&&typeof f.id==="string"&&f.id.trim()).map(f=>f.id))];}
Practice in Google
List files from one disposable folder and log only IDs and safe metadata. Verify the selection before adding any copy, move or trash action. For shared drives, check the supported API and permissions explicitly.
Calendar planning needs unambiguous instants and a positive duration.
Accept ISO-style timestamps with an explicit Z or numeric offset. Parse start and end, require a later end and return duration in minutes. Reject ambiguous local strings that omit a timezone. This contract accepts JavaScript Date.parse-compatible strings ending in the specified offset form; it is not a strict ISO calendar validator. A real scheduling form should validate its date components and explain the chosen timezone, especially around daylight-saving transitions. Workspace automation is most useful when pure transformation logic is separated from service adapters. Compute the desired document, file selection or event proposal first, inspect it, and only then perform an authorized change. A preview is evidence of intent, not proof that the later service call will succeed. Access rights, shared-drive policies, timezone settings and existing files all matter in production. This chapter practices the planning layer; Docs, Drive and Calendar services are not implemented by the browser simulator.
Try it
Implement durationMinutes(start,end): explicit Z or ±HH:MM suffix, valid instants and end > start; otherwise null.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
An ISO-looking string without an offset can be interpreted in the wrong timezone.
Reveal a worked solution
function durationMinutes(start,end){const explicit=s=>typeof s==="string"&&/(Z|[+-]\d{2}:\d{2})$/.test(s);if(!explicit(start)||!explicit(end))return null;const a=Date.parse(start),b=Date.parse(end);return Number.isFinite(a)&&Number.isFinite(b)&&b>a?(b-a)/60000:null;}
Practice in Google
Compare script timezone, spreadsheet timezone and calendar timezone. Preview a proposed event near a daylight-saving change using explicit instants before authorizing CalendarApp.createEvent on a test calendar.
A spreadsheet custom function can transform a whole matrix in one call.
This exercise doubles finite numbers while preserving the rectangular shape and replacing other values with an empty string. Map rows and cells into new arrays so the input is untouched. A custom function that accepts a range can reduce repeated calls compared with one formula per cell. Real custom functions have restrictions on authorization and arbitrary side effects, and their output must have room to expand into neighboring cells. They are not a replacement for an installable workflow. Workspace automation is most useful when pure transformation logic is separated from service adapters. Compute the desired document, file selection or event proposal first, inspect it, and only then perform an authorized change. A preview is evidence of intent, not proof that the later service call will succeed. Access rights, shared-drive policies, timezone settings and existing files all matter in production. This chapter practices the planning layer; Docs, Drive and Calendar services are not implemented by the browser simulator.
Try it
Implement DOUBLE_NUMBERS(values): return a new 2D array with finite numeric cells doubled and other cells "".
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
A custom function should not be used to send mail or modify arbitrary cells.
Reveal a worked solution
function DOUBLE_NUMBERS(values){return values.map(row=>row.map(v=>typeof v==="number"&&Number.isFinite(v)?v*2:""));}
Practice in Google
Add the function to a bound test sheet and call =DOUBLE_NUMBERS(A1:B2) in an empty area. Check output spill space and the custom-function service restrictions in the official documentation.
Client-side validation improves feedback; server validation protects the operation.
Accept a trimmed title with one through eighty characters and a numeric integer priority from one through three. Return an explicit result object, avoiding exceptions for expected form errors. The client might be modified or bypassed, so repeat validation in the server function before writing to a service. Do not silently coerce the string 2 here: the boundary contract requires a number. If your browser form sends strings, convert them deliberately before calling the server and still validate there. HTML Service has a browser side and a server side. Treat the boundary as an API: validate on the server, allowlist the actions, return small predictable records and show failures clearly in the interface. google.script.run is asynchronous and uses success and failure handlers; response order may differ from request order. A web app’s deployment identity and audience decide whose data can be accessed. Browser input and hidden fields are untrusted. The simulated exercises practice boundary logic, not an actual deployed web app.
Try it
Implement validateTask(input): {ok:true,value:{title,priority}} or {ok:false,error:"title"/"priority"}; title error takes precedence.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
Hidden inputs and disabled buttons are not authorization controls.
Reveal a worked solution
function validateTask(input){const title=typeof input?.title==="string"?input.title.trim():"";if(!title||title.length>80)return {ok:false,error:"title"};if(!Number.isInteger(input.priority)||input.priority<1||input.priority>3)return {ok:false,error:"priority"};return {ok:true,value:{title,priority:input.priority}};}
Practice in Google
Build a small HTML Service form with a server submitTask function. Show validation errors near fields, disable duplicate submissions during a request, and validate again server-side before any spreadsheet write.
Project a server record into an explicit object containing a string ID, string title and ISO timestamp. A Date object is converted intentionally before crossing the boundary. Private fields are omitted, not merely hidden in the page. google.script.run supports a constrained set of serializable values and does not accept arbitrary server objects such as ranges. This exercise also avoids returning the input object by reference, which could expose future fields added to the server model. HTML Service has a browser side and a server side. Treat the boundary as an API: validate on the server, allowlist the actions, return small predictable records and show failures clearly in the interface. google.script.run is asynchronous and uses success and failure handlers; response order may differ from request order. A web app’s deployment identity and audience decide whose data can be accessed. Browser input and hidden fields are untrusted. The simulated exercises practice boundary logic, not an actual deployed web app.
Try it
Implement publicTask(record): {id:String(id),title:String(title),updatedAt:Date(updatedAt).toISOString()}; omit other fields.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
Returning an entire record can accidentally disclose newly added sensitive fields.
Reveal a worked solution
function publicTask(record){return {id:String(record.id),title:String(record.title),updatedAt:new Date(record.updatedAt).toISOString()};}
Practice in Google
Inspect the browser network-visible response from your test interface. Confirm that no tokens, internal notes or unused fields are returned. Show a controlled error if a server date is invalid.
The newest request can finish before an older request.
A user changes a search twice. If the first response arrives last, rendering it blindly replaces the newest result with stale information. This exercise accepts a response only when its numeric request ID equals the current request ID. A browser controller can increment an ID before each google.script.run call and close over that ID in its success handler. This does not cancel server work or prevent side effects, so use it for rendering decisions alongside a separate write deduplication strategy. HTML Service has a browser side and a server side. Treat the boundary as an API: validate on the server, allowlist the actions, return small predictable records and show failures clearly in the interface. google.script.run is asynchronous and uses success and failure handlers; response order may differ from request order. A web app’s deployment identity and audience decide whose data can be accessed. Browser input and hidden fields are untrusted. The simulated exercises practice boundary logic, not an actual deployed web app.
Try it
Implement acceptResponse(currentId,response): true only when response.requestId strictly equals currentId and both are integers.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
A loading spinner does not solve response ordering.
Reveal a worked solution
function acceptResponse(currentId,response){return Number.isInteger(currentId)&&Number.isInteger(response?.requestId)&&response.requestId===currentId;}
Practice in Google
Use google.script.run.withSuccessHandler and withFailureHandler in a test page. Introduce two different response delays and verify that the newest selection remains visible even when completion order reverses.
A request should choose among approved actions, not arbitrary function names.
This router permits only preview and status. It returns an operation descriptor rather than executing a side effect. Unknown, inherited and nonstring action names return denied. Separating routing from execution lets you validate the request shape, verify access and choose the appropriate adapter before touching data. A real web app must also select deployment identity and audience deliberately; a route allowlist by itself does not authorize a user to read a particular resource. HTML Service has a browser side and a server side. Treat the boundary as an API: validate on the server, allowlist the actions, return small predictable records and show failures clearly in the interface. google.script.run is asynchronous and uses success and failure handlers; response order may differ from request order. A web app’s deployment identity and audience decide whose data can be accessed. Browser input and hidden fields are untrusted. The simulated exercises practice boundary logic, not an actual deployed web app.
Try it
Implement route(action): preview → {handler:"buildPreview"}, status → {handler:"readStatus"}, otherwise {error:"denied"}.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
Never dispatch arbitrary global function names supplied by a client.
Reveal a worked solution
function route(action){const routes={preview:"buildPreview",status:"readStatus"};return typeof action==="string"&&Object.hasOwn(routes,action)?{handler:routes[action]}:{error:"denied"};}
Practice in Google
Deploy a test web app to a restricted audience. Review execute-as settings, check resource access server-side and keep tokens out of browser responses. Test denied actions and malformed payloads.
For a workload of rows and a positive batch size, estimate one read and one write per batch. Return zero for no rows. This model deliberately excludes metadata calls, retries, formulas and actual service latency. It illustrates why a row-by-row approach scales poorly, but it is not a quota calculator or runtime promise. Measure representative real work in a test environment and leave headroom for slower calls and partial failures. A production workflow needs an operational contract as well as correct syntax. Define inputs, permissions, repeat behavior, limits, ownership, monitoring and recovery. Use a test spreadsheet and artificial data to rehearse failure cases. Record the exact configuration and expected changes before a release. Test results demonstrate the cases you ran, not universal correctness. Quotas can change and executions can fail for reasons outside your code. Prefer small bounded batches with auditable progress and a clear stop or resume path.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
A service-call estimate is not an execution-time guarantee.
Reveal a worked solution
function callBudget(rows,batch){if(!Number.isInteger(rows)||rows<0||!Number.isInteger(batch)||batch<1)throw Error("Invalid budget");return Math.ceil(rows/batch)*2;}
Practice in Google
Benchmark small representative batches and record elapsed time and operation counts. Consult current quotas for the account type and service. Stop before a budget is exhausted and checkpoint a safe resume point.
An allowlisted log record is easier to audit than a copied input object.
Create a safe record with only event, count and ok. Event must be a string, count a nonnegative integer, and ok true only when explicitly true. Everything else is omitted, including tokens, addresses and message bodies. This does not make every event name safe: callers should use controlled event labels, not user content. Redaction belongs at the logging boundary so future fields do not automatically appear in logs. A production workflow needs an operational contract as well as correct syntax. Define inputs, permissions, repeat behavior, limits, ownership, monitoring and recovery. Use a test spreadsheet and artificial data to rehearse failure cases. Record the exact configuration and expected changes before a release. Test results demonstrate the cases you ran, not universal correctness. Quotas can change and executions can fail for reasons outside your code. Prefer small bounded batches with auditable progress and a clear stop or resume path.
Try it
Implement auditRecord(input): {event:string or "unknown",count:nonnegative integer or 0,ok:input.ok===true}; omit other fields.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
Replacing known secret keys is fragile when new sensitive fields are added later.
Reveal a worked solution
function auditRecord(input){return {event:typeof input.event==="string"?input.event:"unknown",count:Number.isInteger(input.count)&&input.count>=0?input.count:0,ok:input.ok===true};}
Practice in Google
Route production logging through one helper. Inspect successful and failed execution logs with artificial personal data and verify that only intended operational metadata appears.
A malformed table should fail before the first mutation.
This contract requires exactly three header cells, Name, Minutes and Status in that order. Every body row must contain exactly three cells. Return a structured reason for header or row-width failure; an empty table fails the header check. This deliberately narrow validator does not validate cell types or status values. Layer those checks separately so the error explains which contract failed. A write adapter should run all required validation before clearing or overwriting any output. A production workflow needs an operational contract as well as correct syntax. Define inputs, permissions, repeat behavior, limits, ownership, monitoring and recovery. Use a test spreadsheet and artificial data to rehearse failure cases. Record the exact configuration and expected changes before a release. Test results demonstrate the cases you ran, not universal correctness. Quotas can change and executions can fail for reasons outside your code. Prefer small bounded batches with auditable progress and a clear stop or resume path.
Try it
Implement validateTable(rows): {ok:true} or {ok:false,error:"header"/"width"}; header check first.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
Checking only the first body row misses malformed later rows.
Reveal a worked solution
function validateTable(rows){if(!rows.length||JSON.stringify(rows[0])!==JSON.stringify(["Name","Minutes","Status"]))return {ok:false,error:"header"};if(rows.slice(1).some(r=>!Array.isArray(r)||r.length!==3))return {ok:false,error:"width"};return {ok:true};}
Practice in Google
Rename a required header and remove a body cell in your test fixtures. Confirm the workflow fails with a clear reason before making any write. Decide whether reordered columns are allowed and adapt the schema policy accordingly.
A release gate turns assumptions into observable checks.
Evaluate a configuration record and return a list of missing safeguards in a fixed order. This exercise checks a nonblank owner, confirmed test evidence, an explicit dryRun false choice and a positive integer maximum batch. It is a small teaching gate, not a comprehensive security review or authorization system. A real release also needs access scope, deployment identity, monitoring, recovery, data handling and trigger ownership decisions. Treat the result as a prompt for evidence rather than a badge of universal readiness. A production workflow needs an operational contract as well as correct syntax. Define inputs, permissions, repeat behavior, limits, ownership, monitoring and recovery. Use a test spreadsheet and artificial data to rehearse failure cases. Record the exact configuration and expected changes before a release. Test results demonstrate the cases you ran, not universal correctness. Quotas can change and executions can fail for reasons outside your code. Prefer small bounded batches with auditable progress and a clear stop or resume path.
Try it
Implement releaseIssues(config): ordered issues "owner","tests","dryRun","batch" for invalid fields; [] when all pass.
Read the input and output contract, then predict the first result.
Implement one small case and inspect the console or service state.
Run every listed check; repair the failing boundary case before continuing.
Common trap
A checked box is not evidence that the test was performed.
Reveal a worked solution
function releaseIssues(config){const a=[];if(typeof config.owner!=="string"||!config.owner.trim())a.push("owner");if(config.testsPassed!==true)a.push("tests");if(config.dryRun!==false)a.push("dryRun");if(!Number.isInteger(config.maxBatch)||config.maxBatch<1)a.push("batch");return a;}
Practice in Google
Use the project evidence workspace to document a dry run, a repeat run, an invalid-input case and a recovery drill. Identify the trigger owner and how to disable the workflow. Then review real permissions and deployment settings before enabling changes.