> ## Content Index
> Fetch the complete content index at: https://adhdhomelab.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Build a Powerful ChatGPT Automation with n8n and Home Assistant
- URL: https://adhdhomelab.com/chatgpt-n8n-home-assistant-automation/
- Published: 2026-08-29T20:00:13.000Z
- Updated: 2026-08-30T02:26:49.000Z
- Description: How I connected scheduled ChatGPT tasks, Google Sheets, n8n, and Home Assistant without losing updates - or hearing the same announcement until the heat death of the universe.
- Author: AJ Tatum
- Tags: Home Assistant, Notifications, Chat GPT, n8n, Google Sheets, AI Automation, Open AI TTS, Automations

I have a habit of building automations for things I absolutely could check manually. But checking manually requires remembering that the thing exists, opening the right site, ignoring seventeen unrelated tabs, and somehow returning to the original task.

So, naturally, I built a pipeline.

The goal was simple: let scheduled ChatGPT tasks research updates I care about, store anything worth seeing in Google Sheets, have n8n bundle the new items together, and send the result to Home Assistant for a spoken announcement.

The finished workflow looks like this:

1. A scheduled ChatGPT task searches for meaningful updates.
2. New results are written to Google Sheets with `processed` set to `FALSE`.
3. n8n collects the unprocessed rows and builds one announcement.
4. n8n sends the announcement and its row IDs to a Home Assistant webhook.
5. Home Assistant reads it through OpenAI TTS when my notification conditions allow it.
6. Home Assistant acknowledges the announcement to n8n.
7. n8n marks only those acknowledged rows as processed.

Google Sheets is not just storing results here. It is the queue between research, orchestration, and delivery. That means an update can survive a failed workflow, a sleeping Home Assistant instance, or one of those delightful network problems that only appears after you confidently say, "It should be working."

## TL;DR

If you're skimming because your brain has already opened three metaphorical browser tabs:

1. **ChatGPT finds stuff worth knowing** and adds it to Google Sheets.
2. **n8n collects the new rows** and sends one combined announcement to Home Assistant.
3. **Home Assistant speaks it and calls n8n back**, which is when those rows finally get marked as processed.

That last part is important. Receiving a webhook isn't the same thing as successfully delivering an announcement.

## What you'll need

You don't need my exact setup, but you'll need the same basic pieces:

- **Home Assistant** \- I'm running **Home Assistant 2026.8.3**. Older versions may work, although YAML syntax, actions, and UI options can differ between releases.
- **OpenAI TTS for Home Assistant** \- I use the [OpenAI TTS custom integration](https://github.com/sfortis/openai%5Ftts/releases/latest?ref=adhdhomelab.com), installed through HACS. This handles the spoken announcements in my setup.
- **n8n** \- I'm running **self-hosted n8n 2.35.7**. Your n8n instance needs to be able to send requests to Home Assistant, and Home Assistant needs to be able to reach the n8n acknowledgement webhook.
- **Google Sheets access in n8n** \- n8n needs credentials with access to the spreadsheet being used as the queue. Their [Google Sheets integration documentation](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.googlesheets?ref=adhdhomelab.com) covers the setup.
- **ChatGPT scheduled tasks** \- these are the research side of the pipeline. Each task searches for whatever you care about and writes meaningful new results to the Google Sheet.

You can also swap out OpenAI TTS entirely. The important part of this architecture is the queue and acknowledgement flow, not which voice eventually starts talking at you from across the room.

## Why I used four different systems

Each part has one job:

| Component                  | Responsibility                                            |
| -------------------------- | --------------------------------------------------------- |
| **Scheduled ChatGPT task** | Research, filter, and summarize updates                   |
| **Google Sheets**          | Store results and track delivery state                    |
| **n8n**                    | Aggregate rows, call Home Assistant, and update the sheet |
| **Home Assistant**         | Decide whether to announce and perform the TTS            |

I use this pattern for several feeds, including a daily interest briefing, Marvel release news, and Pokémon updates. The subjects change, but the plumbing stays almost identical.

I use this pattern for several feeds, including a daily interest briefing, Marvel release news, and Pokemon updates. The subjects change, but the plumbing stays almost identical.

## Step 1: Give ChatGPT something useful to watch

The first part of the pipeline is the scheduled ChatGPT task.

One of mine is a **Daily Interest Briefing** that watches topics I regularly care about, including:

- Home Assistant;
- homelab and networking;
- Marvel;
- Pokemon and entertainment;
- AI and software development;
- anime I'm following;
- general tech news.

The exact topics aren't particularly important. What matters is giving the task clear rules for what qualifies as an update and what it should do when it finds one.

A simplified version of the instructions looks something like this:

> Search for meaningful new developments across my configured topics since the previous run.  
>  
> Prioritize confirmed announcements, releases, major updates, security issues, new features, release dates, and other developments that materially change what I previously knew. Ignore rumors, repetitive coverage, minor promotional stories, and duplicate information.  
>  
> For each genuinely new update, add one row to the Google Sheet with:  
>  
> `update_id`, `processed`, `detected_at`, `project`, `update_type`, `release_date`, `platform`, `source`, `source_url`, `why_it_matters`, and `summary`.  
>  
> Set `processed` to `FALSE` for every new row.  
>  
> Make `update_id` unique and stable so n8n can use it later to identify the row.  
>  
> Write `summary` as a concise combination of what happened + why I should care. Keep it conversational and TTS-friendly because Home Assistant may eventually read it aloud.  
>  
> Do not add anything if there are no meaningful new updates.

You can make the research task as broad or ridiculously specific as you want.

A Marvel watcher might only care about officially confirmed MCU release dates, trailers, and streaming releases. A homelab watcher could look for stable releases of Proxmox, OPNsense, Home Assistant, and Tailscale.

The downstream workflow doesn't care.

As long as ChatGPT writes the same columns and leaves new rows with `processed = FALSE`, Google Sheets and n8n can treat every scheduled task almost identically.

**The research changes. The plumbing doesn't.**

![Google Sheets Banner](https://adhdhomelab.com/content/images/2026/08/google-sheets-00-hero.jpeg)

Google Sheets Banner

## Step 2: Design the sheet as a queue

My `Updates` sheet uses these columns:

| Column           | Purpose                                                           |
| ---------------- | ----------------------------------------------------------------- |
| update\_id       | A unique, stable ID used to update the correct row                |
| processed        | Whether the announcement was acknowledged                         |
| detected\_at     | When the scheduled task found the update                          |
| project          | The title, product, movie, game, or topic                         |
| update\_type     | Release, trailer, announcement, security update, and so on        |
| release\_date    | A confirmed date or release window, when applicable               |
| platform         | The relevant service, platform, or venue                          |
| source           | The source name                                                   |
| source\_url      | A link to the original source                                     |
| why\_it\_matters | The practical reason I should care                                |
| summary          | A concise explanation combining what happened with why it matters |

Every new row starts with `processed` set to `FALSE`.

The `update_id` is important. Row numbers can change when a sheet is sorted or edited. A stable ID gives n8n something reliable to match later.

I also instruct the scheduled task to avoid duplicates and only add genuinely new, confirmed information.

For the written summary, I combine what happened with why it matters. The tone is factual, concise, TTS-friendly, and interesting enough to penetrate an AuDHD brain that has already wandered into another room.

![n8n workflow to collect pending rows from Google Sheets](https://adhdhomelab.com/content/images/2026/08/n8n-workflow.webp)

n8n workflow to collect pending rows from Google Sheets

## Step 3: Let n8n collect the pending rows

The workflow starts on a schedule and reads rows from Google Sheets.

Although the Sheets node can filter rows, I also filter them in a Code node because spreadsheet values have a charming habit of arriving as either booleans or strings depending on the phase of the moon.

Here is a reusable version of the Code node:

```javascript
// Keep only rows that have not already been processed.
// This handles boolean true and strings such as "TRUE" or "true".
const unprocessedItems = items.filter(item => {
  const processed = item.json.processed;

  return !(
    processed === true ||
    String(processed).toLowerCase() === 'true'
  );
});

// Normalize the rows so downstream nodes receive a predictable shape.
const updates = unprocessedItems.map(item => {
  const row = item.json;

  return {
    update_id: row.update_id || '',
    project: row.project || '',
    update_type: row.update_type || '',
    release_date: row.release_date || '',
    platform: row.platform || '',
    source: row.source || '',
    source_url: row.source_url || '',
    why_it_matters: row.why_it_matters || '',
    summary: row.summary || ''
  };
});

// Keep this as an actual array—not a comma-separated string.
const processedIds = updates
  .map(update => update.update_id)
  .filter(Boolean);

// Combine multiple rows into one natural-sounding TTS message.
const aggregatedSummary = updates
  .map((update, index) => {
    const heading = update.project
      ? `Update ${index + 1}: ${update.project}.`
      : `Update ${index + 1}.`;

    // My scheduled task already writes `summary` as a blend of what happened
    // and why it matters. Fall back to `why_it_matters` for older rows.
    const details = update.summary || update.why_it_matters;

    return `${heading} ${details}`.trim();
  })
  .join('\n\n');

return [{
  json: {
    updates,
    aggregated_summary: aggregatedSummary,
    processed_ids: processedIds
  }
}];
```

The result gives the rest of the workflow three useful values:

- `updates` \- the normalized update objects;
- `aggregated_summary` \- one announcement containing all pending updates;
- `processed_ids` \- an actual JavaScript array containing the IDs being sent.

That last one becomes important shortly.

![Home Assistant Logo](https://adhdhomelab.com/content/images/2026/08/homeassistant-logo.png)

Home Assistant Logo

## Step 4: Send one payload to Home Assistant

The n8n HTTP Request node sends a POST request to the Home Assistant webhook endpoint:

```
https://YOUR-HOME-ASSISTANT-DOMAIN/api/webhook/daily_briefing_news_update
```

In the HTTP Request node, I set the body type to JSON and use an expression for the complete body:

```
{{
  {
    aggregated_summary: $json.aggregated_summary,
    processed_ids: $json.processed_ids
  }
}}
```

This preserves the line breaks in the announcement and, more importantly, keeps `processed_ids` as a JSON array.

For example, the actual HTTP body sent to Home Assistant might look like this:

```json
{
  "aggregated_summary": "Update 1: Home Assistant. Home Assistant 2026.8 is available with several changes worth knowing about. Update 2: Marvel. A new MCU release date has been officially confirmed.",
  "processed_ids": [
    "home-assistant-2026-08",
    "marvel-example-2026-08-29"
  ]
}
```

The important detail is that `processed_ids` remains an array.

Turning it into a quoted, comma-separated value makes the later Split Out step needlessly messy.

For separate feeds, I use different webhook IDs - such as `marvel_news_update` and `pokemon_news_update` \- while keeping the payload contract the same.

![Home Assistant automation for announcement](https://adhdhomelab.com/content/images/2026/08/Home-Assistant-Automation.webp)

Home Assistant automation for announcement

## Step 5: Announce it in Home Assistant

My Home Assistant automation only speaks when `binary_sensor.main_bedroom_notification_conditions` is on.

That helper represents all the rules for whether bedroom announcements are currently appropriate, so every automation doesn't need to reinvent the same pile of conditions.

Here is a simplified version of the automation:

```yaml
alias: Daily Briefing News Update TTS
description: >-
  Announces one or more meaningful daily briefing updates received through a
  webhook and acknowledges them to n8n after playback starts.

triggers:
  - trigger: webhook
    webhook_id: daily_briefing_news_update
    allowed_methods:
      - POST
    local_only: false

conditions:
  - condition: state
    entity_id: binary_sensor.main_bedroom_notification_conditions
    state: "on"

actions:
  - variables:
      aggregated_summary: >-
        {{ trigger.json.aggregated_summary | default('', true) }}
      processed_ids: >-
        {{ trigger.json.processed_ids | default([], true) }}

  - condition: template
    value_template: >-
      {{ aggregated_summary | trim | length > 0 and processed_ids | count > 0 }}

  - action: openai_tts.say
    target:
      entity_id: media_player.main_bedroom_home_assistant_voice_media_player
    data:
      language: en
      tts_entity: tts.openai_tts_open_ai_tts_verse
      message: "{{ aggregated_summary }}"
      voice: verse
      volume: 1

  - action: rest_command.n8n_daily_briefing_ack
    data:
      processed_ids: "{{ processed_ids }}"

mode: queued
max: 10
```

The corresponding `rest_command` lives in `configuration.yaml`:

```yaml
rest_command:
  n8n_daily_briefing_ack:
    url: "https://YOUR-N8N-DOMAIN/webhook/daily-briefing-ack"
    method: POST
    content_type: "application/json"
    payload: >-
      {
        "processed_ids": {{ processed_ids | tojson }}
      }
```

There is a sneaky YAML/Jinja detail here: the output of `tojson` must **not** be wrapped in quotes.

This is correct:

```
"processed_ids": {{ processed_ids | tojson }}
```

This is not:

```
"processed_ids": "{{ processed_ids | tojson }}"
```

The second version sends a string that merely looks like an array.

Computers love that sort of technically valid betrayal.

## Sign up for ADHD Homelab

Every week-ish, I share practical homelab builds, smart-home automations, and delightfully overengineered ways to make the house do the remembering for me.

Subscribe 

Email sent! Check your inbox to complete your signup. 

No spam. Unsubscribe anytime.

## Step 6: Process the acknowledgement

This is the part that makes the workflow reliable.

n8n should **not** mark rows as processed immediately after its request to Home Assistant returns.

A successful webhook request only proves that Home Assistant received the request. It does not prove that the notification conditions passed, the TTS action ran, or the announcement was actually accepted for delivery.

Instead, a second n8n workflow receives the acknowledgement sent by `rest_command.n8n_daily_briefing_ack`.

I prefer a separate workflow here because the original execution does not need to sit open while Home Assistant decides whether it is an appropriate time to speak.

The publishing workflow is:

1. Schedule Trigger
2. Google Sheets: Read Rows
3. Code: Filter and Aggregate
4. IF: Any `processed_ids`?
5. HTTP Request: Send to Home Assistant

The acknowledgement workflow is:

1. Webhook: Receive the Home Assistant callback
2. Split Out: `processed_ids`
3. Google Sheets: Update the row matching each `update_id`

The acknowledgement workflow receives the array of IDs. The Split Out node turns that array into individual items, and the Google Sheets node updates each matching row:

```
processed = TRUE
```

Only the IDs Home Assistant acknowledged are changed.

If the bedroom notification condition is off, the automation stops before the callback and the rows remain pending for a future run.

That is deliberate:

**Silence should not count as delivery.**

Because the acknowledgement uses its own workflow, one missing callback cannot leave the publishing execution waiting forever.

I still schedule the publisher carefully - or prevent overlapping executions - so the same pending rows aren't sent again before Home Assistant has time to acknowledge them.

![The workflow for Chat GPT tasks to Home Assistant TTS](https://adhdhomelab.com/content/images/2026/08/Build-a-Powerful-ChatGPT-Automation-with-n8n-and-Home-Assistant-with-text.webp)

The workflow for Chat GPT tasks to Home Assistant TTS

## What I learned

The useful lesson wasn't how to make a speaker read a string. Home Assistant can do that before breakfast.

The real design problem was deciding **when an update counts as processed**.

If n8n marks rows too early, a failed or blocked announcement disappears forever.

If nothing tracks delivery, the same update gets announced repeatedly.

The acknowledgement handshake creates a clean boundary:

```
FALSE = queued or not yet acknowledged
TRUE  = Home Assistant accepted the announcement flow
```

It is small, understandable, and easy to troubleshoot.

I can inspect the sheet, see exactly what is pending, rerun the workflow, or correct a bad row without digging through execution logs like a digital archaeologist.

There's also a broader lesson here:

**Don't let one system pretend it knows what happened inside another system.**

n8n knows it successfully sent an HTTP request.

Home Assistant knows whether its conditions passed and the announcement action started.

So Home Assistant gets to decide when the update has crossed that boundary.

## Where this pattern works next

This setup is not limited to news. The same queue-and-acknowledgement pattern can handle:

- software and security release alerts;
- package delivery summaries;
- calendar or task briefings;
- homelab health reports;
- media release reminders;
- anything else that deserves attention but should not hijack the house one event at a time.

And that is how I turned several scheduled tasks, one spreadsheet, n8n, and Home Assistant into a tiny personalized broadcast network - because apparently opening a website was too straightforward.

---

Thanks for sticking with me through all the queues and webhooks! If you have questions about adapting this for your own setup, or you've built something similar with n8n and Home Assistant, drop a comment below - I'd genuinely love to hear how it went.

---

## Troubleshooting & FAQ

One advantage of splitting this into distinct stages is that troubleshooting becomes surprisingly logical. When something breaks, follow the payload instead of staring suspiciously at all four systems simultaneously.

### Home Assistant receives the webhook, but nothing is announced

Check your Home Assistant automation trace first.

In my setup, `binary_sensor.main_bedroom_notification_conditions` must be `on`. If that condition fails, Home Assistant intentionally stops before TTS **and before sending the acknowledgement**.

Also verify that:

- `aggregated_summary` isn't empty
- `processed_ids` contains at least one ID
- your OpenAI TTS entity is available
- the target media player is available

A useful test is to manually POST a known-good payload to the Home Assistant webhook and inspect the automation trace.

For example:

```json
{
  "aggregated_summary": "This is a test announcement from the daily briefing workflow.",
  "processed_ids": [
    "test-update-001"
  ]
}
```

### The announcement works, but rows stay `FALSE`

Follow the acknowledgement path.

Home Assistant should POST `processed_ids` back to the second n8n webhook after the TTS action starts.

Check that:

- the `rest_command` URL is reachable from Home Assistant
- the n8n acknowledgement workflow is active
- the webhook path matches exactly
- `processed_ids` arrives as an actual JSON array rather than a quoted string

This is an array:

```
["abc", "def"]
```

This is a string that merely contains JSON-looking text:

```
"[\"abc\",\"def\"]"
```

They may look suspiciously similar while staring at an execution log at 1:00 AM, but only the first one is the array you actually wanted.

### The same update keeps getting announced

Check the corresponding row in Google Sheets.

If:

```
processed = FALSE
```

the publishing workflow is doing exactly what you told it to do: that update is still pending.

Work backward through the acknowledgement workflow:

```
Home Assistant
    |
    v
n8n acknowledgement webhook
    |
    v
Split Out: processed_ids
    |
    v
Google Sheets: Update Row
```

Also make sure `update_id` is stable and that the Google Sheets Update node matches against `update_id`, not the spreadsheet's current row number.

### I'm getting duplicate announcements before the acknowledgement finishes

Your publishing workflow may be running again before Home Assistant has enough time to announce and acknowledge the previous batch.

Increase the interval between publishing runs or prevent overlapping executions.

The spreadsheet is intentionally acting as a durable queue, but:

```
processed = FALSE
```

still means:

```
This update needs to be delivered.
```

Until Home Assistant acknowledges it, n8n can't distinguish between "never sent" and "sent thirty seconds ago and still speaking."

### Why use Google Sheets between ChatGPT and n8n?

Because it gives me a visible, durable queue.

I can inspect pending updates, correct a bad row, retry failed deliveries, and see exactly what has or hasn't been processed without digging through workflow execution logs.

You could replace Google Sheets with a database or another queueing system, but Sheets has one enormous advantage for this particular project: I can open it and immediately see what's happening.

### How does the workflow prevent duplicate announcements?

Every row has a stable `update_id` and a `processed` status.

n8n only collects rows that haven't been processed. After Home Assistant accepts the announcement flow, it sends those IDs back to the acknowledgement workflow, which marks only those rows as processed.

That keeps previously acknowledged updates out of future announcements.

### What happens when Home Assistant can't announce the update?

Nothing gets marked as processed.

If the notification conditions aren't met, the automation stops before sending the acknowledgement.

The rows remain:

```
processed = FALSE
```

and are available for a later workflow run.

That's intentional. A quiet house shouldn't accidentally eat an update.

### Does this require OpenAI TTS?

No.

OpenAI TTS is what I'm using, but it isn't fundamental to the architecture.

You could replace the TTS action with another Home Assistant-compatible TTS integration, a mobile notification, persistent notification, display, LED matrix, or another delivery method.

The important part is deciding at what point your chosen delivery mechanism should send the acknowledgement.

### How do I figure out which part actually broke?

Start with Google Sheets and work forward.

If the row exists and says `FALSE`, ChatGPT did its job.

Check the publishing n8n execution next. If it created the expected `aggregated_summary` and `processed_ids` and successfully called Home Assistant, the first n8n workflow did its job.

Then inspect the Home Assistant automation trace. It will show whether the webhook fired, conditions passed, and the TTS action ran.

Finally, check the acknowledgement workflow in n8n and confirm that the IDs reached the Google Sheets Update node.

Follow the payload:

```
ChatGPT
   |
   v
Google Sheets
   |
   v
n8n
   |
   v
Home Assistant
   |
   v
n8n
   |
   v
Google Sheets
```

Follow it one hop at a time instead of questioning every technological decision you've made since childhood.

---