1. Introduction
Google Workspace Studio lets you automate business processes across Google Workspace applications like Gmail, Google Chat, Google Drive, and Google Sheets. When your workflows need to start from an external web application or run custom logic in your own cloud environment, you can extend Google Workspace Studio with an HTTP Google Workspace Add-on in Node.js on Cloud Run:
- Custom starter step (
workflowTrigger): Starts a Google Workspace Studio flow when an external event occurs—such as a customer submitting a ticket in an internal web portal—by calling the Google Workspace Studiotriggers.fireREST API. - Custom action step (
workflowAction): Receives variables from earlier steps in the flow, runs custom logic or AI inference in your Node.js service, and returns output variables back to Google Workspace Studio.
In this codelab, you build and deploy a complete Node.js HTTP extension for Google Workspace Studio on Cloud Run, use Cloud Firestore to persist active flow trigger subscriptions and submitted tickets across container revisions, and classify ticket urgency in your custom action step using Gemini 3.8 Flash (gemini-3.8-flash) on the Gemini Enterprise Agent Platform with a resilient keyword-matching fallback.
What you'll build
- A Support Quick Portal web app and HTTP Google Workspace Add-on built with Node.js, Express, and Firestore on Cloud Run.
- A New Support Ticket custom starter step (
supportTicketTrigger) that persists active flow subscriptions in astudio_triggersFirestore collection, logs submitted tickets in asupport_ticketsFirestore collection, and fires whenever a ticket is submitted in the web portal—passingticketTitleandticketDescriptioninto a Google Workspace Studio flow. - A Detect Urgency custom action step (
urgencyDetectorStep) that classifies ticket urgency using Gemini 3.8 Flash (gemini-3.8-flash) on the Gemini Enterprise Agent Platform (aiplatform.googleapis.com), with an automatic fallback to keyword matching (urgent,blocker,critical).
What you'll learn
- How to configure an HTTP Google Workspace Add-on deployment manifest (
deployment.json) withworkflowTriggerandworkflowActionelements. - How to persist Google Workspace Studio trigger subscriptions (
triggerId,notifyUri, anduserOAuthToken) in astudio_triggersFirestore collection and fire flows using thetriggers.fireREST API. - How to build a
workflowActioncustom step in Node.js using Gemini 3.8 Flash (gemini-3.8-flash) structured output on the Gemini Enterprise Agent Platform with a keyword fallback.
Prerequisites
- A Google Workspace account with access to Google Workspace Studio.
- A Google Cloud project with billing enabled, created within the same Google Workspace organization as your user account so you can install unpublished add-on deployments for testing.
2. Set up your Google Cloud project and Firestore
In this step, you configure your project's OAuth consent screen, enable the required Google Cloud and Google Workspace APIs in Cloud Shell, provision your (default) Firestore database, and initialize your add-on authorization service account.
- Open the Google Cloud console, select your Google Cloud project, and click the Activate Cloud Shell icon in the top navigation bar.
- Configure the OAuth consent screen so Google Workspace can provision an OAuth client for your add-on:
- Open the Google Auth Platform Branding page.
- If prompted, click Get Started. (If your consent screen is already configured, skip to step 3.)
- Under App Information, enter
Support Extensionfor the App name, select your User support email, and click Next. - Under Audience, select Internal and click Next.
- Under Contact Information, enter your email address and click Next.
- Under Finish, agree to the Google API Services: User Data Policy, click Continue, and click Create.
- In Cloud Shell, set your active project (replace
YOUR_PROJECT_IDwith your Google Cloud project ID) and enable the required APIs:gcloud config set project YOUR_PROJECT_ID gcloud services enable \ aiplatform.googleapis.com \ firestore.googleapis.com \ run.googleapis.com \ cloudbuild.googleapis.com \ artifactregistry.googleapis.com \ appsmarket-component.googleapis.com \ gsuiteaddons.googleapis.com \ workspacestudio.googleapis.com - Provision your
(default)Firestore database inus-central1(if it does not already exist) and initialize your Google Workspace Add-ons service account:gcloud firestore databases describe --database="(default)" >/dev/null 2>&1 || \ gcloud firestore databases create \ --database="(default)" \ --location=us-central1 \ --type=firestore-native \ --quiet gcloud workspace-add-ons get-authorization
3. Build the Node.js app with Firestore and Gemini 3.8 Flash
In this step, you build the Node.js application in ~/studio-support-addon. To keep the code modular and readable, you organize the project into five focused files:
firestore.js: Initializes the Cloud Firestore client (studio_triggersandsupport_ticketscollections) and refreshes cached OAuth tokens.starter.js: Implements the New Support Ticket custom starter step webhooks (/onConfigTriggerand/onManageTrigger).action.js: Implements the Detect Urgency custom action step webhooks (/onConfigUrgencyand/onExecuteUrgency) using Gemini 3.8 Flash (gemini-3.8-flash) with a keyword-matching fallback.portal.js: Serves the Support Quick Portal web UI (GET /) and fires active Google Workspace Studio triggers (POST /api/submit-ticket).index.js: Wires all Express routes together and starts the HTTP server.
Initialize the project and package.json
- Create a project directory and navigate into it:
mkdir -p ~/studio-support-addon && cd ~/studio-support-addon - Create
package.jsonwithexpress,@google-cloud/firestore, and@google/genai:cat << 'EOF' > package.json { "name": "studio-support-addon", "version": "1.0.0", "type": "module", "main": "index.js", "scripts": { "start": "node index.js" }, "dependencies": { "@google-cloud/firestore": "^7.11.0", "@google/genai": "^1.0.0", "express": "^4.21.2" } } EOF
firestore.js — Shared Firestore state and OAuth token refresh
Create firestore.js to manage your two Firestore collections and keep cached user OAuth tokens fresh:
studio_triggers: Stores active Google Workspace Studio flow subscriptions (triggerId,notifyUri, anduserOAuthToken) so they survive Cloud Run container restarts and scale-to-zero events.support_tickets: Stores a record of each support ticket submitted through the portal.refreshTokenInFirestore(req): Whenever Google Workspace Studio calls any of your webhook endpoints, it includes a short-livedauthorizationEventObject.userOAuthToken. This helper updates active trigger documents with the latest token so backgroundtriggers.firecalls remain authorized.
cat << 'EOF' > firestore.js
import { Firestore } from '@google-cloud/firestore';
export const db = new Firestore({
projectId: process.env.GOOGLE_CLOUD_PROJECT || undefined,
databaseId: '(default)',
ignoreUndefinedProperties: true,
});
export const triggersCollection = db.collection(
process.env.FIRESTORE_TRIGGERS_COLLECTION || 'studio_triggers'
);
export const ticketsCollection = db.collection(
process.env.FIRESTORE_TICKETS_COLLECTION || 'support_tickets'
);
// Keep cached userOAuthToken fresh across active trigger documents
export async function refreshTokenInFirestore(req) {
const freshToken = req.body?.authorizationEventObject?.userOAuthToken;
if (!freshToken) return;
try {
const snapshot = await triggersCollection.get();
if (snapshot.empty) return;
const batch = db.batch();
snapshot.docs.forEach((doc) => {
batch.set(
doc.ref,
{ userOAuthToken: freshToken, updatedAt: new Date().toISOString() },
{ merge: true }
);
});
await batch.commit();
} catch (err) {
console.error('Error refreshing OAuth token in Firestore:', err.message);
}
}
EOF
starter.js — Custom Starter Step (workflowTrigger)
Create starter.js to implement the two webhook endpoints required by a Google Workspace Studio custom starter step:
onConfigTrigger(POST /onConfigTrigger): Called when a user selects New Support Ticket as their flow's starter in Google Workspace Studio. Returns apushCardnavigation action describing the starter.onManageTrigger(POST /onManageTrigger): Called when a user turns a flow On (req.body.workflow.triggerCreation) or turns it Off / deletes it (req.body.workflow.triggerDeletion). Saves or deletes the subscription document in thestudio_triggersFirestore collection.
cat << 'EOF' > starter.js
import { triggersCollection, refreshTokenInFirestore } from './firestore.js';
// Custom Starter Configuration Card
export async function onConfigTrigger(req, res) {
await refreshTokenInFirestore(req);
res.status(200).json({
action: {
navigations: [
{
pushCard: {
sections: [
{
header: 'Support Ticket Listener',
widgets: [
{
textParagraph: {
text: 'This starter listens for new tickets submitted through the Support Quick Portal and stores subscriptions in Firestore.',
},
},
],
},
],
},
},
],
},
});
}
// Custom Starter Subscription Lifecycle (Firestore studio_triggers collection)
export async function onManageTrigger(req, res) {
try {
const triggerCreation = req.body?.workflow?.triggerCreation;
const triggerDeletion = req.body?.workflow?.triggerDeletion;
const userOAuthToken =
req.body?.authorizationEventObject?.userOAuthToken || null;
if (triggerCreation?.triggerId) {
const triggerId = String(triggerCreation.triggerId);
const docId = encodeURIComponent(triggerId);
await triggersCollection.doc(docId).set(
{
triggerId,
notifyUri: triggerCreation.notifyUri || null,
...(userOAuthToken ? { userOAuthToken } : {}),
updatedAt: new Date().toISOString(),
},
{ merge: true }
);
}
if (triggerDeletion?.triggerId) {
const triggerId = String(triggerDeletion.triggerId);
const docId = encodeURIComponent(triggerId);
await triggersCollection.doc(docId).delete();
}
if (!triggerCreation && !triggerDeletion && userOAuthToken) {
await refreshTokenInFirestore(req);
}
} catch (err) {
console.error('Error in /onManageTrigger:', err.message);
}
res.status(200).json({});
}
EOF
action.js — Custom Action Step (workflowAction) with Gemini 3.8 Flash
Create action.js to implement the two webhook endpoints required by a Google Workspace Studio custom action step:
onConfigUrgency(POST /onConfigUrgency): Returns a configuration card with atextInputwidget that setshostAppDataSource.workflowDataSource.includeVariables: true, allowing users to select theticketDescriptionvariable outputted by the starter step.onExecuteUrgency(POST /onExecuteUrgency): Reads theticketDescriptioninput fromreq.body.workflow.actionInvocation.inputs, classifies its business impact and urgency using Gemini 3.8 Flash (gemini-3.8-flash) on the Gemini Enterprise Agent Platform with structured output (responseSchemaenum["High", "Normal"]), falls back todetectKeywordUrgency()(urgent,blocker,critical) if the model call fails, and returns theurgencyLeveloutput variable usinghostAppAction.workflowAction.returnOutputVariablesAction.
cat << 'EOF' > action.js
import { GoogleGenAI, Type } from '@google/genai';
import { refreshTokenInFirestore } from './firestore.js';
// Initialize Google GenAI SDK for the Gemini Enterprise Agent Platform
const ai = new GoogleGenAI({
vertexai: true,
project: process.env.GOOGLE_CLOUD_PROJECT,
location: 'global',
});
// Fallback keyword classifier if the Gemini API call is unavailable
export function detectKeywordUrgency(description) {
const lower = (description || '').toLowerCase();
return lower.includes('urgent') ||
lower.includes('blocker') ||
lower.includes('critical')
? 'High'
: 'Normal';
}
// Custom Action Step Configuration Card
export async function onConfigUrgency(req, res) {
await refreshTokenInFirestore(req);
res.status(200).json({
action: {
navigations: [
{
pushCard: {
sections: [
{
header: 'Urgency Detector Step',
widgets: [
{
textInput: {
name: 'ticketDescription',
label: 'Source Description',
hintText:
'Select the description variable outputted by the starter',
hostAppDataSource: {
workflowDataSource: {
includeVariables: true,
},
},
},
},
],
},
],
},
},
],
},
});
}
// Custom Action Step Execution: Gemini 3.8 Flash with keyword fallback
export async function onExecuteUrgency(req, res) {
await refreshTokenInFirestore(req);
const inputs = req.body?.workflow?.actionInvocation?.inputs || {};
const description = inputs.ticketDescription?.stringValues?.[0] || '';
let urgencyLevel = detectKeywordUrgency(description);
try {
const response = await ai.models.generateContent({
model: 'gemini-3.8-flash',
contents: `Evaluate the business impact and time sensitivity of this support ticket description and classify its urgency:\n\n${description}`,
config: {
responseMimeType: 'text/x.enum',
responseSchema: {
type: Type.STRING,
enum: ['High', 'Normal'],
},
},
});
const candidate = (response.text || '').trim();
if (candidate === 'High' || candidate === 'Normal') {
urgencyLevel = candidate;
}
} catch (err) {
console.error('Gemini fallback to keyword matching:', err.message);
}
res.status(200).json({
hostAppAction: {
workflowAction: {
returnOutputVariablesAction: {
variables: {
urgencyLevel: {
stringValues: [urgencyLevel],
},
},
},
},
},
});
}
EOF
portal.js — Support Quick Portal UI and triggers.fire dispatcher
Create portal.js to serve the web portal and dispatch ticket submissions to Google Workspace Studio:
renderPortal(GET /): Queries thestudio_triggersFirestore collection to display how many active Google Workspace Studio triggers are registered, and serves the HTML ticket submission form.submitTicket(POST /api/submit-ticket): Iterates through all active trigger documents instudio_triggers, invokes the Google Workspace Studiotriggers.fireendpoint (https://workspacestudio.googleapis.com/v1/triggers/{triggerId}:fire) with theticketTitleandticketDescriptionoutput variables and a uniquerequestId, and saves the ticket to thesupport_ticketsFirestore collection.
cat << 'EOF' > portal.js
import { triggersCollection, ticketsCollection } from './firestore.js';
// Support Quick Portal UI
export async function renderPortal(req, res) {
let activeCount = 0;
try {
const snapshot = await triggersCollection.get();
activeCount = snapshot.size;
} catch (err) {
console.error('Error reading studio_triggers collection:', err.message);
}
res.status(200).send(`<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Support Quick Portal</title>
<style>
body { font-family: Arial, sans-serif; padding: 24px; max-width: 540px; margin: auto; color: #202124; }
.badge { background: #e8f0fe; color: #1967d2; padding: 8px 12px; border-radius: 6px; font-size: 14px; margin-bottom: 16px; }
label { font-weight: bold; display: block; margin-top: 16px; }
input, textarea { width: 100%; margin-top: 6px; padding: 10px; box-sizing: border-box; border: 1px solid #dadce0; border-radius: 4px; }
button { background-color: #1a73e8; color: white; border: none; padding: 10px 18px; margin-top: 16px; font-weight: bold; cursor: pointer; border-radius: 4px; }
button:hover { background-color: #1557b0; }
#status { margin-top: 16px; font-weight: bold; }
</style>
</head>
<body>
<h2>Support Quick Portal</h2>
<div class="badge">
Active Google Workspace Studio triggers in Firestore (<code>studio_triggers</code>):
<strong id="trigger-count">${activeCount}</strong>
</div>
<p>Submit a support ticket below to fire your Google Workspace Studio flow.</p>
<label for="title">Ticket Title</label>
<input type="text" id="title" placeholder="e.g. Database failure">
<label for="description">Issue Description</label>
<textarea id="description" rows="4" placeholder="Describe the issue..."></textarea>
<button onclick="submitTicket()">Submit Ticket</button>
<div id="status"></div>
<script>
async function submitTicket() {
const title = document.getElementById('title').value.trim();
const description = document.getElementById('description').value.trim();
const statusEl = document.getElementById('status');
if (!title || !description) {
statusEl.style.color = '#d93025';
statusEl.textContent = 'Please fill out both fields.';
return;
}
statusEl.style.color = '#188038';
statusEl.textContent = 'Submitting ticket...';
try {
const res = await fetch('/api/submit-ticket', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, description }),
});
const data = await res.json();
if (data.ok) {
statusEl.textContent =
'Ticket saved to Firestore! Fired ' + data.triggersFired + ' Google Workspace Studio trigger(s).';
} else {
statusEl.style.color = '#d93025';
statusEl.textContent = 'Error: ' + (data.error || 'Unknown error');
}
} catch (err) {
statusEl.style.color = '#d93025';
statusEl.textContent = 'Request failed: ' + err.message;
}
}
</script>
</body>
</html>`);
}
// Portal Form Submission -> Fire Active Google Workspace Studio Triggers
export async function submitTicket(req, res) {
const title = req.body?.title || 'Untitled Ticket';
const description = req.body?.description || '';
const results = [];
try {
const snapshot = await triggersCollection.get();
for (const doc of snapshot.docs) {
const trigger = doc.data();
if (!trigger.triggerId || !trigger.userOAuthToken) continue;
const response = await fetch(
`https://workspacestudio.googleapis.com/v1/triggers/${encodeURIComponent(trigger.triggerId)}:fire`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${trigger.userOAuthToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'triggers/' + trigger.triggerId,
outputs: {
ticketTitle: { stringValues: [title] },
ticketDescription: { stringValues: [description] },
},
requestId:
'req_' + Date.now() + '_' + Math.floor(Math.random() * 1000),
}),
}
);
results.push({ triggerId: trigger.triggerId, status: response.status });
}
await ticketsCollection.add({
title,
description,
triggersFired: results.length,
createdAt: new Date().toISOString(),
});
res.status(200).json({ ok: true, triggersFired: results.length, results });
} catch (err) {
console.error('Error in /api/submit-ticket:', err.message);
res.status(500).json({ ok: false, error: err.message });
}
}
EOF
index.js — Express server entry point
Create index.js to wire your portal routes and Google Workspace Studio webhook endpoints together into an Express HTTP server listening on PORT (default 8080 on Cloud Run):
cat << 'EOF' > index.js
import express from 'express';
import { onConfigTrigger, onManageTrigger } from './starter.js';
import { onConfigUrgency, onExecuteUrgency } from './action.js';
import { renderPortal, submitTicket } from './portal.js';
const app = express();
app.use(express.json());
// Support Quick Portal routes
app.get('/', renderPortal);
app.post('/api/submit-ticket', submitTicket);
// Google Workspace Studio Custom Starter Step webhooks
app.post('/onConfigTrigger', onConfigTrigger);
app.post('/onManageTrigger', onManageTrigger);
// Google Workspace Studio Custom Action Step webhooks
app.post('/onConfigUrgency', onConfigUrgency);
app.post('/onExecuteUrgency', onExecuteUrgency);
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`Support Extension listening on port ${PORT}`);
});
EOF
4. Deploy to Cloud Run and install the add-on
In this step, you deploy the Node.js service to Cloud Run, grant IAM permissions for the Google Workspace Add-ons invoker and Firestore and Agent Platform runtime access, generate the deployment.json manifest, and install the add-on deployment.
- Deploy the
support-extensionservice to Cloud Run with your project and Firestore collection environment variables:gcloud run deploy support-extension \ --source . \ --region us-central1 \ --allow-unauthenticated \ --set-env-vars="GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project),FIRESTORE_TRIGGERS_COLLECTION=studio_triggers,FIRESTORE_TICKETS_COLLECTION=support_tickets" \ --quiet - Grant
roles/run.invokeron the Cloud Run service to the Google Workspace Add-ons service account so Google Workspace Studio can call your webhook endpoints:ADDON_SA=$(gcloud workspace-add-ons get-authorization --format="value(serviceAccountEmail)") gcloud run services add-iam-policy-binding support-extension \ --region=us-central1 \ --member="serviceAccount:${ADDON_SA}" \ --role="roles/run.invoker" \ --quiet - Grant
roles/datastore.user(for reading and writing thestudio_triggersandsupport_ticketsFirestore collections) androles/aiplatform.user(for calling Gemini on the Gemini Enterprise Agent Platform) to the Cloud Run runtime service account:PROJECT_ID=$(gcloud config get-value project) PROJECT_NUMBER=$(gcloud projects describe "${PROJECT_ID}" --format="value(projectNumber)") RUNTIME_SA=$(gcloud run services describe support-extension --region=us-central1 --format="value(spec.template.spec.serviceAccountName)") RUNTIME_SA=${RUNTIME_SA:-"${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"} gcloud projects add-iam-policy-binding "${PROJECT_ID}" \ --member="serviceAccount:${RUNTIME_SA}" \ --role="roles/datastore.user" \ --condition=None \ --quiet gcloud projects add-iam-policy-binding "${PROJECT_ID}" \ --member="serviceAccount:${RUNTIME_SA}" \ --role="roles/aiplatform.user" \ --condition=None \ --quiet - Retrieve your deployed Cloud Run URL and generate the
deployment.jsonmanifest defining thesupportTicketTrigger(workflowTrigger) andurgencyDetectorStep(workflowAction) elements:SERVICE_URL=$(gcloud run services describe support-extension --region=us-central1 --format="value(status.url)") cat << EOF > deployment.json { "oauthScopes": [ "https://www.googleapis.com/auth/workspace.studio.trigger", "https://www.googleapis.com/auth/userinfo.email" ], "addOns": { "common": { "name": "Support Extension", "logoUrl": "https://www.gstatic.com/images/branding/productlogos/buganizer/v1/192px.svg" }, "studio": { "flows": { "workflowElements": [ { "id": "supportTicketTrigger", "state": "ACTIVE", "name": "New Support Ticket", "description": "Fires when someone submits a ticket in the Support Quick Portal.", "workflowTrigger": { "inputs": [], "outputs": [ { "id": "ticketTitle", "description": "Ticket Title", "cardinality": "SINGLE", "dataType": { "basicType": "STRING" } }, { "id": "ticketDescription", "description": "Ticket Description", "cardinality": "SINGLE", "dataType": { "basicType": "STRING" } } ], "onConfigFunction": "${SERVICE_URL}/onConfigTrigger", "onManageFunction": "${SERVICE_URL}/onManageTrigger" } }, { "id": "urgencyDetectorStep", "state": "ACTIVE", "name": "Detect Urgency", "description": "Evaluates the ticket description to detect High or Normal urgency.", "workflowAction": { "inputs": [ { "id": "ticketDescription", "description": "Description to scan", "cardinality": "SINGLE", "dataType": { "basicType": "STRING" } } ], "outputs": [ { "id": "urgencyLevel", "description": "Detected Urgency (High / Normal)", "cardinality": "SINGLE", "dataType": { "basicType": "STRING" } } ], "onConfigFunction": "${SERVICE_URL}/onConfigUrgency", "onExecuteFunction": "${SERVICE_URL}/onExecuteUrgency" } } ] } }, "httpOptions": { "authorizationHeader": "SYSTEM_ID_TOKEN" } } } EOF - Create (or replace) and install the Google Workspace Add-on deployment for your account, and print your Support Quick Portal URL:
gcloud workspace-add-ons deployments create support-extension --deployment-file=deployment.json || \ gcloud workspace-add-ons deployments replace support-extension --deployment-file=deployment.json gcloud workspace-add-ons deployments install support-extension echo "Support Quick Portal URL: ${SERVICE_URL}"
5. Test your custom steps in Google Workspace Studio
- Open Google Workspace Studio (refresh the browser tab if it was already open) and click New Flow.
- Click Select a starter > Support Extension > New Support Ticket.
- Click Add step (
+) > Support Extension > Detect Urgency. In the Source Description field, click + Variables and select Ticket Description. - Click Add step (
+) and add a Chat > Notify me in Chat step that includes the Ticket Title, Urgency Level, and Ticket Description variables in the message. - Toggle the flow On in the top-right corner. Google Workspace Studio immediately calls your
/onManageTriggerendpoint on Cloud Run, which writes the active trigger subscription to thestudio_triggerscollection in Firestore. - Open your Support Quick Portal URL (
https://support-extension-...run.app) in a new tab, confirm that the portal displays1active trigger in Firestore, and submit two tickets to test Gemini 3.8 Flash (gemini-3.8-flash) classification:- High-impact ticket (Ticket Title:
London office lockout, Issue Description:All 500 employees in the London office are locked out of their workstations 20 minutes before the board presentation.): Even without hardcoded keywords likeurgentorblocker,gemini-3.8-flashclassifies this ticket asHighurgency. - Routine ticket (Ticket Title:
Conference room adapters, Issue Description:Could we order two extra HDMI adapters for the third-floor conference room next month?):gemini-3.8-flashclassifies this ticket asNormalurgency.
- High-impact ticket (Ticket Title:
- Verify in Google Chat and in the Google Workspace Studio Activity tab that your flow ran for both tickets with
Urgency Level: HighandUrgency Level: Normal.
6. Clean up
To avoid incurring ongoing charges to your Google Cloud account, remove the deployed resources in Cloud Shell:
gcloud workspace-add-ons deployments uninstall support-extension
gcloud workspace-add-ons deployments delete support-extension --quiet
gcloud run services delete support-extension --region=us-central1 --quiet
gcloud firestore databases delete --database="(default)" --quiet
7. Congratulations!
You built, deployed, and tested a Node.js HTTP extension for Google Workspace Studio using Firestore, Cloud Run, and Gemini 3.8 Flash on the Gemini Enterprise Agent Platform!
What we've covered
- Configuring a Google Workspace Studio HTTP Add-on manifest (
deployment.json) withworkflowTriggerandworkflowActionelements. - Persisting Google Workspace Studio
workflowTriggersubscriptions in thestudio_triggersFirestore collection and logging portal submissions insupport_ticketsso triggers survive Cloud Run scaling and redeployments. - Building a
workflowActionstep that classifies support tickets with Gemini 3.8 Flash (gemini-3.8-flash) structured output on the Gemini Enterprise Agent Platform, with a resilient keyword-matching fallback.