AppSheet Custom Steps for Google Workspace Studio

Before we jump in a quick backstory, if nothing else to prove this entire post wasnโ€™t written with Gemini assistance. Just over a year ago I was fortunate to be a Workspace Studio tester (or Flows as was it called then). I was particularly excited by the ability of Extending Google Workspace Flows with Custom Steps using Google Apps Script. As Workspace Studio moved through the Gemini Alpha program to general availability, custom steps disappeared. This is very understandable given Workspace Studio was a new product and Google wanted to make sure users got to access a stable platform. The great news Google has now launched Automate workflows with custom starters and steps, third-party integrations, and webhooks in Workspace Studio.

With custom steps officially back I thought it was revisit an integration I had been experimenting with combining Workspace Studio with AppSheet. The code for this solution is published in the AppSheet Custom Steps for Google Workspace Studio repo and in this post, we’ll walk through:

  1. Why connect Workspace Studio to AppSheet?
  2. A Real-World Example: Intelligent IT Ticket Triage with Gemini & AppSheet
  3. How custom steps are structured in Apps Script (appsscript.json)
  4. The secret sauce: Dynamic schema discovery via Custom Resources
  5. Handling Single vs. Multiple Results: A Two-Tier Approach
  6. Testing & Deployment with Clasp (and the Admin Setting gotcha)
  7. How this project was built using AI coding agents with .agents/ skills

Why Connect Workspace Studio and AppSheet?

Google Workspace Studio is great for automations across Google Workspace allowing you to use Gemini assistance. AppSheet serves as a no-code application platform making it easy to create solutions for your business data. AppSheet Enterprise customers can already use Gemini AI automations to extract, categorise and summarize data, but getting data from a Workspace automation into AppSheet often required intermediary webhooks, custom API scripts, or relying on Google Workspace integrations to Sheets, Forms and Gmail.

With my custom Studio steps, powered by the AppSheet REST API, you have the ability to extend your apps capabilities using the growing list of Workspace Studio starter steps integrating with your AppSheet app to:

  • Add Rows: Directly push structured records into AppSheet tables.
  • Update Rows: Modify records and trigger AppSheet bot workflows in real time.
  • Find Rows: Run AppSheet formula expressions (such as FILTER("Technicians", [On Call] = TRUE)) and feed those records back into downstream Studio steps.

A Real-World Example: Intelligent IT Ticket Triage with Gemini & AppSheet

To see how this works in practice, let’s look at a common scenario, an automated IT helpdesk flow using a slightly modified version of the AppSheet standard IT Ticketing template.

In the IT Ticketing app the schema includes:

  • The Tickets table tracks issues (Issue ID, First Name, Last Name, Email, Description, Symptom, Assigned Tech, Date Created, Resolved).
  • The Technicians table tracks staff (Name, Department, IT Email, On Call boolean, and Can Cover skills: General Issues, Network, Website, Apps). The primary key for this table is IT Email.
  • The Symptoms table stores the allowed categories (Name, Department), used as an AppSheet Ref dropdown in the app.

Rather than forcing users to fill out a form manually or relying on rigid routing rules, we can combine Workspace Studio’s Gmail starter, Gemini steps, and our AppSheet custom steps into an autonomous triage pipeline:

[ Gmail Starter: New Support Email ]
                 โ”‚
                 โ–ผ
[ Step 2 : Find Rows (Fetch On-Call Technicians) ]
                 โ”‚
                 โ–ผ
[ Step 3: Find Rows (Fetch the apps Symptoms list) ]
                 โ”‚
                 โ–ผ
[ Step 4: Extract (Gemini extracts the required data for our app) ]
                 โ”‚
                 โ–ผ
[ Step 5: Add Rows (Create Ticket with Assigned Tech) ]

Step-by-Step Flow Breakdown

  1. Starter (Gmail): Studio listens for incoming emails sent to [email protected] or flagged with [IT Support] in the subject line.
  2. AppSheet Custom Step (Find Rows):
    • Studio queries the Technicians table with the selector:FILTER("Technicians", [On Call] = TRUE)
  3. AppSheet Custom Step (Find Rows):
    • Studio queries the Symptoms table for the current list
  4. Gemini Extract (Smart Triage):
    • Studio passes both the cleaned ticket details, the on-call technician records and our apps symptoms list to Gemini:
  1. AppSheet Custom Step (Add Rows):
    • Studio inserts the new record into Tickets.
    • The step outputs addedRow, immediately exposing newly generated fields (like Ticket ID or calculated dates) for the rest of the flow.
 [
   {
     "Name": "{{Step 1.Starter.Sender Display Name}}",
     "Email": "{{Step 1.Starter.Sender Email}}",
     "Description": "{{Step 4.Gemini.Description}}",
     "Symptom": "{{Step 4.Gemini.Symptom}}",
     "Assigned Tech": "{{Step 4.Gemini.IT Email}}"
   }
 ]

This pattern demonstrates the true power of combining Workspace Studio and AppSheet. Studio orchestrates Workspace events and AI reasoning, while AppSheet provides validated, related business data.


The Anatomy of a Workspace Studio Custom Step

Under the hood, a Workspace Studio custom step is an extension of the Google Workspace Add-on architecture defined in appsscript.json.

Each step declares its inputs, outputs, and two primary Apps Script callback functions:

  1. onConfigFunction: Renders the configuration card displayed in the Studio sidebar when a user selects your step.
  2. onExecuteFunction: Receives input variable bindings at runtime, executes the business logic, and returns output variables back to the flow.

Here is an extract from appsscript.json defining the “Add Rows” step, featuring both bulk JSON outputs and the Dynamic Custom Resource (addedRow):

