AppsScriptPulse

Beyond the Sidebar: Re-imagining Google Workspace Add-ons as Gemini for Workspace Actions

The conversation around Enterprise AI is shifting. In 2024, we were preoccupied with the novelty of the prompt box, marveling at the ability of large language models to draft emails or summarise long documents. By 2026, that novelty has faded. The real frontier is no longer about asking an AI to “write this”; it is about instructing it to “execute this process.” We are witnessing the birth of a managed Action Layer within our digital environments.

The DORA Reality Check: AI as an Amplifier

To understand why this shift is happening, we must look at the data. The 2025 DORA Report provides a sobering reality check for technology leaders. Its central finding is that AI is an amplifier of your existing organisational systems. If your workflows are streamlined and your data is clean, AI accelerates excellence. However, if your processes are fragmented, AI simply creates “unproductive productivity,” generating isolated pockets of speed that eventually lead to downstream chaos.

DORA’s insights suggest that the bottleneck for AI value isn’t the model’s intelligence; it is the “Context Constraint.” AI cannot be effective if it is trapped in a silo, unable to see the broader business environment or act upon the tools that employees use every day. To move from hype to value, we must stop treating AI as a separate chatbot and start treating it as a governed co-worker.

Demystifying MCP

An emerging key component at the heart of this transition will be the Model Context Protocol (MCP). This protocol acts as a universal bridge that allows AI models to connect directly with data sources and local tools without the need for custom code for every new integration. It works by standardising how a model requests information from a database, a file system, or a specific software application. Because this protocol creates a common language between the AI and the external world, developers can swap different models or data sets in and out of their workflows with much less friction.

We are moving away from the era of closed silos. Instead, we see a future where an AI assistant can securely reach into your local environment to read a specific document or query a live database using a single, unified interface. It is a fundamental shift in how we think about connectivity.

Two Philosophies of Action: Managed Features vs. Configurable Platforms

Given the scale and pace of change, it is not surprising that Google’s AI efforts are not singular. The reality is that Gemini Enterprise (the platform formerly known as Agentspace) and Gemini for Workspace are two distinct products developed by separate teams with very different philosophies of control.

Gemini for Workspace primarily operates as a “managed service.” We see a steady expansion of features, but these remain “black boxes” entirely defined and controlled by Google. The user receives the feature, but the administrator has very little say in how that tool is connected, calibrated, or exposed to the model’s reasoning engine. Gemini Enterprise, by contrast, is a more configurable platform. It allows organisations to define their own data connectors, tailor system prompts and even deploy enterprise-owned agents.

From Black Boxes to Federated Actions

Whilst Google continue to expand the actions Gemini for Workspace can perform in products like Google Sheets, these tools remain non-configurable for the domain administrator. The January 23, 2026, Gemini Enterprise release reveals a more radical direction: the expansion of Configurable Actions.

Whilst the internal implementation details of every Gemini feature remain proprietary, Google’s recent announcement of official MCP support suggests that the protocol is becoming the standard connective tissue for their entire AI stack. The Enterprise team is simply the first to expose this plumbing for organisational governance and configuration. To understand the practical power of this protocol when it is stripped of its corporate packaging, we only need look at how MCP is being used in other generative AI tooling.

Lessons from the Gemini CLI

While most Workspace users interact with AI through polished side panels and the Gemini App, some power users are turning to the Gemini CLI. This command-line interface is the playground often being used to explore the automation of AI-driven workflows.

Unlike the “black box” nature of Gemini Enterprise and Gemini for Workspace, the Gemini CLI is easy to extend and customise. Users can easily install and create Extensions which can wrap commands, system prompts and MCP tools. The Google Workspace Extension is a wonderful demonstration of the power of MCP. By providing a few lines of configuration, a user grants the model permission to query and manage core productivity data: it can search files in Drive, draft emails in Gmail, dispatch Chat alerts, or organise Calendar appointments directly from the command line.

The value of the CLI lies in its demonstration that generative AI is capable of much more than just summarisation and analysis. By extending its capabilities through MCP, a user can hand-pick the actions they need based on their specific persona and the systems they inhabit. This shift toward action-oriented tools allows the model to move from passive reasoning to active execution without the user ever leaving their primary environment.

The Aspiration: A Managed Action Marketplace

The challenge now is to take this raw, unfiltered power seen in the CLI and translate it into a governed enterprise experience. Individual productivity is a start, but true organisational scale requires a bridge between these flexible protocols and the managed safety of the Workspace environment. I believe the defining moment for enterprise productivity will arrive when Google closes this gap by utilising its existing, battle-tested infrastructure.

For Gemini for Workspace to truly align with the DORA findings on systemic stability, it must move away from the current “one-size-fits-all” managed feature-set and toward a framework of granular administrative control. This is the aspiration for a truly professional Action Layer.

The Google Workspace Add-on Manifest is an existing way for enterprises to customise Google Workspace. Today, developers use a manifest to define which products an add-on supports, such as Gmail or Docs. To enable the next wave of productivity, Google could add the Gemini App (gemini.google.com) as a first-class, configurable product within this manifest. This would effectively turn the “Add-on” from a static sidebar tool into a dynamic Action that Gemini can call upon.

In this vision, a developer defines the location of their MCP server directly within the manifest. This creates a standardised way to deliver agentic tools through the existing Marketplace. This is not about building another static side panel. Rather, it is about injecting new capabilities directly into the core reasoning engine of the Gemini App and the integrated Gemini side panels across Workspace.

Imagine a “Tableau for Workspace” add-on. Instead of a user switching tabs to query a dashboard, the add-on uses the Tableau MCP toolset to allow Gemini to “see” live visualisations. The user can simply ask their Gemini side panel in Google Docs to “Insert a summary of our regional performance from Tableau,” and the agent executes the query, interprets the data, and drafts the text in situ.

This transformation would provide the necessary governance layer to ensure that AI-accelerated workflows remain under human control. By allowing admins to curate and configure these agentic tools, Google can turn Workspace into a high-quality internal platform. It would magnify the strengths of its users without introducing the chaos of ungoverned automation. The path to enterprise productivity is paved by protocol, not just better models. The winners of the next decade will not be the companies that buy the most AI licenses, but those that have the best platforms for those AI agents to inhabit.

Vertex AI Advanced Service in Apps Script: A Step Forward or a Missed Opportunity?

