1. Introduzione
Google Workspace Studio ti consente di automatizzare i processi aziendali nelle applicazioni Google Workspace come Gmail, Google Chat, Google Drive e Fogli Google. Quando i workflow devono essere avviati da un'applicazione web esterna o eseguire una logica personalizzata nel tuo ambiente cloud, puoi estendere Google Workspace Studio con un componente aggiuntivo di Google Workspace HTTP in Node.js su Cloud Run:
- Passaggio iniziale personalizzato (
workflowTrigger): avvia un flusso di Google Workspace Studio quando si verifica un evento esterno, ad esempio un cliente che invia una richiesta in un portale web interno, chiamando ilnotifyUridel trigger (o l'API RESTtriggers.firedi Google Workspace Studio). - Passaggio di azione personalizzato (
workflowAction): riceve le variabili dai passaggi precedenti del flusso, esegue la logica personalizzata o l'inferenza AI nel servizio Node.js e restituisce le variabili di output a Google Workspace Studio.
In questo codelab, crei ed esegui il deployment di un'estensione HTTP Node.js completa per Google Workspace Studio su Cloud Run, utilizzi Cloud Firestore per rendere persistenti gli abbonamenti ai trigger di flusso attivi e le richieste inviate tra le revisioni dei container, utilizzi Cloud Tasks per riprovare automaticamente a inviare le notifiche dei trigger se notifyUri non restituisce una risposta di successo e classifichi l'urgenza delle richieste nel passaggio di azione personalizzato utilizzando Gemini 3.8 Flash (gemini-3.8-flash) su Gemini Enterprise Agent Platform con un fallback di corrispondenza di parole chiave resiliente.
Cosa creerai
- Un'app web Support Quick Portal e un componente aggiuntivo di Google Workspace HTTP creato con Node.js, Express, Firestore e Cloud Tasks su Cloud Run.
- Un passaggio iniziale personalizzato New Support Ticket (
supportTicketTrigger) che rende persistenti gli abbonamenti ai flussi attivi in una raccolta Firestorestudio_triggers, registra i ticket di assistenza inviati in una raccolta Firestoresupport_ticketse si attiva ogni volta che viene inviato un ticket di assistenza nel portale web, passandoticketTitleeticketDescriptiona un flusso di Google Workspace Studio. - Un worker di ripetizione di Cloud Tasks (coda
studio-trigger-retriesed endpointPOST /api/tasks/fire-trigger) che mette in coda e riprova a inviare le chiamate ai trigger non riuscite con un backoff esponenziale utilizzando l'ultimouserOAuthTokenmemorizzato in Firestore, garantendo la consegna anotifyUri. - Un passaggio di azione personalizzato Detect Urgency (
urgencyDetectorStep) che classifica l'urgenza delle richieste utilizzando Gemini 3.8 Flash (gemini-3.8-flash) su Gemini Enterprise Agent Platform (aiplatform.googleapis.com), con un fallback automatico alla corrispondenza di parole chiave (urgent,blocker,critical).
Architettura
Il seguente diagramma illustra come interagiscono Support Quick Portal, il componente aggiuntivo Node.js su Cloud Run, Cloud Firestore, Cloud Tasks (studio-trigger-retries), Gemini 3.8 Flash e Google Workspace Studio dalla registrazione del flusso alla consegna resiliente dei trigger e all'esecuzione delle azioni:

Obiettivi didattici
- Come configurare un manifest di deployment del componente aggiuntivo di Google Workspace HTTP (
deployment.json) con gli elementiworkflowTriggereworkflowAction. - Come rendere persistenti gli abbonamenti ai trigger di Google Workspace Studio (
triggerId,notifyUrieuserOAuthToken) in una raccolta Firestorestudio_triggerse attivare i flussi utilizzandonotifyUri/triggers.fire. - Come utilizzare Cloud Tasks (
@google-cloud/tasks) per riprovare a inviare le notifiche dei trigger non riuscite con un backoff esponenziale finchénotifyUrinon restituisce una risposta di successo2xx. - Come creare un passaggio personalizzato
workflowActionin Node.js utilizzando l'output strutturato di Gemini 3.8 Flash (gemini-3.8-flash) su Gemini Enterprise Agent Platform con un fallback di parole chiave.
Prerequisiti
- Un account Google Workspace con accesso a Google Workspace Studio.
- Un progetto Google Cloud con la fatturazione abilitata, creato nella stessa organizzazione Google Workspace del tuo account utente, in modo da poter installare i deployment di componenti aggiuntivi non pubblicati per i test.
2. Configura il progetto Google Cloud, Firestore e Cloud Tasks
In questo passaggio, configuri la schermata per il consenso OAuth del progetto, abiliti le API Google Cloud e Google Workspace richieste in Cloud Shell, esegui il provisioning del database Firestore (default), crei la coda di ripetizione di Cloud Tasks e inizializzi il service account di autorizzazione del componente aggiuntivo.
- Apri la console Google Cloud, seleziona il progetto Google Cloud e fai clic sull'icona Attiva Cloud Shell nella barra di navigazione in alto.
- Configura la schermata per il consenso OAuth in modo che Google Workspace possa eseguire il provisioning di un client OAuth per il componente aggiuntivo:
- Apri la pagina Branding della piattaforma di autenticazione Google.
- Se ti viene richiesto, fai clic su Inizia. (Se la schermata per il consenso è già configurata, vai al passaggio 3.)
- In Informazioni sull'app, inserisci
Support Extensionnel campo Nome app, seleziona la tua email di assistenza utente e fai clic su Avanti. - In Pubblico, seleziona Interno e fai clic su Avanti.
- In Dati di contatto, inserisci il tuo indirizzo email e fai clic su Avanti.
- In Fine, accetta le Norme relative ai dati utente: servizi API di Google, fai clic su Continua e poi su Crea.
- In Cloud Shell, imposta il progetto attivo (sostituisci
YOUR_PROJECT_IDcon l'ID del progetto Google Cloud) e abilita le API richieste: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 - Esegui il provisioning del database Firestore
(default)e della coda di attività Cloud Tasksstudio-trigger-retriesinus-central1(se non esistono già) e inizializza il service account dei componenti aggiuntivi di Google Workspace: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. Crea l'app Node.js con Firestore, Cloud Tasks e Gemini 3.8 Flash
In questo passaggio, crei l'applicazione Node.js in ~/studio-support-addon. Per mantenere il codice modulare e leggibile, organizzi il progetto in sei file mirati:
firestore.js: inizializza il client Cloud Firestore (raccoltestudio_triggersesupport_tickets) e aggiorna i token OAuth memorizzati nella cache.starter.js: implementa i webhook del passaggio iniziale personalizzato New Support Ticket (/onConfigTriggere/onManageTrigger).action.js: implementa i webhook del passaggio di azione personalizzato Detect Urgency (/onConfigUrgencye/onExecuteUrgency) utilizzando Gemini 3.8 Flash (gemini-3.8-flash) con un fallback di corrispondenza di parole chiave.tasks.js: invia le notifiche dei trigger atrigger.notifyUri(con fallback ahttps://workspacestudio.googleapis.com/v1/triggers/{triggerId}:fire), mette in coda le consegne non riuscite nella coda Cloud Tasksstudio-trigger-retriese implementa l'endpoint del worker di ripetizionePOST /api/tasks/fire-trigger.portal.js: gestisce l'interfaccia utente web Support Quick Portal (GET /) e attiva i trigger di Google Workspace Studio attivi (POST /api/submit-ticket), con un'opzione per simulare un errore temporaneo e osservare i tentativi di Cloud Tasks.index.js: collega tutte le route Express e avvia il server HTTP.
Inizializza il progetto e package.json
- Crea una directory di progetto e spostati al suo interno:
mkdir -p ~/studio-support-addon && cd ~/studio-support-addon - Crea
package.jsonconexpress,@google-cloud/firestore,@google-cloud/taskse@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: stato di Firestore condiviso e aggiornamento del token OAuth
Crea firestore.js per gestire le due raccolte Firestore e mantenere aggiornati i token OAuth utente memorizzati nella cache:
studio_triggers: memorizza gli abbonamenti ai flussi di Google Workspace Studio attivi (triggerId,notifyUrieuserOAuthToken) in modo che sopravvivano ai riavvii dei container Cloud Run e agli eventi di scale-to-zero.support_tickets: memorizza un record di ogni richiesta di assistenza inviata tramite il portale.refreshTokenInFirestore(req): ogni volta che Google Workspace Studio chiama uno dei tuoi endpoint webhook, include unauthorizationEventObject.userOAuthTokendi breve durata. Questo helper aggiorna i documenti dei trigger attivi con l'ultimo token in modo che le chiamatetriggers.firein background rimangano autorizzate.
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: passaggio iniziale personalizzato (workflowTrigger)
Crea starter.js per implementare i due endpoint webhook richiesti da un passaggio iniziale personalizzato di Google Workspace Studio:
onConfigTrigger(POST /onConfigTrigger): viene chiamato quando un utente seleziona New Support Ticket come punto di partenza del flusso in Google Workspace Studio. Restituisce un'azione di navigazionepushCardche descrive il punto di partenza.onManageTrigger(POST /onManageTrigger): viene chiamato quando un utente attiva un flusso On (req.body.workflow.triggerCreation) o lo disattiva / elimina (req.body.workflow.triggerDeletion). Salva o elimina il documento di abbonamento nella raccolta 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: passaggio di azione personalizzato (workflowAction) con Gemini 3.8 Flash
Crea action.js per implementare i due endpoint webhook richiesti da un passaggio di azione personalizzato di Google Workspace Studio:
onConfigUrgency(POST /onConfigUrgency): restituisce una scheda di configurazione con un widgettextInputche impostahostAppDataSource.workflowDataSource.includeVariables: true, consentendo agli utenti di selezionare la variabileticketDescriptiongenerata dal passaggio iniziale.onExecuteUrgency(POST /onExecuteUrgency): legge l'inputticketDescriptiondareq.body.workflow.actionInvocation.inputs, classifica l'impatto aziendale e l'urgenza utilizzando Gemini 3.8 Flash (gemini-3.8-flash) su Gemini Enterprise Agent Platform con output strutturato (responseSchemaenum["High", "Normal"]), esegue il fallback adetectKeywordUrgency()(urgent,blocker,critical) se la chiamata del modello non riesce e restituisce la variabile di outputurgencyLevelutilizzandohostAppAction.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: coda di ripetizione di Cloud Tasks ed endpoint del worker
Crea tasks.js per inviare le chiamate ai trigger a trigger.notifyUri (con fallback a https://workspacestudio.googleapis.com/v1/triggers/{triggerId}:fire), mettere in coda le chiamate non riuscite nella coda Cloud Tasks studio-trigger-retries ed elaborare i tentativi all'indirizzo POST /api/tasks/fire-trigger:
resolveTriggerUrl(trigger): utilizzatrigger.notifyUrireso persistente datriggerCreation.notifyUrise presente o esegue il fallback ahttps://workspacestudio.googleapis.com/v1/triggers/{triggerId}:fire.fireTriggerDirect(trigger, payload): invia la richiestaPOSTall'URL del trigger con le variabili di outputticketTitleeticketDescriptione unrequestIdidempotente.enqueueTriggerRetry(req, payload): mette in coda un'attività HTTP nella codastudio-trigger-retriesche ha come target l'endpointPOST /api/tasks/fire-triggerdel servizio Cloud Run ogni volta che la chiamata iniziale del trigger non riesce o restituisce uno stato HTTP non 2xx.handleFireTriggerTask(POST /api/tasks/fire-trigger): viene richiamato da Cloud Tasks a ogni tentativo di ripetizione. Cerca l'ultimo documentotriggerinstudio_triggersda Firestore (assicurandosi che i tentativi utilizzino sempre l'ultimouserOAuthToken), controlla l'intestazioneX-CloudTasks-TaskRetryCount(sesimulateFailureè abilitato, la prima esecuzione di Cloud Tasks non riesce con503in modo da poter osservare il backoff di Cloud Tasks e il successo al tentativo#1) e restituisce200 OKsolo quandonotifyUrirestituisce una risposta di successo2xx. Qualsiasi risposta5xxindica a Cloud Tasks di riprovare in base alla policy di backoff della coda.
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: interfaccia utente di Support Quick Portal e dispatcher notifyUri con fallback di Cloud Tasks
Crea portal.js per gestire il portale web e inviare le richieste a Google Workspace Studio con la pianificazione automatica dei tentativi di Cloud Tasks:
renderPortal(GET /): esegue una query sulla raccolta Firestorestudio_triggersper visualizzare il numero di trigger di Google Workspace Studio attivi registrati e gestisce il modulo di invio delle richieste HTML (inclusa una casella di controllo Simulate transient failure per testare i tentativi di Cloud Tasks).submitTicket(POST /api/submit-ticket): scorre tutti i documenti dei trigger attivi instudio_triggerse chiamafireTriggerDirect(). Se l'URL del trigger restituisce una risposta non 2xx, genera un errore di rete o hasimulateFailureabilitato, mette automaticamente in coda un'attività di ripetizione instudio-trigger-retriesutilizzandoenqueueTriggerRetry()e registra la richiesta nella raccolta 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: punto di accesso del server Express
Crea index.js per collegare le route del portale, l'endpoint del worker di ripetizione di Cloud Tasks e gli endpoint webhook di Google Workspace Studio in un server HTTP Express in ascolto su PORT (valore predefinito 8080 su 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. Esegui il deployment in Cloud Run e installa il componente aggiuntivo
In questo passaggio, esegui il deployment del servizio Node.js su Cloud Run, concedi le autorizzazioni IAM per il chiamante dei componenti aggiuntivi di Google Workspace e l'accesso di runtime a Firestore, Cloud Tasks e Agent Platform, genera il manifest deployment.json e installa il deployment del componente aggiuntivo.
- Esegui il deployment del servizio
support-extensionsu Cloud Run con le variabili di ambiente del progetto, della raccolta Firestore e della coda di attività 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 - Concedi il ruolo
roles/run.invokeral servizio Cloud Run al service account dei componenti aggiuntivi di Google Workspace in modo che Google Workspace Studio possa chiamare gli endpoint 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 - Concedi i ruoli
roles/datastore.user(per la lettura e la scrittura delle raccolte Firestorestudio_triggersesupport_tickets),roles/cloudtasks.enqueuer(per la messa in coda delle attività di ripetizione instudio-trigger-retries) eroles/aiplatform.user(per la chiamata di Gemini su Gemini Enterprise Agent Platform) al service account di runtime di 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 - Recupera l'URL di Cloud Run di cui hai eseguito il deployment e genera il manifest
deployment.jsonche definisce gli elementisupportTicketTrigger(workflowTrigger) eurgencyDetectorStep(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 - Crea (o sostituisci) e installa il deployment del componente aggiuntivo di Google Workspace per il tuo account e stampa l'URL di 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. Testa i passaggi personalizzati e i tentativi di Cloud Tasks
- Apri Google Workspace Studio (aggiorna la scheda del browser se era già aperta) e fai clic su Nuovo flusso.
- Fai clic su Seleziona un punto di partenza > Support Extension > Nuovo ticket di assistenza.
- Fai clic su Aggiungi passaggio (
+) > Support Extension > Detect Urgency. Nel campo Source Description, fai clic su + Variables e seleziona Ticket Description. - Fai clic su Aggiungi passaggio (
+) e aggiungi un passaggio Chat > Notify me in Chat che includa le variabili Ticket Title, Urgency Level e Ticket Description nel messaggio. - Attiva il flusso On nell'angolo in alto a destra. Google Workspace Studio chiama immediatamente l'endpoint
/onManageTriggersu Cloud Run, che scrive l'abbonamento al trigger attivo (inclusitriggerId,notifyUrieuserOAuthToken) nella raccoltastudio_triggersin Firestore. - Apri l'URL di Support Quick Portal (
https://support-extension-...run.app) in una nuova scheda, verifica che il portale visualizzi1trigger attivo in Firestore e invia due richieste per testare la classificazione di Gemini 3.8 Flash (gemini-3.8-flash):- Richiesta ad alto impatto (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.): anche senza parole chiave hardcoded comeurgentoblocker,gemini-3.8-flashclassifica questa richiesta come urgenzaHigh. - Richiesta di routine (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-flashclassifica questa richiesta come urgenzaNormal.
- Richiesta ad alto impatto (Ticket Title:
- Verifica in Google Chat e nella scheda Attività di Google Workspace Studio che il flusso sia stato eseguito per entrambe le richieste con
Urgency Level: HigheUrgency Level: Normal. - Testa la consegna dei tentativi di Cloud Tasks quando
notifyUrinon riesce:- In the Support Quick Portal, seleziona Simulate transient failure (enqueue in Cloud Tasks and retry notifyUri).
- Inserisci Ticket Title:
Checkout service outagee Issue Description:Production payment gateway is timing out for all customers, quindi fai clic su Submit Ticket. - Tieni presente che il portale risponde immediatamente con:
Ticket saved! Initial notifyUri call did not succeed; queued 1 retry task(s) in Cloud Tasks. - In background, Cloud Tasks esegue
POST /api/tasks/fire-trigger. Il primo tentativo di attività (retryCount=0) simula un errore503, facendo in modo che Cloud Tasks esegua il backoff per10s(--min-backoff=10s) e riprovi (retryCount=1), che chiamanotifyUrie ha esito positivo. Dopo circa 10-15 secondi, verifica che la notifica di Google Chat sia arrivata.
6. Esegui la pulizia
Per evitare che al tuo account Google Cloud vengano addebitati costi continui, rimuovi le risorse di cui hai eseguito il deployment 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 tasks queues delete studio-trigger-retries --location=us-central1 --quiet
gcloud firestore databases delete --database="(default)" --quiet
7. Complimenti!
Hai creato, eseguito il deployment e testato un'estensione HTTP Node.js per Google Workspace Studio utilizzando Firestore, Cloud Tasks, Cloud Run e Gemini 3.8 Flash su Gemini Enterprise Agent Platform.
Argomenti trattati
- Configurazione di un manifest del componente aggiuntivo HTTP di Google Workspace Studio (
deployment.json) con gli elementiworkflowTriggereworkflowAction. - Persistenza degli abbonamenti
workflowTriggerdi Google Workspace Studio nella raccolta Firestorestudio_triggerse registrazione degli invii del portale insupport_ticketsin modo che i trigger sopravvivano allo scaling e ai nuovi deployment di Cloud Run. - Garanzia della consegna di
notifyUri(triggers.fire) con Cloud Tasks (studio-trigger-retries) in modo che i guasti HTTP temporanei vengano ritentati automaticamente con un backoff esponenziale utilizzando l'ultimouserOAuthTokendi Firestore. - Creazione di un passaggio
workflowActionche classifica le richieste di assistenza con l'output strutturato di Gemini 3.8 Flash (gemini-3.8-flash) su Gemini Enterprise Agent Platform, con un fallback di corrispondenza di parole chiave resiliente.