Google Apps Script Patterns: Keeping a gam generated users report up-to-date with Google Apps Script

In Google Workspace gam is probably the ‘go to’ tool command line tool which allows administrators to easily manage domain and user settings.

With great power, comes great responsibility

GAM is very powerful and you can do extreme damage to your Google Workspace domain.

Recently I was asked about how you can keep gam generated reports up-to-date using Google Apps Script. It’s worth remember that gam uses public Google Workspace APIs when it performs actions and reports. One approach to this problem is to think like gam. More often Google’s APIs will return a responses object. These objects will vary in structure. For example, below is an excerpt from the Google Admin SDK for a user response:

 "kind": "directory#user",
 "id": "the unique user id",
 "primaryEmail": "[email protected]",
 "name": {
  "givenName": "Liz",
  "familyName": "Smith",
  "fullName": "Liz Smith"
 "organizations": [
   "name": "Google Inc.",
   "title": "SWE",
   "primary": true,
   "customType": "",
   "description": "Software engineer"

As gam can output as .csv and directly to Google Sheets this structured data is flattened is a ‘.’ path notation making it easy to transpose into column headings and rows:

 "kind": "directory#user",
 "id": "the unique user id",
 "primaryEmail": "[email protected]",
 "name.givenName": "Liz",
 "name.familyName": "Smith",
 "name.fullName": "Liz Smith",
 "": "Google Inc.",
 "organizations.0.title": "SWE",
 "organizations.0.primary": true,
 "organizations.0.customType": "",
 "organizations.0.description": "Software engineer",

Using a similar approach in Google Apps Script means with minimum effort and code we can take an existing gam export and refresh the report contents. Take, for example, the following gam command:

gam print users name ou organizations relations todrive

Using the following Apps Script code we can replicate the gam command and keep a Google Workspace account list up-to-date (also available if you make a copy of this Google Sheet)

// @OnlyCurrentDoc

 * This function is designed to read a heading row and update the data from the Admin SDK
function updateSheet(){
  const doc = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = doc.getSheetByName('Directory');

  // this will return two arrays one with the column headings, the other with all the other rows
  const [heads,] = sheet.getDataRange().getValues();

  // get a flattened list of accounts
  const accounts = listAllUsers_();

  // if we just want to update the entire sheet with the new data we can create a 2d array
  const tr = => => row[String(key)] || ''));
  // clear any existing rows (except the header)
  if (sheet.getLastRow() > 1) sheet.getRange(2,1, sheet.getLastRow()-1, tr[0].length).clearContent();
  // write the data we got back from the Admin SDK
  sheet.getRange(2, 1, tr.length, tr[0].length).setValues(tr);


 * Lists all the users in a domain sorted by first name.
 * @see
 * Based on
function listAllUsers_() {
  let pageToken = null;
  let users = [];
  do {
    let page = AdminDirectory.Users.list({
      customer: 'my_customer',
      orderBy: 'givenName',
      maxResults: 500,
      pageToken: pageToken,
      fields: 'users(primaryEmail,name,orgUnitPath,relations,organizations),nextPageToken'
    // here we flatten the user response 
    users = users.concat( => flatten_(d)));
    pageToken = page.nextPageToken;
  } while (pageToken);

  return users

 * This function will take take a tree object and flatten it into so it's only 1 deep
 * @see
const flatten_ = (obj, prefix = '', res = {}) =>
  Object.entries(obj).reduce((r, [key, val]) => {
    const k = `${prefix}${key}`;
    if(typeof val === 'object' && val !== null){
      flatten_(val, `${k}.`, r);
    } else {
      res[k] = val;
    return r;
  }, res);

gam has more super powers and depending on the public APIs you need may require a little more work to get up and running. The key aspects of this design pattern are:

  • Using column headings that match the flattened response object means the column order isn’t important (changing the column heading text will break the script); and
  • the function listAllUsers_() is a common pattern for getting paged responses from Google APIs

Finally, if you’d like a little more inspiration with what is possible the new Google Workspace API Explorer lets you see just how much is possible.


Join the conversation

comment 3 comments
  • Henry Blanchard

    Thanks for creating this example. This is exactly what I was looking for and has got me about 90% of the way of displaying all the fields I need in my Google Sheets Directory. In your provided script I’ve updated the fields as fields: ‘users(primaryEmail,name,orgUnitPath,relations,organizations, addresses, phones),nextPageToken’ . As you can see I’ve added addresses, phones and also added these headers to the spreadsheet: addresses.0.formatted & phones.0.value. My issue is that each user has multiple values for both of these properties. The spreadsheet is displaying the first property but I can’t figure out how to get the second one. For example, let’s say one user has the following:
    “addresses”: [
    “type”: “work”,
    “formatted”: “123 Main St”
    “type”: “work”,
    “formatted”: “Almer, GA 40947”

    “phones”: [
    “value”: “:123-456-7890”,
    “type”: “work”
    “value”: “101-123-1314”,
    “type”: “mobile”
    My spreadsheet will show “123 Main St” for the address & “123-456-7890” for the phone number. How do I get the other 2 values? I tried duplicating the header column names but that just gives me duplicate results in the spreadsheet.

    • Martin Hawksey

      Hi Henry – glad you found this example useful. To get the other values you should be able to add additional column names incrementing the number in the middle e.g. phones.1.value, phones.2.value and so on 🙂

      • Henry Blanchard

        Yes, that did the trick. Exactly what I needed. Thanks for your help. This script is going to save me a lot of time. 🙂

Leave a comment

Your email address will not be published. Required fields are marked *