As Google Apps Script developers, we are used to waiting. We wait for new runtime features, we wait for quotas to reset, and recently, we have been waiting for a first-class way to integrate Gemini into our projects.

With the recent release of the Vertex AI Advanced Service, the wait is technically over. But as detailed in Justin Poehnelt’s recent post, Using Gemini in Apps Script, you might find yourself asking if this was the solution we were actually looking for.

While the new service undoubtedly reduces the boilerplate code required to call Google’s AI models, it brings its own set of frustrations that leave me, and others in the community, feeling somewhat underwhelmed.

The “Wrapper” Trap

On the surface, the new VertexAI service looks like a win. As Justin highlights, replacing complex UrlFetchApp headers with a single VertexAI.Endpoints.generateContent() call is a significant cleanup.

However, this convenience comes with an administrative price tag. The Vertex AI Advanced Service requires a standard Google Cloud Project, understandable for billing, but requires the creation of an oAuth consent screen. For the majority of internal enterprise applications, I would imagine either a service account or a https://www.googleapis.com/auth/cloud-platform scope and associated IAM will be the preferred approach. This removes the need for a consent screen and, in the case of Service Accounts, rules out the Vertex AI Advanced Service.

It begs the question: Why didn’t Google take the approach of the Google Gen AI SDK?

In the Node.js and JavaScript world, the new Google Gen AI SDK offers a unified interface. You can start with a simple API key (using Google AI Studio) for prototyping, and switch to Vertex AI (via OAuth) for production, all without changing your core code logic. The Apps Script service, by contrast, locks us strictly into the “Enterprise” Vertex path. We seem to have traded boilerplate code for boilerplate configuration.

A Third Way: The Community Approach

If you are looking for that Unified SDK experience I mentioned earlier, where you can use the standard Google AI Studio code patterns within Apps Script, there is a third way.

I have published a library, GeminiApp, which wraps UrlFetchApp but mimics the official Google Gen AI SDK for Node.js. This allows you to write code that looks and feels like the modern JavaScript SDK, handling the complex UrlFetchApp configuration under the hood.

As you can see in the comparison above, the Advanced Service (left) abstracts away the request complexity, the UrlFetchApp method (middle) gives you the transparency and control you often need in production, and the GeminiApp library (right) offers a balance of both.

Disclaimer: As the creator of this library, I admit some bias, but it was built specifically to address the gap.

It is important to note a distinction in scope. Both the Google Gen AI SDK and GeminiApp are focused strictly on generative AI features. The Vertex AI Advanced Service, much like the platform it wraps, offers a broader range of methods beyond just content generation.

If your needs extend into those wider Vertex AI capabilities, but you still require the authentication flexibility of UrlFetchApp (such as using Service Accounts), I have a solution for that as well. My Google API Client Library Generator for Apps Script includes a build for the full Vertex AI (AI Platform) API. This gives you the comprehensive coverage of the Advanced Service with the architectural flexibility of an open-source library.

Here is how you can use the generated client library to authenticate with a Service Account, something impossible with the official Advanced Service:

/**
 * Example using the generated Aiplatform library with a Service Account.
 * Library: https://github.com/mhawksey/Google-API-Client-Library-Generator-for-Apps-Script/tree/main/build/Aiplatform
 */
function callGemini(prompt) {
  const projectId = 'GOOGLE_CLOUD_PROJECT_ID';
  const region = 'us-central1';
  const modelName = 'gemini-2.5-flash';

  const modelResourceName = `projects/${projectId}/locations/${region}/publishers/google/models/${modelName}`;

  const serviceAccountToken = getServiceAccountToken_(); 

  const vertexai = new Aiplatform({
    token: serviceAccountToken
  });

  const payload = {
    contents: [{
      role: 'user',
      parts: [{
        text: prompt
      }]
    }],
    generationConfig: {
      temperature: 0.1,
      maxOutputTokens: 2048
    }
  };

  const result = vertexai.projects.locations.publishers.models.generateContent({
    model: modelResourceName,
    requestBody: payload
  });

  return result.data.candidates?.[0]?.content?.parts?.[0]?.text || 'No response generated.';
}

When “Advanced” Means “Behind”

There is another catch that Justin uncovered during his testing: the service struggles with the bleeding edge.

If you are trying to access the latest “Preview” models to prototype, such as the highly anticipated gemini-3-pro-preview, the advanced service may fail you. It appears the wrapper doesn’t yet support the auto-discovery needed for these newer endpoints.

In his companion post, UrlFetchApp: The Unofficial Documentation, Justin reminds us why UrlFetchApp is still the backbone of Apps Script development. When the “official” wrapper doesn’t support a specific header or a beta model, UrlFetchApp is the only way to bypass the limitations.

The Verdict

The Vertex AI service is a welcome addition for stable, enterprise-focused applications. But for developers, particularly those who want to test the latest Gemini 3 capabilities, it feels rigid compared to the flexibility seen in other Google developer ecosystems.

It serves as a good reminder that in Apps Script, convenience services are great, but understanding the underlying HTTP requests via UrlFetchApp  extends what you can achieve.

Resolve Google Chat Space User IDs to Emails: Least Privilege Approach with Service Accounts and Custom Admin Roles

If you’ve built a Google Chat bot, you’ve likely hit this wall: the API sends you a membership event with a User ID… but omits the email field entirely.

For Google Workspace developers, this limitation in the Chat API is a frequent stumbling block. As detailed in the User resource documentation, while you can use an email address as an alias in requests (e.g., referencing users/[email protected]), the API insists on returning only the canonical resource name (e.g., users/123456789). This ID corresponds to the user’s profile in the People API or Admin SDK, but the email itself is stripped from the response, forcing developers to perform a secondary lookup.

Handling User ID Conversions

I’ve explored this lookup logic before in Beyond the Limits: Automating Google Chat Space Archives. In that solution, I used a helper function to convert the users/{id} format to people/{id} for use with the Advanced People Service.

/**
 * Fetches user details from the People API.
 * @private
 * @param {string} userResourceName The "users/{user}" string from the Chat API.
 * @return {{name: string, displayName: string, email: string|null}} An object with user details.
 */
