Skip to content

Start typing to search the documentation.

Google Chat

Last updated View as Markdown @flue/google-chat

Quickstart

Add authenticated interactions, optional Workspace Events, and project-owned outbound messaging to an existing Flue project with the Google 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

VariablePurpose
GOOGLE_CHAT_APP_URLRequired for interaction endpoint-URL authentication — Exact public interaction endpoint used as the Google OIDC token audience.
GOOGLE_CHAT_PUBSUB_SUBSCRIPTIONRequired for Workspace Events — Exact projects/<project>/subscriptions/<subscription> resource required in the push body.
GOOGLE_CHAT_PUBSUB_AUDIENCERequired for Workspace Events — Exact audience configured on the authenticated Pub/Sub push subscription.
GOOGLE_CHAT_PUBSUB_SERVICE_ACCOUNTRequired for Workspace Events — Verifies the service-account identity in the Pub/Sub push OIDC token.
GOOGLE_CHAT_CLIENT_EMAILRequired for outbound API calls — Identifies the service account used to request a chat.bot access token.
GOOGLE_CHAT_PRIVATE_KEYRequired 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 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 surfaceWebhook path
Google Chat interaction events/channels/google-chat/interactions
Google Workspace Events for Google 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 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.