Benutzerdefinierte Google Workspace Studio-Schritte in Node.js in Cloud Run erstellen

1. Einführung

Mit Google Workspace Studio können Sie Geschäftsprozesse in Google Workspace-Anwendungen wie Gmail, Google Chat, Google Drive und Google Sheets automatisieren. Wenn Ihre Workflows in einer externen Webanwendung gestartet werden oder benutzerdefinierte Logik in Ihrer eigenen Cloud-Umgebung ausgeführt werden muss, können Sie Google Workspace Studio mit einem HTTP-Google Workspace-Add-on in Node.js in Cloud Run erweitern:

  1. Benutzerdefinierter Auslöserschritt (workflowTrigger): Startet einen Google Workspace Studio-Flow, wenn ein externes Ereignis eintritt, z. B. wenn ein Kunde eine Anfrage in einem internen Webportal einreicht. Dazu wird die notifyUri des Auslösers (oder die Google Workspace Studio triggers.fire REST API) aufgerufen.
  2. Benutzerdefinierter Aktionsschritt (workflowAction): Empfängt Variablen aus früheren Schritten im Flow, führt benutzerdefinierte Logik oder KI-Inferenz in Ihrem Node.js-Dienst aus und gibt Ausgabevariablen an Google Workspace Studio zurück.

In diesem Codelab erstellen und stellen Sie eine vollständige Node.js-HTTP-Erweiterung für Google Workspace Studio in Cloud Run bereit. Sie verwenden Cloud Firestore, um aktive Flow-Trigger-Abos und eingereichte Anfragen in verschiedenen Containerversionen beizubehalten. Mit Cloud Tasks werden Triggerbenachrichtigungen automatisch wiederholt, wenn notifyUri keine Erfolgsantwort zurückgibt. Außerdem klassifizieren Sie die Dringlichkeit von Anfragen in Ihrem benutzerdefinierten Aktionsschritt mit Gemini 3.8 Flash (gemini-3.8-flash) in der Gemini Enterprise Agent Platform mit einem robusten Fallback für die Keyword-Übereinstimmung.

Aufgaben

  • Eine Webanwendung für das Support Quick Portal und ein HTTP-Google Workspace-Add-on, das mit Node.js, Express, Firestore und Cloud Tasks in Cloud Run erstellt wurde.
  • Ein benutzerdefinierter Auslöserschritt für New Support Ticket (supportTicketTrigger), der aktive Flow-Abos in einer studio_triggers-Firestore-Sammlung beibehält, eingereichte Anfragen in einer support_tickets-Firestore-Sammlung protokolliert und immer dann ausgelöst wird, wenn eine Anfrage im Webportal eingereicht wird. Dabei werden ticketTitle und ticketDescription an einen Google Workspace Studio-Flow übergeben.
  • Ein Cloud Tasks-Worker für Wiederholungen (studio-trigger-retries-Warteschlange und POST /api/tasks/fire-trigger), der fehlgeschlagene Triggeraufrufe mit exponentiellem Backoff in die Warteschlange stellt und wiederholt. Dabei wird das neueste in Firestore gespeicherte userOAuthToken verwendet, um die Zustellung an notifyUri zu garantieren.
  • Ein benutzerdefinierter Aktionsschritt für Detect Urgency (urgencyDetectorStep), der die Dringlichkeit von Anfragen mit Gemini 3.8 Flash (gemini-3.8-flash) in der Gemini Enterprise Agent Platform (aiplatform.googleapis.com) klassifiziert. Dabei wird automatisch auf die Keyword-Übereinstimmung (urgent, blocker, critical) zurückgegriffen.

Architektur

Das folgende Diagramm zeigt, wie das Support Quick Portal, Ihr Node.js-Add-on in Cloud Run, Cloud Firestore, Cloud Tasks (studio-trigger-retries), Gemini 3.8 Flash und Google Workspace Studio von der Flow-Registrierung über die robuste Triggerzustellung bis zur Aktionsausführung interagieren:

Google Workspace Studio Node.js-Add-on-Architektur mit Firestore, Cloud Tasks und Gemini 3.8 Flash