function getUserDetails_(userResourceName) {
  const defaultUserDetails = {
    name: userResourceName,
    displayName: userResourceName.replace('users/', 'Unknown User '),
    email: null,
  };

  // Fail fast for app users or invalid formats
  if (!userResourceName.startsWith('users/') || userResourceName === 'users/app') {
    return defaultUserDetails;
  }
  
  try {
    const peopleApiResourceName = userResourceName.replace(/^users\//, 'people/');
    
    const person = People.People.get(peopleApiResourceName, {
      personFields: 'names,emailAddresses',
    });
    
    const displayName = person.names?.[0]?.displayName ?? defaultUserDetails.displayName;
    const email = person.emailAddresses?.[0]?.value ?? null;

    return {
      name: userResourceName,
      displayName: displayName,
      email: email,
    };
    
  } catch (e) {
    console.warn(`Could not fetch details for ${userResourceName} from People API: ${e.message}`);
    return defaultUserDetails;
  }
}

However, bridging this gap for a Service Account leads to a security dilemma. The People API often returns empty fields because the Service Account lacks a contact list. You might find yourself reaching for Domain-Wide Delegation to impersonate an admin—effectively using a sledgehammer to crack a nut.

In a recent guide, Justin Poehnelt outlines a more secure strategy that avoids granting blanket domain access. By assigning a custom “Users > Read” Admin Role directly to a Service Account, developers can resolve emails securely without the risks associated with full impersonation.

The Strategy at a Glance

  • Custom Roles: Create a strictly read-only role in the Admin Console.
  • Direct Assignment: Assign this role specifically to the Service Account’s email.
  • No Long-Lived Keys: Use Application Default Credentials (ADC) in production and Service Account Impersonation for local development.

This approach ensures your bot has just enough permission to identify users, keeping your security team happy and your audit logs clean. For more information into the implementation, including the specific configuration steps, I encourage you to read Justin’s full post linked below.

Source: Resolve Chat User IDs to Emails: Least Privilege

Breaking the Grid: 4 Ways to Generate Excel Files from Google Workspace

We have all been there: You build a beautiful, automated dashboard in Google Sheets. It’s dynamic, it’s collaborative, and it’s perfectly integrated with your data sources. Then, the stakeholder request comes in: “Can you email this to me as an Excel file every Monday morning?”

For Google Workspace developers, the “Excel Requirement” is often a stumbling block. While Google Sheets is powerful, the bridge to Excel can be shaky. In this post, I’m going to explore four architectural approaches to solving this problem. If you would like to play along here is the Google Sheet Invoice Template and the Excel Invoice Template.

1. The Low-Code Agent (AppSheet Automation)

Before you write a single line of code, you should consider if the platform can do the heavy lifting for you. Since AppSheet is now a core part of Google Workspace, it offers a native “No-Code” way to generate Excel files that is robust, scheduled, and template-based.

The Strategy

Instead of writing a script to fetch data and build a file, you connect your Google Sheet to an AppSheet app. You then configure an Automation that triggers on a schedule or a data change.

The Workflow:

  1. Trigger: A new row is added to your “Invoices” sheet (or a weekly schedule fires).
  2. Task: AppSheet runs a “Create a new file” task.
  3. Template: It reads a simplified Excel template stored in Drive, replaces tags like <<[Qty]>> with actual data, and generates the file.
  4. Delivery: The file is saved to Drive or emailed directly to the client.

Why This Wins

  • Zero Maintenance: No scripts to debug, no OAuth tokens to manage, and no breaking changes when APIs update.
  • Native Templates: It supports complex Excel formatting out of the box.

The Limitation

  • Syntax: You must use AppSheet’s specific templating syntax (<< >>) inside your Excel file.
  • Scaling: Generating reports with thousands of line items can hit automation timeout limits.

2. The Native Export (Apps Script)

If you need more granular control or don’t want to spin up an AppSheet app, the most common starting point is attempting to do everything within Apps Script. You create a temporary Google Sheet, fill it with data, and then export it using the Drive API.

The Code

In this scenario, we assume you have a Google Sheet version of your Invoice template. We first write the specific line items to the Sheet, then export it.

function generateInvoiceExport() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('InvoiceTemplate');
  
  // 1. Prepare Invoice Data
  const invoiceData = [
    { desc: "Web Development - Phase 1", qty: 40, unit: 100 },
    { desc: "Server Hosting (Annual)", qty: 1, unit: 250 },
    { desc: "Domain Registration", qty: 2, unit: 15 },
    { desc: "Maintenance Retainer", qty: 10, unit: 85 }
  ];
  
  // 2. Map data to the Sheet structure (Cols B, C, D, E, F)
  // B=Desc, E=Qty, F=Unit. (C and D are skipped/empty)
  const rows = invoiceData.map(item => [item.desc, "", "", item.qty, item.unit]);

  // 3. Write data to the Google Sheet
  // Matches Template: Start at Row 19, Column B (2)
  if (rows.length > 0) {
    // Corrected: ensure the number of columns (5) matches your data structure
    sheet.getRange(19, 2, rows.length, 5).setValues(rows);
  }
  
  // 4. Construct the export URL for the populated sheet
  const fileName = `Invoice_${new Date().toISOString().slice(0,10)}.xlsx`;
  const url = `https://docs.google.com/spreadsheets/d/${ss.getId()}/export?format=xlsx&gid=${sheet.getSheetId()}`;
  
  // 5. Fetch and Save
  const params = {
    method: "GET",
    headers: { "Authorization": `Bearer ${ScriptApp.getOAuthToken()}` },
    muteHttpExceptions: true
  };
  
  const response = UrlFetchApp.fetch(url, params);
  
  if (response.getResponseCode() === 200) {
    const blob = response.getBlob().setName(fileName);
    const file = DriveApp.createFile(blob);
    console.log(`Success: Invoice saved to Drive (ID: ${file.getId()})`);
  } else {
    console.log(`Error: ${response.getContentText()}`);
  }
};

The Limitations

  • Speed & Scale: The overhead of creating a temporary sheet and exporting it adds up. Generating 50 invoices in a loop will take a long time.
  • The “Source” Gap: Your source material must be a Google Sheet, meaning you lose Excel-specific features like Power Query or VBA macros from your template.

3. The Browser Round-Trip (ExcelJS)

If you need to maintain stricter fidelity to an existing Excel template (preserving fonts, colors, and layouts) and are interested in speed, you can use the “Round Trip” technique.

