I tried building a Google Chat Bot with minimal configuration using Cloud Functions + Python + uv

I tried building a Google Chat Bot with minimal configuration using Cloud Functions + Python + uv

I built a Google Chat echo bot with minimal configuration using Cloud Functions 2nd generation, Python 3.14, and uv. I will introduce the fact that the Google Chat API has migrated to the Workspace Add-ons format and that the old tutorial request/response format no longer works, along with how to resolve this.
2026.05.27

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).

SCR-20260527-mhih-redacted_dot_app

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.toml is read directly — no need to generate requirements.txt
  • Dependencies locked with uv are 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

SCR-20260527-mqnh-redacted_dot_app

Visibility Settings

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

SCR-20260527-mrgs-redacted_dot_app

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.

  1. Open Google Chat
  2. Click "New chat" in the left sidebar
  3. Search for the bot name (the App name you configured) and select it

choose-your-bot

  1. If prompted to install the bot, click Install

install-app-for-bot

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

SCR-20260527-mhih-redacted_dot_app

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 from pyproject.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.message format
  • For debugging, using print(..., file=sys.stderr, flush=True) is the reliable way to output to Cloud Run logs

References

Share this article

Related articles