Lerninhalte

  • Konfigurieren eines Bereitstellungsmanifests für ein HTTP-Google Workspace-Add-on (deployment.json) mit den Elementen workflowTrigger und workflowAction.
  • Anleitung zum persistenten Speichern von Google Workspace Studio-Trigger-Abos (triggerId, notifyUri und userOAuthToken) in einer studio_triggers-Firestore-Sammlung und zum Auslösen von Flows mit notifyUri / triggers.fire.
  • Verwenden von Cloud Tasks (@google-cloud/tasks), um fehlgeschlagene Triggerbenachrichtigungen mit exponentiellem Backoff zu wiederholen, bis notifyUri eine 2xx-Erfolgsantwort zurückgibt.
  • Erstellen eines benutzerdefinierten workflowAction-Schritts in Node.js mit strukturierter Ausgabe von Gemini 3.8 Flash (gemini-3.8-flash) in der Gemini Enterprise Agent Platform mit einem Keyword-Fallback.

Vorbereitung

  • Ein Google Workspace-Konto mit Zugriff auf Google Workspace Studio.
  • Ein Google Cloud-Projekt mit aktivierter Abrechnung, das in derselben Google Workspace-Organisation wie Ihr Nutzerkonto erstellt wurde, damit Sie nicht veröffentlichte Add-on-Bereitstellungen zum Testen installieren können.

2. Google Cloud-Projekt, Firestore und Cloud Tasks einrichten

In diesem Schritt konfigurieren Sie den OAuth-Zustimmungsbildschirm Ihres Projekts, aktivieren die erforderlichen Google Cloud- und Google Workspace-APIs in Cloud Shell, stellen Ihre (default)-Firestore-Datenbank bereit, erstellen die Cloud Tasks-Warteschlange für Wiederholungen und initialisieren das Dienstkonto für die Add-on-Autorisierung.

  1. Öffnen Sie die Google Cloud Console, wählen Sie Ihr Google Cloud-Projekt aus und klicken Sie in der oberen Navigationsleiste auf das Symbol Cloud Shell aktivieren.
  2. Konfigurieren Sie den OAuth-Zustimmungsbildschirm , damit Google Workspace einen OAuth-Client für Ihr Add-on bereitstellen kann:
    1. Öffnen Sie die Seite Google Auth Platform Branding.
    2. Klicken Sie bei Aufforderung auf Jetzt starten. (Wenn Ihr Zustimmungsbildschirm bereits konfiguriert ist, fahren Sie mit Schritt 3 fort.)
    3. Geben Sie unter App-Informationen Support Extension für den App-Namen ein, wählen Sie Ihre E‑Mail-Adresse für den Nutzersupport aus und klicken Sie auf Weiter.
    4. Wählen Sie unter Zielgruppe die Option Intern aus und klicken Sie auf Weiter.
    5. Geben Sie unter Kontaktdaten Ihre E‑Mail-Adresse ein und klicken Sie auf Weiter.
    6. Akzeptieren Sie unter Abschließen die Richtlinie zu Nutzerdaten für Google API-Dienste, klicken Sie auf Weiter und dann auf Erstellen.
  3. Legen Sie in Cloud Shell Ihr aktives Projekt fest (ersetzen Sie YOUR_PROJECT_ID durch Ihre Google Cloud-Projekt-ID) und aktivieren Sie die erforderlichen APIs:
    gcloud config set project YOUR_PROJECT_ID
    gcloud services enable \
      aiplatform.googleapis.com \
      firestore.googleapis.com \
      cloudtasks.googleapis.com \
      run.googleapis.com \
      cloudbuild.googleapis.com \
      artifactregistry.googleapis.com \
      appsmarket-component.googleapis.com \
      gsuiteaddons.googleapis.com \
      workspacestudio.googleapis.com
    
  4. Stellen Sie Ihre (default)-Firestore-Datenbank und die studio-trigger-retries-Cloud Tasks-Aufgabenwarteschlange in us-central1 bereit (falls sie noch nicht vorhanden sind) und initialisieren Sie Ihr Google Workspace-Add-ons-Dienstkonto:
    gcloud firestore databases describe --database="(default)" >/dev/null 2>&1 || \
      gcloud firestore databases create \
        --database="(default)" \
        --location=us-central1 \
        --type=firestore-native \
        --quiet
    
    gcloud tasks queues describe studio-trigger-retries --location=us-central1 >/dev/null 2>&1 || \
      gcloud tasks queues create studio-trigger-retries \
        --location=us-central1 \
        --max-attempts=5 \
        --min-backoff=10s \
        --max-backoff=300s \
        --max-doublings=4
    
    gcloud workspace-add-ons get-authorization
    