This method uses the user’s browser as a processing engine. You fetch the Invoice.xlsx template from Drive, process it client-side with ExcelJS, and send the result back.

View the Code Project

The Strategy

  1. Parallel Fetching (Client): The browser fires concurrent requests to Apps Script to fetch the template (Base64) and invoice data.
  2. Client-Side Processing: ExcelJS loads the template, injects data into specific cells (preserving layout), and recompiles the file.
  3. Upload: The new file is encoded and sent back to Drive.

The Code

This snippet focuses on the core orchestration logic. For the full UI setup and helper functions, see the linked project.

/* --- CLIENT SIDE (Core Logic) --- */
async function startGeneration() {
  
  // 1. Parallel Fetching
  // Fire both requests at once to speed up the wait time
  const [templateResponse, invoiceData] = await Promise.all([
    runGoogleScript('getTemplateBase64', null), 
    runGoogleScript('getInvoiceData')
  ]);

  // 2. Load into ExcelJS
  const workbook = new ExcelJS.Workbook();
  const binaryString = atob(templateResponse.base64);
  const bytes = new Uint8Array(binaryString.length);
  for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } await workbook.xlsx.load(bytes.buffer); // 3. Modify the Sheet const sheet = workbook.worksheets[0]; let currentRow = 19; // Starting at Row 19 based on template invoiceData.forEach((item, index) => {
    // Ensure we do not write past the visual template limit (e.g. Row 22)
    if (currentRow + index > 22) return;

    const row = sheet.getRow(currentRow + index);
    
    // Inject data into specific cells (B, E, F)
    row.getCell(2).value = item.desc;  
    row.getCell(5).value = item.qty;   
    row.getCell(6).value = item.unit;  
    
    // ExcelJS preserves the existing styles of these cells!
    row.commit();
  });

  // 4. Export & Save
  const outBuffer = await workbook.xlsx.writeBuffer();
  
  // Convert buffer to base64 (simplified for brevity)
  let binary = '';
  const outBytes = new Uint8Array(outBuffer);
  for (let i = 0; i < outBytes.length; i++) {
    binary += String.fromCharCode(outBytes[i]);
  }
  const outBase64 = btoa(binary);

  // Upload back to Drive
  await runGoogleScript('saveProcessedFile', outBase64, `Invoice_${Date.now()}.xlsx`);
}

/* --- SERVER SIDE (Apps Script) --- */
function getTemplateBase64(templateId) {
  const file = DriveApp.getFileById(templateId);
  return {
    base64: Utilities.base64Encode(file.getBlob().getBytes())
  };
}

function saveProcessedFile(base64Content, fileName) {
  const blob = Utilities.newBlob(
    Utilities.base64Decode(base64Content), 
    MimeType.MICROSOFT_EXCEL, 
    fileName
  );
  return { url: DriveApp.createFile(blob).getUrl() };
}

The Trade-off

  • Payload Limits: google.script.run calls can become unstable when the payload exceeds 50MB.
  • Base64 Inflation: Remember that Base64 encoding increases file size by ~33%.

4. The Enterprise Engine (Cloud Functions + Python)

For robust, high-fidelity reporting, we need to leave the Google Sheets conversion pipeline behind. By using Google Cloud Functions with Python, we can manipulate native Excel files directly using libraries like openpyxl.

The “User Identity” Pattern

Instead of managing complex Service Account permissions, this solution uses the User’s Identity to perform Drive operations.

  1. Apps Script: Generates an OIDC ID Token (for identity), using getIdentityToken()) and an OAuth Access Token (for Drive permissions) with getOAuthToken().
  2. Cloud Function: Verifies the ID Token and uses the Access Token to initialise the Drive API client as the user.

Setup Instructions

1. Google Cloud Function (Python)

  • Deploy the Function:
    • Source: cloud-fn.py
    • Runtime: Python 3.x
    • Entry point: generate_excel_report
    • Requirements: functions-frameworkgoogle-authgoogle-api-python-clientopenpyxl
  • Configure Environment:
    • Open cloud-fn.py and update APPS_SCRIPT_CLIENT_ID with your Apps Script’s OAuth Client ID.
      Note: You can find this ID by running the logClientId() helper function in Code.js.

2. Google Apps Script (Code.js)

  • Update Manifest (appsscript.json): Add the https://www.googleapis.com/auth/userinfo.email scope.
  • Configuration: Update YOUR_CLOUD_FUNCTION_URL in callInvoiceEngine() with your deployed function URL.

The Code

Google Apps Script (Code.js)

function callInvoiceEngine() {
    const url = "YOUR_CLOUD_FUNCTION_URL";

    // 1. Get Tokens
    const accessToken = ScriptApp.getOAuthToken();
    const idToken = ScriptApp.getIdentityToken(); // Requires 'openid' scope in manifest

    // --- LOGGING FOR LOCAL TESTING ---
    console.log("--- CREDENTIALS FOR LOCAL TESTING ---");
    console.log("ID Token:", idToken);
    console.log("Access Token:", accessToken);
    console.log("-------------------------------------");

    const payload = {
        templateId: "ID_OF_TEMPLATE_FILE",
        folderId: "ID_OF_DESTINATION_FOLDER",
        // Matches the same dataset used in our other examples
        data: [
            { desc: "Web Development - Phase 1", qty: 40, unit: 100 },
            { desc: "Server Hosting (Annual)", qty: 1, unit: 250},
            { desc: "Domain Registration", qty: 2, unit: 15},
            { desc: "Maintenance Retainer", qty: 10, unit: 85}
        ],
        // The Magic Key: Passing the active user's Access Token for Drive operations
        userToken: accessToken
    };

    const options = {
        method: "post",
        contentType: "application/json",
        headers: {
            "Authorization": `Bearer ${idToken}`
        },
        payload: JSON.stringify(payload)
    };

    UrlFetchApp.fetch(url, options);
}

function logClientId() {
    // Helper to find the Project's OAuth Client ID
    var token = ScriptApp.getOAuthToken();
    var url = 'https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=' + token;
    var response = UrlFetchApp.fetch(url);
    var data = JSON.parse(response.getContentText());
    
    console.log('Client ID:', data.issued_to);
}

Pro Tip: Batching Requests

If you need to generate 50 invoices at once, don’t loop through them one by one. Use UrlFetchApp.fetchAll(requests) to fire off parallel requests to your Cloud Function. Since the heavy lifting happens in the cloud, Apps Script just acts as the orchestrator.

