Skip to content

Basics

Custom functions in Google Sheets

A Google Sheets custom function runs in a silent sandbox with its own rules: 30-second timeouts, raw 2D arrays instead of Range objects, no personal services, and no cell edits.

1 of 8

How custom functions differ from normal scripts

A normal Apps Script function runs when you click Run in the editor, click a custom menu, or set up a trigger. A custom function is typed directly into a spreadsheet cell as a formula, such as =DOUBLE(A2), and recalculates on its own whenever referenced cells change.

Google Sheets can call any top-level function declaration whose name does not end in an underscore. Arrow functions and functions declared inside objects or classes are not available. The @customfunction tag in the comment block immediately above the function adds it to the formula autocomplete list; it is not required for the formula to work.

Because custom functions run inside a formula, Google Sheets treats them as pure calculations. They compute a result from their inputs and return it to the spreadsheet.

A minimal custom function with JSDoc metadata
/**
 * Multiplies a number by two.
 *
 * @param {number} input The number to double.
 * @return {number} The input multiplied by two.
 * @customfunction
 */
function DOUBLE(input) {
  return Number(input) * 2;
}

2 of 8

The silent authorization sandbox

Normal scripts open an authorization dialog the first time they run so you can review the permissions they need. Custom functions run in a silent authorization sandbox called AuthMode.CUSTOM_FUNCTION, which never opens an authorization prompt.

Because custom functions cannot ask for user consent, Google forbids them from calling services that access personal data. Calling GmailApp, DriveApp, CalendarApp, or ContactsApp fails immediately with "You do not have permission to call...".

Services that do not require personal authorization are allowed, including UrlFetchApp to query public websites. If a calculation needs private user data, write a normal script that runs from a custom menu or a time-driven trigger and writes the values into the sheet in advance.

3 of 8

Custom functions cannot edit other cells or open dialogs

A custom function can only return a value to the cell that called it and to the adjacent cells below and to the right if the result is an array. It cannot modify arbitrary cells across the workbook.

Calling mutating methods such as range.setValue(), range.clear(), sheet.appendRow(), or sheet.deleteRow() throws an exception at runtime. Methods that use the user interface, such as SpreadsheetApp.getUi() or Browser.msgBox(), also fail because formula recalculation has no interactive user session.

Think of custom functions like built-in formulas such as SUM or VLOOKUP. They read the arguments you pass them, perform math or text manipulation, and return the answer.

4 of 8

The Range trap: cell inputs arrive as raw values, not Range objects

A common mistake is assuming that passing a range like A1:B10 hands your function a SpreadsheetApp.Range object. In Google Sheets, formulas pass raw cell values, never Range objects.

A single cell argument arrives as a primitive value: a number, a string, a boolean, or a Date object. An empty cell arrives as an empty string. A multi-cell range arrives as a two-dimensional JavaScript array of rows, where each row is an array of cell values.

Writing input.getValue() throws "TypeError: input.getValue is not a function" because raw arrays and numbers do not have Range methods. To support both single cells and range arguments, check whether the input is an array.

Handle both single cells and multi-cell ranges in one function
/**
 * Converts temperatures from Celsius to Fahrenheit.
 *
 * @param {number|string|boolean|Date|null|undefined|Array<Array<number|string|boolean|Date|null|undefined>>} input A single cell or a rectangular range.
 * @return {number|string|Array<Array<number|string>>} The converted temperature or grid of temperatures.
 * @customfunction
 */
function CELSIUS_TO_FAHRENHEIT(input) {
  if (Array.isArray(input)) {
    return input.map(function(row) {
      return row.map(function(cell) {
        return convertOneCell(cell);
      });
    });
  }
  return convertOneCell(input);
}

/**
 * @param {number|string|boolean|Date|null|undefined} cell
 * @return {number|string}
 */
function convertOneCell(cell) {
  if (cell === '' || cell === null || cell === undefined) {
    return '';
  }
  const celsius = Number(cell);
  if (isNaN(celsius)) {
    throw new Error('CELSIUS_TO_FAHRENHEIT needs a number. Received: ' + cell);
  }
  return Math.round((celsius * 9 / 5 + 32) * 10) / 10;
}

5 of 8

Return values, grid spills, and what you cannot return

A custom function can return a primitive value (a number, string, boolean, or Date), a one-dimensional array, or a two-dimensional array. When you return a two-dimensional array, Google Sheets spills the values across and down into adjacent cells.

If adjacent cells already contain data, Sheets displays #REF! with the message "Array result was not expanded because it would overwrite data in...". Clear the blocking cells so the grid can expand.

Custom functions cannot return plain JavaScript objects like { total: 100 } or Promises from async functions. Returning a plain object displays [object Object] or #VALUE!, while an async function returns an unfulfilled Promise that Sheets cannot display.

6 of 8

Naming rules, private underscores, and collisions

By convention, custom function names are written in capital letters so they look like standard Google Sheets formulas. Three naming mistakes cause confusing errors.

First, function names ending with an underscore, such as DOUBLE_(), are treated as private in Apps Script. Google Sheets cannot see private functions, so typing =DOUBLE_(A1) results in #NAME? Unknown function.

Second, do not name a custom function after an existing built-in Sheets formula, such as SUM or INDEX. Built-in formulas take priority and shadow your script. Third, avoid names that match cell coordinates, such as A1 or R1C1, which confuse the formula parser.

7 of 8

Formula caching and the 30-second timeout

A normal Apps Script execution can run for up to six minutes. Custom functions get a 30-second execution timeout per call because Google Sheets may evaluate dozens of cell formulas simultaneously when a spreadsheet opens.

Google Sheets caches custom function results based on the formula arguments. If the cell arguments do not change, Sheets does not re-run your function, even when you reload the browser tab.

Calling non-deterministic functions like Math.random() or Date.now() returns a cached value that stays frozen until you edit an argument. If your calculation needs to refresh with the current time, pass a timestamp cell or a trigger-updated cell as an explicit formula argument.

8 of 8

JSDoc comments control formula autocomplete and help text

Google Sheets reads the JSDoc comment block above your custom function to build the formula help card that appears as users type in a cell. The first lines of the comment become the function summary.

Each @param tag describes one argument. The parameter name in the @param tag must match the argument name in the function declaration. If the names do not match, Sheets cannot connect your description to the argument, leaving the help box empty.

The @return tag describes the output shown at the bottom of the formula card. Always place the @customfunction tag on its own line within the same comment block.

Remember

The short version

  • Custom function arguments are raw primitive values or 2D arrays, never SpreadsheetApp.Range objects.
  • Custom functions run in a silent authorization sandbox and cannot call personal services such as GmailApp or DriveApp.
  • A custom function cannot edit other cells, append rows, or display UI alerts with getUi().
  • Functions ending with an underscore are private in Apps Script and return #NAME? when typed in a cell.
  • Custom functions have a 30-second timeout, and their results are cached until formula arguments change.

Keep going

Put it to work

All basics