---
description: Receive authenticated Google Chat interactions and Workspace Events with a project-owned REST client.
title: Google Chat | Flue
image: https://flueframework.com/docs/og4.jpg
---

# Google Chat

Last updated Jun 14, 2026 [ View as Markdown](https://flueframework.com/docs/ecosystem/channels/google-chat/index.md) [  @flue/google-chat ](https://www.npmjs.com/package/@flue/google-chat) 

## Quickstart

Add authenticated interactions, optional Workspace Events, and project-owned outbound messaging to an existing Flue project with the [Google Chat](https://developers.google.com/workspace/chat) blueprint. Run the following command in your terminal or coding agent of choice:

```
flue add channel google-chat
```

## Overview

The blueprint installs `@flue/google-chat` and `jose`. It creates a narrow service-account Fetch client at `<source-root>/lib/google-chat-client.ts` and`<source-root>/channels/google-chat.ts` with named `channel`, project-owned`client`, and message-tool exports, then wires the tool into an agent. The primary generated path handles direct interactions; authenticated Pub/Sub push for Workspace Events is an optional section in the same channel module.

```
import { createGoogleChatChannel } from '@flue/google-chat';
import { dispatch } from '@flue/runtime';
import assistant from '../agents/assistant.ts';
import { createGoogleChatClient } from '../lib/google-chat-client.ts';

export const client = createGoogleChatClient({
  clientEmail: process.env.GOOGLE_CHAT_CLIENT_EMAIL!,
  privateKey: process.env.GOOGLE_CHAT_PRIVATE_KEY!,
});

export const channel = createGoogleChatChannel({
  interactions: {
    authentication: {
      type: 'endpoint-url',
      audience: process.env.GOOGLE_CHAT_APP_URL!,
    },
    async handler({ c, payload }) {
      if (payload.type !== 'MESSAGE') return;
      const ref = conversationFromPayload(payload);
      if (!ref) return;

      await dispatch(assistant, {
        id: channel.conversationKey(ref),
        input: { type: `google-chat.${payload.type}`, payload },
      });
      return c.body(null, 200);
    },
  },
});
```

The abridged example omits the `conversationFromPayload()` helper; the complete helper appears in the interaction example below.

An authenticated message is admitted to the agent bound to its Google Chat space and thread and acknowledged with `200`; other authenticated interactions receive an empty successful response. The full generated module validates thread and space identity and lets the bound agent post a reply through the project-owned client. Workspace Events add an authenticated `/events` route and preserve the Pub/Sub wrapper for application-owned decoding and deduplication. Both Node and Cloudflare targets use standards-based Fetch and Web Crypto.

## Configure

| Variable                               | Purpose                                                                                                                              |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| GOOGLE\_CHAT\_APP\_URL                 | **Required for interaction endpoint-URL authentication** — Exact public interaction endpoint used as the Google OIDC token audience. |
| GOOGLE\_CHAT\_PUBSUB\_SUBSCRIPTION     | **Required for Workspace Events** — Exact projects/<project>/subscriptions/<subscription> resource required in the push body.        |
| GOOGLE\_CHAT\_PUBSUB\_AUDIENCE         | **Required for Workspace Events** — Exact audience configured on the authenticated Pub/Sub push subscription.                        |
| GOOGLE\_CHAT\_PUBSUB\_SERVICE\_ACCOUNT | **Required for Workspace Events** — Verifies the service-account identity in the Pub/Sub push OIDC token.                            |
| GOOGLE\_CHAT\_CLIENT\_EMAIL            | **Required for outbound API calls** — Identifies the service account used to request a chat.bot access token.                        |
| GOOGLE\_CHAT\_PRIVATE\_KEY             | **Required for outbound API calls** — Signs the service-account JWT assertion used for the OAuth token exchange.                     |

The blueprint installs and configures `@flue/google-chat` for authenticated inbound requests and `jose` for a project-owned outbound Fetch client. After running the command, you will have a new `src/channels/google-chat.ts` module exporting`channel`, `client`, and an application-owned message tool.

Configure only the credentials for the surfaces your application uses.

Set the Google Chat app connection to **HTTP endpoint URL** and use the full public interaction route:

```
https://example.com/channels/google-chat/interactions
```

Set `GOOGLE_CHAT_APP_URL` to that exact URL. With endpoint-URL authentication,`@flue/google-chat` verifies Google’s signature, issuer, expiration, exact audience, and `chat@system.gserviceaccount.com` identity before invoking the handler. The package also supports Google’s project-number authentication mode; see the [API reference](https://flueframework.com/docs/api/google-chat-channel/) when the Chat app is configured for that mode.

For Workspace Events, the audience and service-account email must match the Pub/Sub push subscription’s OIDC configuration. The subscription variable must match the exact subscription resource in every push body.

## Supported Webhooks

| Google surface                                                                                               | Webhook path                       |
| ------------------------------------------------------------------------------------------------------------ | ---------------------------------- |
| [Google Chat interaction events](https://developers.google.com/workspace/chat/receive-respond-interactions)  | /channels/google-chat/interactions |
| [Google Workspace Events for Google Chat](https://developers.google.com/workspace/events/guides/events-chat) | /channels/google-chat/events       |

Configure only the surfaces your application handles. Omitting `interactions` or`workspaceEvents` from `createGoogleChatChannel()` omits its route.

### Google Chat interactions

```
import { createGoogleChatChannel, type GoogleChatConversationRef } from '@flue/google-chat';
import { dispatch } from '@flue/runtime';
import assistant from '../agents/assistant.ts';

export const channel = createGoogleChatChannel({
  interactions: {
    authentication: {
      type: 'endpoint-url',
      audience: process.env.GOOGLE_CHAT_APP_URL!,
    },
    async handler({ c, payload }) {
      switch (payload.type) {
        case 'MESSAGE':
        case 'APP_COMMAND': {
          const ref = conversationFromPayload(payload);
          if (!ref) return c.body(null, 200);

          await dispatch(assistant, {
            id: channel.conversationKey(ref),
            input: {
              type: `google-chat.${payload.type}`,
              user: payload.user,
              payload,
            },
          });
          return c.body(null, 200);
        }
        default:
          return c.body(null, 200);
      }
    },
  },
});

function conversationFromPayload(payload: {
  space?: {
    name?: string;
    spaceType?: GoogleChatConversationRef['spaceType'];
  };
  message?: {
    space?: {
      name?: string;
      spaceType?: GoogleChatConversationRef['spaceType'];
    };
    thread?: { name?: string };
  };
  thread?: { name?: string };
}): GoogleChatConversationRef | undefined {
  const space = payload.space ?? payload.message?.space;
  if (!space?.name || !/^spaces\/[^/]+$/.test(space.name)) return;

  const thread = payload.message?.thread?.name ?? payload.thread?.name;
  if (thread !== undefined) {
    const match = /^(spaces\/[^/]+)\/threads\/[^/]+$/.exec(thread);
    if (!match || match[1] !== space.name) return;
  }

  return {
    space: space.name,
    ...(thread === undefined ? {} : { thread }),
    ...(space.spaceType === undefined ? {} : { spaceType: space.spaceType }),
  };
}
```

The callback receives `{ c, payload }`. `payload` preserves Google Chat’s native field names and uppercase discriminants such as `MESSAGE`, `ADDED_TO_SPACE`,`CARD_CLICKED`, and `APP_COMMAND`. Authenticated future types pass through without conversion, so the handler decides which interactions affect the application.

Derive the canonical space from `payload.space.name` or`payload.message.space.name`. Use `space.spaceType` for descriptive metadata, not the deprecated `space.type`, and accept a thread only when its resource name belongs to that exact space. Conversation keys are identifiers, not authorization capabilities; see the shared [Channels guide](https://flueframework.com/docs/guide/channels/)for dispatch and authorization guidance.

Google Chat requires the direct endpoint to respond within 30 seconds. The channel awaits the handler and does not race it against a timeout that would leave uncancelled work running. Keep admission short, dispatch durable work promptly, and return nothing or an explicit `200`. JSON-compatible return values become Google Chat response bodies, while `c` can create an explicit Hono response.

### Workspace Events

Direct interactions cover activity addressed to the Chat app. Use a Google Workspace Events subscription backed by an authenticated Pub/Sub push subscription for broader space activity such as messages, reactions, memberships, and space updates.

```
export const channel = createGoogleChatChannel({
  workspaceEvents: {
    authentication: {
      subscription: process.env.GOOGLE_CHAT_PUBSUB_SUBSCRIPTION!,
      audience: process.env.GOOGLE_CHAT_PUBSUB_AUDIENCE!,
      serviceAccountEmail: process.env.GOOGLE_CHAT_PUBSUB_SERVICE_ACCOUNT!,
    },
    async handler({ c, delivery }) {
      const bytes = Uint8Array.from(atob(delivery.message.data), (value) => value.charCodeAt(0));
      const event: unknown = JSON.parse(new TextDecoder().decode(bytes));

      await handleWorkspaceEvent({
        event,
        attributes: delivery.message.attributes,
        messageId: delivery.message.messageId,
      });
      return c.body(null, 200);
    },
  },
});
```

The callback receives `{ c, delivery }`, preserving the complete Pub/Sub push wrapper. CloudEvent attributes remain in `delivery.message.attributes` and the`application/json` event remains a base64-encoded string in`delivery.message.data`. Decode the base64 bytes and then parse their UTF-8 JSON in application code, as shown above; the channel validates the envelope but does not replace it with a normalized event.

Workspace Event subscriptions expire and can be suspended. Subscription lifecycle deliveries reach the same callback so application code can renew or repair the affected subscription. Creating and renewing subscriptions, storing their state, and any domain-wide delegation or user impersonation remain application concerns.

## Outbound REST

Outbound Google Chat operations belong to the generated project-owned Fetch client, not `@flue/google-chat`:

```
import { createGoogleChatClient } from '../lib/google-chat-client.ts';

export const client = createGoogleChatClient({
  clientEmail: process.env.GOOGLE_CHAT_CLIENT_EMAIL!,
  privateKey: process.env.GOOGLE_CHAT_PRIVATE_KEY!,
});
```

The client signs a short-lived service-account assertion, exchanges it for a`chat.bot` access token, caches that token, and posts through the Google Chat REST API. It validates that a bound thread belongs to the bound space.

## Google Chat Tools

Use the client to define an application-owned tool whose destination and credentials are bound in trusted code:

```
import type { GoogleChatConversationRef } from '@flue/google-chat';
import { defineTool } from '@flue/runtime';

export function postMessage(ref: GoogleChatConversationRef) {
  return defineTool({
    name: 'post_google_chat_message',
    description: 'Post a message to the Google Chat conversation bound to this agent.',
    parameters: {
      type: 'object',
      properties: { text: { type: 'string', minLength: 1 } },
      required: ['text'],
      additionalProperties: false,
    },
    async execute({ text }) {
      const message = await client.postMessage(ref, text);
      return JSON.stringify({ message: message.name });
    },
  });
}
```

Bind the destination when creating the agent:

```
import { createAgent } from '@flue/runtime';
import { channel, postMessage } from '../channels/google-chat.ts';

export default createAgent(({ id }) => ({
  model: 'anthropic/claude-haiku-4-5',
  tools: [postMessage(channel.parseConversationKey(id))],
}));
```

The model selects only message text. It does not select arbitrary service accounts, spaces, threads, URLs, or REST operations.

## Delivery and runtime behavior

Returning `200` from the Workspace Events handler acknowledges the Pub/Sub push after the awaited admission work completes. Pub/Sub retries failed or unacknowledged pushes according to the subscription’s delivery policy and configurable acknowledgement deadline.

Use `delivery.message.messageId` as the Pub/Sub delivery identity. Atomically claim it in application-owned durable storage before dispatch when duplicate admission is unacceptable. `delivery.deliveryAttempt` is retry metadata, not a unique identifier. The channel is stateless and does not deduplicate Pub/Sub message ids, CloudEvent ids, or direct interactions.

`@flue/google-chat` ingress is tested in Node and workerd using Fetch and Web Crypto. The generated Fetch client is also exercised in both runtimes for service-account assertion signing, OAuth token exchange construction, and one threaded message request against a fail-closed fake transport. Cloudflare builds use Flue’s required `nodejs_compat` setting. Validate any additional outbound operations your application adds.

## Docs Navigation

Current page: [Google Chat](https://flueframework.com/docs/ecosystem/channels/google-chat/)

### Sections

* [Guide](https://flueframework.com/docs/getting-started/quickstart/)
* [Reference](https://flueframework.com/docs/api/agent-api/)
* [CLI](https://flueframework.com/docs/cli/overview/)
* [SDK](https://flueframework.com/docs/sdk/overview/)
* [Ecosystem](https://flueframework.com/docs/ecosystem/)

* [  Overview ](https://flueframework.com/docs/ecosystem/)

### Channels

* [ Discord ](https://flueframework.com/docs/ecosystem/channels/discord/)
* [ Facebook ](https://flueframework.com/docs/ecosystem/channels/messenger/)
* [ GitHub ](https://flueframework.com/docs/ecosystem/channels/github/)
* [ Google Chat ](https://flueframework.com/docs/ecosystem/channels/google-chat/)
* [ Intercom ](https://flueframework.com/docs/ecosystem/channels/intercom/)
* [ Linear ](https://flueframework.com/docs/ecosystem/channels/linear/)
* [ Microsoft Teams ](https://flueframework.com/docs/ecosystem/channels/teams/)
* [ Notion ](https://flueframework.com/docs/ecosystem/channels/notion/)
* [ Resend ](https://flueframework.com/docs/ecosystem/channels/resend/)
* [ Salesforce ](https://flueframework.com/docs/ecosystem/channels/salesforce-marketing-cloud/)
* [ Shopify ](https://flueframework.com/docs/ecosystem/channels/shopify/)
* [ Slack ](https://flueframework.com/docs/ecosystem/channels/slack/)
* [ Stripe ](https://flueframework.com/docs/ecosystem/channels/stripe/)
* [ Telegram ](https://flueframework.com/docs/ecosystem/channels/telegram/)
* [ Twilio ](https://flueframework.com/docs/ecosystem/channels/twilio/)
* [ WhatsApp ](https://flueframework.com/docs/ecosystem/channels/whatsapp/)
* [ Zendesk ](https://flueframework.com/docs/ecosystem/channels/zendesk/)

### Sandboxes

* [ boxd ](https://flueframework.com/docs/ecosystem/sandboxes/boxd/)
* [ Cloudflare Shell ](https://flueframework.com/docs/ecosystem/sandboxes/cloudflare-shell/)
* [ Cloudflare Sandbox ](https://flueframework.com/docs/ecosystem/sandboxes/cloudflare/)
* [ Daytona ](https://flueframework.com/docs/ecosystem/sandboxes/daytona/)
* [ E2B ](https://flueframework.com/docs/ecosystem/sandboxes/e2b/)
* [ exe.dev ](https://flueframework.com/docs/ecosystem/sandboxes/exedev/)
* [ islo ](https://flueframework.com/docs/ecosystem/sandboxes/islo/)
* [ Mirage ](https://flueframework.com/docs/ecosystem/sandboxes/mirage/)
* [ Modal ](https://flueframework.com/docs/ecosystem/sandboxes/modal/)
* [ Vercel Sandbox ](https://flueframework.com/docs/ecosystem/sandboxes/vercel/)

### Deploy

* [ AWS ](https://flueframework.com/docs/ecosystem/deploy/aws/)
* [ Cloudflare ](https://flueframework.com/docs/ecosystem/deploy/cloudflare/)
* [ Docker ](https://flueframework.com/docs/ecosystem/deploy/docker/)
* [ Fly.io ](https://flueframework.com/docs/ecosystem/deploy/fly/)
* [ GitHub Actions ](https://flueframework.com/docs/ecosystem/deploy/github-actions/)
* [ GitLab CI/CD ](https://flueframework.com/docs/ecosystem/deploy/gitlab-ci/)
* [ Node.js ](https://flueframework.com/docs/ecosystem/deploy/node/)
* [ Railway ](https://flueframework.com/docs/ecosystem/deploy/railway/)
* [ Render ](https://flueframework.com/docs/ecosystem/deploy/render/)
* [ SST ](https://flueframework.com/docs/ecosystem/deploy/sst/)

### Databases

* [ libSQL ](https://flueframework.com/docs/ecosystem/databases/libsql/)
* [ MongoDB ](https://flueframework.com/docs/ecosystem/databases/mongodb/)
* [ MySQL ](https://flueframework.com/docs/ecosystem/databases/mysql/)
* [ Postgres ](https://flueframework.com/docs/ecosystem/databases/postgres/)
* [ Redis ](https://flueframework.com/docs/ecosystem/databases/redis/)
* [ Supabase ](https://flueframework.com/docs/ecosystem/databases/supabase/)
* [ Turso ](https://flueframework.com/docs/ecosystem/databases/turso/)
* [ Valkey ](https://flueframework.com/docs/ecosystem/databases/valkey/)

### Tooling

* [ Braintrust ](https://flueframework.com/docs/ecosystem/tooling/braintrust/)
* [ OpenTelemetry ](https://flueframework.com/docs/ecosystem/tooling/opentelemetry/)
* [ Sentry ](https://flueframework.com/docs/ecosystem/tooling/sentry/)