Google Apps Script (appsscript.json)

{
    "oauthScopes": [
        "https://www.googleapis.com/auth/drive",
        "https://www.googleapis.com/auth/script.external_request",
        "https://www.googleapis.com/auth/userinfo.email"
    ]
}

Cloud Function (cloud-fn.py)

import requests
import json

def run_test():
    print("--- Local Cloud Function Tester ---")
    print("Ensure you are running: functions-framework --target=generate_excel_report --source=cloud-fn.py --port=8080\n")

    # 1. Gather Tokens & IDs
    print("Paste your Apps Script Tokens (use Logger.log in GAS):")
    id_token = input("1. ID Token (ScriptApp.getIdentityToken()): ").strip()
    if not id_token: return
    
    access_token = input("2. Access Token (ScriptApp.getOAuthToken()): ").strip()
    if not access_token: return

    template_id = input("3. Google Sheet/Excel Template ID: ").strip()
    folder_id = input("4. Destination Folder ID: ").strip()

    # 2. Prepare Payload
    payload = {
        "templateId": template_id,
        "folderId": folder_id,
        "data": [
            { "desc": "Local Test Item 1", "qty": 1, "unit": 10 },
            { "desc": "Local Test Item 2", "qty": 5, "unit": 20 }
        ],
        "userToken": access_token
    }

    headers = {
        "Authorization": f"Bearer {id_token}",
        "Content-Type": "application/json"
    }

    # 3. Send Request
    url = "http://localhost:8080"
    print(f"\nSending POST request to {url}...")
    
    try:
        response = requests.post(url, json=payload, headers=headers)
        print(f"\nStatus Code: {response.status_code}")
        print(f"Response Body: {response.text}")
            
    except requests.exceptions.ConnectionError:
        print(f"\n[!] Could not connect to {url}.")
        print("Is the functions-framework running?")

if __name__ == "__main__":
    run_test()

Local Test Script (test_request.py)

Before deploying, you can test the entire flow locally to ensure your tokens and permissions are working correctly.

  1. Start the Function: functions-framework --target=generate_excel_report --source=cloud-fn.py --port=8080
  2. Run the Tester: python3 test_request.py (Paste tokens from Apps Script logs when prompted).
import requests
import json

def run_test():
    print("--- Local Cloud Function Tester ---")
    print("Ensure you are running: functions-framework --target=generate_excel_report --source=cloud-fn.py --port=8080\n")

    # 1. Gather Tokens & IDs
    print("Paste your Apps Script Tokens (use Logger.log in GAS):")
    id_token = input("1. ID Token (ScriptApp.getIdentityToken()): ").strip()
    if not id_token: return
    
    access_token = input("2. Access Token (ScriptApp.getOAuthToken()): ").strip()
    if not access_token: return

    template_id = input("3. Google Sheet/Excel Template ID: ").strip()
    folder_id = input("4. Destination Folder ID: ").strip()

    # 2. Prepare Payload
    payload = {
        "templateId": template_id,
        "folderId": folder_id,
        "data": [
            { "desc": "Local Test Item 1", "qty": 1, "unit": 10 },
            { "desc": "Local Test Item 2", "qty": 5, "unit": 20 }
        ],
        "userToken": access_token
    }

    headers = {
        "Authorization": f"Bearer {id_token}",
        "Content-Type": "application/json"
    }

    # 3. Send Request
    url = "http://localhost:8080"
    print(f"\nSending POST request to {url}...")
    
    try:
        response = requests.post(url, json=payload, headers=headers)
        print(f"\nStatus Code: {response.status_code}")
        print(f"Response Body: {response.text}")
            
    except requests.exceptions.ConnectionError:
        print(f"\n[!] Could not connect to {url}.")
        print("Is the functions-framework running?")

if __name__ == "__main__":
    run_test()

Why This Wins

  • True Excel Support: You are editing the actual binary file, not a conversion.
  • Separation of Concerns: The heavy lifting is done in the cloud, not in the browser or the Apps Script runtime.

Summary: Which Tool for Which Job?

Feature AppSheet Apps Script (Native) Client-Side (ExcelJS) Cloud Functions (Python)
Complexity Very Low Low High High
Cost Free Free Free GCP Usage (Low)
Best For Standard Workflows Simple Reports Interactive Template Fills Enterprise Reports

Final Thoughts

There is no “one size fits all” here. The “Excel Requirement” is a spectrum:

  • For quick internal tools: Stick to Method 1 (AppSheet) and Method 2 (Native). It’s fast to build and free.
  • For polished client reports: Method 3 (ExcelJS) is your best friend. It keeps the heavy lifting in the browser and preserves your branding.
  • For enterprise scale: If you are generating hundreds of invoices a week, Method 4 (Cloud Functions) is the robust choice. It decouples the generation logic from the spreadsheet UI.

Choose the architecture that fits your volume and your “pixel-perfect” tolerance. I’m also keen to hear have got another ‘Excel Generator’ approach? Happy scripting!

StackOverflow Trends 2026: The Structural Shift from Human Support to Generative AI

Analyzing StackOverflow data (2008–2026) reveals a massive activity decline post-ChatGPT. Using Google Apps Script as a case study, this report quantifies the migration from human support to AI. We explore how the platform is pivoting from a help desk to a critical verification layer for AI-generated code to prevent model collapse.

It’s that time of year again. Kanshi Tanaike has just released his seventh annual report on the state of the google-apps-script tag on Stack Overflow. The numbers show a massive drop in platform activity since late 2022, which Tanaike identifies as a “structural shift” from human support to Generative AI.

But if you’re like me, you’re probably wondering whether the community is actually shrinking or if we are just working differently.

It’s Not Just Automation; It’s “Vibe Coding”

Tanaike’s data points to a decline in question volume, suggesting that AI has taken over the routine heavy lifting. While that is part of the story, I believe there is a more nuanced explanation for those of us in the Apps Script world.

Rather than task automation I think it’s more about “vibe coding”. We are all using LLMs to prototype, troubleshoot, and get through that initial hurdle of “how do I even start this?”. AI is absorbing the iterative problem-solving that used to happen in public threads, effectively acting as a filter for the simpler questions that used to dominate the tag.

The Shift to a “Verification Layer”