3. Node.js-Anwendung mit Firestore, Cloud Tasks und Gemini 3.8 Flash erstellen

In diesem Schritt erstellen Sie die Node.js-Anwendung in ~/studio-support-addon. Um den Code modular und lesbar zu halten, organisieren Sie das Projekt in sechs Dateien:

  • firestore.js: Initialisiert den Cloud Firestore-Client (studio_triggers- und support_tickets-Sammlungen) und aktualisiert im Cache gespeicherte OAuth-Tokens.
  • starter.js: Implementiert die Webhooks für den benutzerdefinierten Auslöserschritt New Support Ticket (/onConfigTrigger und /onManageTrigger).
  • action.js: Implementiert die Webhooks für den benutzerdefinierten Aktionsschritt Detect Urgency (/onConfigUrgency und /onExecuteUrgency) mit Gemini 3.8 Flash (gemini-3.8-flash) und einem Fallback für die Keyword-Übereinstimmung.
  • tasks.js: Sendet Triggerbenachrichtigungen an trigger.notifyUri (Fallback auf https://workspacestudio.googleapis.com/v1/triggers/{triggerId}:fire), stellt fehlgeschlagene Zustellungen in die studio-trigger-retries-Cloud Tasks-Warteschlange und implementiert den Wiederholungs-Worker-Endpunkt POST /api/tasks/fire-trigger.
  • portal.js: Stellt die Web-UI des Support Quick Portal bereit (GET /) und löst aktive Google Workspace Studio-Trigger aus (POST /api/submit-ticket). Außerdem gibt es eine Option, einen vorübergehenden Fehler zu simulieren und die Wiederholungen von Cloud Tasks zu beobachten.
  • index.js: Verknüpft alle Express-Routen und startet den HTTP-Server.

Projekt und package.json initialisieren

  1. Erstellen Sie ein Projektverzeichnis und wechseln Sie zu diesem Verzeichnis:
    mkdir -p ~/studio-support-addon && cd ~/studio-support-addon
    
  2. Erstellen Sie package.json mit express, @google-cloud/firestore, @google-cloud/tasks und @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-cloud/tasks": "^5.6.0",
        "@google/genai": "^1.0.0",
        "express": "^4.21.2"
      }
    }
    EOF
    

firestore.js – Gemeinsamer Firestore-Status und OAuth-Token-Aktualisierung

Erstellen Sie firestore.js, um Ihre beiden Firestore-Sammlungen zu verwalten und im Cache gespeicherte OAuth-Tokens auf dem neuesten Stand zu halten:

  • studio_triggers: Speichert aktive Google Workspace Studio-Flow-Abos (triggerId, notifyUri und userOAuthToken), damit sie Cloud Run-Containerneustarts und Skalierung auf null-Ereignisse überstehen.
  • support_tickets: Speichert einen Eintrag für jedes über das Portal eingereichte Support-Ticket.
  • refreshTokenInFirestore(req): Wenn Google Workspace Studio einen Ihrer Webhook-Endpunkte aufruft, wird ein kurzlebiges authorizationEventObject.userOAuthToken eingefügt. Mit diesem Helfer werden aktive Triggerdokumente mit dem neuesten Token aktualisiert, damit triggers.fire-Aufrufe im Hintergrund autorisiert bleiben.
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 – Benutzerdefinierter Auslöserschritt (workflowTrigger)

