
I tried building a Google Chat Bot with minimal configuration using Cloud Functions + Python + uv
This page has been translated by machine translation. View original
Introduction
I wanted to create a simple bot for Google Chat in an organization using Google Workspace, so I built a minimal echo bot using Cloud Functions (2nd generation).

This article covers everything from setting up the gcloud CLI to deployment, along with the issues I ran into and how I resolved them.
I'll also touch on differences in GCP's authentication compared to AWS, from an AWS user's perspective.
Configuration
| Item | Choice |
|---|---|
| Runtime | Cloud Functions 2nd generation |
| Language | Python 3.14 |
| Package manager | uv |
| Trigger | HTTPS endpoint |
| Region | asia-northeast1 (Tokyo) |
Prerequisites
- An account belonging to a Google Workspace organization
- A GCP project already created with billing enabled
- macOS environment (assuming Homebrew is available)
gcloud CLI Setup
Installation
brew install --cask google-cloud-sdk
Authentication
GCP authentication is simpler compared to AWS. Unlike AWS, you don't need tools like aws-vault — the gcloud CLI itself manages credentials.
# CLI authentication (opens a browser)
gcloud auth login
# Setting up Application Default Credentials (ADC)
gcloud auth application-default login
Note that there are two types of authentication here.
| Command | Purpose |
|---|---|
gcloud auth login |
Authentication for the gcloud command itself |
gcloud auth application-default login |
Authentication when code running locally calls GCP APIs |
gcloud auth login is for the CLI tool, and gcloud auth application-default login is for code. Running the latter generates a file at ~/.config/gcloud/application_default_credentials.json, which Google's client libraries automatically reference.
Setting the Quota Project
When setting up ADC, you may see the following warning.
WARNING:
Cannot find a quota project to add to ADC. You might receive a "quota exceeded" or "API not enabled" error.
This is because the API usage quota is not linked to a project. Resolve it with the following command.
gcloud auth application-default set-quota-project YOUR_PROJECT_ID
Project and API Configuration
# Set the default project
gcloud config set project YOUR_PROJECT_ID
# Enable required APIs
gcloud services enable cloudfunctions.googleapis.com \
cloudbuild.googleapis.com \
chat.googleapis.com \
run.googleapis.com
Cloud Functions 2nd generation runs internally on Cloud Run, so run.googleapis.com is also required. Additionally, cloudbuild.googleapis.com is used to build container images during deployment.
Switching Between Multiple Projects
As the equivalent of AWS's --profile, gcloud has configurations.
# Create a new configuration
gcloud config configurations create my-other-project
gcloud config set project other-project-id
gcloud auth login
# List configurations
gcloud config configurations list
# Switch
gcloud config configurations activate my-other-project
ADC can also be switched per project in the same way.
# Switch the ADC quota project
gcloud auth application-default set-quota-project other-project-id
However, the ADC credential itself (~/.config/gcloud/application_default_credentials.json) is a single file per account, so if you want to use ADC with a different Google account, you need to re-run gcloud auth application-default login.
Cloud Functions Natively Supports uv and Python 3.14
Upon investigation, I found that the Python 3.14 runtime for Cloud Functions has adopted uv as the default package manager.
This means:
pyproject.tomlis read directly — no need to generaterequirements.txt- Dependencies locked with
uvare deployed as-is
However, note that if both pyproject.toml and requirements.txt exist, requirements.txt takes priority.
Project Structure
google-chat-bot/
├── main.py # Cloud Function entry point
├── pyproject.toml # uv-managed dependencies
├── uv.lock # uv lock file
├── .python-version # Python version
└── .gitignore
Project Initialization
# Initialize project with uv
uv init --no-readme
rm hello.py # Remove the auto-generated file
# Add dependencies
uv add functions-framework
uv add --dev pytest
Bot Code
main.py:
import json
import sys
import functions_framework
from flask import jsonify
def create_message(text):
"""Wrap text in the Google Workspace Add-ons response format"""
return {
"hostAppDataAction": {
"chatDataAction": {
"createMessageAction": {
"message": {
"text": text,
}
}
}
}
}
@functions_framework.http
def handle_chat(request):
"""Entry point for the Google Chat bot"""
body = request.get_json(silent=True)
if not body:
return jsonify(create_message("Empty request"))
chat = body.get("chat", {})
message_payload = chat.get("messagePayload", {})
message = message_payload.get("message", {})
user_text = message.get("text", "")
sender = message.get("sender", {}).get("displayName", "someone")
return jsonify(create_message(f"Hello {sender}! You said: {user_text}"))
The key here is the request and response format. This is explained in detail in the troubleshooting section below.
Local Testing
Using functions-framework, you can start a Cloud Function locally for testing.
uv run functions-framework --target=handle_chat --port=8080
From a separate terminal:
curl -X POST http://localhost:8080 \
-H "Content-Type: application/json" \
-d '{"chat": {"messagePayload": {"message": {"text": "hello", "sender": {"displayName": "Test User"}}}}}'
Deployment
gcloud functions deploy google-chat-bot \
--gen2 \
--runtime=python314 \
--region=asia-northeast1 \
--source=. \
--entry-point=handle_chat \
--trigger-http \
--allow-unauthenticated
| Flag | Meaning |
|---|---|
--gen2 |
Use Cloud Functions 2nd generation |
--runtime=python314 |
Python 3.14 runtime |
--entry-point=handle_chat |
The function name in main.py to be called |
--trigger-http |
HTTPS trigger |
--allow-unauthenticated |
Accessible without authentication (to be changed later) |
After deployment, a URL like the following will be issued:
https://asia-northeast1-YOUR_PROJECT_ID.cloudfunctions.net/google-chat-bot
Google Chat API Configuration
Manual configuration in the GCP Console is required. This cannot be done via the gcloud CLI.
Open Google Chat API Configuration and select the Configuration tab.
Configure the following:
| Item | Value |
|---|---|
| App name | Any bot name you like |
| Avatar url | Avatar image for the app |
| Description | Description of the bot |
Trigger Configuration
For Connection settings (triggers), there are two options:
- Use a common HTTP endpoint URL for all triggers — Handle all events with a single URL
- Specify HTTP endpoint URLs for each trigger — Set different URLs per event type
Since this is an echo bot, handling all events with a single Cloud Function is sufficient. Select "Use a common HTTP endpoint URL for all triggers" and paste in the Cloud Function URL issued at deployment:
https://asia-northeast1-YOUR_PROJECT_ID.cloudfunctions.net/google-chat-bot