One of the most interesting parts of the report is Tanaike’s argument that Stack Overflow is transitioning from a high-volume help desk to a high-quality Verification Layer.

As the web gets flooded with AI snippets, the value of the “ground truth” on Stack Overflow actually goes up. Tanaike noticed that while scripting languages saw a sharp drop in activity, enterprise-heavy languages like PHP and Swift held steady. To me, that is a clear sign of where AI still struggles. It can write a function, but it cannot always understand the deep architectural context of a complex legacy system. That remains a human-only job.

Maturation, Not Decline

AI is great at the first 80%. It handles the syntax and the boilerplate, but the remaining 20%, the part that actually makes a solution robust and secure, is where we come in. You can read more and make your own conclusions in Kanshi Tanaike’s post.

Source: StackOverflow Trends 2026: The Structural Shift from Human Support to Generative AI

Agentic Security Audits with the Google Workspace Policy API and ADK

“Clicking through the Google [Workspace] Admin Console to verify hundreds of settings is manual labour. How to create an agent with Gemini 3 (Flash) that will check the settings continuously?”

In a recent post, Ivan Kutil explores how to move beyond manual checks for the Google Workspace Admin console by building a specialised “Auditing Agent” using the Agent Development Kit (ADK) and the Policy API.

“Clicking through the Google [Workspace] Admin Console to verify hundreds of settings is manual labour. How to create an agent with Gemini 3 (Flash) that will check the settings continuously?”

The Architecture: Policy API meets ADK

At the heart of Ivan’s solution is a very repeatable pattern, bridging the gap between Google Workspace APIs, in this case the Policy API, and a natural language interface. By using the ADK, developers can create tools that allow an LLM to “handshake” with Workspace data without writing a custom function for every single API endpoint:

ADK simplifies interacting with external REST APIs by automatically generating callable tools directly from an OpenAPI Specification (v3.x). This eliminates the need to manually define individual function tools for each API endpoint [ref].

In Ivan’s example, he uses AI Studio to generate the OpenAPI 3 YAML file required by the OpenAPIToolset in ADK. Google currently defines many of its APIs through the Google Discovery service in JSON syntax, and Ivan highlights the need for transformation. Gemini is very good at this, but there are a number of community tools and repositories to transform the Discovery Document into an OpenAPI file, such as the StackQL Provider Google project and its associated YAML repository (the Policy API is found in the Cloud Identity service).

Note: While the Policy API provides a powerful programmatic window into your Workspace domain, it is worth noting that it does not yet provide 100% coverage of all settings available in the Admin Console. Developers should verify the specific policy types supported when designing their auditing logic.

Agentic Auditing in Action

The resulting agent allows an administrator to simply ask questions about their Workspace environment, the agent then fetches live data, help identify risks (such as missing MFA or risky API access), and provides a structured summary with actionable steps.

This project serves as a powerful blueprint for developers looking to build “agentic” workflows enhances capabilities by grounding them with data from Workspace APIs with intelligent, conversational automation.

Source: Building a Google Workspace Security Agent with ADK and Policy API

Q4 2025 Developer Roundup: AI Agents, Custom Chat Workflows, and the Future of Apps Script

The final quarter of 2025 has been a transformative period for the Google Workspace ecosystem, centred on the evolution of “Agentic” workflows. With the transition of Google Workspace Flows into the newly branded Google Workspace Studio, developers now have a centralised platform for building AI-powered automations. From deep Chat customisation to enhanced Meet artifacts, here’s a roundup of the key updates from October, November, and December that you need to know about.

Google Workspace Studio & Add-ons

The most significant shift this quarter is the general availability of Google Workspace Studio (formerly Workspace Flows). This platform allows developers to build AI agents that automate everyday work across the ecosystem.

  • Studio Extensions: Developers can now build Google Workspace Add-ons that extend Studio, allowing for custom logic and integrations within AI-driven processes (this feature is in “Limited Preview” likely due to the extended rollout period for Workspace Studio).
  • Dialogflow Integration: In a new developer preview, Chat apps built as Google Workspace add-ons can now use Dialogflow CX and Dialogflow ES, enabling more sophisticated natural language conversations within the Workspace interface.

Google Chat API: Deep Personalization

The Chat API received continued substantial updates focused on helping users organise information and enhancing app interactivity.

  • Custom Sections: A major developer preview now allows the Chat API to programmatically create and manage custom sections. Developers can now createdelete, and position these sections to organise conversations in a personalised view.
  • Membership Roles: Membership management has been refined with the introduction of new role types. In the Chat UI, existing space managers are now Owners, and a new Manager role (Role Assistant Manager) has been added to help administer spaces.
  • Rich Links and Accessories: Generally available updates include support for reading rich links to Gmail threads and the ability for Chat apps to include interactive accessory widgets in private messages.

Google Meet: Working with Artifacts

Real-time data accessibility was a core focus for Meet this quarter, particularly around the Developer Preview Program.

  • Smart Notes Retrieval: You can now retrieve smart notes files in a conference record using the get and list methods on the smartNotes resource.
  • Event Subscriptions: Calendar invitees can now receive started and transcript generated events through the Google Workspace Events API.
  • Meeting Identification: New fields for phoneAccess and gatewaySipAccess have been added to the spaces resource to help identify and join meeting spaces.

Google Drive API

  • Comment and Reply Events: In a new developer preview, developers can now subscribe to comment and reply events in Google Docs, Sheets, and Slides. This is a powerful feature for building reactive applications that monitor file collaboration in real-time.

Gmail & Calendar: Ownership and Insights

  • Postmaster Tools v2: The Gmail API v1 now offers a developer preview for Postmaster Tools v2. This includes compliance status data and the queryDomainStats method for flexible querying.
  • Secondary Calendar Ownership: As of November, secondary calendars have moved to a single data-owner model. This change restricts the ability to delete or manage secondary calendars to the designated data owner, preventing unintended actions across organisations.

The Road Ahead

This quarter has shown a clear trajectory toward more intelligent, “agentic” integrations within Google Workspace. From the rollout of Google Workspace Studio to the granular control provided by new Chat and Drive events, the platform is becoming increasingly responsive to automated workflows.

It will be exciting to see how the community uses these new capabilities in the coming months. As for the future of Apps Script, Q1 2026 I think will be very exciting, stay tuned!!!