Erstellen Sie starter.js, um die beiden Webhook-Endpunkte zu implementieren, die für einen benutzerdefinierten Auslöserschritt in Google Workspace Studio erforderlich sind:

  • onConfigTrigger (POST /onConfigTrigger): Wird aufgerufen, wenn ein Nutzer in Google Workspace Studio New Support Ticket als Auslöser für seinen Flow auswählt. Gibt eine pushCard-Navigationsaktion zurück, die den Auslöser beschreibt.
  • onManageTrigger (POST /onManageTrigger): Wird aufgerufen, wenn ein Nutzer einen Flow aktiviert (req.body.workflow.triggerCreation) oder deaktiviert / löscht (req.body.workflow.triggerDeletion). Speichert oder löscht das Abo-Dokument in der studio_triggers-Firestore-Sammlung.
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 – Benutzerdefinierter Aktionsschritt (workflowAction) mit Gemini 3.8 Flash

Erstellen Sie action.js, um die beiden Webhook-Endpunkte zu implementieren, die für einen benutzerdefinierten Aktionsschritt in Google Workspace Studio erforderlich sind:

  • onConfigUrgency (POST /onConfigUrgency): Gibt eine Konfigurationskarte mit einem textInput-Widget zurück, das hostAppDataSource.workflowDataSource.includeVariables: true festlegt. So können Nutzer die vom Auslöserschritt ausgegebene Variable ticketDescription auswählen.
  • onExecuteUrgency (POST /onExecuteUrgency): Liest die ticketDescription Eingabe aus req.body.workflow.actionInvocation.inputs, klassifiziert die geschäftlichen Auswirkungen und die Dringlichkeit mit Gemini 3.8 Flash (gemini-3.8-flash) in der Gemini Enterprise Agent Platform mit strukturierter Ausgabe (responseSchema Enum ["High", "Normal"]). Wenn der Modellaufruf fehlschlägt, wird auf detectKeywordUrgency() (urgent, blocker, critical) zurückgegriffen. Die Ausgabevariable urgencyLevel wird mit hostAppAction.workflowAction.returnOutputVariablesAction zurückgegeben.
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

tasks.js – Cloud Tasks-Warteschlange für Wiederholungen und Worker-Endpunkt

Erstellen Sie tasks.js, um Triggeraufrufe an trigger.notifyUri zu senden (Fallback auf https://workspacestudio.googleapis.com/v1/triggers/{triggerId}:fire), fehlgeschlagene Aufrufe in die studio-trigger-retries-Cloud Tasks-Warteschlange zu stellen und Wiederholungen unter POST /api/tasks/fire-trigger zu verarbeiten:

  • resolveTriggerUrl(trigger): Verwendet trigger.notifyUri, das aus triggerCreation.notifyUri beibehalten wurde, falls vorhanden, oder greift auf https://workspacestudio.googleapis.com/v1/triggers/{triggerId}:fire zurück.
  • fireTriggerDirect(trigger, payload): Sendet die POST-Anfrage mit den Ausgabevariablen ticketTitle und ticketDescription sowie einer idempotenten requestId an die Trigger-URL.
  • enqueueTriggerRetry(req, payload): Stellt eine HTTP-Aufgabe in die studio-trigger-retries-Warteschlange, die auf den POST /api/tasks/fire-trigger-Endpunkt Ihres Cloud Run-Dienstes ausgerichtet ist, wenn der erste Triggeraufruf fehlschlägt oder ein HTTP-Status ungleich 2xx zurückgegeben wird.
  • handleFireTriggerTask (POST /api/tasks/fire-trigger): Wird bei jedem Wiederholungsversuch von Cloud Tasks aufgerufen. Ruft das neueste trigger-Dokument in studio_triggers aus Firestore ab (damit bei Wiederholungen immer das aktuellste userOAuthToken verwendet wird), prüft den Header X-CloudTasks-TaskRetryCount (wenn simulateFailure aktiviert ist, schlägt die erste Cloud Tasks-Ausführung mit 503 fehl, damit Sie beobachten können, wie Cloud Tasks zurücktritt und bei Wiederholung #1 erfolgreich ist) und gibt nur dann 200 OK zurück, wenn notifyUri eine 2xx-Erfolgsantwort zurückgibt. Bei jeder 5xx-Antwort wird Cloud Tasks angewiesen, die Wiederholung gemäß der Backoff-Richtlinie Ihrer Warteschlange zu wiederholen.
cat << 'EOF' > tasks.js
import { CloudTasksClient } from '@google-cloud/tasks';
import { triggersCollection } from './firestore.js';

const tasksClient = new CloudTasksClient();
const PROJECT_ID = process.env.GOOGLE_CLOUD_PROJECT;
const LOCATION = process.env.CLOUD_TASKS_LOCATION || 'us-central1';
const QUEUE = process.env.CLOUD_TASKS_QUEUE || 'studio-trigger-retries';

// Prefer trigger.notifyUri from triggerCreation, falling back to triggers.fire
export function resolveTriggerUrl(trigger) {
  return (
    trigger.notifyUri ||
    `https://workspacestudio.googleapis.com/v1/triggers/${encodeURIComponent(trigger.triggerId)}:fire`
  );
}

// Call the Google Workspace Studio trigger URL directly
export async function fireTriggerDirect(
  trigger,
  { title, description, requestId }
) {
  const targetUrl = resolveTriggerUrl(trigger);
  const response = await fetch(targetUrl, {
    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,
    }),
  });
  return { ok: response.ok, status: response.status, targetUrl };
}

