# We Stopped Reading G2 Reviews Manually. An Agent Does It Now.
> A B2B SaaS team spent an hour a week reading reviews across G2, Capterra, and TrustRadius. They missed a recurring API bug buried across 3 platforms. We built a Struere automation that scrapes reviews, categorizes them by theme, and flags urgent issues.

Published: 2026-03-31
Tags: automation, agents, case-study, product-feedback, review-monitoring


# The Spreadsheet That Missed a Bug

A product team at a B2B SaaS company (analytics platform, ~2,000 customers) tracked product reviews across three platforms: G2, Capterra, and TrustRadius. Every Friday, a PM opened each site, checked for new reviews, and logged them in a Google Sheet. Each row got a theme tag: onboarding, pricing, support, bugs, performance, integrations.

This took about an hour per week. The PM was thorough. She read every review, pulled out feature requests, noted sentiment, and flagged anything that needed engineering attention.

In February, four separate reviewers mentioned the same problem: the REST API returned stale data after bulk imports. One review was on G2. Two were on Capterra. One was on TrustRadius. They appeared over a 14-day window. The PM caught two of them but tagged one as "bugs" and the other as "performance." She never connected them. The pattern was invisible in the spreadsheet.

Engineering found the bug three weeks later through a support ticket. By then, two customers had started evaluating competitors.

# What We Built

Five files. A custom scraping tool, an entity type for reviews, an entity type for extracted insights, an AI agent, and a trigger to run it on schedule.

The scraping tool hits each review platform and pulls structured data. The agent reads every new review, categorizes it, extracts specific feature requests and bug reports, and flags anything urgent. Everything lands in Struere as queryable entities with structured tags.

## The Review Entity Type

Each scraped review becomes an entity with a consistent shape, regardless of which platform it came from.

```typescript
import { defineData } from "struere"

export default defineData({
  name: "Product Review",
  slug: "product-review",
  schema: {
    platform: { type: "string", description: "g2, capterra, or trustradius" },
    reviewerName: { type: "string" },
    reviewerTitle: { type: "string" },
    companySize: { type: "string" },
    rating: { type: "number" },
    title: { type: "string" },
    pros: { type: "string" },
    cons: { type: "string" },
    reviewDate: { type: "string" },
    reviewUrl: { type: "string" },
  },
  searchFields: ["title", "pros", "cons", "platform"],
})
```

## The Insight Entity Type

The agent creates one insight entity per actionable item it finds in a review. A single review might produce zero insights or three.

```typescript
import { defineData } from "struere"

export default defineData({
  name: "Review Insight",
  slug: "review-insight",
  schema: {
    theme: { type: "string", description: "onboarding, pricing, support, bugs, performance, integrations, ux, security" },
    type: { type: "string", description: "feature-request, bug-report, praise, complaint" },
    summary: { type: "string" },
    severity: { type: "string", description: "low, medium, high, critical" },
    sourceReviewId: { type: "string" },
    sourcePlatform: { type: "string" },
    verbatimQuote: { type: "string" },
  },
  searchFields: ["summary", "theme", "type"],
})
```

## The Scraper: Custom Tool

Each review platform renders reviews differently. G2 uses structured HTML with consistent CSS classes. Capterra has a different layout. TrustRadius uses yet another. The custom tool handles all three.

```typescript
import { defineTools } from "struere"

export default defineTools({
  name: "scrape-reviews",
  slug: "scrape-reviews",
  description: "Scrape new product reviews from G2, Capterra, and TrustRadius",
  parameters: {
    productSlug: { type: "string", description: "Product identifier on each platform" },
    platform: { type: "string", description: "g2, capterra, or trustradius" },
    since: { type: "string", description: "ISO date string, only return reviews after this date" },
  },
  handler: async ({ productSlug, platform, since }, struere) => {
    const urls: Record<string, string> = {
      g2: `https://www.g2.com/products/${productSlug}/reviews`,
      capterra: `https://www.capterra.com/p/${productSlug}/reviews`,
      trustradius: `https://www.trustradius.com/products/${productSlug}/reviews`,
    }

    const page = await struere.web.fetch({
      url: urls[platform],
      returnFormat: "html",
    })

    const html = page.data?.html || ""
    const sinceDate = new Date(since)

    return { html, platform, sinceDate: sinceDate.toISOString() }
  },
})
```

The handler returns raw HTML. The agent does the extraction. This is intentional. Review site HTML changes often. A regex parser breaks when G2 tweaks a class name. The agent reads the HTML and pulls out structured data regardless of minor layout changes. The scraper stays simple. The intelligence lives in the agent.

## The Agent

The agent receives HTML from the scraper, extracts review data, creates review entities, analyzes each review for insights, and creates insight entities.

```typescript
import { defineAgent } from "struere"

