Build custom Google Workspace Studio steps in Node.js on Cloud Run

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:

  1. 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 Studio triggers.fire REST API.
  2. 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 a studio_triggers Firestore collection, logs submitted tickets in a support_tickets Firestore collection, and fires whenever a ticket is submitted in the web portal—passing ticketTitle and ticketDescription into 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) with workflowTrigger and workflowAction elements.
  • How to persist Google Workspace Studio trigger subscriptions (triggerId, notifyUri, and userOAuthToken) in a studio_triggers Firestore collection and fire flows using the triggers.fire REST API.
  • How to build a workflowAction custom 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.

  1. Open the Google Cloud console, select your Google Cloud project, and click the Activate Cloud Shell icon in the top navigation bar.
  2. Configure the OAuth consent screen so Google Workspace can provision an OAuth client for your add-on:
    1. Open the Google Auth Platform Branding page.
    2. If prompted, click Get Started. (If your consent screen is already configured, skip to step 3.)
    3. Under App Information, enter Support Extension for the App name, select your User support email, and click Next.
    4. Under Audience, select Internal and click Next.
    5. Under Contact Information, enter your email address and click Next.
    6. Under Finish, agree to the Google API Services: User Data Policy, click Continue, and click Create.
  3. In Cloud Shell, set your active project (replace YOUR_PROJECT_ID with 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
    
  4. Provision your (default) Firestore database in us-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_triggers and support_tickets collections) and refreshes cached OAuth tokens.
  • starter.js: Implements the New Support Ticket custom starter step webhooks (/onConfigTrigger and /onManageTrigger).
  • action.js: Implements the Detect Urgency custom action step webhooks (/onConfigUrgency and /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

  1. Create a project directory and navigate into it:
    mkdir -p ~/studio-support-addon && cd ~/studio-support-addon
    
  2. Create package.json with express, @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, and userOAuthToken) 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-lived authorizationEventObject.userOAuthToken. This helper updates active trigger documents with the latest token so background triggers.fire calls 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 a pushCard navigation 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 the studio_triggers Firestore 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 a textInput widget that sets hostAppDataSource.workflowDataSource.includeVariables: true, allowing users to select the ticketDescription variable outputted by the starter step.
  • onExecuteUrgency (POST /onExecuteUrgency): Reads the ticketDescription input from req.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 (responseSchema enum ["High", "Normal"]), falls back to detectKeywordUrgency() (urgent, blocker, critical) if the model call fails, and returns the urgencyLevel output variable using hostAppAction.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 the studio_triggers Firestore 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 in studio_triggers, invokes the Google Workspace Studio triggers.fire endpoint (https://workspacestudio.googleapis.com/v1/triggers/{triggerId}:fire) with the ticketTitle and ticketDescription output variables and a unique requestId, and saves the ticket to the support_tickets Firestore 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.

  1. Deploy the support-extension service 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
    
  2. Grant roles/run.invoker on 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
    
  3. Grant roles/datastore.user (for reading and writing the studio_triggers and support_tickets Firestore collections) and roles/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
    
  4. Retrieve your deployed Cloud Run URL and generate the deployment.json manifest defining the supportTicketTrigger (workflowTrigger) and urgencyDetectorStep (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
    
  5. 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

  1. Open Google Workspace Studio (refresh the browser tab if it was already open) and click New Flow.
  2. Click Select a starter > Support Extension > New Support Ticket.
  3. Click Add step (+) > Support Extension > Detect Urgency. In the Source Description field, click + Variables and select Ticket Description.
  4. 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.
  5. Toggle the flow On in the top-right corner. Google Workspace Studio immediately calls your /onManageTrigger endpoint on Cloud Run, which writes the active trigger subscription to the studio_triggers collection in Firestore.
  6. Open your Support Quick Portal URL (https://support-extension-...run.app) in a new tab, confirm that the portal displays 1 active 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 like urgent or blocker, gemini-3.8-flash classifies this ticket as High urgency.
    • 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-flash classifies this ticket as Normal urgency.
  7. Verify in Google Chat and in the Google Workspace Studio Activity tab that your flow ran for both tickets with Urgency Level: High and Urgency 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) with workflowTrigger and workflowAction elements.
  • Persisting Google Workspace Studio workflowTrigger subscriptions in the studio_triggers Firestore collection and logging portal submissions in support_tickets so triggers survive Cloud Run scaling and redeployments.
  • Building a workflowAction step 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.

Next steps