{
  "id": "appsheet_add_rows",
  "state": "ACTIVE",
  "name": "Add Rows in AppSheet",
  "description": "Adds one or more rows to a specified AppSheet table.",
  "workflowAction": {
    "inputs": [
      { "id": "appId", "cardinality": "SINGLE", "dataType": { "basicType": "STRING" } },
      { "id": "accessKey", "cardinality": "SINGLE", "dataType": { "basicType": "STRING" } },
      { "id": "tableName", "cardinality": "SINGLE", "dataType": { "basicType": "STRING" } },
      { "id": "rowsData", "cardinality": "SINGLE", "dataType": { "basicType": "STRING" } }
    ],
    "outputs": [
      {
        "id": "addedRow",
        "description": "The first newly added row with individual column fields.",
        "cardinality": "SINGLE",
        "dataType": {
          "resourceType": {
            "workflowResourceDefinitionId": "appsheet_row_resource"
          }
        }
      },
      {
        "id": "addedRows",
        "description": "The full rows added, including server-computed values.",
        "cardinality": "SINGLE",
        "dataType": { "basicType": "STRING" }
      }
    ],
    "onConfigFunction": "onConfigAddRows",
    "onExecuteFunction": "onExecuteAddRows"
  }
}

Notice that addedRow has a dataType pointing to appsheet_row_resource. At the top level of addOns.flows, we declare this custom resource alongside a dynamic definition provider:

"workflowResourceDefinitions": [
  {
    "id": "appsheet_row_resource",
    "name": "AppSheet Row",
    "providerFunction": "onDynamicAppSheetRowProvider",
    "resourceType": "DYNAMIC"
  }
],
"dynamicResourceDefinitionProvider": "onDynamicAppSheetRowDefinition"

Dynamic Schema Discovery via Custom Resources

One challenge with database integrations in no-code flow builders is usability. Asking users to hand-write raw JSON payloads or use secondary “Extract” steps to parse out field names isn’t straightforward.

Initially, I tried adding interactive buttons in the configuration card (e.g. a “Fetch Columns” button). However, Google Workspace Studio does not currently display interactive CardService action buttons or in-place card replacement widgets.

Whilst it is not possible to dynamically add input fields to a step, you can dynamically expose the outputs using Dynamic Custom Resources. With Dynamic Custom Resources, you can dynamically update the schemas to define individual column variables automatically.

How it Works Behind the Scenes

  1. Design-Time Schema Introspection (onDynamicAppSheetRowDefinition):
    When the user configures the step in Studio (entering their App ID, Access Key, and Table Name), Studio calls onDynamicAppSheetRowDefinition(e). Apps Script queries AppSheet using TOP(FILTER("Table", TRUE), 1) to discover existing column headers, registering each as an individual ResourceField:
function onDynamicAppSheetRowDefinition(e) {
  const resourceDefinitions = AddOnsResponseService.newDynamicResourceDefinition()
    .setResourceId("appsheet_row_resource");

  const inputs = e.workflow?.resourceFieldsDefinitionRetrieval?.inputs || {};
  const appId = inputs.appId?.stringValues?.[0];
  const accessKey = inputs.accessKey?.stringValues?.[0];
  const tableName = inputs.tableName?.stringValues?.[0];

  if (appId && accessKey && tableName) {
    const AppSheet = new AppSheetApp(appId, accessKey);
    // Fetch schema sample (cached for 6 hours to keep editor fast)
    const sample = AppSheet.Find(tableName, [], { "Selector": `TOP(FILTER("${tableName}", TRUE), 1)` });
    if (sample && sample.length > 0) {
      Object.keys(sample[0]).forEach(colName => {
        resourceDefinitions.addResourceField(
          AddOnsResponseService.newResourceField()
            .setSelector(colName)
            .setDisplayText(colName)
        );
      });
    }
  }

  const workflowAction = AddOnsResponseService.newResourceFieldsDefinitionRetrievedAction()
    .addDynamicResourceDefinition(resourceDefinitions);

  return AddOnsResponseService.newRenderActionBuilder()
    .setHostAppAction(AddOnsResponseService.newHostAppAction().setWorkflowAction(workflowAction))
    .build();
}

  1. Step Execution & Caching (onExecuteFunction):
    When the step runs, it executes the AppSheet API operation, caches the row data in CacheService under a unique UUID ("row_find_" + Utilities.getUuid()), and returns a resource reference variable alongside the standard JSON output:
const variables = {
  "firstRow": AddOnsResponseService.newVariableData().addResourceReference(resourceId),
  "foundRows": AddOnsResponseService.newVariableData().addStringValue(JSON.stringify(foundRows)),
  "rowCount": AddOnsResponseService.newVariableData().addIntegerValue(foundRows.length)
};

  1. Runtime Value Resolution (onDynamicAppSheetRowProvider):
    When a downstream step references a chip like Step 2 > First row > IT Email, Studio calls onDynamicAppSheetRowProvider(e) with the resourceId. The provider grabs the cached row and returns the requested column values on the fly.

Handling Single vs. Multiple Results: A Two-Tier Approach

When building steps that interact with databases, you inevitably face a question: What happens if the query returns one row versus ten rows versus zero rows?

In Google Workspace Studio’s currently, custom resources strictly mandate "cardinality": "SINGLE":

“The output variable has a dataType with the property resourceType. The value of cardinality must be SINGLE.”

Because Studio does not yet support repeated arrays of custom resource chips, our custom steps implement a two-tier output design:

