1. Introduction
In this codelab, you'll build an interactive Support Quick Portal that directly interfaces with Google Workspace Studio. You'll learn how to build both a custom starter step and a custom action step that can be instantly tested without relying on any third-party infrastructure.
We'll use Google Apps Script to create:
- A Web App with a sleek form for users to manually submit support tickets.
- A Custom Starter that captures those Web App submissions and triggers a Workspace Studio flow.
- A Custom Step that analyzes the incoming ticket's description to identify urgency levels ("High" or "Normal").
What you'll learn
- How to configure starters and steps in your
appsscript.jsonmanifest. - How to construct the
notifyUriduring the starter registration lifecycle. - How to programmatically trigger a Workspace Studio flow from external code using
UrlFetchApp. - How to construct output variables in your custom steps to pass data downstream.
Prerequisites
- A Google Workspace account with Google Workspace Studio enabled.
- The Allow unpublished (test) custom steps setting enabled in your domain's Admin Console (under Apps > Google Workspace > Workspace Studio > custom steps settings).
- Familiarity with Google Apps Script.
2. Setup the Apps Script Project
First, let's create a new Apps Script project to house our code:
- Navigate to script.google.com and click New project.
- Name the project Support Quick Portal.
- Click
Project Settings on the left sidebar.
- Check the box to Show appsscript.json manifest file in editor.
- Return to the
Editor view.
Link a standard Google Cloud Project
By default, Apps Script projects are associated with a hidden, default Google Cloud project. To call the Google Workspace Studio API and avoid PERMISSION_DENIED errors, you must switch your script to a standard Cloud project, enable the API, and properly configure the Google Auth Platform.
- Select or Create a Project: Go to the Google Cloud Console and use the project dropdown menu at the top of the page to choose an existing project or create a new one.
- Enable the API: Once your chosen project is active in the top bar, open the API Enablement Flow and click Next and Enable for the Workspace Studio API.
- Configure OAuth Branding: Open the Google Auth Platform Branding page.
- If prompted, click Get Started. (If already configured, skip to step 4).
- Under App Information, enter an App name and User support email. Click Next.
- Under Audience, select Internal (or External if Internal isn't available) and click Next.
- Provide Contact Information, agree to the Data Policy, and click Create.
- Configure Data Access: Open the Data Access page and click Add or Remove Scopes.
- Under Manually add scopes, paste
https://www.googleapis.com/auth/workspace.studio.trigger. - Click Add to Table, then Update, and finally click Save at the bottom of the page.
- Under Manually add scopes, paste
- Go to IAM & Admin > Settings (or the Cloud Console dashboard) and copy the Project number.
- Return to your Apps Script project and click
Project Settings on the left sidebar.
- Under the Google Cloud Platform (GCP) Project section, click Change project.
- Enter the GCP Project number you copied and click Set project.
Configure the Manifest
Open the newly visible appsscript.json file. Replace its contents with the code below. This manifest explicitly lists our required OAuth scopes and defines our starter as workflowTrigger and action as workflowAction under the studio.flows.workflowElements block.
{
"timeZone": "America/Los_Angeles",
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"webapp": {
"executeAs": "USER_ACCESSING",
"access": "MYSELF"
},
"oauthScopes": [
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/workspace.studio.trigger",
"https://www.googleapis.com/auth/script.storage",
"https://www.googleapis.com/auth/script.locale"
],
"urlFetchWhitelist": [
"https://workspacestudio.googleapis.com/"
],
"addOns": {
"common": {
"name": "Support Extension",
"logoUrl": "https://www.gstatic.com/images/branding/productlogos/buganizer/v1/192px.svg",
"useLocaleFromApp": true
},
"studio": {
"flows": {
"workflowElements": [
{
"id": "supportTicketTrigger",
"state": "ACTIVE",
"name": "New Support Ticket",
"description": "Fires when someone submits a ticket via the Web App.",
"workflowTrigger": {
"inputs": [],
"outputs": [
{
"id": "ticketTitle",
"description": "The title of the ticket",
"cardinality": "SINGLE",
"dataType": { "basicType": "STRING" }
},
{
"id": "ticketDescription",
"description": "The full description of the issue",
"cardinality": "SINGLE",
"dataType": { "basicType": "STRING" }
}
],
"onConfigFunction": "onConfigTrigger",
"onManageFunction": "onManageTrigger"
}
},
{
"id": "urgencyDetectorStep",
"state": "ACTIVE",
"name": "Detect Urgency",
"description": "Scans the ticket description to detect High or Normal urgency.",
"workflowAction": {
"inputs": [
{
"id": "ticketDescription",
"description": "The ticket description",
"cardinality": "SINGLE",
"dataType": { "basicType": "STRING" }
}
],
"outputs": [
{
"id": "urgencyLevel",
"description": "The urgency High/Normal",
"cardinality": "SINGLE",
"dataType": { "basicType": "STRING" }
}
],
"onConfigFunction": "onConfigUrgency",
"onExecuteFunction": "onExecuteUrgency"
}
}
]
}
}
}
}
3. Build the Web App UI
To provide a visual and testable way to trigger our starter without using terminal tools or third-party webhooks, we'll build an Apps Script Web App.
In the Apps Script editor, next to Files, click Add a file and select HTML. Name it
index.html (the .html extension is added automatically).
Paste the following simplified UI code:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
body { font-family: Arial, sans-serif; padding: 20px; max-width: 500px; margin: auto; }
label { font-weight: bold; display: block; margin-top: 15px; }
input, textarea { width: 100%; margin-top: 5px; padding: 10px; box-sizing: border-box; }
button { background-color: #1a73e8; color: white; border: none; padding: 10px 15px; margin-top: 15px; font-weight: bold; cursor: pointer; border-radius: 4px; }
button:hover { background-color: #1557b0; }
#status { margin-top: 15px; color: green; font-weight: bold; }
</style>
</head>
<body>
<h2>Support Quick Portal</h2>
<p>Submit your issue below to trigger the Workspace Studio flow.</p>
<label for="title">Ticket Title</label>
<input type="text" id="title" placeholder="e.g. Broken monitor">
<label for="description">Issue Description</label>
<textarea id="description" rows="4" placeholder="Explain your issue (use words like 'urgent' to test the step!)..."></textarea>
<button onclick="submitTicket()">Submit Ticket</button>
<div id="status"></div>
<script>
function submitTicket() {
const title = document.getElementById('title').value;
const description = document.getElementById('description').value;
const statusEl = document.getElementById('status');
if (!title || !description) {
statusEl.style.color = "red";
statusEl.innerText = "Please fill out both fields.";
return;
}
statusEl.style.color = "green";
statusEl.innerText = "Submitting ticket...";
google.script.run
.withSuccessHandler(function(response) {
statusEl.innerText = response;
})
.withFailureHandler(function(error) {
statusEl.style.color = 'red';
statusEl.innerText = 'Error: ' + error.message;
})
.submitTicketToStudio(title, description);
}
</script>
</body>
</html>
4. Build the Backend Scripts
Now let's wire it all together. To keep the code readable, we'll separate the Web App, Starter logic, and Action Step logic into three different script files.
WebApp.gs
Rename the default Code.gs file in the editor to WebApp.gs, delete the boilerplate myFunction, and copy and paste the block below into it. This file handles the Web App hosting:
/** --- WEB APP HOSTING --- **/
/**
* Serves the HTML UI when users visit the web app URL.
*/
function doGet() {
return HtmlService.createHtmlOutputFromFile('index')
.setTitle('Support Quick Portal')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
/**
* Called by the Web App form. It calls the Workspace Studio API.
*/
function submitTicketToStudio(title, description) {
const props = PropertiesService.getUserProperties();
const notifyUri = props.getProperty('notifyUri');
const triggerId = props.getProperty('triggerId');
if (!notifyUri || !triggerId) {
throw new Error('Trigger URL missing! Make sure the flow is enabled in Workspace Studio.');
}
// Use ScriptApp.getOAuthToken() (requires the workspace.studio.trigger scope in manifest)
const token = ScriptApp.getOAuthToken();
const requestId = Utilities.getUuid();
// Package our outputs mapped in appsscript.json
const payload = {
"name": "triggers/" + triggerId,
"outputs": {
"ticketTitle": { "stringValues": [title] },
"ticketDescription": { "stringValues": [description] }
},
"requestId": requestId
};
const options = {
"method": "POST",
"contentType": "application/json",
"headers": { "Authorization": "Bearer " + token },
"payload": JSON.stringify(payload),
"muteHttpExceptions": true
};
// Fire!
const response = UrlFetchApp.fetch(notifyUri, options);
if (response.getResponseCode() !== 200) {
console.error(response.getContentText());
throw new Error('Workspace Studio API Error: ' + response.getResponseCode());
}
return 'Ticket successfully routed to Workspace Studio!';
}
Starter.gs
Next, hover over Files, click Add a file, select Script, and name it
Starter.gs. Copy and paste the block below into it. This file handles Starter configuration and Workspace Studio lifecycle management:
/** --- STARTER LOGIC --- **/
/**
* Workspace Studio calls this to display the config UI when the user adds the starter.
* We just return a static message since there are no properties to configure.
*/
function onConfigTrigger() {
const section = CardService.newCardSection()
.setHeader("Configure Web App Starter")
.addWidget(CardService.newTextParagraph().setText("Ready to go! Once you enable this flow, deploy your Apps Script project as a Web App to submit tickets."));
return CardService.newCardBuilder().addSection(section).build();
}
/**
* Workspace Studio posts data here to manage the lifecycle (enable/disable the trigger).
* Critically, we must construct or save the `notifyUri` and `triggerId` to call it later.
*/
function onManageTrigger(event) {
const triggerCreation = event.workflow.triggerCreation;
const triggerDeletion = event.workflow.triggerDeletion;
const props = PropertiesService.getUserProperties();
if (triggerCreation) {
// Save the unique URL and ID when the flow is enabled
// Note: notifyUri might be omitted, so fallback to constructing it dynamically
const notifyUri = triggerCreation.notifyUri || `https://workspacestudio.googleapis.com/v1/triggers/${triggerCreation.triggerId}:fire`;
props.setProperty('notifyUri', notifyUri);
props.setProperty('triggerId', triggerCreation.triggerId);
} else if (triggerDeletion) {
// Cleanup if the user deletes the flow
props.deleteProperty('notifyUri');
props.deleteProperty('triggerId');
}
}
Action.gs
Next, hover over Files, click Add a file, select Script, and name it
Action.gs. Copy and paste the Urgency Step logic below into it:
/** --- ACTION STEP LOGIC --- **/
/**
* Workspace Studio configuration card for the Step.
* We prompt the user to bind a variable to our "ticketDescription" input.
*/
function onConfigUrgency() {
const input = CardService.newTextInput()
.setFieldName("ticketDescription")
.setTitle("Source Description")
.setHint('Select the description variable outputted by the starter')
.setHostAppDataSource(
CardService.newHostAppDataSource().setWorkflowDataSource(CardService.newWorkflowDataSource().setIncludeVariables(true))
);
const section = CardService.newCardSection()
.setHeader("Urgency Detector Step")
.addWidget(input);
return CardService.newCardBuilder().addSection(section).build();
}
/**
* The synchronous logic executed during the Step.
*/
function onExecuteUrgency(event) {
// Read mapped inputs
let description = event.workflow.actionInvocation.inputs["ticketDescription"].stringValues[0];
if (!description) description = "";
const urgentKeywords = ['urgent', 'crash', 'broken', 'down', 'fire'];
let urgencyLevel = 'Normal';
if (urgentKeywords.some(keyword => description.toLowerCase().includes(keyword))) {
urgencyLevel = 'High';
}
// Package outputs correctly
const variableDataMap = {
"urgencyLevel": AddOnsResponseService.newVariableData().addStringValue(urgencyLevel)
};
return outputVariables(variableDataMap);
}
/**
* Helper strictly enforcing the expected Step object return type.
*/
function outputVariables(variableDataMap) {
const workflowAction = AddOnsResponseService.newReturnOutputVariablesAction()
.setVariableDataMap(variableDataMap);
const hostAppAction = AddOnsResponseService.newHostAppAction()
.setWorkflowAction(workflowAction);
return AddOnsResponseService.newRenderActionBuilder()
.setHostAppAction(hostAppAction)
.build();
}
5. Deploy and Test
With our code written, let's deploy the project and configure it.
Step 1: Re-authorize the Script
Because you linked a new standard Google Cloud project, you must explicitly authorize the script's scopes (like script.external_request used by UrlFetchApp) against the new project.
- In the Apps Script editor, select
WebApp.gsunder Files, then select thedoGetfunction from the top toolbar dropdown. - Click Run.
- A prompt for "Authorization required" will appear. Click Review permissions.
- Choose your account and authorize the script. If you see an "Unverified app" warning, click Advanced -> Go to Support Quick Portal to bypass.
- Wait for the Execution log to show Execution completed, which confirms the script is now fully authorized.
Step 2: Test the Extension and Web App
Since your appsscript.json defines both an Add-on and a Web App, you can configure both simultaneously.
- Click Deploy > Test deployments in the Apps Script top-right corner.
- Next to Select type, click
Enable deployment types and ensure Google Workspace Add-ons is checked, then click Install.
- Click
Enable deployment types again, ensure Web app is checked, and copy the URL (it will end in
/dev). This is the URL for the Support Quick Portal web app you built. - Click Done. You might need to refresh your browser before Workspace Studio registers the add-on.
Step 3: Configure the Studio Flow
- Open Google Workspace Studio at studio.workspace.google.com.
- Click
Add tool on the left and select Flow.
- To configure the Starter, click on Choose a starter, then select your installed Support Extension > New Support Ticket. If prompted with a "Permission required" message, click Grant permission to allow the add-on to access your data.
- Next, under Actions click Choose a step, scroll to "Support Extension", and select the Detect Urgency step.
- In Source Description, click
Variables and select Step 1: New Support Ticket > The full description of the issue (
ticketDescription). - Click Choose a step again, and select a built-in step like Gmail > Send an email or Chat > Notify me in Chat.
- If you chose Gmail:
- To: Put your own email.
- Subject: Ticket
ticketTitlepriority isurgencyLevel - Message:
ticketDescription
- If you chose Chat:
- Message: Ticket
ticketTitlepriority isurgencyLevel.ticketDescription
- Message: Ticket
- If you chose Gmail:
- Turn on the workflow. If prompted to Rename your flow, enter a name like
Support Ticket Routerand save. This triggers theonManageTriggerlifecycle, saving thenotifyUri.
Step 4: Run the Simulation
- Open the Web App URL that you copied earlier within Step 2 of this page in a new browser tab (it ends in /dev).
- Fill out the form. Ensure you include a trigger keyword (like "urgent" or "crash") in the description.
- Click Submit. Wait for the "Ticket successfully routed" message.
- Check your Workspace Studio Activity Log, your Gmail Inbox, or Google Chat (depending on the action you chose). You should see a message or email tagged as "High" urgency natively integrated using your custom Apps Script elements.
6. Clean Up
To avoid cluttering your workspace and Google Cloud account, you can clean up the resources you created during this codelab.
- Delete the Workspace Studio Flow:
- Open studio.workspace.google.com.
- Find your
Support Ticket Routerflow. - Click the three-dot menu next to it and select Delete.
- Delete the Apps Script Project:
- Go to script.google.com.
- Find the
Support Quick Portalproject. - Click the three-dot menu and select Remove.
- Shut down the Google Cloud Project:
- Navigate to the Google Cloud Console.
- Ensure your new project is selected in the top dropdown.
- Go to IAM & Admin > Settings and click Shut Down.
7. Conclusion
You've successfully built a complete ecosystem for Google Workspace Studio custom extensions without relying on any external third-party subscriptions.
What we covered:
- Using
workflowTriggeralongsideonManageFunctionto correctly capture and handle Google's lifecycle events. - Authenticating backend requests using
ScriptApp.getOAuthToken()against theworkspacestudio.googleapis.comAPI endpoints. - Using
workflowActionto process input variables synchronously and generate downstream outputs.
You can now scale out these principles to hook into real SaaS platforms like Jira, Salesforce, or Zendesk by modifying where your Web App (or incoming webhook) triggers from.