// Enqueue a retry task in Cloud Tasks targeting POST /api/tasks/fire-trigger
export async function enqueueTriggerRetry(req, payload) {
  const parent = tasksClient.queuePath(PROJECT_ID, LOCATION, QUEUE);
  const baseUrl = process.env.SERVICE_URL || `https://${req.get('host')}`;
  const workerUrl = `${baseUrl}/api/tasks/fire-trigger`;

  const task = {
    httpRequest: {
      httpMethod: 'POST',
      url: workerUrl,
      headers: { 'Content-Type': 'application/json' },
      body: Buffer.from(JSON.stringify(payload)).toString('base64'),
    },
  };

  const [response] = await tasksClient.createTask({ parent, task });
  return response.name;
}

// Cloud Tasks worker endpoint: retries calling notifyUri using fresh Firestore state
export async function handleFireTriggerTask(req, res) {
  const { triggerId, title, description, requestId, simulateFailure } =
    req.body || {};
  const retryCount = Number(req.get('X-CloudTasks-TaskRetryCount') || 0);

  if (!triggerId) {
    return res.status(200).json({ skipped: true, reason: 'Missing triggerId' });
  }

  try {
    const docId = encodeURIComponent(String(triggerId));
    const doc = await triggersCollection.doc(docId).get();
    if (!doc.exists) {
      return res
        .status(200)
        .json({ skipped: true, reason: 'Trigger no longer active' });
    }

    const trigger = doc.data();
    if (!trigger.userOAuthToken) {
      return res
        .status(503)
        .json({ ok: false, error: 'Missing userOAuthToken in Firestore' });
    }

    // When simulateFailure is enabled, fail the 1st Cloud Task attempt (retryCount === 0)
    // so learners can observe Cloud Tasks back off and succeed on retry #1.
    if (simulateFailure && retryCount < 1) {
      console.warn(
        `[Cloud Tasks] Simulated transient failure on initial task attempt (retryCount=${retryCount}) for trigger ${triggerId}`
      );
      return res
        .status(503)
        .json({ ok: false, simulated: true, retryCount, triggerId });
    }

    const outcome = await fireTriggerDirect(trigger, {
      title,
      description,
      requestId,
    });

    if (outcome.ok) {
      console.log(
        `[Cloud Tasks] Successfully delivered trigger ${triggerId} to ${outcome.targetUrl} on retryCount=${retryCount}`
      );
      return res
        .status(200)
        .json({ ok: true, status: outcome.status, retryCount });
    }

    console.error(
      `[Cloud Tasks] Non-2xx response (${outcome.status}) from ${outcome.targetUrl} on retryCount=${retryCount}`
    );
    return res
      .status(502)
      .json({ ok: false, status: outcome.status, retryCount });
  } catch (err) {
    console.error(
      `[Cloud Tasks] Error firing trigger ${triggerId} on retryCount=${retryCount}:`,
      err.message
    );
    return res
      .status(503)
      .json({ ok: false, error: err.message, retryCount });
  }
}
EOF