StepOutput VariableType & CardinalityBest ForBehavior
Find RowsfirstRowDynamic Custom Resource (SINGLE)Single result / First matchExposes individual column chips (Email, Name, ID, etc.) directly to downstream steps.
foundRowsString (JSON Array) (SINGLE)Multiple resultsContains the full JSON array [ {...}, {...} ] of all matching rows.
rowCountInteger (SINGLE)Conditionals & branchingThe total number of rows found (0, 1, 5, etc.).
Add RowsaddedRowDynamic Custom Resource (SINGLE)Single record addedExposes column chips of the newly created row (useful for generated IDs or computed columns).
addedRowsString (JSON Array) (SINGLE)Bulk additionsFull JSON array of all rows inserted.
Update RowsupdatedRowDynamic Custom Resource (SINGLE)Single record updatedExposes column chips of the updated record.
updatedRowsString (JSON Array) (SINGLE)Bulk updatesFull JSON array of all updated rows.

How Flows Use This in Practice:

  • Single Record Workflows: Use firstRow, addedRow, or updatedRow chips directly in downstream steps (Gmail, Google Docs, Slack, Chat) without any formula or extraction step. If 0 rows match, chips safely resolve to empty strings ("").
  • Multi-Record Collections: Pass foundRows (JSON string) directly into the rowsData input of another step or iterate over the collection using Studio list actions.
  • Conditional Logic: Add a Studio branch on rowCount > 0 to safeguard downstream actions when records might not exist.

Testing & Deployment with Clasp

Because Studio custom steps run on Google Workspace Add-on infrastructure, testing is fast:

  1. Either Copy with Apps Script project or follow the steps to clone the repo and push via clasp:
  2. Install the Test Deployment: Open the Apps Script project, go to Deploy > Test deployments, select Google Workspace Add-on, and click Install.
  3. Use in Studio: Head to studio.workspace.google.com, add a step to any flow, and search for “AppSheet Utilities for Studio”.

[!IMPORTANT] Admin Setting Required for Google Workspace Accounts:
As announced in the Google Workspace Updates, custom steps are OFF by default for Google Workspace accounts.

If you see a lock icon stating “๐Ÿ”’ Access to these steps is restricted by your Workspace admin”, have a domain admin navigate to:
Google Admin Console > Apps > Google Workspace > Settings for Workspace Studio > Custom steps settings
Turn custom steps ON and check “Allow unpublished (test) custom steps”.

AI-Assisted Development with .agents/

An interesting aspect of developing for newly released or evolving APIs like Workspace Studio is that public LLMs often have knowledge cutoffs or hallucinate manifest formats and Card Service specifications.

To build this project efficiently, the repository includes a curated .agents/ directory containing specialized runbooks, rules, and reference specs:

  • Contract Parity: Enforces that input/output IDs in appsscript.json match Code.js execution handlers.
  • Real-time API Adaptation: When testing revealed that Studio sidebar cards don’t render interactive card buttons, the agent used the reference docs to quickly pivot to native Dynamic Custom Resources (dynamicResourceDefinitionProvider).
  • Platform Constraints: Encodes crucial platform invariantsโ€”like the requirement that dataType.resourceType outputs mandate "cardinality": "SINGLE"โ€”saving hours of trial and error.
  • AppSheet Selectors: Pre-validated AppSheet formula expressions (FILTER, SELECT, TOP) for REST API calls.

When working with tools like Gemini Code Assist or AI coding agents, these in-repo skills guide the model to write accurate, production-ready code on the first attempt and navigate rapid API evolutions.

Summary and Next Steps

Having custom steps and starters back in Workspace Studio opens up huge possibilities for developers. Google Apps Script acts as the glue code, letting you build custom integrations for internal systems, specialized SaaS platforms, or databases like AppSheet.

You can find the complete source code and clasp setup instructions on GitHub: ๐Ÿ‘‰ mhawksey/appsheet-custom-studio-step-example

Gemini in the Apps Script IDE: 8 Practical Developer Workflows by Stรฉphane Giron

Summer 2026 has been huge for Google Apps Script developers, marked by two major milestones. First, Apps Script officially graduated to a Google Workspace Core Service … Second, Gemini is now natively integrated into the Apps Script IDE side panel. No more juggling browser tabs, context-switching to Google AI Studio, or copy-pasting prompts back and forth. To help you take full advantage of this workflow upgrade, here are 8 things you can do with Gemini directly inside Apps Script.

In August we highlighted the news Google Apps Script Editor: Gemini Side Panel Rolling Out!. This is a major feature update and as part of the beta you can read the latest information on Google’s Use the Gemini side panel in the Apps Script editor support page. A couple of requirements to headline.

Consumer accounts

  1. You have Google AI Pro / Ultra subscription
  2. Your account language is set to English

Google Workspace account

  1. The account has a Business Standard, Business Plus, Enterprise Standard, Enterprise Plus, Workspace Gemini Add-on, or Google AI Pro for Education licence
  2. Your account has been included in theย Gemini Beta programย by your admin
  3. Your account language is set to English

If you do have access to this feature then Stรฉphane Giron highlights 8 quick tips for getting started with Gemini in the side panel. These include:

  • Full project scaffolding & modification: Prompting Gemini to create multi-file architectures from scratch (such as generating pairedย .gsย script logic andย EmailTemplate.htmlย templates).
  • Modernizing legacy code: Refactoring large script projects into clean, modular files with improved helper functions and error handling.
  • Language translation & unit tests: Converting legacy VBA macros automations directly into Apps Script, as well as creating automated unit tests.
  • One-click log debugging: A standout feature – clicking a button directly within execution logs to have Gemini diagnose and propose fixes for errors on the spot.

Follow the source link to explore Stรฉphane’s full walkthrough and visual breakdown of each prompt in action.

Source: Google Apps Script just got Gemini: 8 practical ways to use it in IDE โ€” Stรฉphane Giron

Google Apps Script Editor: Gemini Side Panel Rolling Out!

Earlier in the year Google announced their plan to include Gemini in the side panel in the Apps Script IDE. The wait is now over for Gemini Beta and consumers on paid Google AI tiers as this feature is now rolling out!