export default defineAgent({
  name: "Review Monitor",
  slug: "review-monitor",
  version: 3,
  model: { model: "anthropic/claude-sonnet-4", temperature: 0.3 },
  systemPrompt: `You monitor product reviews across G2, Capterra, and TrustRadius.

When given HTML from a review page:
1. Extract each review: reviewer name, title, company size, rating, review title, pros, cons, date, URL
2. Create a product-review entity for each new review
3. Analyze each review for actionable insights
4. Create a review-insight entity for each insight with the correct theme, type, severity, and a verbatim quote
5. If any review has rating <= 2 AND mentions a bug or outage, set severity to critical

Themes: onboarding, pricing, support, bugs, performance, integrations, ux, security
Types: feature-request, bug-report, praise, complaint

Be specific in summaries. "API returns stale data after bulk import" is good. "API issues" is not.`,
  tools: [
    { tool: "scrape-reviews" },
    { tool: "entity.create" },
    { tool: "entity.query" },
  ],
})
```

`temperature: 0.3` keeps categorization consistent. Higher temperatures made the agent inconsistent about whether "slow dashboard loading" was "performance" or "ux." At 0.3 it picks one and sticks with it.

## The Trigger

A trigger runs the agent every Monday morning. It queries for the last scan date, scrapes all three platforms, and lets the agent process the results.

```typescript
import { defineTrigger } from "struere"

export default defineTrigger({
  name: "Weekly Review Scan",
  slug: "weekly-review-scan",
  on: {
    schedule: "0 8 * * 1",
  },
  actions: [
    {
      type: "agent.chat",
      agent: "review-monitor",
      message: "Scrape new reviews from all three platforms (g2, capterra, trustradius) for product slug 'acme-analytics' since last Monday. Extract all reviews, create entities, and flag any critical insights.",
    },
  ],
})
```

# Debugging: Two Things Broke

**First problem:** the scraper returned truncated HTML for TrustRadius. TrustRadius lazy-loads reviews. The initial HTML only had the first 5 reviews. We added a scroll parameter to `web.fetch` and set `waitForSelector` to ensure the page rendered fully before returning HTML.

**Second problem:** the agent created duplicate review entities when the same reviewer posted on both G2 and Capterra. We added a step to the system prompt: before creating a review entity, query existing entities for the same reviewer name and similar review text. If a match exists with >80% text overlap, link the entities instead of creating a duplicate.

The duplicate fix required one line in the system prompt and one extra tool:

```typescript
tools: [
  { tool: "scrape-reviews" },
  { tool: "entity.create" },
  { tool: "entity.query" },
  { tool: "entity.update" },
],
```

The agent now queries before creating. No code change. Just a prompt update and an extra tool permission.

# Setup: 15 Minutes to First Scan

```bash
struere init
struere add data-type product-review
struere add data-type review-insight
struere add agent review-monitor
struere add trigger weekly-review-scan
```

Edit the five files. Then:

```bash
struere dev
```

The dev watcher syncs everything to your development environment. Create a test scan by chatting with the agent in the dashboard Studio panel. Verify the entities appear. Deploy:

```bash
struere deploy
```

The trigger fires every Monday at 8 AM. Reviews appear as entities. Insights get tagged and categorized. Critical issues get flagged.

# The Bug They Would Have Caught

We loaded the February reviews retroactively. The agent processed all 23 reviews from the three-week window. It created 31 insights. Four of those insights had theme "bugs", type "bug-report", and summaries that all mentioned stale data after bulk operations. The agent grouped them under the same pattern because it read the actual text, not a spreadsheet tag.

The PM's spreadsheet had the reviews. It had the data. But a human scanning 20+ reviews per week across three browser tabs, tagging each one with a single theme, will miss a pattern split across platforms and weeks. The agent reads every word of every review, every time, and its categorization never drifts because it forgot what it tagged last Tuesday.

The weekly review hour is gone. The PM now spends 10 minutes on Monday scanning the insight entities, sorted by severity. She focuses on the critical and high items. The spreadsheet is archived.