The Age of the Agent: Google’s Managed MCP Servers and Community Contributions

Image credit: Kanshi Tanaike

 

The “USB-C for AI” has officially arrived at Google.

If you have been following the rapid rise of Anthropic’s Model Context Protocol (MCP), you know it promises a standardised way for AI models to connect to data and tools. Until now, connecting an LLM to your Google Drive or BigQuery data meant building custom connectors or relying on fragmented community solutions.

That changed this week with Google’s announcement of fully managed, remote MCP servers for Google and Google Cloud services. But typically for the Google Workspace developer community, we aren’t just waiting for official endpoints—we are building our own local solutions too.

Here is an overview of the different approaches emerging for MCP tools and what the future looks like for Workspace developers.

The Official Path: Managed MCP Servers

Google’s announcement represents a massive shift in how they view AI agents. Rather than asking developers to wrap every granular API endpoint (like compute.instances.insert) into a tool, Google is releasing Managed MCP Servers.

Think of these as “Client Libraries for Agents.” Just as Google provides SDKs for Python or Node.js to handle the heavy lifting of authentication and typing, these managed servers provide a simplified, reasoned interface for agents.

The announcement lists a massive rollout over the “next few months” covering everything from Cloud Run and Storage to the Android Management API. The goal described is to make MCP the standard way agents connect to the entire Google ecosystem. To start with Google announced MCPs for:

  • Google Maps: instead of just raw data, the agent gets “Grounding Lite” to accurately answer location queries without hallucinating.
  • BigQuery: The agent can interpret schemas and execute queries in place, meaning massive datasets don’t need to be moved into the context window.
  • Google Kubernetes Engine (GKE): Agents can diagnose issues and optimize costs without needing to string together brittle CLI commands.

The Community Path: Privacy-First and Scriptable

While managed servers are excellent for scale and standardisation, there are some other notable approaches using local execution.

1. The Gemini CLI Workspace Extension As detailed in a recent tutorial by Romin Irani, the official google-workspace extension for the Gemini CLI takes a privacy-first approach. Instead of running in the cloud, the MCP server runs locally on your machine.

It uses your own local OAuth token, meaning your data flow is strictly Google Cloud <-> User's Local Machine <-> Gemini CLI. This is ideal for tasks like searching Drive, checking your Calendar, or drafting Gmail replies where you want to ensure no third-party intermediary is processing your data.

2. The Power of Apps Script (gas-fakes) For true customisability, Kanshi Tanaike has been pushing Google Apps Script. A recent proof-of-concept using gas-fakes CLI demonstrates how to build MCP tools directly using Apps Script.

This is a big step for Apps Script developers. It allows you to write standard GAS code (e.g., DriveApp.getFilesByName()) and expose it as a tool to an MCP client like Gemini CLI or Google Antigravity. Because it uses the gas-fakes sandbox, you can execute these scripts locally, bridging the gap between your local AI agent and your cloud-based Workspace automation.

The Future: A Universal MCP Strategy?

We are likely moving toward a world where every Google product launch includes three things: API documentation, Client SDKs, and an Official MCP Server Endpoint.

For developers, the choice will come down to granularity and control:

  • Use Managed Servers when you need reliable, standard access to robust services like BigQuery or Maps with zero infrastructure overhead.
  • Use Community/Local Tools (like the Workspace extension or gas-fakes) when you need rapid prototyping, deep customization, or strict data privacy on your local machine.

The “Agentic” future is here, and whether you are using Google’s managed infrastructure or writing your own tools, the ecosystem is ready for you to build.

Sources:

Bring Your AI Agents to Life Across Google Workspace with the New Travel Concierge Sample

This tutorial shows you how to publish AI agents to Google Workspace as Google Workspace add-ons, using Apps Script or HTTP endpoints. After your publish your add-on, your users can interact with the AI agents within their workflows.

Google has released a comprehensive new tutorial that demonstrates how to bridge the gap between complex AI agents and the daily workflows of Google Workspace users. The Travel Concierge sample uses the Agent Development Kit (ADK) to deploy a conversational, multi-agent AI directly into the Google Workspace platform.

While many AI demos focus on standalone chat interfaces, this solution stands out by implementing the agent as a Google Workspace Add-on. This means the AI isn’t just a browser tab; it is a helpful sidebar accessible directly within Gmail, Calendar, Drive, Docs, Sheets, and Slides, as well as a fully functioning Google Chat app.

Key Features for Developers

The sample provides a robust blueprint for developers looking to build “agentic” workflows. Key capabilities include:

  • Multi-Agent Architecture: The solution uses Vertex AI to manage a “Travel Concierge” that orchestrates sub-agents for specific tasks, utilising tools like the Google Maps Platform Places API and Google Search Grounding.
  • Context Awareness: In applications like Gmail, the agent can read the context of the currently selected email to help plan trips based on actual correspondence.
  • Rich User Interfaces: The add-on moves beyond simple text, utilising the Card framework to display rich widgets and interactive elements.
  • Cross-Platform Persistence: User sessions are managed by Vertex AI, ensuring that a conversation started in Google Chat can be seamlessly continued in a Google Doc sidebar.

Flexible Implementation

The Apps Script implementation demonstrates how to connect a standard Workspace Add-on to the powerful Vertex AI backend using UrlFetchApp. It serves as an excellent reference for handling synchronous execution limits and rendering complex agent responses within the constraints of the Add-on sidebar.

Whether you are looking to build a travel planner or a complex enterprise assistant, this sample provides the architectural patterns needed to bring your AI agents to where your users actually work.

Source: Plan travels with an AI agent accessible across Google Workspace  |  Google Workspace add-ons  |  Google for Developers

Creating a ‘Knowledge Doc’ for Gemini: Consolidating Google Workspace Flows Documentation

How to build a server-less crawler using Google Apps Script to create a unified knowledge base for AI-assisted development.

The Goal: A Unified Context for Gemini

When building Google Workspace add-ons, particularly those using newer features like Workspace Flows, the documentation is often spread across dozens of nested pages. While excellent for browsing, this structure presents a challenge when working with Large Language Models (LLMs) like Gemini.

