---
title: "Build a Customer Feedback Sentiment Pipeline in n8n"
description: "Build an n8n pipeline that ingests reviews, scores sentiment with an AI node, extracts themes, and alerts on spikes — AI credits included. Step-by-step guide."
canonical: https://agentroost.app/en/blog/feedback-sentiment-pipeline-n8n-ai
date: 2026-06-10T12:00:00Z
---

[Canonical URL](https://agentroost.app/en/blog/feedback-sentiment-pipeline-n8n-ai)

Reading through 300 survey responses by hand to find out whether customers are happy or frustrated is one of those tasks that sounds manageable at first, then slowly eats an afternoon every week. Sentiment analysis automates exactly that: every new review, survey answer, or support ticket gets a score and a label, trends surface on their own, and you get a Slack ping when something goes sideways — without touching the data yourself.

This guide builds that pipeline step by step in n8n, using the AI/LLM node to do the heavy lifting.

## What the pipeline does

1. **Ingests** new feedback on a schedule or via a webhook push.
2. **Scores** each item with an AI node — positive / neutral / negative, plus 1–3 extracted themes.
3. **Writes** the enriched row to a Google Sheet (your running log).
4. **Alerts** to Slack when the rolling negative ratio in the last 50 items crosses 30 %.

By the end you will have a workflow you can leave running indefinitely.

---

## The nodes, in order

### 1. Schedule Trigger (or Webhook)

**Schedule Trigger** is the easiest entry point. Set it to run every 30 minutes:

```
Trigger interval: Every 30 minutes
```

If your review source can push events in real time (Typeform, a custom app, Stripe's `review.created` webhook), use a **Webhook** node instead — it gives you a POST endpoint like `https://<your-id>.agentroost.app/webhook/feedback` that you can paste straight into any third-party webhook settings panel.

### 2. HTTP Request or Google Sheets (ingest)

Pull the unprocessed rows from wherever you store feedback.

**Option A — Google Sheets:**
Add a **Google Sheets** node in "Read Rows" mode. Point it at a sheet called `raw_feedback`. Filter to rows where column `processed` is empty so you only touch new entries.

**Option B — HTTP Request:**
If feedback comes from a REST endpoint (your own product API, a Typeform results API, etc.):

```json
{
  "method": "GET",
  "url": "https://api.yourapp.com/feedback?since={{ $now.minus(30, 'minutes').toISO() }}",
  "authentication": "headerAuth"
}
```

Use a **Split In Batches** node after ingest if you expect more than ~20 items at a time, to keep LLM calls predictable.

### 3. Basic LLM Chain (sentiment + themes)

This is the core. Add a **Basic LLM Chain** node (under the AI section in n8n). Set the system prompt:

```
You are a product feedback analyst. Given a customer review, return ONLY valid JSON with these fields:
- sentiment: "positive" | "neutral" | "negative"
- score: a float from -1.0 (most negative) to 1.0 (most positive)
- themes: an array of 1-3 short lowercase strings (e.g. ["slow load time", "great support"])
- one_line_summary: one sentence, max 15 words

Review:
{{ $json.feedback_text }}
```

Set the user message to `{{ $json.feedback_text }}` and choose any available model. Because the node returns plain text, add a **Code** node (or **Set** node with an expression) to parse it:

```js
// Code node — JavaScript
const parsed = JSON.parse($input.first().json.text);
return [{ json: { ...parsed, original: $input.first().json } }];
```

> **Why not the full AI Agent node here?** Agents are great for multi-step reasoning. For a single structured extraction like this, Basic LLM Chain is faster, cheaper on tokens, and the output is predictable JSON.

### 4. Set — flatten for the sheet

A **Set** node maps the parsed fields to flat column names:

| Field | Value expression |
|---|---|
| `feedback_id` | `{{ $json.original.id }}` |
| `feedback_text` | `{{ $json.original.feedback_text }}` |
| `sentiment` | `{{ $json.sentiment }}` |
| `score` | `{{ $json.score }}` |
| `themes` | `{{ $json.themes.join(', ') }}` |
| `summary` | `{{ $json.one_line_summary }}` |
| `processed_at` | `{{ $now.toISO() }}` |

### 5. Google Sheets — write enriched row

Add a second **Google Sheets** node in "Append Row" mode. Point it at a sheet called `sentiment_log`. Map the columns from the Set node above. Also add another Google Sheets node in "Update Row" mode back on `raw_feedback` to mark the row `processed = true` so the Schedule Trigger skips it next time.

### 6. IF — spike detection

Add an **IF** node. The condition reads the last 50 rows of `sentiment_log` and checks the negative ratio. One clean way: add a **Google Sheets** node in "Read Rows" mode (limit 50, sorted by `processed_at` descending), then a **Code** node that counts negatives:

```js
const rows = $input.all().map(i => i.json);
const negCount = rows.filter(r => r.sentiment === 'negative').length;
const ratio = negCount / rows.length;
return [{ json: { ratio, negCount, total: rows.length, spike: ratio > 0.3 } }];
```

The IF node branches on `{{ $json.spike }} === true`.

### 7. Slack (alert branch)

On the `true` branch, add a **Slack** node in "Send Message" mode:

```
Channel: #product-alerts
Message: Feedback spike: {{ $json.negCount }} of the last {{ $json.total }} reviews are negative ({{ ($json.ratio * 100).toFixed(0) }}%). Check the sentiment log.
```

On the `false` branch, add a **No Operation** node or nothing — the workflow ends quietly.

---

## Tips and common pitfalls

**Prompt the model to return ONLY JSON.** Without "return ONLY valid JSON" in the system prompt, models often wrap output in markdown fences or add a preamble. The Code node's `JSON.parse` will throw. Add a try/catch and route parse errors to a separate error sheet so nothing silently drops.

**Batch carefully.** If you have hundreds of reviews queued up on first run, the 30-minute schedule will call the LLM hundreds of times back-to-back. Use **Split In Batches** (batch size 10) with a **Wait** node (2 seconds) between batches to stay within rate limits.

**Theme vocabulary drift.** Free-form themes like "load time" and "slow loading" are duplicates to a human but treated as different strings. Once you have a few weeks of data, review the themes column and feed a short canonical list back into the prompt: `Classify themes using ONLY these labels: [billing, performance, onboarding, support, feature-request]`.

**Test with a Webhook trigger first.** Before enabling the Schedule Trigger for production, use a manual Webhook test run with three sample rows — one positive, one neutral, one negative — so you can verify the JSON parsing and the Slack alert without waiting 30 minutes.

---

## Run it on AgentRoost

Getting a workflow like this running on your own server means provisioning a VPS, installing n8n, wiring up SSL, and then creating and paying for OpenAI API credentials separately. The AI node cost is invisible until the invoice arrives.

On AgentRoost you sign up, pick the **n8n** framework, name your instance, and your private n8n editor opens at `https://<your-id>.agentroost.app` in about two minutes. You own the instance — your login, your workflows, your data. The AI/LLM node is already connected to included credits — no API key to create, no separate billing account. The public HTTPS URL on your instance means the Webhook trigger works from the first minute, no ngrok or Cloudflare tunnel.

The base plan starts at $19.99/mo all-in. There is a 14-day money-back guarantee if the workflow does not do what you need.

[See what's included in each plan](/en/pricing) — or go straight to [your own n8n instance](/en/agents/n8n) to get the editor open.
