Case Study — Keeping a Customer's Feature-Request Tracking in Sync with an Agent

Published

Project article; see sources and editorial standards.

How Claude Code (browser automation plus MCP) and Glean kept one enterprise customer’s feature requests aligned across Aha!, a Monday.com board, and a Google Sheet — idempotently, with no pasted API tokens and no customer data deleted.

Audience: technical account managers (TAMs) and product managers (PMs). This is an engineering case study, not marketing. Figures marked [est.] are estimates; the Outcomes figures come from the production run.


TL;DR

One enterprise customer’s feature requests lived in three places that drifted apart: Aha! (the PM source of truth), a Monday.com board the account team worked from, and a Google Sheet the customer and the account executive (AE) reviewed. Reconciling them by hand was slow, error-prone, and always stale.

We had Claude Code drive the whole loop end to end. It read the customer’s ~77 ideas out of Aha!, enriched each from its detail page, then idempotently upserted them onto the Monday board (create or update, so a re-run changes nothing further) and filled only the empty cells of the sheet.

By design, the run reads before it writes. It never deletes the customer’s data, never overwrites a populated sheet cell, and writes Monday fields only where they differ from Aha!. It uses the browser’s own session instead of a pasted token and treats every fetched value as data rather than instructions. The result is a single-pass reconciliation of work that took a person the better part of a day by hand [est.].


Problem & context

A high-touch enterprise account generates feature requests faster than any one system captures them cleanly:

Each system is authoritative for something and stale for everything else. When a PM moved an Aha! idea from In Development to Shipped, that change did not reach the board or the sheet until someone noticed and hand-edited two more places. Status labels diverged (Aha!’s vocabulary ≠ the sheet’s vocabulary), PM assignments went missing, and nobody could trust the customer-facing sheet without re-checking Aha! first. The manual reconciliation was the kind of repetitive, careful, cross-tab work that humans do slowly and agents do well, if the guardrails are right.


Architecture

Claude Code acted as the orchestrator, with Glean used up front to find the Aha! report, the board, and the sheet and to pull surrounding account context. Everything downstream ran through the agent’s Chrome browser automation and MCP tooling, against the operator’s already-authenticated sessions.

Layer System Access pattern
Source of truth (read-only) Aha! shared report + per-idea detail pages Browser fetch of the report filtered to the customer; parse the iframe table, then fetch each idea’s detail page
Operational tracker (read/write) The customer’s Monday board Internal GraphQL POST /v2/ with x-csrf-token + credentials:'include'; no pasted API token
Customer-facing surface (read/write) The feature-request tab of the Google Sheet Read via clipboard-copy of the canvas grid (gviz, Google’s Visualization query API, needs an OAuth credential unless the sheet is public); write only empty cells
Discovery & context Glean Locate assets, pull account context

Two design choices in this table carry most of the safety story. First, Monday writes use the browser’s own CSRF token and session cookies rather than a long-lived API token, so no API token is pasted or stored. Second, the sheet is read by copying the rendered grid to the clipboard, because the clean programmatic path (gviz) sits behind an OAuth grant we deliberately chose not to request.


Sync logic

The run is a single pass through four steps:

  1. Build the roster. Open the Aha! shared report, clear the org filter, select the customer’s organization, refresh, and parse the resulting table into ~77 rows of (Feedback Reference, Name, Status, Created By). The Feedback Reference is the ref: the key that matches records across all three systems.
  2. Enrich each ref. For every ref, fetch its detail page and parse the fields PMs care about: Assigned to (PM), Impact, Current State, Potential Future State, Product Group, Priority, Business Impact, Promoted to Feature. Fetches are throttled (~1.5s apart) with a single retry on HTTP 429, so the run does not flood Aha!.
  3. Upsert to Monday. For each ref, find the board item whose AHA Ref equals that ref. If none exists, create one in group topics, with create_labels_if_missing so new statuses don’t fail the write (the flag adds missing status labels, so it needs permission to change the board’s structure). Then set AHA Ref, Aha Status, Customer (the same value on every item), Product Manager, Impact, and a composite Description (Current State + Potential Future State + a metadata line of Product Group | Priority | Business Impact | Promoted to Feature), but only if the value isn’t already correct.
  4. Reconcile the sheet. Map each roster ref to its row via a temporary helper column (=ROW() & ":" & REGEXEXTRACT(...)), parse it, then delete the helper. For each matched row, fill only empty cells: Ticket Status (H) from the mapped status, MDB PM (J, the sheet’s PM column) from the Aha! assignee, and Request Summary (G) from a single-line Current/Future-State summary.

The Aha! status vocabulary is mapped into the sheet’s vocabulary on the way in:

Aha! status Sheet status
Shipped Resolved
Shipped in Private Preview In Private Preview
In Design / In Development In-progress
Back to Curation Needs Curation
(anything else) same name

Any status outside this map reaches the customer-facing sheet under its Aha! name, so check new Aha! statuses before they appear there.


Safety & guardrails

These guardrails are what let an agent run on a live customer account:


Outcomes

The production run over the customer’s roster produced these results:

The sync’s job is not only to copy data. It also exposes where the three systems disagree, so a person can decide what’s authoritative.


Lessons learned & reuse guidance

What made the agent approach work

What was fragile

When to reuse

Reach for this pattern when (a) one system is the clear source of truth, (b) the downstream systems are append- or update-only and you can match records on a stable key (here, the Aha! idea reference ↔ AHA Ref), and (c) you can express “already correct” precisely enough to skip it. Swap the org filter, board ID, sheet tab, status map, and column mappings and the same skeleton should serve another account. If a downstream surface holds content people entered by hand (here, the sheet) and the sync would need to delete or overwrite it, stop and put a human in the loop. This design refuses to automate those operations.