back to all work

Prism: Meeting Intelligence Platform

A Gong-class revenue intelligence system built on Zoom, n8n, and pgvector. Every sales call is scored against CRM data, and an AI analyst answers pipeline questions with cited sources.

Role
Architect & sole engineer
Timeline
2025 to present
Status
In production
Context
Built as the internal platform of FullFunnel, a Boston-based GTM agency; extended to a PE-backed enterprise deployment
Next.jsSupabasepgvectorn8nClaudeGemini embeddingsHubSpotSlack

// stage.problem

Call recordings pile up; nobody knows what the pipeline is actually saying

Sales calls carry more signal than any other data a revenue team owns, and most teams never use them. Recordings sat in Zoom. CRM stages said one thing, conversations said another, and forecasting ran on the CRM version. Gong and its class solve this at enterprise prices that a services business and most mid-market teams will not pay.

The goal: capture every call automatically, score it against what the CRM claims about the deal, and make the whole corpus queryable at infrastructure cost.

// stage.system

Zoom to scored, searchable, cited intelligence

System architecture · call to cited answer
Zoomrecording + VTTspeaker-attributedn8n orchestratorcentral OAuth8-hour sweephistorical + newSupabasePostgres +pgvector~8 chunks/meeting6 scoring chainsstage-specificvs CRM truthclassify by call type"Ask" analystRAG + citations3 context layersHubSpotsyncDashboardSlackdigestsHard boundary: n8n writes pipeline tables, the app reads them, never the reverse.Failure modes chosen per subsystem: pipeline fails loud in n8n, analytics fail open.Gemini embeddings · Claude answers · 50 queries/day cost guardrail

Capture is centralized: an n8n workflow holds the Zoom OAuth and sweeps recordings, historical and new, on an 8-hour cycle. Nothing depends on reps remembering a bot invite. Each call is classified into one of six stage types (discovery and scoping, follow-up, onboarding, client meetings, internal client meetings, internal) and routed to a stage-specific scoring chain, because the questions that matter in a discovery call are noise in an onboarding call. Scores land next to CRM data: account health, BANT/MEDDIC gaps, and the delta between what the deal stage claims and what the conversation shows.

There is no separate ASR step. Zoom cloud recording produces the transcript itself, and the same n8n workflow pulls that VTT alongside the recording. Speaker attribution therefore comes from Zoom's own participant mapping rather than from an acoustic diarization model, which is what makes per-rep scoring and coaching possible without training or tuning anything. The trade is real: transcript quality is Zoom's to control, not mine, and any conversation that does not happen on Zoom is invisible to the system.

Transcripts are chunked and embedded into pgvector inside Supabase; the RAG layer gets its own section below. The n8n pipeline and the dashboard never write to each other’s tables. That boundary kept a two-runtime system debuggable.

Digest workflows push weekly summaries and deal alerts into Slack, and scores sync to HubSpot so the intelligence shows up where reps already work.

// stage.product.view

What the team sees every Monday

meeting-intelligence / scorecardALLSALESCSINTERNALTOOLS · SYSTEM HEALTH · NOTIFICATIONS · ADMINAll Analysis Scorecard810 MEETINGS · 15 REPS · 4 SURFACES · PIPELINE REFRESHES EVERY 8HMEETINGS SCORED810+64 this weekAVG SCORE6.5-0.1 vs last wkAVG HEALTH6.6-0.2 vs last wkAT-RISK ACCOUNTS36health below 5WEEKLY BRIEFING · GENERATEDScore slipped on higher volume. Check-ins dominate the mix and six accounts crossed into at-risk.AT-RISK ACCOUNTS4.84.44.03.63.2+31 MORETEAMNAMES REMOVEDREPMTGSAVG SCORETREND · 8 WKHEALTHWORST · BEST1567.17.4926.97.0746.86.5516.56.2386.25.8125.85.49 MORE REPS BELOW · VOLUMES RANGE 1 TO 156 MEETINGSASK“Which accounts need attention this week?”ANSWERS CITE MEETINGSmeeting-intelligence / meeting detailDISCOVERYACCELERATING48 MINSUMMARY / COACHING8.0MEETING OUTCOME7.0REP PERFORMANCE7.0ICP FITEACH DIAL SHIPS WITH A WRITTEN ANALYSISHEALTH PULSE6.6PREV 6.8−0.2FRAMEWORK COVERAGEEXTRACTED FROM CONVERSATIONBANT 75%BudgetAuthorityNeedTimingMEDDIC 58%MetricsEcon BuyerCriteriaProcessPainChampionKNOWNPARTIALMISSINGAuthorityMISSINGno decision-maker named in any callNeedKNOWNtooling spend confirmed · 2 citationsCHURN RISKS3 auto-detectedEXPANSION SIGNALS5 auto-detectedOPEN ACTIONS4STAKEHOLDERS7COMPETITORS2
TWO SCREENS RECREATED FROM THE RUNNING SYSTEM · COMPANY, REP AND TRANSCRIPT DETAIL REMOVED

// stage.deep.dive

Inside the RAG layer

The n8n pipeline splits each transcript into chunks of roughly 500 to 800 tokens, which works out to roughly 10 chunks per meeting across the current corpus of 8,412 chunks. The figures in the rest of this section come from an earlier snapshot of 611 chunks across 76 meetings, when the rate sat nearer 8. Every chunk carries its meeting ID in metadata. That one field is what makes citations possible later.

Embeddings come from Gemini (gemini-embedding-001, 3,072 dimensions), stored as halfvec in Postgres to halve the storage per vector. The model choice was about consistency: the n8n ingestion pipeline and the dashboard’s query-time embedding must match exactly, or retrieval degrades in ways no error log will report. Retrieval runs cosine similarity over an IVFFlat index, keeps only chunks above a 0.3 similarity floor, and sizes top-k to the question: 8 chunks for a standard query, 12 for long ones, 15 when the question spans reps or the whole pipeline.

