# Google Chat Unread Messages Brief with Workspace Studio and Gemini

Author: [Mete Atamel](https://atamel.dev/) | Published: 2026-10-20 | Topics: GenAI, Gemini, Google Workspace, Apps Script | Canonical: https://atamel.dev/posts/2026/10-20_google_chat_unread_brief/


![Google Chat Unread Messages Brief](/img/2026/google_chat_unread_brief.png)

Coming to work every morning often means facing a wall of unread Google Chat
messages from the night before across colleagues in different time zones.
Skimming through every space and thread to catch up takes time.

To make my mornings easier, I built an automated **Unread Messages Brief**
directly inside Google Chat—powered by
[Google Workspace Studio](https://studio.workspace.google.com), **Gemini**, and
a **Google Apps Script Custom Step** (with zero Google Cloud Console setup
required). Everything was built with the help of **Gemini 4** and
**Antigravity**, which made figuring out the Google Chat read-state API nuances
and wiring up the Workspace Studio custom step effortless.

In this post, I'll share how it works, the design decisions behind it, and how
you can set it up for yourself using my
[google-chat-unread-brief repository](https://github.com/meteatamel/google-chat-unread-brief).

## Why not just use Google Chat's "Ask Gemini" tab?

At this point, you might be wondering: **Can't you just use the built-in "Ask
Gemini" tab in Google Chat?**

I had the same question and gave it a try, but ran into two main limitations:

1. **Too much noise from background threads**: When you ask the built-in **Ask
   Gemini** tab to summarize your unread messages, it includes *all* unread
   messages across every background thread—even threads you aren't following. I
   wanted a brief that filters strictly for **top-level unread messages** and
   **messages from threads I actively follow**.
2. **No scheduled summaries**: There is no way to schedule an automatic daily
   summary in the **Ask Gemini** tab so that it's waiting for you when you start
   your day.

## What does Unread Messages Brief do?

With **Google Workspace Studio** and a small Google Apps Script custom step, the
workflow gives you:

* **Daily 7:45 AM Scheduled Brief**: Automatically summarizes your unread
  messages from the last 24 hours (top-level messages and threads you follow,
  **without marking them as read**) and posts a clean bulleted brief with
  clickable space links into your private `Unread Messages Brief` space each
  weekday morning.
* **On-Demand Trigger (`/briefnow`)**: Type `/briefnow` in your
  `Unread Messages Brief` space anytime during the day to generate a fresh
  summary on demand.

Let's look at how to set it up step by step.

## Step 1: Create your private space in Google Chat

First, create a dedicated space in Google Chat where your briefs will be posted:

1. Open [Google Chat](https://chat.google.com).
2. Click **New chat** (`+`) → **Create a space**.
3. Name the space **`Unread Messages Brief`** (keep it private with just
   yourself).

## Step 2: Set up & install the Apps Script Custom Step

While **Google Workspace Studio** has built-in steps for Google Chat and Gemini,
we need a custom step in Google Apps Script to query the Google Chat API's read
state (`getSpaceReadState` and `getThreadReadState`) and fetch only the unread
top-level messages and followed threads without marking them as read.

> [!note]
> Why isn't there a "Mark all unread messages as read" command? While reading
> unread state (`spaces.list`, `messages.list`, `getSpaceReadState`,
> `getThreadReadState`) works out of the box with Apps Script's default project,
> the Google Chat API blocks write calls (`updateSpaceReadState`) unless you
> create, link, and configure a standard Google Cloud project in the Cloud
> Console. Keeping it read-only avoids any Google Cloud project setup.

Here's how to set up and install the custom step:

1. Go to [script.google.com](https://script.google.com) and click **New
   project** (name it `Unread Messages Brief`).
2. Enable the manifest file:
   * Click **Project Settings** (⚙️ icon on the left sidebar).
   * Check the box **"Show 'appsscript.json' manifest file in editor"**.
3. Copy the files from my
   [google-chat-unread-brief](https://github.com/meteatamel/google-chat-unread-brief)
   repository into the editor:
   * Click **Editor** (`< >` icon on the left sidebar).
   * Open `appsscript.json` and replace its contents with
     [`appsscript.json`](https://github.com/meteatamel/google-chat-unread-brief/blob/main/appsscript.json).
   * Open `Code.gs` and replace its contents with
     [`Code.gs`](https://github.com/meteatamel/google-chat-unread-brief/blob/main/Code.gs).
   * Click **Save** (💾).
4. **Authorize the Script in Apps Script**:
   * In the top toolbar dropdown (next to **Run** / **Debug**), select
     `onExecuteFetchUnread` and click **Run**.
   * Approve the Google OAuth permissions prompt when asked.
5. **Install Test Deployment for Workspace Studio**:
   * Click **Deploy** (top right) → **Test deployments**.
   * Next to **Application(s): Google Workspace Add-on**, click **Install**.
   * Click **Done**.

Let's take a quick look at what's happening under the hood in both files.

First, `appsscript.json` declares the Google Chat Advanced Service (`Chat` v1),
read-only OAuth scopes, and exposes a custom workflow action (`Fetch Unread Chat
Messages`) to **Google Workspace Studio** with two inputs (`excludeSpaceName`
and `maxLookbackHours`) and one output (`unreadMessagesText`):

```json
"flows": {
  "workflowElements": [
    {
      "id": "fetch_unread_chat_messages",
      "state": "ACTIVE",
      "name": "Fetch Unread Chat Messages",
      "description": "Fetches unread Google Chat messages across your spaces with space links, without marking them as read.",
      "workflowAction": {
        "inputs": [
          {
            "id": "excludeSpaceName",
            "description": "Name or Space ID of your Unread Messages Brief space to exclude from the summary (optional)",
            "cardinality": "SINGLE",
            "dataType": { "basicType": "STRING" }
          },
          {
            "id": "maxLookbackHours",
            "description": "Maximum hours to look back for unread messages (e.g. 24)",
            "cardinality": "SINGLE",
            "dataType": { "basicType": "INTEGER" }
          }
        ],
        "outputs": [
          {
            "id": "unreadMessagesText",
            "description": "Formatted unread messages and space links ready for Gemini summarization",
            "cardinality": "SINGLE",
            "dataType": { "basicType": "STRING" }
          }
        ],
        "onConfigFunction": "onConfigFetchUnread",
        "onExecuteFunction": "onExecuteFetchUnread"
      }
    }
  ]
}
```

Second, `Code.gs` implements the configuration UI (`onConfigFetchUnread`), the
execution handler (`onExecuteFetchUnread`), and the core `fetchUnreadChatData`
function. For each active space within the lookback window, it checks your
space-level `lastReadTime` and then inspects each thread's `lastReadTime` so
that only unread top-level messages and replies in threads you follow are
included:

```javascript
// Get the user's space-level read state
const readState = Chat.Users.Spaces.getSpaceReadState(
  `users/me/${space.name}/spaceReadState`
);
const spaceLastReadMs = new Date(readState.lastReadTime).getTime();
const lastActiveMs = new Date(space.lastActiveTime).getTime();

// Skip if the space is already read
if (lastActiveMs <= spaceLastReadMs) continue;

// Fetch candidate messages newer than spaceLastReadTime (within lookback window)
const msgResponse = Chat.Spaces.Messages.list(space.name, {
  filter: `createTime > "${effectiveSinceIso}"`,
  pageSize: 40
});

// Filter to ONLY top-level messages + replies in new or followed threads
for (const m of candidateMessages) {
  const trs = Chat.Users.Spaces.Threads.getThreadReadState(
    `users/me/${m.thread.name}/threadReadState`
  );
  const threadLastReadMs = (trs && trs.lastReadTime)
    ? new Date(trs.lastReadTime).getTime()
    : null;

  const isTopLevel = !m.threadReply;
  const isReplyToNewUnreadThread = newTopLevelThreads.has(m.thread.name);
  const isFollowedThread = threadLastReadMs !== null && threadLastReadMs > spaceLastReadMs;

  if (isTopLevel || isReplyToNewUnreadThread || isFollowedThread) {
    // Format sender, message text, and space link for Gemini
  }
}
```

Finally, `onExecuteFetchUnread` packages the formatted messages and space links
into the `unreadMessagesText` output variable using `AddOnsResponseService` so
the next step in the flow can pass it directly to Gemini.

## Step 3: Build the on-demand flow in Google Workspace Studio

Next, open **Google Workspace Studio** (refresh the page if it was already open
so your newly installed **Unread Messages Brief** custom step appears).

> [!tip]
> **Google Workspace Studio** displays each output variable using its
> `description` from `appsscript.json` (for example, `unreadMessagesText`
> appears in the **`+ Variables`** picker under **`Fetch Unread Chat Messages` →
> `Formatted unread messages and space links ready...`**).

Create a new flow named `Unread Messages Brief` with the following steps:

1. **Starter (`Chat → When I get a chat message`)**:
   * **Conversation type**: Choose **Spaces & named group DMs**.
   * **Spaces**: Select **`Unread Messages Brief`**.
   * Enter `/briefnow` in the **"Message has"** field and choose **"Match text
     exactly"**.
2. **Step 2 (`Chat → Post in a space` — Progress Indicator)**:
   * Select your **`Unread Messages Brief`** space and paste:
     ```text
     ⏳ _Scanning unread messages and generating your brief. Please wait..._
     ```
3. **Step 3 (`Custom Step → Fetch Unread Chat Messages`)**:
   * Click **+ Add step** → scroll to **Unread Messages Brief** → select **Fetch
     Unread Chat Messages**.
   * Set **Exclude Space Name or ID** to `Unread Messages Brief`.
   * Set **Max Lookback Window (Hours)** to `24`.
4. **Step 4 (`Ask Gemini` — Summarize)**:
   * Add an **Ask Gemini** step with the following prompt:
     ```text
     Provide a high-level summary of my unread Google Chat messages below, formatted as a bulleted list grouped by Space.
     - If the input says NO_UNREAD_MESSAGES, just reply: "🎉 You're all caught up! No unread Google Chat messages."
     - Format each Space as a bullet point starting with the Space name in bold (* **Space Name**:) followed by an overall summary whose length is proportional to the discussion (keep it brief for 1–2 messages, and more detailed for longer discussions).
     - Focus on the topic and outcome rather than listing who said what. Only mention a person's name (in bold, e.g. **Aaron**) when attribution is genuinely useful—such as a specific announcement, question, or action item—and omit names entirely for general group discussions or quick updates.
     - At the end of each Space's bullet item, include a clickable chat icon linked to its exact Link from the input: [💬](https://chat.google.com/room/...).
     - Do not add any concluding remarks or trailing blank lines after the last bullet item.

     Unread Messages Data:
     ```
   * On the last line under `Unread Messages Data:`, click **`+ Variables`** and
     select **`Step 3: Fetch Unread Chat Messages` → `Formatted unread messages
     and space links ready...`**.
5. **Step 5 (`Chat → Post in a space`)**:
   * Select your **`Unread Messages Brief`** space and format the message with
     the title `☕ *Unread Messages Brief*`, followed by the **`Step 4: Ask
     Gemini` → `Content created by Gemini`** variable, and the footer `💡
     _`/briefnow` to refresh summary_`.
6. Click **Turn on**.

Here's what the complete on-demand flow looks like:

<img src="https://raw.githubusercontent.com/meteatamel/google-chat-unread-brief/main/images/flow-on-demand.png" alt="On-Demand Unread Messages Brief Flow" width="450" />

## Step 4: Test the flow & schedule the daily 7:45 AM brief

To test the flow, mark any conversation in Google Chat as unread, head over to
your **`Unread Messages Brief`** space, and send `/briefnow`.

Within seconds, you'll see the progress indicator followed by your **☕ Unread
Messages Brief** with bold space names, high-level topic summaries, and a
clickable `💬` link at the end of each item that jumps straight to that
space—while all your unread badges in the sidebar remain intact:

![Unread Messages Brief in Google Chat](https://raw.githubusercontent.com/meteatamel/google-chat-unread-brief/main/images/briefnow-chat.jpeg)

Once your `/briefnow` flow is working, you can easily create the daily morning
schedule by making a copy of the flow (since **Google Workspace Studio** only
allows one Starter per flow):

1. In your **My flows** list, click **More (`⋮`)** next to `Unread Messages
   Brief` and select **Make a copy**.
2. Rename it to `Unread Messages Brief - Scheduled`.
3. Change the **Starter** at the top to **Schedule** → **Every weekday at 7:45
   AM** (and delete the `⏳` progress step in Step 2 so it only posts the
   finished brief), then click **Turn on**.

<img src="https://raw.githubusercontent.com/meteatamel/google-chat-unread-brief/main/images/flow-scheduled.png" alt="Scheduled Unread Messages Brief Flow" width="450" />

## Wrapping up

In this post, we built an automated **Unread Messages Brief** for Google Chat
that delivers both a scheduled morning summary and an on-demand `/briefnow`
summary without marking your messages as read.

For the full code and step-by-step instructions, check out the repository on
GitHub:

* [Sample: google-chat-unread-brief](https://github.com/meteatamel/google-chat-unread-brief)