portal.js – UI des Support Quick Portal und notifyUri-Dispatcher mit Cloud Tasks-Fallback

Erstellen Sie portal.js, um das Webportal bereitzustellen und Anfragen an Google Workspace Studio zu senden. Dabei wird automatisch eine Wiederholung mit Cloud Tasks geplant:

  • renderPortal (GET /): Fragt die studio_triggers-Firestore-Sammlung ab, um die Anzahl der registrierten aktiven Google Workspace Studio-Trigger anzuzeigen, und stellt das HTML-Formular für die Anfrageeinreichung bereit. Außerdem gibt es ein Kontrollkästchen Simulate transient failure (Vorübergehenden Fehler simulieren), um Cloud Tasks-Wiederholungen zu testen.
  • submitTicket (POST /api/submit-ticket): Durchläuft alle aktiven Triggerdokumente in studio_triggers und ruft fireTriggerDirect() auf. Wenn die Trigger-URL eine Antwort ungleich 2xx zurückgibt, ein Netzwerkfehler auftritt oder simulateFailure aktiviert ist, wird automatisch eine Wiederholungsaufgabe in studio-trigger-retries mit enqueueTriggerRetry() in die Warteschlange gestellt und die Anfrage in der support_tickets-Firestore-Sammlung protokolliert.