The Ask analyst assembles context in three layers with a fixed token budget. Meeting scores get 45%: one line per scored meeting with rep, topic, stage, score, and health. Coaching intelligence gets 35%: strengths, blind spots, deal sentiment, and next actions, pulled only for the meetings retrieval matched. Raw transcript excerpts get the final 20%. Scores frame the answer, coaching explains it, transcripts ground it.

Citations are a SQL join. Because chunk metadata carries the meeting ID, linking an answer back to the exact meeting is a join from the chunks table to the scored-meetings tables inside one Postgres database. No cross-system lookup, no reconciling IDs between a vector store and an application database.

Cost is governed in the product: 50 queries per user per day, plus a burst cap of 10 queries per 5 minutes. LLM cost per answer is real money at team scale, and a limit in the product beats a surprise on the invoice. The rate-limit check itself fails open, so an analytics outage never blocks a legitimate question.

// stage.key.decisions

Why it’s built this way

// decision.01

n8n owns capture, the app owns everything after

Chose
A hard table boundary between pipeline and dashboard
Over
One Next.js codebase doing capture, scoring, and UI

OAuth token refresh, retry sweeps, and rate-limited Zoom API paging are workflow problems, and n8n does them with visual debuggability. The dashboard reads what the pipeline writes and never writes pipeline tables. When something breaks, the boundary tells you which half to open.

// decision.02

Six scoring chains instead of one mega-prompt

Chose
Stage-specific LLM scoring chains
Over
A single universal call-scoring prompt

Discovery quality and onboarding quality are different constructs; one universal prompt flattens them into a single vague grade. Separate chains keep scores comparable within a stage and coachable: a low discovery score maps to specific missing behaviors.

// decision.03

pgvector over a dedicated vector DB

Chose
Embeddings live in Supabase Postgres
Over
Pinecone or a separate vector service

Retrieval always joins transcripts against meetings, deals, and scores. One database means a citation is a single SQL join. It also means one bill, one backup story, one access-control model.

// decision.04

A view for the list, rows for the detail

Chose
A meetings_list view that projects only the columns the index shows
Over
Selecting from scored_meetings everywhere

Scored meeting rows run ~23K characters with full JSON payloads. The list view projects only what the index page needs; detail pages pay the full cost only when opened. Dashboard went from sluggish to instant without caching infrastructure.

// stage.production.notes

What broke, and what it taught the system

LLMs return prose when you demand JSON

A batch of meetings failed scoring with zero scores because one model returned explanatory text instead of JSON. Fix: hardened prompts, a JSON-repair fallback that extracts the payload from prose, and raised max_tokens so answers stop truncating mid-object. The failed batch re-scored cleanly.

Fail-open for analytics, fail-loud for pipeline

Usage analytics write on a fire-and-forget pattern: an analytics outage should never block a scoring run. The scoring pipeline itself does the opposite. Any failed step is visible in n8n with the exact call that broke. Choosing a failure mode per subsystem beat one global policy.

// stage.outcomes

What the numbers say

810meetings scored across 15 repsmeasured in production
8,412transcript chunks indexed for cited retrievalmeasured in production
~15Kcalls analyzed in the enterprise deploymentclient-reported
6stage-specific scoring chainscounted in the codebase
ENTERPRISE PIPELINE ANALYZED57% FLAGGED AT RISKsurfaced by scoring vs CRM stage

Internally, the platform runs across the agency’s sales team: every call scored within hours, weekly Slack digests, and an analyst that answers "what did we tell them about pricing?" with a citation instead of a guess.

The enterprise deployment proved the architecture scales past its origin: ~15,000 calls across a nine-figure pipeline for a PE-backed client. 57% of that pipeline was flagged at-risk, meaning the conversation record contradicted the CRM stage or deal health, along with the specific rep behaviors that correlate with deals advancing. The system the client evaluated against was quoted at ~$200K a year; this one they own.

// stage.retro

The honest retro

  • I shipped scoring before I built an eval for it. Six stage-specific chains produce numbers reps act on, and I validated them by reading transcripts alongside their scores rather than against a labelled set. That is fine for catching obvious breakage and useless for catching drift. The first thing I would build now is a hand-scored sample per stage type, so classifier routing and score movement are measurable instead of felt.
  • Chunking strategy deserved more experimentation up front. ~8 chunks per meeting works, but score-aware chunk boundaries (splitting on topic shifts rather than length) would improve citation precision.
  • I built the dashboard before the Slack digests; usage data says digests drive most weekly engagement. Distribution-first would have been the better order.
Neeraj, builder of the machine.
VP of Operations · GTM Agencysource: client feedback

// stage.common.questions

Common questions

What does Gong cost compared to building your own meeting intelligence?

The equivalent enterprise conversation-intelligence contract was quoted at roughly $200K per year; this platform was built instead. The build runs on Supabase, n8n, and per-call LLM costs: infrastructure pricing instead of per-seat licensing. The trade is engineering time; this system took one engineer to architect and ship.

How does the RAG "Ask" analyst avoid hallucinated answers?

Every answer is grounded in retrieved transcript chunks that must clear a 0.3 cosine-similarity floor, and each citation links back to the exact meeting through a SQL join on the chunk’s meeting ID. Context is assembled from real scores and coaching data, so the model quotes the corpus instead of inventing it.

What stack does the meeting intelligence platform run on?

A Next.js dashboard, Supabase Postgres with pgvector for embeddings, n8n for Zoom capture and scoring orchestration, Claude for scoring and answers, and Gemini for embeddings. Scores sync to HubSpot and digests go to Slack.