The primary motivation for this project was to create a single, consolidated “Knowledge Doc” containing the complete technical specification for Workspace Flows. By feeding this unified document into Gemini, I can:

  • Control the Context: Ensure the model answers based on the specific, official documentation rather than outdated training data or hallucinated APIs.
  • Accelerate Development: Ask complex architectural questions (“How do I build a flow that integrates X and Y?”) and get answers grounded in the full API reference without the latency of multiple web searches.
  • Bridge the Knowledge Gap: Work effectively with brand-new products and services that haven’t yet been absorbed into the LLM’s core training set.

This article details how I built the tool to create this document—a server-less crawler that runs entirely within Google Apps Script. If you are less concerned about the how and more about the end result here is the [Shared] Google Workspace Flows Guide

The Technical Challenge: Apps Script vs. The DOM

My initial plan was straightforward:

  1. Fetch the HTML of each documentation page.
  2. Parse it into Markdown (using a library like Turndown or Showdown).
  3. Convert that Markdown into a Google Doc.

However, I hit a major roadblock immediately. Most JavaScript parsing libraries rely on the DOM (windowdocumentDOMParser). Google Apps Script runs in a server-side environment (similar to a Node.js runtime but without standard packages) where no DOM exists. Libraries like Turndown crashed instantly with ReferenceError: document is not defined.

The Solution: A Hybrid HTML Pipeline

Instead of fighting to make a browser library work on the server, I pivoted to a hybrid approach:

  1. Cheerio for Parsing: I used the Cheerio library (which parses HTML using pure JavaScript strings, no DOM required) to “clean” the content.
  2. Direct HTML Injection: Instead of converting to Markdown (which lost formatting for tables and images), I switched to the Google Drive API which can natively convert HTML files into Google Docs.

This pipeline proved superior because it preserved:

  • Code Blocks: <pre> tags with specific formatting.
  • Tables: Complex data tables rendered perfectly.
  • Images: <img> tags with absolute URLs were automatically fetched and embedded by Google Drive.

Important Note: This script was developed and tested specifically for Google Workspace Flows documentation. Web scraping inherently carries the risk that the target website’s HTML structure may change over time. Other documentation sites will use different CSS classes and HTML structures. While the core logic (crawling + HTML cleaning + Docs conversion) remains valid, you will need to inspect the source of your target site and update the Cheerio selectors (like .devsite-article-body or .devsite-book-nav) to match its specific layout.

Step 1: The Crawler

First, we needed to find all the relevant pages. Traversing the DOM tree proved fragile—class names change, and nesting levels vary.

The robust solution was Path Filtering. We fetch the main navigation bar and simply filter for any link that starts with our target root URL.

function extractLinksByPath(html, rootUrl) {
  const $ = Cheerio.load(html);
  const links = new Set();

  $('.devsite-book-nav a').each(function() {
    let href = $(this).attr('href');
    if (href) {
      if (href.startsWith('/')) href = CONFIG.BASE_URL + href;
      if (href.startsWith(rootUrl) && !href.includes('#')) links.add(href);
    }
  });
  return Array.from(links);
}

Step 2: The HTML Cleaner

We couldn’t just dump the raw page HTML into a Doc; it would include navigation bars, “Feedback” buttons, and footer links. We used Cheerio to strip these out.

We also encountered a specific challenge with Definition Lists (<dl><dt><dd>). Google Docs doesn’t have a native “Definition List” element, so they often flattened into messy text.

The fix was to transform them into semantic HTML paragraphs with inline styling to force the visual layout we wanted:

// Transform <dt> (Terms) into bold/italic paragraphs
$body.find('dl').each((i, dl) => {
    let newContent = '';
    $(dl).children().each((j, child) => {
      const $child = $(child);
      if ($child.is('dt')) {
        newContent += `<p style="font-weight:bold; font-style:italic; margin-top:12pt; margin-bottom:4pt; font-size:11pt; color:#202124;">${$child.html()}</p>`;
      } else if ($child.is('dd')) {
        newContent += `<p style="margin-left:24pt; margin-bottom:8pt; font-size:11pt; line-height:1.15;">${$child.html()}</p>`;
      }
    });
    $(dl).replaceWith(`<div>${newContent}</div>`);
  });

Step 3: Post-Processing with the Docs API

Even with clean HTML, the Google Docs converter can be stubborn. It sometimes ignores semantic headings (<h1>) or allows images to overflow the page width.

To fix this, we added a post-processing step. After the HTML file is converted to a Google Doc, we open it with DocumentApp to programmatically enforce styles and constraints.

Fixing Headings: We scan for our specific “Title” styling (24pt Bold) and force the Heading 1 style.

function applyHeadingsToDoc(docId) {
  const doc = DocumentApp.openById(docId);
  const body = doc.getBody();
  const paragraphs = body.getParagraphs();

  paragraphs.forEach(p => {
    const text = p.getText();
    if (text.length === 0) return;

    const fontSize = p.getAttributes().FONT_SIZE;
    const isBold = p.getAttributes().BOLD;
    // If it looks like a H1 (24pt + Bold), make it a H1!
    if (fontSize >= 24 && isBold) {
      p.setHeading(DocumentApp.ParagraphHeading.HEADING1);
    }
  });

  doc.saveAndClose();
}

Resizing Images: We also scan for images wider than the standard page width (e.g., 600px) and scale them down to fit, ensuring the document layout remains printable and clean.

function resizeImagesInDoc(docId) {
  const doc = DocumentApp.openById(docId);
  const body = doc.getBody();
  const MAX_WIDTH = 600;

  body.getImages().forEach(image => {
    const width = image.getWidth();
    const height = image.getHeight();
    if (width > MAX_WIDTH) {
      image.setWidth(MAX_WIDTH);
      image.setHeight((height / width) * MAX_WIDTH);
    }
  });
  doc.saveAndClose();
}

The Workflow in Action

Once generated, this document becomes the cornerstone of my development workflow:

  1. Consolidate: I run the script to pull the latest documentation into a fresh Google Doc.
  2. Enrich: I use tools like Gemini Deep Research to find community patterns, or architectural best practices and paste them directly into the same doc.
  3. Contextualise: This single document is then uploaded to Gemini (or another LLM workspace), providing a focused, hallucination-resistant context for answering questions and generating code.

By combining the UrlFetchApp for crawling, Cheerio for parsing, and the Drive API for document generation, we built a custom scraping pipeline in under 200 lines of code without spinning up a single server. If you’d like to make your own feel free to copy this script project which comes with a compiled Apps Script friendly version of Cheerio