The most striking demonstration at Google Cloud Next ‘26 was the “Chat Command Centre”. Julie Quan Kim showed how a user might invoke a “regional campaign skill” to orchestrate complex tasks across Docs, Slides, and external CRM data. While we have yet to see how “skills” will be created and managed, developers can already prototype these agentic behaviours using the Workspace CLI (gws) and the Gemini CLI.
These tools are the perfect laboratories for developing “skills” that could eventually be used in your own Workspace Intelligence platform. To test the boundaries of this, I have been building a “recipe” that fetches a webpage, converts the content into a natively formatted Google Doc, and then executes additional logic via Google Apps Script.
The Practical Bridge: The Web-to-Doc Recipe
This recipe, published in this repo, hopefully demonstrates how we can orchestrate a multi-step workflow using different tools. It is worth noting that the development of this very skill was facilitated by a specialised skill-creator subagent included in the latest Gemini CLI. This dedicated assistant handles the initialisation and packaging of new capabilities, which in itself makes the transition from a manual workflow to a reusable agentic skill remarkably easy.
The core logic of the Web-to-Doc recipe is defined in a simple SKILL.md file, which instructs the local agent to follow a strict pipeline:
Extraction via Node.js: The skill instructs the CLI to execute a local Node.js script (extract_article.js), which uses Mozilla’s readability and turndown libraries. This gives us a clean, “Reader View” markdown version of the article, stripping away the navigation menus and ads. With the announcement of the Gemini Enterprise Agent Platform I think it is very probable that Workspace customers would be able to use similar functions hosted in Cloud Run.
Native Conversion via CLI: Next, the agent passes this markdown file directly to the Workspace CLI. By explicitly setting the MIME type to application/vnd.google-apps.document, the CLI takes advantage of the Drive API’s ability to convert markdown into formatted Google Docs. With the announcement Workspace MCPs, including Drive, a similar conversion should be possible without the Workspace CLI
Refinement via Apps Script: This is my favourite part of the process. To ensure the embedded images do not overflow the document margins, the skill triggers a deployed Google Apps Script function (resizer.gs) using the gws script scripts run command. This is a level of precise document manipulation, specifically calculating page margins to ensure images do not overflow, is one that the high-level Drive API cannot perform during a simple import.
Governance and Security
Moving to this atomic, skill-based architecture is not without its friction. This homebrew approach of using different CLIs and managing authentication scopes across different local and cloud environments can be a severe headache and is very difficult to scale.
It also remains unclear if the Workspace Intelligence skills will include a script.run functionality found in the Workspace CLI. If it is included, it would be an exceptionally powerful way to extend skill capabilities. It would mean being able to build modular, portable sets of very repeatable code using the existing power, functionality and affordances of Apps Script.
This is where it will be very interesting to see how Workspace Intelligence manages this with the appropriate security and governance, especially considering the risks associated with Workspace MCP servers and the risk of indirect prompt injection attacks.
This is a rapidly evolving space. The ability to prototype these behaviours today using the Workspace CLI provides a direct path to understanding how the Agentic Enterprise could operate.
I have spent a significant portion of my career working with various command-line interfaces for Google Workspace, from gcloud to gam. The recent release of the Google Workspace CLI (gws), which is not an officially supported Google product but has a number of Googler contributions, represents a fundamental shift in how we interact with the entire Google API surface.
In this post, I will provide an overview of the gws tool and explain how it differs from the Gemini CLI Workspace Extensions. More importantly, we will examine the modular architecture of “Skills”, “Recipes” and “Personas” that makes this tool in my view a masterpiece in modern, role-based automation.
GWS vs. GAM: Understanding the Overlap
Many Workspace Admins are already deeply familiar with gam (Google Apps Manager), the open-source command-line tool that has long served as a staple for domain administration. While gam is exceptional for bulk administrative tasks, such as creating users, modifying groups, or auditing devices, gws serves a different primary audience.
There is certainly a slight overlap between the two. You will notice administrative skills like gws-admin-reports present in the new repository. However, gws is fundamentally a developer and agentic tool focused on individual productivity applications like Drive, Gmail, and Docs, rather than domain-wide directory administration.
GWS vs. Gemini CLI Workspace Extensions
It is easy to confuse the Google Workspace CLI with the Workspace Extensions available for the Gemini CLI, but they serve entirely different purposes. The Gemini CLI Extension is a high-level interface designed for natural language tasks, such as “Summarize my unread emails” or “Find a file in Drive.” It is a convenient, user-centric tool for common, daily interactions.
In contrast, gws is a developer’s infrastructure tool. While the Gemini Extension is about “doing,” gws is about “orchestrating.” It provides the granular, programmatic control needed to build complex, multi-step workflows that can be automated, tested, and shared across a team.
The Modular Approach: Atomic Skills
The repository breaks down Google Workspace API interactions into highly modular components called “skills”. Instead of a monolithic script handling an entire workflow, the architecture relies on specific, atomic actions. Some examples from the repository include gws-gmail-send and gws-docs-write.
This atomic design means each skill has a single responsibility. For Apps Script developers, this is highly reminiscent of creating focused, single-purpose functions within an Apps Script library. By keeping these functions isolated, the code becomes easier to test, maintain, and reuse across different parts of an application.
Combining Forces: Recipes and Personas
Where this architecture becomes particularly interesting is how it combines these basic building blocks. The repository introduces the concept of “recipes,” such as recipe-backup-sheet-as-csv or recipe-create-expense-tracker. These recipes orchestrate multiple underlying skills to achieve a larger, practical goal.
In addition, these recipes are mapped to specific user “personas” like persona-content-creator or persona-it-admin. By grouping actions based on the user’s operational role, the tool becomes highly intuitive. When building Google Workspace automation tools, adopting a persona-driven approach can significantly improve the user experience: you are no longer just presenting a list of features, but offering targeted workflows tailored to what the user actually needs to accomplish their daily work.
Why this Approach Fits Workspace APIs Perfectly
This architectural choice is particularly effective given the versatile nature of Google Workspace APIs. Consider the Google Drive API, which features a generic endpoint for creating files. A basic integration might simply expose this raw “create file” capability. However, given the massive range of situations where you might want to create a file, a generic tool often leaves users or AI agents guessing how to properly format the request.
This is where skills, recipes, and personas show their true value. They allow us to dictate exactly how a file should be created for a specific context. For instance, if you want to turn a Markdown document into a natively formatted Google Doc, you could try to use the complex Google Docs API to insert text paragraph by paragraph. Instead, a targeted recipe can use the Drive API’s built-in file conversion features. By packaging instructions to upload the Markdown content and explicitly set the target MIME type to application/vnd.google-apps.document, the recipe abstracts away a complex API quirk into a reliable, repeatable action.
The Future of Agentic AI: The CLI + Skills Paradigm
As a recent VentureBeat article highlighted, the real breakthrough of gws is not that it makes Google Workspace programmable, given those APIs have existed for years. Instead, the innovation is the interface itself. By eliminating the need for custom wrappers and endless glue code, the CLI transforms Workspace from a collection of siloed applications into a single, unified programmable runtime.
This touches on a growing industry debate around the Model Context Protocol (MCP). Interestingly, while gws initially launched with a native MCP server mode, the maintainers explicitly removed it in version 0.8.0. Justin Poehnelt, one of the main contributors behind gws, refers to this overhead as the “MCP Abstraction Tax”. Exposing hundreds of granular API tools via a standard MCP server requires passing massive JSON schemas that bloat the AI’s context window and often lead to complex mapping bugs.
Instead, gws is now focused on a pure “CLI + Skills” architecture. In a related post on why developers should “Rewrite your CLI for AI Agents”, Justin outlines a shift from imperative to declarative design. An AI agent should “think” in terms of the high-level business goal (the “what”) rather than worrying about the underlying API steps to edit a document (the “how”). By moving the complex reconciliation logic directly into the CLI library, agents can simply use bash commands, a language they are already highly fluent in, to execute tasks efficiently.
Agent skills supply the “recipes” for these declarative commands, acting as modular, portable sets of instructions that teach an AI agent exactly which CLI tools to execute. The repository already incorporates over 100 of these Markdown skills, following a standardised structure. This framework is a powerful example of how we are moving away from simple prompt engineering toward “Skill Engineering”. By encoding our organisational knowledge and engineering judgement into these portable files, we ensure that AI agents follow token-efficient, reliable workflows every time they interact with our data.
Summary
The Google Workspace CLI is a glimpse into a future where our development environments are natively aware of our roles and workflows. For those interested in mastering this new framework, Anthropic recently shared a Complete Guide to Building Skills for Claude which explores the technical patterns and best practices for creating your own high-order skills.
Whether you are looking to automate your local development loop or build complex agentic workflows, the googleworkspace/cli provides the infrastructure you should keep an eye on!
This article explores integrating remote subagents built with Google Apps Script into the Gemini CLI using the Agent-to-Agent (A2A) protocol.
Google recently announced that subagents have arrived in the Gemini CLI, allowing developers to extend the capabilities of the command-line interface by connecting it to external tools and services. This update provides a way to build more complex, multi-step workflows where Gemini can call upon specialized agents to perform specific tasks.
For the Google Apps Script community, this opens up interesting possibilities for connecting local terminal workflows with the Google Workspace ecosystem. Kanshi Tanaike has already explored this area, sharing a detailed guide on integrating remote subagents built with Google Apps Script with the Gemini CLI.
Extending the CLI with Apps Script
In his post, Tanaike-san demonstrates how to bridge the gap between a local environment and remote script functions. By deploying an Apps Script as a web app, it can act as a “remote subagent” that the Gemini CLI calls to perform actions within Google Workspace or process data using the Apps Script environment.
As Tanaike-san notes:
“By using Google Apps Script as a subagent, Gemini CLI can interact with Google Workspace services like Google Drive, Google Sheets, Google Docs.”
You can find the full technical breakdown, including the necessary code snippets and configuration steps, in Kanshi Tanaike’s blog post.
The growing landscape of AI agent development is overloaded with acronyms: MCP, A2A, UCP, AP2, A2UI, and AG-UI, just to name a few. If you’ve ever looked at this list of protocols and felt like you were staring at a wall of competing standards, you are not alone. To help you understand their value, we are going to demonstrate what each one does to save you from writing and maintaining custom integration code for every single tool, API, and frontend component your agent touches
In Pulse we have recently featured some excellent examples of agentic integration into Google Workspace, in particular, Pierrick Voulet’s insightful posts on extending Chat apps with Universal Actions provide a great template for building intelligent assistants. For those following Pierrick on LinkedIn you’ll know these are just the tip of the iceberg.
With users increasingly expecting these intelligent assistants to communicate seamlessly across their everyday tools, knowing the right standards to connect different systems becomes a significant challenge. Fortunately, the Google Developers Blog has published a comprehensive guide to help navigate this exact problem.
In a recent post there is an overview of the growing list of acronyms associated with AI agent development. The post breaks down six main protocols:
Model Context Protocol or MCP: This standardizes connection patterns for servers. Instead of writing custom API requests for every service, your agent discovers tools automatically.
Agent2Agent Protocol or A2A: This standardizes how remote agents discover and communicate with each other using well-known URLs.
Universal Commerce Protocol or UCP: This modularizes the shopping lifecycle into strongly typed schemas that remain consistent across any underlying transport.
Agent Payments Protocol or AP2: This adds cryptographic proof of authorization to checkout flows to enforce configurable guardrails on transactions.
Agent-to-User Interface Protocol or A2UI: This lets the agent dynamically compose novel layouts from a fixed catalog of safe component primitives.
Agent-User Interaction Protocol or AG-UI: This acts as middleware that translates raw framework events into a standardized stream for the frontend.
To illustrate these concepts, the guide walks through building a multi-step supply chain agent using the Agent Development Kit. The scenario starts with a bare large language model and progressively adds protocols until the agent can check inventory, get quotes, place orders, authorize payments, and render interactive dashboards.
Adopting standard protocols is only half the equation, and how effectively you apply them shouldn’t be overlooked. As an example, Richard Seroter recently highlighted this in his analysis of agent token consumption. He discovered that simply attaching an MCP to an agent often leads to excessive planning iterations and high token costs. By pairing an MCP with a highly focused “Skill”, a structured set of instructions that guides the tool’s application, developers can drastically cut down on wasted turns. In one test, combining an MCP with a specific skill resulted in an 87% reduction in token usage compared to letting the agent figure out the tool on its own.
For Google Workspace developers building complex integrations, it’s important not to just be aware of the emerging protocols, but also the best practices for implementing them. Giving your agent a connection is a great start; teaching it the specific skill to use that connection efficiently is essential.
Late last year, Google introduced Jules, an experimental coding agent designed to handle the heavy lifting of software development asynchronously. While you may have explored Jules through its web interface, the recent release of a dedicated Apps Script SDK by David East opens up interesting possibilities for the Google Workspace developer community.
What is Jules?
For those who missed the initial announcement, Jules is an autonomous agent that integrates with GitHub to fix bugs, refactor code, and build new features. Unlike a standard code completion tool, Jules works in the background within a secure virtual machine. As Google describes it:
“Jules is an asynchronous, agentic coding assistant that integrates directly with your existing repositories”
As of the latest updates, Jules now can use Gemini 3.1 Pro. This upgrade provides a smarter baseline for complex problem-solving, allowing the agent to create multi-step plans with clearer reasoning and stronger intent alignment.
Google also released an alpha API which allows management of your Jules agent programmatically. This means developers can move beyond manual prompting. One potential use case is building a triage system that listens for bug reports in a support channel and automatically invokes Jules to analyse the codebase and prepare a fix.
Integrating with Google Apps Script
The new jules-apps-script-sdk simplifies the process of making these API calls from within the Apps Script environment. The library handles the authentication and request structures required to interact with the Jules service, allowing you to trigger agentic tasks directly from a spreadsheet, a form submission, or a time-based trigger.
For example, you could set up a workflow where a specific comment in a Google Doc triggers an Apps Script function to send a prompt to Jules, requesting a unit test for a new function in your repository. You can find out more by following the source link on GitHub.
Bruce Mcpherson has been working on a series of articles exploring how to liberate native Apps Script logic from its browser-based sandbox. If you’ve been following along, you’ll know he’s previously covered AWS Lambda, Google Cloud Run, and Kubernetes. Bruce has now added Azure Container Apps (ACA) to the list of supported environments.
TL;DR: By using the @mcpher/gas-fakes library and Workload Identity Federation, you can run native Apps Script logic on Azure Container Apps Jobs. This approach offers a massive 24-hour execution window, making it suitable for high-intensity tasks like large data migrations or security scans that would otherwise time out in the standard Google environment.
Moving logic to Azure introduces some complexity, particularly around authentication, which Bruce covers in his post and to bridge this gap, the project includes a custom Identity Bridge to handle the handshake between Azure and Google.
You can read the full technical breakdown and access the comparison matrix on Bruce’s blog.
Editor: We’ve previously featured Zig Mandel’s comprehensive framework that integrates Google Apps Script web apps into a standard website. Zig has been busy with an update which we are reposting from Reddit with permission:
I’ve shipped a major update to my Apps Script Website Integration Framework. The framework now allows running an HTMLService frontend entirely outside the GAS iframe, directly on your website.
Why use this?
HTMLService is convenient, but the iframe environment blocks a lot of modern web-dev capabilities: slow load, limited browser APIs, no TypeScript, no React, no Vite/live-reload, no custom domains, etc.
This update removes all of those constraints. You can develop, debug, and deploy a GAS webapp like a normal website – using any tooling, libraries, or build process you want.
How this compares to the previous method
The original method already bypassed several HTMLService limitations. The new approach goes further by running completely outside the iframe (faster, full capabilities), with one trade-off: it doesn’t support HTML templates. If you rely on templates, you can start with the original method and later migrate to this new method once templates are no longer needed.
The monorepo includes live working examples. Star if you like it!
Containerize apps script code and run it on cloud platforms such as Cloud Run outside the context limitations of the Apps Script IDE.
Bruce Mcpherson continues to expand the capabilities of his gas-fakes library, a tool that has already proven valuable for running Google Apps Script locally on Node.js. In his latest update, Bruce demonstrates how to take this a step further by containerising Apps Script code to run on Google Cloud Run.
For developers familiar with the constraints of the Apps Script IDE, particularly the execution time limits, moving logic to a serverless container environment offers a powerful alternative. With the release of version 2.0.2, gas-fakes now includes a managed configuration for Workspace Domain-Wide Delegation (DWD) enabling secure keyless authentication.
Essentially, this allows developers to package their Apps Script logic into containers, enabling execution on scalable platforms like Cloud Run, free from the constraints of the standard IDE.
Authentication and Service Accounts
One of the friction points in moving from a bound script to a cloud environment is authentication. Bruce highlights that while Application Default Credentials (ADC) work well for local development, a more secure method is required for Cloud Run. The updated gas-fakes CLI simplifies this by handling the service account configuration automatically.
The library supports two primary authentication types:
Domain-Wide Delegation (DWD): Recommended for production environments and cross-platform scenarios, such as Cloud Run or Kubernetes.
Application Default Credentials (ADC): A fallback method primarily for local development.
Containerisation Workflow
For those looking to deploy their own scripts, Bruce’s guide walks through the essentials required to containerise a project.
It is important to note that while the core logic can be written in JavaScript, you will need to manually set up the infrastructure configuration. The guide provides the specific code required for:
The Dockerfile: To mimic the Apps Script runtime environment.
Cloud Build Configuration: A cloudbuild.yaml file to manage the build steps.
To tie it all together, the article includes a deployment script (referred to as deploy-cloudrun.sh in the text) which automates the pipeline. It handles everything from creating the Artifact Registry repository and submitting the build, to monitoring the deployment.
Code Example
To help developers get started, Bruce has provided a dedicated repository containing a working example.
Note on usage: The repository contains the core Node.js logic (example.js) and a deployment helper script (named exgcp.sh in the repo). However, to deploy this successfully, you will need to combine these files with the Dockerfile and cloudbuild.yaml configurations detailed in the main article. Bruce notes there is more to come.
The example.js file illustrates how standard Apps Script services are imported and used within the Node.js environment. By requiring gas-fakes, you can access services like DriveApp or SpreadsheetApp using the exact same syntax you use in the Apps Script editor, bridging the gap between local Node development and the Google Cloud runtime.
Summary
This development opens up interesting possibilities for hybrid workflows where lightweight tasks remain in Apps Script, while heavier processing is offloaded to Cloud Run without needing to rewrite the core logic in a different language. If you are interested in trying this out, Bruce has provided example deployment scripts and a troubleshooting guide to help you get started.
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.
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:
Trigger: A new row is added to your “Invoices” sheet (or a weekly schedule fires).
Task: AppSheet runs a “Create a new file” task.
Template: It reads a simplified Excel template stored in Drive, replaces tags like <<[Qty]>> with actual data, and generates the file.
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.
Parallel Fetching (Client): The browser fires concurrent requests to Apps Script to fetch the template (Base64) and invoice data.
Client-Side Processing: ExcelJS loads the template, injects data into specific cells (preserving layout), and recompiles the file.
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.
Apps Script: Generates an OIDC ID Token (for identity), using getIdentityToken()) and an OAuth Access Token (for Drive permissions) with getOAuthToken().
Cloud Function: Verifies the ID Token and uses the Access Token to initialise the Drive API client as the user.
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.
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.
Start the Function:functions-framework --target=generate_excel_report --source=cloud-fn.py --port=8080
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) andMethod 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!