# How Review Monitor works

Read this beside the **Architecture** view in the visual explorer. The diagrams are available separately as system ownership and review journey.

## Start with the mental model

Think of three different responsibilities:

1. **n8n decides when to run the business steps.** It schedules source acquisition and the Monday digest, and provides a controlled manual trigger.
2. **The worker decides how each step is carried out.** It talks to the source, normalizes records, applies the conservative interpretation rules, validates evidence, and makes destination requests.
3. **PostgreSQL remembers what happened.** It stores the latest review, its revisions, the work that is waiting, and receipts for work that was delivered.

HubSpot is the support work surface. Slack is the theme-reporting surface. Neither publishes a review response. The person who reviews the task owns that final public action outside this system.

The custom logic is in the worker. The n8n canvas orchestrates authenticated HTTP calls to it. Saying that every classification rule lives in n8n nodes would misdescribe this implementation.

## The system boundary

```mermaid
flowchart TD
  N[Existing server n8n] --> W[Review Monitor worker]
  W --> S[Official Steam review API]
  S --> W
  W <--> P[(Project PostgreSQL)]
  W --> H[HubSpot developer-test tasks]
  W --> L[Controlled Slack channel]
  H -. human reviews wording .-> U[Person publishes separately]
```

The server worker has no public host port. n8n reaches it on the shared Docker network through the project-specific alias `review-monitor-worker`. Operational API routes require a bearer credential. The read-only inspection is accessible through a local SSH tunnel and displays sanitized records and receipts.

The worker and database live under this project’s deployment directory. Existing sales/support workflows and native lifecycle configuration remain separate.

## Follow one real review

The example is Steam review **237850686**, about Coffee Talk by Toge Productions. Its native HubSpot task receipt is **417943964402**. The readback evidence connects that receipt to the source link and unposted draft.

### 1. Read the source

`reviewMonitorIntake` starts manually or on its configured six-hour schedule. Its first HTTP step calls `POST /intake` on the worker.

The worker calls the official Steam `GetAppReviews` interface. It requests English reviews, all purchase types, sorting by most recently updated. It follows the returned cursor. For this demonstration, it keeps reviews updated since October 1, 2026 at midnight America/Chicago.

The scan stops when the source is exhausted or a full page predates that boundary. A 30-page cap protects the run. Hitting the cap is recorded as partial, not complete. The connected scans completed two pages and retained 33 in-scope identities.

The provider's anonymous responses may be cached. This is not a push feed and does not establish immediate detection of a newly written review. Deletion detection is unsupported.

### 2. Normalize the record

The source recommendation ID becomes `steam:914800:237850686`. The application ID is part of the key, so the identity also names its source/product scope.

Normalization keeps the real business/product identity, source URL, bounded text, recommendation value, created/updated timestamps, and collection time. It drops the author’s profile object and hardware details. The source link remains for attribution.

A version hash includes text, recommendation value, and source update timestamp. **Identity answers “which review?” Version answers “which observed form of that review?”**

The three times answer different questions:

| Field | What it means | What it does not prove |
| --- | --- | --- |
| `createdAt` | When the source says the review was written | When this monitor first saw it |
| `updatedAt` | When the source says it changed | When the support task was ready |
| `collectedAt` | When this project first observed it | That the review itself was newly written |

### 3. Interpret, then validate

The current interpreter is a deterministic rules baseline. It recognizes selected positive/negative terms and topic terms. Topics include shipping, price, product, service/staff, technical, and story. A review can carry multiple topics.

Empty, mixed, conflicting, suspicious, and certain sarcastic/negated text is routed to `needs_review`. The recommendation value can reveal a conflict with text; it is not a substitute for interpreting the text.

The output validator accepts only known labels and topics. Every supporting excerpt must occur in the actual bounded source text. An invalid output becomes `needs_review`. Source text cannot change credentials, destinations, or workflow instructions.

The evaluation matched 22 of 25 authored cases. Three negatives were not automatically routed as negative and stayed in review. That is a disclosed limitation, not a general accuracy estimate.

### 4. Save the review and queue the handoff

A PostgreSQL transaction takes a per-review advisory lock and checks the existing identity. This serializes competing updates to the same review.

- Same version: return unchanged.
- Older update: leave the newer stored record intact.
- New identity: store the review and revision.
- Changed version: store a revision and set a reconciliation flag. Do not overwrite a person's task or draft.

A confident negative inserts a pending task effect with the key `task:steam:914800:237850686`. A unique primary key prevents a repeated source page from creating another copy of that same effect.

## What each table remembers

| Table | Responsibility | Example question |
| --- | --- | --- |
| `rm_reviews` | Latest record and reconciliation flag | What is the current interpretation of this review? |
| `rm_revisions` | Observed versions | Did the review change? |
| `rm_effects` | Task/digest payload, claim state, receipt, attempts and error | Is delivery waiting, confirmed, or uncertain? |
| `rm_scans` | Source scan state, pages and observed records | Did acquisition finish, fail, or stop at the cap? |