Visibility Settings
| Item | Value |
|---|---|
| Visibility | Select "Make this app available to specific people and groups" and add your own email address |

Click Save. It may take a few minutes for the changes to take effect.
Verification
Once the Google Chat API settings are saved, send a message to the bot to verify it works.
- Open Google Chat
- Click "New chat" in the left sidebar
- Search for the bot name (the App name you configured) and select it

- If prompted to install the bot, click Install

- Once the chat window opens, send a message (e.g.,
hello) - If you get a reply like
Hello YourName! You said: hello, it's working

If the bot doesn't appear in search results, the settings may still be propagating — wait a few minutes and try searching again.
Issue I Ran Into: Bot Shows "No Response"
This is the most important part of this article. After deployment, when I sent a message to the bot in Google Chat, it showed "No response from ○○".
Hitting the Cloud Function directly with curl returned a normal response, but going through Google Chat resulted in no response. Here's the process I went through to identify the cause.
Cause 1: Authentication Method
I initially deployed with --allow-unauthenticated, making it accessible to everyone. However, checking the Google Chat documentation, the recommended approach is authenticated — granting invoker permissions only to the Google Chat service account.
# Redeploy with authentication
gcloud functions deploy google-chat-bot \
--gen2 \
--runtime=python314 \
--region=asia-northeast1 \
--source=. \
--entry-point=handle_chat \
--trigger-http \
--no-allow-unauthenticated
# Grant invoker permissions to the Google Chat service account
gcloud run services add-iam-policy-binding google-chat-bot \
--region=asia-northeast1 \
--member="serviceAccount:chat@system.gserviceaccount.com" \
--role="roles/run.invoker"
However, this alone didn't resolve the issue. Even after setting the IAM policy, 403 errors occurred. I ultimately reverted to --allow-unauthenticated (this point requires further investigation).
Cause 2 (The Real Cause): Different Request/Response Format
Checking the Cloud Function logs, I found that the format of requests sent by Google Chat differed from what most tutorials describe.
Commonly Shown (Old) Format
{
"type": "MESSAGE",
"message": {
"text": "hello",
"sender": {"displayName": "User"}
}
}
Actual Format Received
{
"commonEventObject": { ... },
"authorizationEventObject": { ... },
"chat": {
"messagePayload": {
"message": {
"text": "hello",
"sender": {"displayName": "Lin Yuchen"}
}
}
}
}
The message was nested at body["chat"]["messagePayload"]["message"], not at body["message"].
Furthermore, the response format was also different.
Commonly Shown (Old) Response
{"text": "Hello!"}
Actually Expected Response
{
"hostAppDataAction": {
"chatDataAction": {
"createMessageAction": {
"message": {
"text": "Hello!"
}
}
}
}
}
This is because the Google Chat API has migrated to the Google Workspace Add-ons format. With the current HTTP endpoint approach, both requests and responses must use the Workspace Add-ons format.
Debugging Tips
When checking Cloud Functions logs, output from Python's logging module may not appear. Using print(..., file=sys.stderr, flush=True) made the output show up in the logs.
print(f"REQUEST BODY: {json.dumps(body, ensure_ascii=False)}", file=sys.stderr, flush=True)
Command to check Cloud Run logs:
gcloud logging read \
'resource.type="cloud_run_revision" AND resource.labels.service_name="google-chat-bot"' \
--limit=10 \
--format="table(timestamp,severity,textPayload)" \
--project=YOUR_PROJECT_ID
Summary
- Built a Google Chat bot using Cloud Functions 2nd generation + Python 3.14 + uv
- The Python 3.14 runtime for Cloud Functions natively supports
uv, allowing direct deployment frompyproject.toml - The current Google Chat HTTP endpoint uses the Google Workspace Add-ons format. The old
{"text": "..."}format from older tutorials will not work - Retrieve the message from requests at
body["chat"]["messagePayload"]["message"] - Responses must be returned in the
hostAppDataAction.chatDataAction.createMessageAction.messageformat - For debugging, using
print(..., file=sys.stderr, flush=True)is the reliable way to output to Cloud Run logs