cat << 'EOF' > portal.js
import { triggersCollection, ticketsCollection } from './firestore.js';
import { fireTriggerDirect, enqueueTriggerRetry } from './tasks.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; }
      .checkbox-label { font-weight: normal; margin-top: 14px; display: flex; align-items: center; gap: 8px; cursor: pointer; }
      .checkbox-label input { width: auto; margin: 0; }
      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>

    <label class="checkbox-label" for="simulateFailure">
      <input type="checkbox" id="simulateFailure">
      Simulate transient failure (enqueue in Cloud Tasks and retry notifyUri)
    </label>

    <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 simulateFailure = document.getElementById('simulateFailure').checked;
        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, simulateFailure }),
          });
          const data = await res.json();
          if (data.ok) {
            if (data.tasksQueued > 0) {
              statusEl.textContent =
                'Ticket saved! Initial notifyUri call did not succeed; queued ' +
                data.tasksQueued +
                ' retry task(s) in Cloud Tasks.';
            } else {
              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 -> Call notifyUri directly, or enqueue Cloud Task on failure
export async function submitTicket(req, res) {
  const title = req.body?.title || 'Untitled Ticket';
  const description = req.body?.description || '';
  const simulateFailure = Boolean(req.body?.simulateFailure);
  const results = [];
  let triggersFired = 0;
  let tasksQueued = 0;

  try {
    const snapshot = await triggersCollection.get();
    for (const doc of snapshot.docs) {
      const trigger = doc.data();
      if (!trigger.triggerId || !trigger.userOAuthToken) continue;

      const requestId =
        'req_' + Date.now() + '_' + Math.floor(Math.random() * 1000);

      let outcome = { ok: false, status: 503 };
      if (!simulateFailure) {
        try {
          outcome = await fireTriggerDirect(trigger, {
            title,
            description,
            requestId,
          });
        } catch (fetchErr) {
          console.error(
            `Direct trigger call failed for ${trigger.triggerId}:`,
            fetchErr.message
          );
          outcome = { ok: false, status: 0, error: fetchErr.message };
        }
      }

      if (outcome.ok) {
        triggersFired++;
        results.push({
          triggerId: trigger.triggerId,
          status: outcome.status,
          queuedRetry: false,
        });
      } else {
        const taskName = await enqueueTriggerRetry(req, {
          triggerId: trigger.triggerId,
          title,
          description,
          requestId,
          simulateFailure,
        });
        tasksQueued++;
        results.push({
          triggerId: trigger.triggerId,
          status: outcome.status,
          queuedRetry: true,
          taskName,
        });
      }
    }

    await ticketsCollection.add({
      title,
      description,
      triggersFired,
      tasksQueued,
      createdAt: new Date().toISOString(),
    });

    res.status(200).json({
      ok: true,
      triggersFired,
      tasksQueued,
      results,
    });
  } catch (err) {
    console.error('Error in /api/submit-ticket:', err.message);
    res.status(500).json({ ok: false, error: err.message });
  }
}
EOF

index.js – Einstiegspunkt für den Express-Server

Erstellen Sie index.js, um Ihre Portalrouten, den Cloud Tasks-Worker-Endpunkt für Wiederholungen und die Google Workspace Studio-Webhook-Endpunkte in einem Express-HTTP-Server zu verknüpfen, der auf PORT wartet (Standard 8080 in 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';
import { handleFireTriggerTask } from './tasks.js';

const app = express();
app.use(express.json());

// Support Quick Portal routes
app.get('/', renderPortal);
app.post('/api/submit-ticket', submitTicket);

// Cloud Tasks retry worker route
app.post('/api/tasks/fire-trigger', handleFireTriggerTask);

// 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. In Cloud Run bereitstellen und Add-on installieren

In diesem Schritt stellen Sie den Node.js-Dienst in Cloud Run bereit, erteilen IAM-Berechtigungen für den Aufrufer von Google Workspace-Add-ons und den Laufzeitzugriff auf Firestore, Cloud Tasks und die Agent Platform, generieren das deployment.json Manifest und installieren die Add-on-Bereitstellung.

  1. Stellen Sie den support-extension-Dienst mit den Umgebungsvariablen für Ihr Projekt, Ihre Firestore-Sammlung und Ihre Cloud Tasks-Aufgabenwarteschlange in Cloud Run bereit:
    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,CLOUD_TASKS_QUEUE=studio-trigger-retries,CLOUD_TASKS_LOCATION=us-central1" \
      --quiet
    
  2. Erteilen Sie dem Google Workspace-Add-ons-Dienstkonto die Rolle roles/run.invoker für den Cloud Run-Dienst, damit Google Workspace Studio Ihre Webhook-Endpunkte aufrufen kann:
    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. Erteilen Sie dem Cloud Run-Laufzeitdienstkonto die Rollen roles/datastore.user (zum Lesen und Schreiben der studio_triggers- und support_tickets-Firestore-Sammlungen), roles/cloudtasks.enqueuer (zum Einreihen von Wiederholungsaufgaben in studio-trigger-retries) und roles/aiplatform.user (zum Aufrufen von Gemini in der Gemini Enterprise Agent Platform):
    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/cloudtasks.enqueuer" \
      --condition=None \
      --quiet
    
    gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
      --member="serviceAccount:${RUNTIME_SA}" \
      --role="roles/aiplatform.user" \
      --condition=None \
      --quiet
    
  4. Rufen Sie die bereitgestellte Cloud Run-URL ab und generieren Sie das Manifest deployment.json, das die Elemente supportTicketTrigger (workflowTrigger) und urgencyDetectorStep (workflowAction) definiert:
    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. Erstellen (oder ersetzen) Sie die Google Workspace-Add-on-Bereitstellung für Ihr Konto und installieren Sie sie. Geben Sie die URL des Support Quick Portal aus:
    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. Benutzerdefinierte Schritte und Cloud Tasks-Wiederholungen testen

  1. Öffnen Sie Google Workspace Studio (aktualisieren Sie den Browsertab, falls er bereits geöffnet war) und klicken Sie auf Neuer Flow.
  2. Klicken Sie auf Auslöser auswählen > Support Extension > New Support Ticket.
  3. Klicken Sie auf Schritt hinzufügen (+) > Support Extension > Detect Urgency. Klicken Sie im Feld Quellbeschreibung auf + Variablen und wählen Sie Ticket Description aus.
  4. Klicken Sie auf Schritt hinzufügen (+) und fügen Sie einen Schritt Chat > Notify me in Chat hinzu, der die Variablen Ticket Title, Urgency Level und Ticket Description in der Nachricht enthält.
  5. Aktivieren Sie den Flow rechts oben. Google Workspace Studio ruft sofort den Endpunkt /onManageTrigger in Cloud Run auf, der das aktive Trigger-Abo (einschließlich triggerId, notifyUri und userOAuthToken) in die studio_triggers-Sammlung in Firestore schreibt.
  6. Öffnen Sie die URL des Support Quick Portal (https://support-extension-...run.app) in einem neuen Tab, bestätigen Sie, dass im Portal 1 aktiver Trigger in Firestore angezeigt wird, und reichen Sie zwei Anfragen ein, um die Klassifizierung von Gemini 3.8 Flash (gemini-3.8-flash) zu testen:
    • Anfrage mit hoher Priorität (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.): Auch ohne fest codierte Keywords wie urgent oder blocker, klassifiziert gemini-3.8-flash diese Anfrage als High (hoch).
    • Routineanfrage (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 klassifiziert diese Anfrage als Normal urgency.
  7. Prüfen Sie in Google Chat und auf dem Tab Aktivität von Google Workspace Studio, dass Ihr Flow für beide Anfragen mit Urgency Level: High und Urgency Level: Normal ausgeführt wurde.
  8. Testen Sie die Cloud Tasks-Wiederholungszustellung , wenn notifyUri fehlschlägt:
    1. Aktivieren Sie im Support Quick Portal das Kontrollkästchen Simulate transient failure (enqueue in Cloud Tasks and retry notifyUri) (Vorübergehenden Fehler simulieren (in Cloud Tasks in die Warteschlange stellen und notifyUri wiederholen)).
    2. Geben Sie unter Ticket Title: Checkout service outage und unter Issue Description: Production payment gateway is timing out for all customers ein und klicken Sie dann auf Submit Ticket.
    3. Das Portal antwortet sofort mit: Ticket saved! Initial notifyUri call did not succeed; queued 1 retry task(s) in Cloud Tasks. (Anfrage gespeichert. Der erste Aufruf von notifyUri ist fehlgeschlagen. 1 Wiederholungsaufgabe in Cloud Tasks in die Warteschlange gestellt.)
    4. Im Hintergrund führt Cloud Tasks POST /api/tasks/fire-trigger aus. Der erste Aufgabenversuch (retryCount=0) simuliert einen 503-Fehler, wodurch Cloud Tasks 10s (--min-backoff=10s) wartet und es noch einmal versucht (retryCount=1). Dabei wird notifyUri aufgerufen und der Vorgang ist erfolgreich. Nach etwa 10 bis 15 Sekunden sollte Ihre Google Chat-Benachrichtigung eintreffen.

6. Bereinigen

Entfernen Sie die bereitgestellten Ressourcen in Cloud Shell, um laufende Kosten für Ihr Google Cloud-Konto zu vermeiden:

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 tasks queues delete studio-trigger-retries --location=us-central1 --quiet
gcloud firestore databases delete --database="(default)" --quiet

7. Glückwunsch!

Sie haben eine Node.js-HTTP-Erweiterung für Google Workspace Studio mit Firestore, Cloud Tasks, Cloud Run und Gemini 3.8 Flash in der Gemini Enterprise Agent Platform erstellt, bereitgestellt und getestet.

Behandelte Themen

  • Konfigurieren eines Manifests für ein HTTP-Google Workspace Studio-Add-on (deployment.json) mit den Elementen workflowTrigger und workflowAction.
  • Beibehalten von Google Workspace Studio-workflowTrigger-Abos in der Firestore-Sammlung studio_triggers und Protokollieren von Portaleinreichungen in support_tickets, damit Trigger Cloud Run-Skalierungen und -Neubereitstellungen überstehen.
  • Garantieren der Zustellung von notifyUri (triggers.fire) mit Cloud Tasks (studio-trigger-retries), damit vorübergehende HTTP-Fehler automatisch mit exponentiellem Backoff und dem neuesten userOAuthToken aus Firestore wiederholt werden.
  • Erstellen eines workflowAction-Schritts, der Support-Tickets mit Gemini 3.8 Flash (gemini-3.8-flash) strukturierter Ausgabe in der Gemini Enterprise Agent Platform klassifiziert. Dabei wird ein robustes Fallback für die Keyword-Übereinstimmung verwendet.

Nächste Schritte