This is not a generic event-sourcing framework. It is a small set of tables for the review identity and delivery problems this workflow actually has.

### 5. Claim and deliver one task

The next n8n HTTP step calls `POST /dispatch-task`. The worker selects one pending task and atomically changes its state to claimed. Concurrent callers cannot both claim the same pending effect.

It creates a HubSpot TODO in the developer test account, without contact/company associations or reminders. The task contains the source link, factual topic reason, and generic wording labeled **UNPOSTED PUBLIC RESPONSE DRAFT**. No fix or refund is promised.

A successful response supplies the native task ID. The worker stores that receipt and marks the effect sent. The separate readback checked that the actual native task retained the draft label and source link.

The controlled dispatcher deliberately handles one pending task per execution. The second real negative was delivered on the next controlled run. This project does not establish production batch capacity.

## Delivery state is more important than retrying quickly

```mermaid
stateDiagram-v2
  [*] --> pending
  pending --> claimed: atomic claim
  claimed --> sent: accepted response and stored receipt
  claimed --> failed: definitive rejection
  claimed --> uncertain: timeout or unknown outcome
  claimed --> uncertain: claim expires
  uncertain --> sent: operator finds the matching remote receipt
  uncertain --> pending: operator confirms remote absence
  failed --> pending: operator confirms absence and resolves rejection
```

Suppose Slack accepted a message but the network connection broke before the worker received the response. Sending again immediately could create a duplicate. The effect therefore stays uncertain until a person checks the destination.

The operator searches for the task receipt key or the digest receipt hash. A matching native item resolves it as sent. Only confirmed absence permits another attempt. The recovery test exercises reconciliation without another send. It does not claim a real production outage occurred.

Source GET requests have bounded retries for 429/server errors. Destination writes are not blindly retried. A failed or uncertain effect also causes the n8n HTTP step to fail, making the run and the durable recovery state agree.

## How the digest is different

The task workflow acts on an individual review. The digest workflow acts on a defined observation window.

`reviewMonitorDigest` is configured for Monday 09:00 in America/Chicago. It computes the previous local Monday 00:00 through the current Monday 00:00 interval. Start is included; end is excluded. The boundary function accounts for daylight-saving changes.

This implementation groups by **first observation time**, not source review creation time. Older reviews discovered during a new monitoring period can therefore appear together. The digest says so explicitly.

It excludes synthetic fixtures, counts sentiment/topics, and identifies recurring negative topics only when at least two distinct source URLs support them. Product and story each have two examples in the verified run, but they refer to the same two reviews. Topic counts must not be added as if they were unique customers.

The digest effect key includes source, product, and window. Its payload is frozen when first queued. Replaying that window returns the stored payload and its delivery state; it does not silently revise a digest that someone already received.

The controlled demonstration used November 2 to November 9 and was triggered on November 7. It was forced early and marked incomplete. It proves a digest reached Slack, not that a future Monday schedule executed.

## Source completeness and timeouts

A completed individual scan does not prove uninterrupted monitoring across a week. Coverage checks the elapsed window, completed polling observations, failures, and gaps. The current run is partial monitoring coverage even though its individual scans completed.

Scans restart from the source head, with overlap and stable identities. There is no durable resumable cursor in this version. A failed scan is not marked complete, and the next scan rereads safely. An in-process busy flag prevents overlapping acquisition inside the single worker; database identity/claim constraints handle concurrent record/effect work.

The n8n HTTP timeout is 120 seconds. A large backfill, provider delay, or higher volume would need a longer-running job and an explicit throughput policy. The small verified cohort is not evidence that those larger workloads are solved.

## Retention and the paused state

The worker sweeps at startup and hourly. After the seven-day threshold it removes raw review text and supporting excerpts. Identity, timestamps, version hash, sentiment/topic labels, flags, and effect receipts remain. Keeping derived labels prevents expiry from erasing digest counts. An hourly sweep can remove content up to an hour after the threshold.

All five n8n workflows remain inactive after the demonstration. Privacy maintenance stays active inside the project worker. There were no pending or uncertain effects in the final readback. The system is not currently performing continuous monitoring or sending future Monday digests.

## Read the code in this order

1. Source adapter: request, paging contract, bounded GET retries.
2. Core behavior: normalization, interpretation, validation and windows.
3. Intake: scan state and activation boundary.
4. Durable store: transaction, identity, claims, receipts and retention.
5. Destinations: HubSpot and Slack request boundaries.
6. Service: the authenticated HTTP steps n8n calls.
7. Maintenance: privacy sweep and status.
8. Workflow exports: the actual orchestration graph. Local exports use the local worker host; server preparation substitutes the namespaced Docker alias.

## Check your understanding

Try explaining these aloud before recording. They are practice prompts, not claims that understanding has already been assessed.

- Why can the same source review be read four times without producing four tasks?
- What is the difference between a review identity and its version?
- Why does an unknown Slack outcome require reconciliation?
- Why is `needs_review` useful even when it means less automatic routing?
- Why can 33 observed reviews include feedback written before the observation week?
- Which action is deliberately outside the automation, and why?
