An AI pipeline for product analytics: measure your PostHog funnels week over week, have Claude write up the steps that got worse, and file them as deduped Linear issues.
Built on One. It files one ticket per broken step, not one per run.
Every run, for each funnel you declare in config/funnels.md:
- Measures the funnel twice in PostHog, over the last 7 days and the 7 before that. Steps are ordered by each person's first occurrence of each event, so the counts are per person and monotonic. Counting raw events gives you the above-100% conversions you get when somebody retries a step four times.
- Decides what counts as a regression in code, never in the model. A step has to clear a volume floor, an absolute point drop, and then either a relative conversion drop or a rise in the failure rate.
- Writes the ticket with Claude: a title with both rates in it, a severity, likely causes ordered by likelihood, and concrete first moves. The model is told the numbers are settled, so it writes the ticket rather than re-arguing the arithmetic.
- Files it to Linear, once. Every issue body carries a hidden fingerprint. If the same step is still regressed next week it gets a comment with the new numbers, not a second ticket.
$ npm start
── window: 2026-07-24 to 2026-07-31, against 2026-07-17 to 2026-07-24
connect-a-platform
148 base opened the connection picker
89 60.1% got redirected to the provider (was 46.8%)
67 75.3% the provider sent them back with a code (was 83.8%)
61 91.0% the connection was saved (was 91.9%)
1 step past the threshold
new-account-activation
141 base signed up
93 66.0% opened the connection picker (was 68.1%)
55 59.1% saved a first connection (was 62.0%)
23 41.8% reached the activation milestone (was 45.6%)
nothing past the threshold
catalog-to-connection
72 base opened a platform from the catalog
67 93.1% started connecting it (was 91.0%)
35 52.2% saved the connection (was 49.2%)
nothing past the threshold
1 regression to triage
[medium] OAuth redirect to callback on connect-a-platform fell from 83.8% to 75.3%
On the connect-a-platform funnel, the step from connection.oauth_initiated to
connection.oauth_callback_succeeded dropped from 83.8% to 75.3%, a fall of 8.5
points. That is roughly 8 more people per window who get sent to a provider and
never come back with a code. With 89 people in the step this window and 74 in the
prior one, a handful of people moves this rate by a point, so the size of the drop
is less trustworthy than the direction.
filed CON-26 https://linear.app/.../oauth-redirect-to-callback-on-connect-a-platform
That is a real run from 2026-07-31 against One's own PostHog project, with live PostHog, Claude and Linear. The three funnels, the counts and the ticket are all real. npm start -- --demo replays it with no keys and no network.
Both platform calls go through One, so there is one auth setup and one SDK rather than a PostHog client, a Linear GraphQL client, and two sets of credentials to rotate.
1. Connect your accounts through One
npm i -g @withone/cli
one init # create an account or log in
one add posthog
one add linear2. Configure this repo
git clone https://github.com/withoneai/funnel-triage-agent
cd funnel-triage-agent
npm install
cp .env.example .envFill in .env. The two connection keys come from one --agent list. POSTHOG_PROJECT_ID is the number in your PostHog dashboard URL. LINEAR_TEAM_KEY is the prefix on your issue ids, the CON in CON-42.
3. Describe your funnels
Open config/funnels.md and replace the example funnels with yours. Each ## heading is one funnel, and the numbered list under it is the ordered steps: the literal PostHog event name in backticks, a hyphen, then the label you want to read in an issue title.
The heading is a slug, and it is half of every fingerprint. Renaming it, or reordering steps, will make an already-filed regression look new and file a second ticket. Everything else in the file is safe to edit.
4. Check and run
npm run check # proves both connections work and every event exists
npm start -- --dry-run # measures and writes the tickets, files nothing
npm start # files themnpm run check is worth running first. It tells you which of your event names have not fired in the last 30 days, which is the failure mode that would otherwise read as a total collapse of a funnel rather than a typo.
npm start # last 7 days against the 7 before
npm start -- --days 14 # 14-day windows instead
npm start -- --funnel connect-a-platform # one funnel
npm start -- --dry-run # print the tickets, write nothing
npm start -- --demo # replay a recorded run, no keysPoint cron or a GitHub Action at npm start on whatever cadence matches your window. Weekly with 7-day windows is the obvious pairing.
A relative-drop threshold alone goes blind where your product works best. A step converting at 90% cannot shed 15% of its rate without collapsing to 76%, so any funnel in good health sails under a relative floor no matter how bad the week was. The step in the sample run above fell 8.5 points and only lost 10.1% of its rate, under a 15% floor. Measured as failures it is unmissable: 16.2% failing became 24.7% failing, a rise of 52%. This runs both tests and fires on either.
The fingerprint cannot touch anything the model wrote. The first version of this built the dedupe key out of the step description, and it duplicated an issue that was still open, because Claude wrote connect-a-platform-from-the-dashboard on one run and connect-platform-from-dashboard on the next. Deriving it from the steps the model picked failed the same way, because the model picked a different set of steps each run. That is why the funnel catalog is a file parsed by code: the fingerprint is funnelId#stepIndex, both of which come from config/funnels.md and cannot move unless you move them.
A closed issue deliberately does not match. If a step regressed, got fixed, and regressed again, that is a new problem and gets a new ticket rather than a comment on something somebody already signed off.
| File | What it does |
|---|---|
config/funnels.md |
The catalog: your funnels, thresholds, and background for Claude |
src/catalog.ts |
Parses that file, deterministically. No model involved |
src/posthog.ts |
The per-person funnel query, via One's raw passthrough |
src/detect.ts |
The regression gate. Pure arithmetic |
src/triage.ts |
Claude writes the ticket, as a structured tool call |
src/linear.ts |
Fingerprint lookup, create, comment, read back |
src/index.ts |
The run |
PostHog has no typed wrapper in the One SDK yet, so it goes through runRaw, which still handles auth, the base URL, retries and parsing. Linear has typed actions and uses them.
MIT