I Built HelloBible's Premium Triage Queue for $0
HelloBible was migrating support from a tired Gmail inbox to a properly configured Zendesk. Zendesk's native RevenueCat integration showed who paid, but didn't let us prioritise their tickets in the queue. I built an n8n workflow that scans every ticket, cross-references an exported RevenueCat sheet, and auto-tags premium ones. Result: a dedicated 'Premium Clients' Zendesk view, 323 paying customers triagable at a glance, $0 in API/SaaS costs.
Context. For years HelloBible's support ran on Gmail. As we scaled past 100k downloads, we migrated to Zendesk to centralise and professionalise. The migration revealed a gap: paying subscribers were invisible in the new queue. I was solo support at the time, and I needed every premium ticket flagged before I opened it.
Result. In production. 2,687 tickets scanned in the initial run, 383 premium identified (~14%). 323 of them visible right now in a dedicated 'Premium Clients' Zendesk view, refreshed every 12 hours by a delta workflow. Self-hosted on my Mac, $0 in API/SaaS costs. The view is now part of the support team's daily triage.
A small system, one outcome: turn HelloBible’s paying subscribers into a visible cohort inside Zendesk. Before the workflow, premium tickets were diluted in a queue of 1.3K. After it, they’re a dedicated view (323 tickets right now), one click away in the sidebar.
The workflow made premium tickets visible directly in the Zendesk queue instead of only inside the user profile.
Two underlying skills made the workflow possible: pragmatic pivot when an API doesn’t do what you expected (RevenueCat API v2 can’t filter by email), and scope discipline (manual sheet refresh once a month, not a complex auto-sync pipeline).
The friction I found
Two layers of friction stacked on top of each other.
Layer 1 - the migration. For years HelloBible’s support ran on Gmail. Tickets, replies, attachments, all in a personal inbox. As we scaled past 100k downloads it stopped working. We migrated to Zendesk to centralise the team, get proper macros, build a queue we could actually triage. I led that migration on the ops side.
Layer 2 - the gap inside Zendesk. Once migrated, I was running support solo. 100% of tickets crossed my desk. The numbers:
- ~18,000 free users on the app
- 1,860 active RevenueCat subscribers
- 2,687 tickets in the cumulative backlog
- 75% via email, 25% via in-app widget or web form
Zendesk’s native RevenueCat integration exists. It shows total_spent on the user profile, after I’ve clicked into the ticket. There’s no native way to filter the queue by “paid users first” or to spot a premium ticket in the list before opening it. A premium subscriber whose audio playback breaks waits the same amount of time as a free user asking how to unsubscribe.
I needed every premium ticket flagged with a visible marker, in the queue, automatically.
Why the RevenueCat API approach failed
First instinct: call the RevenueCat API per ticket with the requester’s email, get subscription status back, tag accordingly.
The blocker: RevenueCat API v2 doesn’t filter customers by email. To check whether a single email is premium, I’d have to paginate ~18,000 customer records. Per ticket. Not viable.
I tested the direct API path, proved it would not work per ticket (the HTTP Request returned 20 random customers with no email filter, and one ticket from a real $29 paying customer came back unmatched), and switched to a sheet-backed lookup loaded once per run.
The Google Sheet approach:
- Export the full customer list manually from RevenueCat once a month (with
total_spentper email) - n8n downloads the CSV on the first ticket of each run, builds an in-memory map of email to total_spent, caches it for 24 hours via
getWorkflowStaticData - Every subsequent lookup hits the in-memory map, not RevenueCat
Manual refresh ~once a month is the trade-off I made consciously. If someone subscribes today and writes in tomorrow, they might not be tagged on the spot. The cost of missing one ticket is much smaller than the cost of building an auto-refresh pipeline that becomes another thing to monitor.
What this changes operationally
The workflow runs in the background. What changes is the support team’s daily workflow.
Before: I open Zendesk. I see 1.3K tickets. I scan them top-to-bottom. I might miss a paying customer’s urgent ticket because nothing tells me they pay until I open it.
After: I open Zendesk. The left sidebar has a dedicated view called ”🌟 Clients Premium” with 323 tickets. Premium tickets move into the first queue I check instead of being mixed into the general backlog.
The downstream effects:
- Premium tickets get served first as a structural rule. Paying customers get faster responses than non-paying ones. It’s now enforced by the queue itself rather than by my memory.
- Expected retention benefit. When a premium subscriber writes in and their complaint gets resolved fast, that should reduce the “I cancelled because nobody responded” class of churn. Effect to be measured over time.
- A focused listening channel for product. Premium tickets become a high-signal cohort to listen to: these are the people who put money down. The dedicated view doubles as a focused input source for the product team.
The workflow turned paying users from hidden profile data into a queue I could act on before opening any ticket.
The scope choices
Scope choices I made and defended:
- Manual sheet refresh, not auto-sync (less fragile, lower maintenance overhead)
- Tag premium only, not free users (I don’t need to know who’s free, I need to know who pays)
- Acceptable delay of 12h on new premium tickets (workflow runs twice daily, no webhook needed)
- Tag never gets removed even if a user cancels (a paying customer with an open ticket stays priority)
The technical architecture
Stack
- n8n v2.17.7 self-hosted on my Mac at
~/Documents/Dev/n8n-local/n8n_data - Zendesk API with cursor-based pagination
- Google Sheets as the intermediate lookup layer (manual CSV export from RevenueCat)
- RevenueCat (project_id
proj01861a64) used for the export, not the runtime lookup - JavaScript in n8n Code nodes via
this.helpers.httpRequestfor authenticated calls
Two workflows
Workflow 1 - Full backlog scan (manual trigger)
Manual Trigger
→ Get seed ticket (limit=1, to extract subdomain)
→ Fetch All Tickets (HTTP Request, cursor pagination)
→ Split Out on "tickets"
→ Loop Over Items
→ Get user (resolve requester email)
→ Code: lookup against in-memory email map
→ If matched && total_spent > 0:
→ Edit Fields → Update ticket (add tag + internal note)
→ Loop back
Workflow 2 - Delta scan (cron, 2x daily at 08:00 and 20:00)
Same structure, but the initial fetch uses /api/v2/search.json?query=type:ticket+created>{date-24h} with Split Out on results. Max delay before a new premium ticket gets tagged: 12 hours.
The applied state per ticket
- Tag:
priorite_payant - Internal note:
✅ Client détecté : {total_spent}$ dépensés. - A Zendesk view filtered by this tag gives me the triage queue I needed.
Why a Google Sheet, not RevenueCat direct
RevenueCat API v2 doesn’t filter customers by email. The list endpoint paginates ~18,000 records with no email filter parameter. Per-ticket lookups would mean paginating 18k records per ticket. The intermediate sheet solves this by being a static snapshot n8n loads once per run, in memory.
Caching
The CSV gets downloaded once on the first ticket of each run, stored via $getWorkflowStaticData('global') with a 24h TTL. Every subsequent ticket in the same run hits the in-memory map. Zero re-downloads.
Safety and idempotence
continueOnFail: trueon Update ticket (closed/archived tickets don’t break the run)retryOnFail: true, waitBetweenTries: 5000on Update ticketmaxRequests: 50on the HTTP Request pagination- 24h cache on the CSV minimises Google Sheets calls
- Tag is never removed (a paying customer with an open ticket stays priority even if they cancel)
The workflow running
What broke
The bugs worth telling about, the ones that shaped the architecture:
-
n8n Code node sandbox restrictions. Four consecutive errors before finding the right HTTP method:
fetch is not defined,$http is not defined,Module 'https' is disallowed,$helpers is not defined. Solution:this.helpers.httpRequest(), not in the docs as the recommended method. The sandbox is more restricted than what most n8n tutorials show. -
Zendesk node
returnAll=truebroken. The native n8n Zendesk node failed systematically with “Your request is invalid” when asked for the full ticket list. Worked around it: seed ticket to extract the subdomain, then HTTP Request with manual cursor pagination. -
The
matched: !!matchbug. First version flagged any email present in the sheet as premium. The sheet contained all users (60-70k rows), not just paying ones. Fix: checktotal_spent > 0explicitly.matched: !!(match && match.total_spent > 0). Caught it by reasoning about what’s actually in the sheet, not by testing. -
Premature
Update ticketfailures on closed/archived tickets. Run failed silently. AddedcontinueOnFail: trueandretryOnFailto make it resumable end-to-end. -
Initial run with
returnAll=truecrashed immediately. This is what forced the seed-ticket + HTTP Request architecture in the first place. -
n8n folder confusion after a Mac restart. n8n launched on
~/.n8ninstead of~/Documents/Dev/n8n-local/n8n_data, showing an empty account. Fix: explicit env vars at launch,N8N_USER_FOLDER=... N8N_SECURE_COOKIE=false n8n.
Where this lives now
Workflow active in production. Full backlog tagged. Delta workflow runs every 12 hours.
Quality check: I manually verified 15 tagged accounts (all 15 had actually spent money - zero false positives) and spot-checked known paying users in the queue for missed tags - none found in that spot-check.
The discrepancy I observed: 383 tickets tagged by the workflow, 323 visible in the Zendesk view right now (~84%). Cause: Zendesk drops closed tickets from standard views after ~28 days, plus a small number of silent Update failures absorbed by continueOnFail. The trade-off was acceptable given the support volume and queue usage.
Maintenance: refresh the Google Sheet from RevenueCat about once a month. n8n needs to stay running on my Mac, which is the architecture’s main weakness. The day support volume justifies moving to a VPS or n8n.cloud, that risk goes away. For now, the trade-off is fine: $0 in API/SaaS costs, runs without my attention, premium subscribers get served first in the queue.
Want to talk about something like this?
Email me, send a LinkedIn message, or download the CV. Conversations are what this site is built for.