1. Introduction
Google Workspace Studio vous permet d'automatiser les processus métier dans les applications Google Workspace telles que Gmail, Google Chat, Google Drive et Google Sheets. Lorsque vos workflows doivent démarrer à partir d'une application Web externe ou exécuter une logique personnalisée dans votre propre environnement cloud, vous pouvez étendre Google Workspace Studio avec un module complémentaire Google Workspace HTTP en Node.js sur Cloud Run :
- Étape de déclencheur personnalisé (
workflowTrigger) : démarre un flux Google Workspace Studio lorsqu'un événement externe se produit (par exemple, un client envoie un ticket dans un portail Web interne) en appelant lenotifyUridu déclencheur (ou l'API RESTtriggers.firede Google Workspace Studio). - Étape d'action personnalisée (
workflowAction) : reçoit des variables des étapes précédentes du flux, exécute une logique personnalisée ou une inférence d'IA dans votre service Node.js et renvoie des variables de sortie à Google Workspace Studio.
Dans cet atelier de programmation, vous allez créer et déployer une extension HTTP Node.js complète pour Google Workspace Studio sur Cloud Run, utiliser Cloud Firestore pour conserver les abonnements actifs aux déclencheurs de flux et les tickets envoyés dans les différentes révisions de conteneur, utiliser Cloud Tasks pour réessayer automatiquement les notifications de déclencheur si notifyUri ne renvoie pas de réponse de réussite, et classer l'urgence des tickets dans votre étape d'action personnalisée à l'aide de Gemini 3.8 Flash (gemini-3.8-flash) sur Gemini Enterprise Agent Platform avec une solution de secours résiliente basée sur la correspondance de mots clés.
Objectifs de l'atelier
- Une application Web Support Quick Portal et un module complémentaire Google Workspace HTTP créés avec Node.js, Express, Firestore et Cloud Tasks sur Cloud Run.
- Une étape de déclencheur personnalisé New Support Ticket (
supportTicketTrigger) qui conserve les abonnements actifs aux flux dans une collection Firestorestudio_triggers, enregistre les tickets envoyés dans une collection Firestoresupport_ticketset se déclenche chaque fois qu'un ticket est envoyé dans le portail Web, en transmettantticketTitleetticketDescriptiondans un flux Google Workspace Studio. - Un worker de nouvelle tentative Cloud Tasks (file d'attente
studio-trigger-retriesetPOST /api/tasks/fire-trigger) qui met en file d'attente et réessaye les appels de déclencheur ayant échoué avec un intervalle exponentiel entre les tentatives à l'aide du dernieruserOAuthTokenstocké dans Firestore, garantissant la diffusion versnotifyUri. - Une étape d'action personnalisée Detect Urgency (
urgencyDetectorStep) qui classe l'urgence des tickets à l'aide de Gemini 3.8 Flash (gemini-3.8-flash) sur Gemini Enterprise Agent Platform (aiplatform.googleapis.com), avec une solution de secours automatique basée sur la correspondance de mots clés (urgent,blocker,critical).
Architecture
Le schéma suivant illustre l'interaction entre Support Quick Portal, votre module complémentaire Node.js sur Cloud Run, Cloud Firestore, Cloud Tasks (studio-trigger-retries), Gemini 3.8 Flash et Google Workspace Studio, de l'enregistrement du flux à l'exécution de l'action en passant par la diffusion résiliente du déclencheur :