The update brings AI assistance directly to the online Apps Script Editor. Rather than switching between browser tabs to copy code snippets, you can now generate, edit, and debug scripts with the help of Gemini. There is a dedicated Use the Gemini side panel in the Apps Script editor documentation page that includes a number of troubleshooting and best practices tips.

Testing the side panel with container bound scripts it was very useful to see that Gemini had the context of their parent document. For situations like building data processes in Google Sheets, this built-in context saves significant time as it is aware of the headings and existing data. In addition you can also use @drive_url to reference other files.

Less obvious is the option to run a custom Gem directly from the side panel menu. Iโ€™ve been experimenting using a Gem as a way of holding some Apps Script development skills. My current iteration is in this sharedย GASDev Gem.

[Update 06 Aug 2027: At this time it looks like Gems can be used in the Gemini side panel for the Apps Script IDE, but you can reference this skill doc e.g. Using @https://docs.google.com/document/d/1bng3y3ndTfRju0Ud9ANvdzXwdrhBvyvopQ0uniyUg_4/edit ...]

Overall, the side panel is a fantastic addition for both new and existing users looking to rapidly build custom solutions across Google Workspace. Personally it does cause some concern around users executing generated code without fully understanding its underlying logic or security permissions, particularly when you use services like Gmail and Drive.

Source: Use the Gemini side panel in the Apps Script editor  |  Google for Developers

From AppSheet to Agents: Exploring Antigravity SDK, A2A and A2UI for Agent generation

Today marks the finish line of an intense coding sprint. I am excited to share my project: AppSheet2Agent (available at github.com/mhawksey/appsheet2agent).

If you are an AppSheet creator, you have already built amazing, structured databases and workflows. But what if your users want to interact with that same data conversationally, without leaving their chat?

Usually, connecting business applications to large language models is a massive headache:

  • You lose security context, often forcing you to rely on a single, risky service account.
  • You get flat, uninspiring text dumps that make structured data hard to read.
  • You waste hours writing manual boilerplate code.

AppSheet2Agent is an experimental solution designed specifically for existing AppSheet app creators. It allows them to rapidly create brand-new, highly interactive custom agents to interact with their existing apps. This makes it possible to use your AppSheet app directly inside platforms likeย Gemini Enterprise.

Here is what the MVP delivers:

  1. ๐Ÿค– Powered by Google Antigravity SDK Under the hood, it uses Google’s new Antigravity SDK to build the agent based on a conversation with the AppSheet creator. As part of this the user can provide the existing Application Definition and OpenAPI spec for their app, so Antigravity has the context if their app.
  2. ๐ŸŽจ Rich Interactive Layouts (A2UI v0.8) Nobody wants walls of boring markdown text. The agent automatically renders search results and forms as beautiful, structured visual cards directly in the Gemini Enterprise chat window. It automatically reorders columns to put primary labels first, hides messy system rows, and maps data types to native elements like checklists and date pickers.
  3. ๐Ÿ” True User Identity Delegation (RunAsUserEmail) Security is paramount. The agent uses OAuth to request and access the users email address which is used directly in the AppSheet API. Every single query and update runs strictly under the identity of the person chatting with the model. No shared credentials. No security compromises.

Where the Project Stands (The Reality Check) Because this was a sprint, it is a work in progress! I have successfully compiled, registered, and deployed custom AppSheet agents to Gemini Enterprise. Right now, I am still ironing out the automated “Creator Studio” wizard workflow that compiles the packaging on the fly.

I would love to hear your thoughts! If you are an AppSheet creator, how would your users benefit from interacting with your apps inside Gemini Enterprise?

Google Cloud credits are provided for this project.

AppSheet Training YouTube Channel – Keep Up to Date with NOCODE NEWS

No-Code Business Automation – Made Simple. This channel is here to help you learn the basics of AppSheet so that you can make your business more efficient.

Many AppSheet creators are likely familiar with the AppSheet Training YouTube channel and its associated materials. Managed by QREW Technologies, a business specialising in AppSheet consulting from the early days of the platform, the channel offers hundreds of videos covering all aspects of building and managing applications.

A particularly useful addition is a “NOCODE NEWS” segment, where the QREW team highlights the latest platform updates and community contributions. Given that founder Stefan Quartemont and several team members, including Cam Lupton, hold Google Developer Expert status, this segment offers an authoritative view. This segment is also included in their wider regular live broadcasts, so hit like and subscribe to stay informed.

To get a flavour of what is included, I have picked out a few recent highlights:

  • Shift to Platform Stabilityย [2 July 2026] – The QREW team highlights a recent community update where the AppSheet team openly acknowledged developer frustration and announced they are pausing new feature releases, including the highly anticipated Gantt chart view, and cancelling upcoming office hours. This hard choice is their way of finally tackling the bug backlog and system stability issues that creators have been pushing for.
  • User Limits Surfacingย [2 July 2026] – The hosts discuss a wave of anxiety on the AppSheet community forum as Google actively enforces the 10-user limit on free-tier and prototype accounts, which includes guest users. They walk through how this sudden enforcement is temporarily blocking application access, leaving creators scrambling to either clean up user lists or upgrade to a paid licence.
  • Domain Authenticated Email Betaย [18 June 2026] – The team shares details on a new “Bring Your Own Email” closed beta for Google Workspace customers. This is a massive relief for developers after AppSheet blocked external emails from the defaultย [email protected]ย address to combat phishing, which broke many existing workflows. This new beta uses Google OAuth to let developers send automated emails directly from their own authenticated domains.

The QREW team does an excellent job of translating policy updates into practical developer steps, saving you the time of digging through the forums yourself. For all of these updates check out the video descriptions for additional links and resources.

Source: AppSheet Training – YouTube

 

Pushing past single-tab data limits in Google Sheets canvas with =AI() and JSON

Following my previous look at the Google Sheets canvas tool I wanted to explore how we can push past single-tab data limits by using the =AI() function to generate structured JSON. If you are new to JSON, it is just a clean, standardised way of packaging information using plain text and clear labels. This technique is universally applicable to any workflow where you need to extract, evaluate and format complex data in a single step.

One of the key design rules of the new Canvas tool is that it can only build interfaces using data from a single sheet tab. If you want to show a detailed breakdown of multiple metrics, a standard spreadsheet setup would force you to create dozens of separate columns to hold every rating, piece of feedback and recommendation. This quickly becomes messy and difficult to manage.

By instructing the =AI() function to bundle all our evaluation data into a single, structured JSON block, we can squeeze a complete relational database into a single cell. This keeps our spreadsheet clean while giving the Canvas tool all the deep metadata it needs to build a rich, interactive dashboard.

What we will be making

When helping customers adopt generative AI tools in their daily workflows, one of the biggest challenges for new users is prompting. To solve this, we are going to build a personalised prompt coaching directly inside Google Sheets. Our tool will automatically analyse any draft prompt, grade its structure, highlight which key elements are missing and provide instant, actionable advice on how to improve it.

The finished, interactive Prompt Coach Dashboard rendering directly over your spreadsheet cells, complete with colour coded status badges, highlighting and a copyable master prompt. Click here to view the readonly version

To build this I will use the official Google Prompting Guide 101. The guide uses a framework with four core pillars:

  • Persona: The specific role, voice or professional perspective assigned to the model (e.g., “You are an expert technical writer”).
  • Task: The core instruction or action verb directing the model (e.g., “Draft an onboarding guide”). This is the only non-negotiable component.
  • Context: Relevant documents, emails, file identifiers or situational details that ground the model in accurate facts (e.g. referencing ambient tags like “@UAT_Results”).
  • Format: The layout, style, length limits or output type requested for the final response (e.g., “Output as a bulleted list of 5 steps”).

By checking draft prompts against these pillars, our Sheets-based coach can pinpoint exactly what a user needs to add to get a great result on their first try. Let us look at how to set up this workflow to parse, score and visualise your prompting templates using dynamic JSON.

Step 1: Set Up the Prompt Auditor Formula

Open your Google Sheet and prepare your columns. Label Column A as “Raw Prompt” and Column B as “JSON Evaluation”.

Enter a draft prompt in cell A2, such as: “Draft an email to my colleague who is running this UAT and explain what they need to do next.”

To construct a prompt to return structured JSON data from the Google Sheet =AI() function I used Gemini to draft and refine our system instructions. Developing a complex, multi-layered instruction that outputs strict JSON without broken formatting is an iterative process. Here is the system prompt I came up with (select cell B2 and write the following formula to analyse the prompt against the four prompting pillars):

=AI("Analyse the draft prompt provided against the Google Prompting Guide 101 framework, which utilises 4 pillars: Persona, Task, Context and Format.

### THE SYNERGY-BASED COACHING BLUEPRINT:
Your goal is to evaluate if the user's prompt is structurally matched to their operational goal, rather than strictly checking off all the components of a prompt. Always recommend including a strong verb or command as part of the prompt task; this is the most important part of a prompt.

1. Task Classification:
   - Identify if the task is primarily CREATIVE/WRITTEN (e.g., drafting emails, brainstorming, translating, slide content) or DATA/OPERATIONAL/ANALYTICAL (e.g., summarising documents, extracting metrics, creating task lists, setting limits).

2. Core vs. Enhancer Logic:
   - 'Task' is the only non-negotiable core pillar.
   - The other 3 pillars (Persona, Context, Format) are optional 'Enhancers'. Evaluate if the user chose the RIGHT enhancers for their specific goal:
     - For CREATIVE/WRITTEN goals: Look primarily for 'Persona', 'Tone' or 'Context' enhancers. Creative prompts do not strictly need constraints or format grids.
     - For DATA/OPERATIONAL goals: Look primarily for 'Format', 'Context' (referencing active file context) or 'Constraints'. Analytical prompts do not strictly need a persona or a tone.

3. Dynamic Tier Ratings:
   Based on the synergy check above, assign one of these three ratings:
   - Foundation: The prompt has a core command but lacks appropriate enhancers (leaving the AI guessing on essential details or voice).
   - Effective: The prompt provides a clear task accompanied by relevant enhancers (e.g., standard persona or basic format), but lacks precise structural boundaries, styling constraints or negative limitations.
   - Masterful: The prompt is highly optimised. It features a strong command, targeted context (or active ambient tags) and multiple precise constraints (such as explicit word counts, custom layout instructions, specific tone rules or negative exclusions). It is designed to get the perfect result on the first run with zero back-and-forth iteration.

### PILLAR MAPPING:

For each of the 4 pillars (task, persona, context, format), status must be exactly 'found', 'weak' or 'missing'. 

- Ambient references like 'this document', 'the open email' or '@Roadmap' represent strong contextual grounding. Mark them 'found'.
- Format and Constraints: Note that 'Format' governs structural layout, timing rules, explicit constraints and negative exclusions (e.g., 'Do not suggest...'). You MUST map these structural guidelines and behavioral boundaries to the 'format' pillar text array.
- Highlight Substrings: When extracting text substrings for the 'text' array, you must copy them EXACTLY as they appear in the original draft prompt. Do not correct spelling, alter punctuation, adjust capitalisation or paraphrase, as this will break the frontend highlighting engine.

### RULE OF COMPLETENESS:

- 'improvedPrompt' MUST NEVER BE EMPTY. You must upgrade the original prompt so that it strictly qualifies for the Masterful rating. To do this, you MUST enrich the original request by injecting:
  1. A highly tailored, expert persona suited to the task.
  2. Explicit structural and formatting guidelines (such as a markdown table with designated column headings or a multi-part layout).
  3. Precise constraints and absolute metrics (such as explicit time limits, word limits or step limits).
  4. At least one negative constraint or exclusion (such as what the model should avoid or ignore).
- 'keyRecommendations' MUST NEVER BE EMPTY. It must contain exactly 3 concrete, actionable pieces of advice explaining what they should add or adjust to improve their prompt's effectiveness inside Google Workspace apps.

### ANALYSIS DIRECTIVES:

- Not every prompt requires all 4 pillars. Think of it like a stool: it only needs 3 or 4 strong legs to stand. DO NOT force or hallucinate a pillar if it is not explicitly present. If a pillar is missing, status must be 'missing' and the text array should be empty.
- A single pillar may appear more than once in a prompt (for example, a prompt can have multiple distinct constraints or background contexts). Return ALL matched case-sensitive substrings for that pillar inside the 'text' string array.
You MUST respond with a single, raw, valid JSON object. Do not include markdown wraps, backticks or preamble text. Use this exact schema. Ratings must be one of these exact strings: [`Foundation` | `Effective` | `Masterful`].

{
  ""rating"": ""Effective"",
  ""improvedPrompt"": ""[Rewritten optimised prompt here]"",
  ""keyRecommendations"": [
    ""First action recommendation"",
    ""Second action recommendation"",
    ""Third action recommendation""
  ],
  ""pillars"": {
    ""task"": {""status"": ""found"", ""text"": [""[substring 1]"", ""[substring 2]""], ""feedback"": ""[guidance]""},
    ""persona"": {""status"": ""missing"", ""text"": [], ""feedback"": ""[guidance]""},
    ""context"": {""status"": ""weak"", ""text"": [""[substring]""], ""feedback"": ""[guidance]""},
    ""format"": {""status"": ""found"", ""text"": [""[substring]""], ""feedback"": ""[guidance]""}
  }
}", B2)

Writing a prompt that outputs perfectly formatted data every single time might feel intimidating, but there are two key features built into this instruction that helps with consistency:

  • Strict JSON Directives: We explicitly tell Gemini to return a single, raw, valid JSON object without any conversational filler or markdown formatting (like triple backticks ```). This ensures that the cell gets only the clean code it needs to be parsed, with nothing extra to cause formatting errors.
  • Exemplar Output: At the very end of the prompt, we provide an empty example structure of the exact JSON we expect. By providing this template, Gemini does not have to guess what keys or structures to use, it simply swaps our placeholder text with its actual evaluation, improving the consistency of the output.

Drag this formula down your column. The =AI() function processes the input text and outputs structured JSON directly into the cell:

{
"rating": "Foundation",
"improvedPrompt": "You are a Senior QA Lead and Project Management Expert. Based on the UAT results in this document, draft a professional and encouraging email to my colleague who is running this UAT. Explain the next steps required to reach sign-off, specifically outlining the bug triaging process and the re-testing cycle. Format the email with a clear 'Action Items' bulleted list and a 'Timeline' section. Constraints: The email should be under 200 words and maintain a collaborative tone. Do not include technical jargon that might confuse non-technical stakeholders.",
"keyRecommendations": [
"Define a professional persona, such as a Senior QA Lead, to establish the right tone and authority for the email.",
"Specify exactly what the 'next steps' are (e.g., bug triaging or re-testing) to reduce ambiguity for the AI.",
"Incorporate structural constraints like word counts and specific section headers to ensure the output is ready to send."
],
"pillars": {
"task": {
"status": "found",
"text": [
"Draft an email to my colleague who is running this UAT and explain what they need to do next."
],
"feedback": "The task is clear and uses a strong verb, though the specific 'next steps' are left for the AI to guess."
},
"persona": {
"status": "missing",
"text": [],
"feedback": "Adding an expert persona like a QA Lead would help set a professional and authoritative tone."
},
"context": {
"status": "found",
"text": [
"this UAT"
],
"feedback": "There is a brief mention of context, but it doesn't explicitly reference the specific document or data the AI should use."
},
"format": {
"status": "missing",
"text": [],
"feedback": "Providing structural guidelines, such as using bullet points for steps, would improve the email's clarity."
}
}
}

This formula pulls out the key parts of each prompt, giving us a neat, organised set of details to display.

What the structured JSON output looks like inside your Google Sheets cell after dragging the =AI() formula down Column B.

Step 2: Create Your Sheets Canvas

Once your evaluation column is populated, open the canvas creation tool to build your custom interface. For this your account needs to be in the Gemini Alpha or if using a personal Gmail account paying for Google AI Ultra or Google AI Pro.

  1. Select Insert from the top menu bar.
  2. Click Create a canvas.

This action opens the Gemini side panel directly inside your spreadsheet.

Step 3: Link Your JSON Data to the Canvas Layout

This is where the magic happens. Since Canvas can read data directly from your active sheet tab, we can instruct Gemini to build visual dashboard elements that bind directly to our JSON column.

To help Gemini understand how to split our single-cell JSON block into distinct UI elements (like progress cards, recommendation lists and copyable text blocks), we provide it with a clear design spec. This spec defines the visual layout and includes a sample set of mock data to show Gemini exactly how our JSON keys map to each display card.

Enter this layout instruction into the side panel prompt box to construct the dashboard:

### **FRONTEND REQUIREMENTS**
The application must feature a modern, minimalist Dashboard interface. Use the **Inter** font family, soft shadows, rounded borders (rounded-2xl), and a clean layout featuring:
1. **Sidebar or Dropdown Selector:** * A select dropdown or list of prompts loaded dynamically from the Google Sheet.  
   * On change, it loads the selected prompt's diagnostic metrics into the main canvas.  
2. **Interactive Highlight Map:** * A container displaying the selected original prompt text with color-coded, underlined highlights representing found pillars.  
   * It MUST support multiple occurrences per pillar (i.e., if pillars[key].text is an array of strings, loop through and highlight every substring occurrence).  
   * If a highlighted segment is clicked, it highlights that specific pillar in the Diagnostic View below.  
3. **Pillar Diagnostic Matrix:** * A 4-column visual status strip with colored indicators showing which pillars are Found (Green), Weak (Amber), or Not Included (Red).  
   * Below the status strip, a card showing details of the active/selected pillar: its definition, the specific matched substring(s) extracted from the prompt, and actionable "Pillar Optimization Guide" feedback.  
4. **Key Recommendations & Optimized Version:** * Displays the 3 top recommendations as numbered target boxes.  
   * Shows the final rewritten "Master Optimized Prompt" inside a clear, styled, copyable container with a one-click clipboard copy button.

### Sample Mock Data to pre-load as default state:
```json
{
  "rating": "Effective",
  "improvedPrompt": "As an expert copywriter, draft a warm, professional thank-you email to @Client for attending the Q3 planning workshop, highlighting our next steps to finalize the product roadmap by Friday.",
  "keyRecommendations": [
    "Specify a persona like 'expert copywriter' to give the email's tone a more distinct and polished feel.",
    "Add a constraint limiting the length of the email to keep it concise and easy to read.",
    "Clarify the format if you want specific sections or a bulleted summary of the next steps."
  ],
  "pillars": {
    "task": {
      "status": "found",
      "text": ["draft a warm, professional thank-you email"],
      "feedback": "Strong core instruction with clear action verbs."
    },
    "persona": {
      "status": "missing",
      "text": [],
      "feedback": "No explicit role or persona defined. Gemini will write from a generic baseline."
    },
    "context": {
      "status": "found",
      "text": ["@Client", "attending the workshop"],
      "feedback": "Excellent use of ambient context referencing a client and specific workshop."
    },
    "format": {
      "status": "weak",
      "text": [],
      "feedback": "You didn't specify length or structure. Consider adding constraints like 'under 150 words' or 'in 3 paragraphs'."
    }
  }
}
```

Gemini hopefully processes your prompt, compiles the interactive layout and renders the dynamic dashboard directly over your spreadsheet cells, similar to this readonly version

Summary

This workflow hopefully demonstrates an easy approach to make more powerful Google Sheets canvas apps. Instead of managing sprawling, hard-to-maintain tables with dozens of columns, or writing hundreds of lines of Google Apps Script to parse text, we can use a single, well-structured formula to run the entire analytical process. The =AI() function does not just gather raw data, it simultaneously classifies tasks, maps dynamic structures, extracts exact highlights and packages this data into a single, clean cell.

Connecting GCP Budget Alerts to AppSheet: A Step-by-Step Guide

Have you ever woken up to a massive Google Cloud bill because a developer accidentally left a cluster… Tagged with googlecloud, appsheet, finops, cloudbilling.

Weโ€™ve probably all been there, creating and forgetting a test Google Cloud project. Here is a nice solution published by Google Developer Expert Aryan Irani knitting together various Google Cloud / Workspace services. In a guide Aryan walks through how to set up an event-driven workflow, using Cloud Pub/Sub to help prevent unwanted Google Cloud spend right from your phone. Instead of waiting for an email notification, this system uses Google Sheets, AppSheet and Google Cloud Functions to monitor your budget and give you a push notification and easy way to shut down a project.

If you want to simplify this architecture further you can replace the webhook target with a Google Apps Script function. By using the auto-generated Cloud Resource Manager client library from the Google API Client Library Generator for Apps Script, you can programmatically shut down the entire target project directly from your app. Configuring the single cloud-platform OAuth scope in your script manifest allows you to delegate security control directly to Google Cloud IAM roles. Moving the project to a soft-delete state instantly stops all active billing across all services, databases and compute instances, providing a complete emergency stop-switch.

This project is a fantastic example of using low-code tools to solve real infrastructure problems. You can read the full step-by-step tutorial and learn how to configure this for your own projects.

Source: Connecting GCP Budget Alerts to AppSheet: A Step-by-Step Guide – DEV Community

Beyond the Grid: First Look at Google Sheets Gemini Canvas and AppSheet

Google recently introduced the Sheets canvas feature to the Gemini Alpha.

Sheets canvas allows users to conversationally build interactive, read-write mini-apps directly from their spreadsheet data. At this time, Sheets canvas is limited to supporting only single-table and single-header tables.

The new Sheets canvas is undeniably a powerful tool for creating interactive applications directly from spreadsheet data. It removes a lot of friction for rapid app creation and allows the building of visual interfaces through simple conversation beyond the current tools in Google Sheets. It is an impressive leap forward.

One question I had was how Sheets Canvas compares to other no-code tools like AppSheet. To help explore this, I thought it would be useful to run a practical comparison. In this post, we will build a Kanban board using the same project management dataset, allowing us to evaluate the new Sheets canvas against Google AppSheet.

The Kanban Board Test: Relational vs Flat Data

We will use the official AppSheet Kanban dashboard template as our starter.

Because we are using a template, there is zero setup required. A look at the underlying data structure and youโ€™ll see this app has five distinct tables connecting projects to tasks and workstreams, while mapping people to specific notes. Whilst it still uses a standard Google Sheet as a data source, AppSheet allows us to use true relational data. For example, projects can have related tasks, assigned team members, and detailed histories.

Sheets canvas takes a different approach. Inside your spreadsheet, you open the Gemini side panel, enable the canvas tool, and provide an instruction like โ€œCan you turn this data into a kanban board app?โ€, which you can view (readonly) here.

This experience is similar to the existing AppSheet โ€˜Gemini for App Creationโ€™ with two key differences: in the Sheet canvas we are starting with existing data and the development process is iterative; and if you do continue to ask Gemini to update the canvas, for example, to pull sub-tasks and user avatars from other tabs, something to be aware of is that the result is a clever illusion. Because canvas only ingests the active sheet, you cannot dynamically query other tabs. This is a known current limitation:

Currently, canvases only use data from a single Sheet tab. The canvas provides a read-write interface for your spreadsheet data. If you have edit access, you can edit, delete, or add data directly within the canvas as an alternative to the grid view. If you have comment or view access, you canโ€™t modify the canvas.

Under the Hood: Actions vs Bespoke Code

Comparing how these tools actually generate the interface provides another stark contrast. Asking Gemini to build the board generated hundreds of lines of custom React code. It imported standard web development libraries to render a bespoke interface directly over the spreadsheet cells. You are quite literally prompting an AI front-end developer to build a miniature web application that sits on top of your grid.

Data Integrity: Strict Rules vs Surface Forms

AppSheet enforces data integrity at the system level. You can configure deep initial value formulas that populate fields dynamically based on user context or timestamps. You can deploy strict validation rules that prevent a user from selecting a date in the past or entering a mismatched email address. The application simply will not save the record until the conditions are met.

Sheets canvas handles data entry through surface-level web forms. Looking at the generated code for our Kanban board reveals that the AI used standard HTML form attributes to require certain fields and injected fallback text into the React state to mimic initial values. It is functional. It works well for simple applications; however, the canvas interface does not natively inherit the data validation rules you build into the underlying spreadsheet cells. If you want strict input controls, you must explicitly prompt the AI to write custom JavaScript to catch those errors in the browser.

Security, Governance, and Shadow IT

The biggest difference between Sheets canvas and AppSheet emerges when you look at it at an enterprise level. A Sheets canvas is simply a file formatting layer. Its security model relies entirely on standard Google Drive permissions. If a user has edit access to the spreadsheet, they have edit access to the canvas.

AppSheet operates as a true enterprise platform. It lives behind a dedicated Admin Console where IT teams maintain oversight over every application built across the organisation. For teams using AppSheet Enterprise Plus licences, administrators can define governance policies that automatically enforce corporate compliance across hundreds of independent projects. An administrator might create a rule that instantly blocks deployment if a user attempts to connect an unapproved external database or fails to mandate strict domain authentication.

The Feature Comparison

To help you decide which tool fits your current project, here is a breakdown of their core capabilities.

Feature Sheets Canvas (Alpha) AppSheet
Primary Purpose Building custom interfaces to visualise and update single-sheet data. Building robust business applications with a dedicated desktop mode.
Data Source A single Google Sheet tab. Multiple sources including Sheets, SQL, and external APIs.
Data Structure Flat. Cannot access other tabs dynamically. AI relies on faking relationships with hardcoded static data. Relational.
Data Validation Basic HTML/React form rules. Requires custom JavaScript for complex logic. Deep system-level validation and computed initial values.
Read/Write Access Editors can edit, delete, or add data. Viewers cannot. Granular, role-based database access.
Complexity Level Low. Prompt-driven visualisations generating React code. High. Requires structured logic and intentional design.
Security & Governance Relies on standard Google Drive file permissions. Centralised admin console with strict policy-driven enterprise governance.

The Verdict

If you need a quick dashboard to track your team’s weekly tasks, use Sheets canvas. It is fast, intuitive, and keeps everyone working within a familiar spreadsheet environment while the AI handles the complex front-end code.

If your workflows are growing, if you need to connect multiple data sources like mapping notes and users to specific projects reliably, or if your IT department requires strict visibility over internal tools, AppSheet remains the undisputed choice.

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-framework,ย google-auth,ย google-api-python-client,ย openpyxl
  • 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!

Mastering Google Workspace Flows: A Comprehensive Guide to the New AI-Powered Automation Tool from Stรฉphane Giron

With the announcement ofย Google Workspace Flows at Google Cloud Next 2025, Workspace developers and power users have been eager to see how this new platform integrates with the ecosystem we know and love. Currently available in the Gemini for Workspace Alpha programme, Workspace Flows promises to bridge the gap between simple tasks and complex, AI-driven processes.

In a fantastic new three-part series, Google Workspace expertย Stรฉphane Gironย takes us on a complete tour of the platform. From the initial interface to complex logic handling, this series is the perfect primer for anyone looking to automate their organisation’s daily grind.

Here is a breakdown of the series:

Part 1: A First Lookย Theย introductory article sets the stage, explaining how to enable Workspace Flows within the Admin console and providing a tour of the dashboard. Stรฉphane highlights how Flows is built with Gemini at its foundation, allowing users to describe workflows in natural language to generate automations instantly.

Part 2: The Starters (Triggers)ย In theย second instalment, the focus shifts to the engine of every Agent: theย Starter. Stรฉphane provides a deep dive into Application-based starters (reacting to events in Gmail, Drive, Chat, Sheets, Calendar, and Forms) versus Schedule-based starters. He details how granular these triggers can beโ€”such as filtering emails by specific criteria before a flow even begins.

Part 3: Steps and Logicย Theย final piece explores the “Steps”โ€”the actions that happen after the trigger. The article covers:

  • Core App Steps:ย Standard actions like sending emails or creating Calendar events.
  • AI Integration: Using ‘Ask Gemini’ to reason, summarise, or transform content.
  • Logic:ย Implementing ‘Check if’ and ‘Decide’ steps to create dynamic paths.

Apps Script Integration

Of particular interest to the AppsScriptPulse community is the mention in Part 3 regardingย Custom Steps. Stรฉphane notes that for ultimate power users, Workspace Flows will offer the ability to create custom steps using Google Apps Script. This feature will unlock the full potential of the ecosystem, allowing developers to build highly specific integrations that aren’t available out of the box.

Click the source links to find out more!

Sources