Points abordés
- Configurer un fichier manifeste de déploiement de module complémentaire Google Workspace HTTP (
deployment.json) avec des élémentsworkflowTriggeretworkflowAction. - Conserver les abonnements aux déclencheurs Google Workspace Studio (
triggerId,notifyUrietuserOAuthToken) dans une collection Firestorestudio_triggerset déclencher des flux à l'aide denotifyUri/triggers.fire. - Utiliser Cloud Tasks (
@google-cloud/tasks) pour réessayer les notifications de déclencheur ayant échoué avec un intervalle exponentiel entre les tentatives jusqu'à ce quenotifyUrirenvoie une réponse de réussite2xx. - Créer une étape personnalisée
workflowActionen Node.js à l'aide de la sortie structurée Gemini 3.8 Flash (gemini-3.8-flash) sur Gemini Enterprise Agent Platform avec une solution de secours basée sur les mots clés.
Prérequis
- Un compte Google Workspace ayant accès à Google Workspace Studio.
- Un projet Google Cloud pour lequel la facturation est activée, créé dans la même organisation Google Workspace que votre compte utilisateur afin que vous puissiez installer des déploiements de modules complémentaires non publiés à des fins de test.
2. Configurer votre projet Google Cloud, Firestore et Cloud Tasks
Dans cette étape, vous allez configurer l'écran de consentement OAuth de votre projet, activer les API Google Cloud et Google Workspace requises dans Cloud Shell, provisionner votre base de données Firestore (default), créer votre file d'attente de nouvelles tentatives Cloud Tasks et initialiser votre compte de service d'autorisation de module complémentaire.
- Ouvrez la console Google Cloud, sélectionnez votre projet Google Cloud, puis cliquez sur l'icône Activer Cloud Shell dans la barre de navigation supérieure.
- Configurez l'écran de consentement OAuth afin que Google Workspace puisse provisionner un client OAuth pour votre module complémentaire :
- Ouvrez la page Branding de Google Auth Platform.
- Si vous y êtes invité, cliquez sur Premiers pas. (Si votre écran de consentement est déjà configuré, passez à l'étape 3.)
- Sous Informations sur l'application, saisissez
Support Extensionpour le nom de l'application, sélectionnez votre adresse e-mail d'assistance utilisateur, puis cliquez sur Suivant. - Sous Audience, sélectionnez Interne, puis cliquez sur Suivant.
- Sous Coordonnées, saisissez votre adresse e-mail, puis cliquez sur Suivant.
- Sous Terminer, acceptez le Règlement sur les données utilisateur dans les services d'API Google, cliquez sur Continuer, puis sur Créer.
- Dans Cloud Shell, définissez votre projet actif (remplacez
YOUR_PROJECT_IDpar l'ID de votre projet Google Cloud) et activez les API requises :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 - Provisionnez votre base de données Firestore
(default)et la file d'attente Cloud Tasksstudio-trigger-retriesdansus-central1(si elles n'existent pas déjà), puis initialisez votre compte de service Google Workspace Add-ons :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. Créer l'application Node.js avec Firestore, Cloud Tasks et Gemini 3.8 Flash
Dans cette étape, vous allez créer l'application Node.js dans ~/studio-support-addon. Pour que le code reste modulaire et lisible, vous allez organiser le projet en six fichiers ciblés :
firestore.js: initialise le client Cloud Firestore (collectionsstudio_triggersetsupport_tickets) et actualise les jetons OAuth mis en cache.starter.js: implémente les Webhooks de l'étape de déclencheur personnalisé New Support Ticket (/onConfigTriggeret/onManageTrigger).action.js: implémente les Webhooks de l'étape d'action personnalisée Detect Urgency (/onConfigUrgencyet/onExecuteUrgency) à l'aide de Gemini 3.8 Flash (gemini-3.8-flash) avec une solution de secours basée sur la correspondance de mots clés.tasks.js: distribue les notifications de déclencheur àtrigger.notifyUri(en revenant àhttps://workspacestudio.googleapis.com/v1/triggers/{triggerId}:fire), met en file d'attente les diffusions ayant échoué dans la file d'attente Cloud Tasksstudio-trigger-retrieset implémente le point de terminaison du worker de nouvelle tentativePOST /api/tasks/fire-trigger.portal.js: diffuse l'interface utilisateur Web Support Quick Portal (GET /) et déclenche les déclencheurs Google Workspace Studio actifs (POST /api/submit-ticket), avec une option permettant de simuler un échec temporaire et de regarder Cloud Tasks réessayer.index.js: relie toutes les routes Express et démarre le serveur HTTP.
Initialiser le projet et package.json
- Créez un répertoire de projet et accédez à celui-ci :
mkdir -p ~/studio-support-addon && cd ~/studio-support-addon - Créez
package.jsonavecexpress,@google-cloud/firestore,@google-cloud/taskset@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 : état Firestore partagé et actualisation du jeton OAuth
Créez firestore.js pour gérer vos deux collections Firestore et actualiser les jetons OAuth utilisateur mis en cache :
studio_triggers: stocke les abonnements actifs aux flux Google Workspace Studio (triggerId,notifyUrietuserOAuthToken) afin qu'ils survivent aux redémarrages des conteneurs Cloud Run et aux événements de scaling à zéro instance.support_tickets: stocke un enregistrement de chaque demande d'assistance envoyée via le portail.refreshTokenInFirestore(req): chaque fois que Google Workspace Studio appelle l'un de vos points de terminaison de Webhook, il inclut unauthorizationEventObject.userOAuthTokende courte durée. Cet assistant met à jour les documents de déclencheur actifs avec le dernier jeton afin que les appelstriggers.fireen arrière-plan restent autorisés.
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 : étape de déclencheur personnalisé (workflowTrigger)
Créez starter.js pour implémenter les deux points de terminaison de Webhook requis par une étape de déclencheur personnalisé Google Workspace Studio :
onConfigTrigger(POST /onConfigTrigger) : appelé lorsqu'un utilisateur sélectionne Nouvelle demande d'assistance comme déclencheur de son flux dans Google Workspace Studio. Renvoie une action de navigationpushCarddécrivant le déclencheur.onManageTrigger(POST /onManageTrigger) : appelé lorsqu'un utilisateur active un flux (req.body.workflow.triggerCreation) ou le désactive / le supprime (req.body.workflow.triggerDeletion). Enregistre ou supprime le document d'abonnement dans la collection Firestorestudio_triggers.
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 : étape d'action personnalisée (workflowAction) avec Gemini 3.8 Flash
Créez action.js pour implémenter les deux points de terminaison de Webhook requis par une étape d'action personnalisée Google Workspace Studio :
onConfigUrgency(POST /onConfigUrgency) : renvoie une carte de configuration avec un widgettextInputqui définithostAppDataSource.workflowDataSource.includeVariables: true, ce qui permet aux utilisateurs de sélectionner la variableticketDescriptiongénérée par l'étape de déclencheur.onExecuteUrgency(POST /onExecuteUrgency) : lit l'entréeticketDescriptionà partir dereq.body.workflow.actionInvocation.inputs, classe son impact commercial et son urgence à l'aide de Gemini 3.8 Flash (gemini-3.8-flash) sur Gemini Enterprise Agent Platform avec une sortie structurée (responseSchemaenum["High", "Normal"]), revient àdetectKeywordUrgency()(urgent,blocker,critical) si l'appel de modèle échoue et renvoie la variable de sortieurgencyLevelà l'aide dehostAppAction.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
tasks.js : file d'attente de nouvelles tentatives Cloud Tasks et point de terminaison du worker
Créez tasks.js pour distribuer les appels de déclencheur à trigger.notifyUri (en revenant à https://workspacestudio.googleapis.com/v1/triggers/{triggerId}:fire), mettre en file d'attente les appels ayant échoué dans votre file d'attente Cloud Tasks studio-trigger-retries et traiter les nouvelles tentatives sur POST /api/tasks/fire-trigger :
resolveTriggerUrl(trigger): utilisetrigger.notifyUriconservé à partir detriggerCreation.notifyUrilorsqu'il est présent, ou revient àhttps://workspacestudio.googleapis.com/v1/triggers/{triggerId}:fire.fireTriggerDirect(trigger, payload): envoie la requêtePOSTà l'URL du déclencheur avec les variables de sortieticketTitleetticketDescription, ainsi qu'unrequestIdidempotent.enqueueTriggerRetry(req, payload): met en file d'attente une tâche HTTP dans la file d'attentestudio-trigger-retriesciblant le point de terminaisonPOST /api/tasks/fire-triggerde votre service Cloud Run chaque fois que l'appel de déclencheur initial échoue ou renvoie un état HTTP non 2xx.handleFireTriggerTask(POST /api/tasks/fire-trigger) : appelé par Cloud Tasks à chaque tentative. Recherche le dernier documenttriggerdansstudio_triggersà partir de Firestore (en s'assurant que les nouvelles tentatives utilisent toujours leuserOAuthTokenle plus récent), vérifie l'en-têteX-CloudTasks-TaskRetryCount(sisimulateFailureest activé, la première exécution de Cloud Tasks échoue avec503afin que vous puissiez voir Cloud Tasks réessayer après un intervalle entre les tentatives et réussir à la tentative#1), et renvoie200 OKuniquement lorsquenotifyUrirenvoie une réponse de réussite2xx. Toute réponse5xxindique à Cloud Tasks de réessayer conformément à la règle d'intervalle entre les tentatives de votre file d'attente.
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 : interface utilisateur Support Quick Portal et répartiteur notifyUri avec solution de secours Cloud Tasks
Créez portal.js pour diffuser le portail Web et distribuer les envois de tickets à Google Workspace Studio avec une planification automatique des nouvelles tentatives Cloud Tasks :
renderPortal(GET /) : interroge la collection Firestorestudio_triggerspour afficher le nombre de déclencheurs Google Workspace Studio actifs enregistrés et diffuse le formulaire d'envoi de tickets HTML (y compris une case à cocher Simuler un échec temporaire pour tester les nouvelles tentatives Cloud Tasks).submitTicket(POST /api/submit-ticket) : itère sur tous les documents de déclencheur actifs dansstudio_triggerset appellefireTriggerDirect(). Si l'URL du déclencheur renvoie une réponse non 2xx, génère une erreur réseau ou sisimulateFailureest activé, il met automatiquement en file d'attente une tâche de nouvelle tentative dansstudio-trigger-retriesà l'aide deenqueueTriggerRetry()et enregistre le ticket dans la collection Firestoresupport_tickets.
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 : point d'entrée du serveur Express
Créez index.js pour relier vos routes de portail, le point de terminaison du worker de nouvelle tentative Cloud Tasks et les points de terminaison de Webhook Google Workspace Studio dans un serveur HTTP Express écoutant sur PORT (valeur par défaut 8080 sur 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. Déployer sur Cloud Run et installer le module complémentaire
Dans cette étape, vous allez déployer le service Node.js sur Cloud Run, accorder des autorisations IAM pour l'appelant Google Workspace Add-ons et l'accès à l'exécution de Firestore, Cloud Tasks et Agent Platform, générer le fichier manifeste deployment.json et installer le déploiement du module complémentaire.
- Déployez le service
support-extensionsur Cloud Run avec les variables d'environnement de votre projet, de votre collection Firestore et de votre file d'attente de tâches Cloud Tasks :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 - Accordez
roles/run.invokersur le service Cloud Run au compte de service Google Workspace Add-ons afin que Google Workspace Studio puisse appeler vos points de terminaison de Webhook :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 - Accordez
roles/datastore.user(pour lire et écrire les collections Firestorestudio_triggersetsupport_tickets),roles/cloudtasks.enqueuer(pour mettre en file d'attente les tâches de nouvelle tentative dansstudio-trigger-retries) etroles/aiplatform.user(pour appeler Gemini sur Gemini Enterprise Agent Platform) au compte de service d'exécution Cloud Run :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 - Récupérez l'URL Cloud Run déployée et générez le fichier manifeste
deployment.jsondéfinissant les élémentssupportTicketTrigger(workflowTrigger) eturgencyDetectorStep(workflowAction) :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 - Créez (ou remplacez) et installez le déploiement du module complémentaire Google Workspace pour votre compte, puis affichez l'URL de votre portail Support Quick Portal :
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. Tester vos étapes personnalisées et les nouvelles tentatives Cloud Tasks
- Ouvrez Google Workspace Studio (actualisez l'onglet du navigateur s'il était déjà ouvert), puis cliquez sur Nouveau flux.
- Cliquez sur Sélectionner un déclencheur > Support Extension > Nouvelle demande d'assistance.
- Cliquez sur Ajouter une étape (
+) > Support Extension > Detect Urgency. Dans le champ Description de la source , cliquez sur + Variables , puis sélectionnez Description du ticket. - Cliquez sur Ajouter une étape (
+) et ajoutez une étape Chat > M'envoyer une notification dans Chat qui inclut les variables Titre du ticket, Niveau d'urgence et Description du ticket dans le message. - En haut à droite, activez le flux. Google Workspace Studio appelle immédiatement votre point de terminaison
/onManageTriggersur Cloud Run, qui écrit l'abonnement actif au déclencheur (y compristriggerId,notifyUrietuserOAuthToken) dans la collectionstudio_triggersde Firestore. - Ouvrez l'URL de votre portail Support Quick Portal (
https://support-extension-...run.app) dans un nouvel onglet, vérifiez que le portail affiche1déclencheur actif dans Firestore, puis envoyez deux tickets pour tester la classification Gemini 3.8 Flash (gemini-3.8-flash) :- Ticket à fort impact (Titre du ticket :
London office lockout, Description du problème :All 500 employees in the London office are locked out of their workstations 20 minutes before the board presentation.) : même sans mots clés codés en dur tels queurgentoublocker,gemini-3.8-flashclasse ce ticket commeHighurgence. - Ticket de routine (Titre du ticket :
Conference room adapters, Description du problème :Could we order two extra HDMI adapters for the third-floor conference room next month?) :gemini-3.8-flashclasse ce ticket comme urgenceNormal.
- Ticket à fort impact (Titre du ticket :
- Vérifiez dans Google Chat et dans l'onglet Activité de Google Workspace Studio que votre flux s'est exécuté pour les deux tickets avec
Urgency Level: HighetUrgency Level: Normal. - Testez la diffusion de nouvelles tentatives Cloud Tasks lorsque
notifyUriéchoue :- Dans le portail Support Quick Portal, cochez la case Simuler un échec temporaire (mettre en file d'attente dans Cloud Tasks et réessayer notifyUri).
- Saisissez Titre du ticket :
Checkout service outageet Description du problème :Production payment gateway is timing out for all customers, puis cliquez sur Envoyer le ticket. - Notez que le portail répond immédiatement par :
Ticket saved! Initial notifyUri call did not succeed; queued 1 retry task(s) in Cloud Tasks.(Ticket enregistré ! L'appel notifyUri initial n'a pas abouti. 1 tâche de nouvelle tentative a été mise en file d'attente dans Cloud Tasks.) - En arrière-plan, Cloud Tasks exécute
POST /api/tasks/fire-trigger. La première tentative de tâche (retryCount=0) simule un échec503, ce qui entraîne un intervalle entre les tentatives de10s(--min-backoff=10s) et une nouvelle tentative (retryCount=1) de Cloud Tasks, qui appellenotifyUriet réussit. Au bout de 10 à 15 secondes environ, vérifiez que votre notification Google Chat est arrivée.
6. Effectuer un nettoyage
Pour éviter que des frais permanents ne soient facturés sur votre compte Google Cloud, supprimez les ressources déployées dans 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 tasks queues delete studio-trigger-retries --location=us-central1 --quiet
gcloud firestore databases delete --database="(default)" --quiet
7. Félicitations !
Vous avez créé, déployé et testé une extension HTTP Node.js pour Google Workspace Studio à l'aide de Firestore, Cloud Tasks, Cloud Run et Gemini 3.8 Flash sur Gemini Enterprise Agent Platform.
Points abordés
- Configurer un fichier manifeste de module complémentaire HTTP Google Workspace Studio (
deployment.json) avec des élémentsworkflowTriggeretworkflowAction. - Conserver les abonnements
workflowTriggerde Google Workspace Studio dans la collection Firestorestudio_triggerset enregistrer les envois de portail danssupport_ticketsafin que les déclencheurs survivent à la mise à l'échelle et aux redéploiements de Cloud Run. - Garantir la diffusion
notifyUri(triggers.fire) avec Cloud Tasks (studio-trigger-retries) afin que les échecs HTTP temporaires soient automatiquement réessayés avec un intervalle exponentiel entre les tentatives à l'aide du dernieruserOAuthTokende Firestore. - Créer une étape
workflowActionqui classe les tickets d'assistance avec Gemini 3.8 Flash (gemini-3.8-flash) sortie structurée sur Gemini Enterprise Agent Platform, avec une solution de secours résiliente basée sur la correspondance de mots clés.