Run 018 journal. cold-call-workflow

Run: 018 · Date: 2026-05-06 · Charlie's Domain Guide: Salesfinity/Fireflies integration, call classification, meeting triage

S1. Finding: Charlie's Complete Domain Guide

1. How Calls Flow Into the System

Two independent pipelines feed Charlie's /meetings page. Both land in Supabase; neither requires Charlie to do anything for ingestion.

Pipeline A: Salesfinity (cold calls)

A rep dials through Salesfinity. The moment the call ends, Salesfinity fires a webhook to src/app/api/webhooks/salesfinity/route.ts. The webhook verifies an HMAC SHA256 signature (header x-salesfinity-signature, secret in SALESFINITY_WEBHOOK_SECRET). The payload is parsed into a SalesfinityCallRow via buildCallLogRow() in src/lib/call-pipeline.ts. Entity is resolved from the rep's email domain (e.g. @chapter.guide maps to next_chapter, @revsup.com maps to revsup). DNC suppression runs: normalized phone checked against do_not_call.phone, company checked against do_not_call where block_company=true. Suppressed calls return 200 but are not stored. The row upserts into call_log (keyed on salesfinity_id). Contact metadata (name, company, title, email, phone, LinkedIn, recording URL, list name) goes into extra_fields JSONB. Every invocation (pass or fail) logs to webhook_invocations for operator debugging. Backup path: scripts/sync-calls.ts runs nightly via GitHub Actions, polling the Salesfinity API for the last 7 days and upserting the same way. This catches anything the webhook missed.

Pipeline B: Fireflies (recorded meetings)

Fireflies records and transcribes meetings automatically (Zoom, phone, Google Meet). scripts/sync-meetings.ts runs nightly via GitHub Actions. It calls recentTranscripts() from src/lib/fireflies.ts, which hits the Fireflies GraphQL API for transcripts since a given date. Each transcript includes: title, date, duration, host/organizer emails, participants, summary (overview, keywords, action items), and a transcript URL. Records upsert into the local content/meetings/index.json file, then separately into Supabase meeting_notes for the web app. On the detail page, the app fetches the full transcript via /api/meetings/transcript and audio via /api/meetings/fireflies-media (CloudFront signed URLs, roughly 7 day TTL).

What Charlie sees: The /meetings page queries both meeting_notes and call_log from Supabase, merges them into a single sorted list, and renders them with source badges (microphone icon for Fireflies meetings, phone icon for Salesfinity calls).

2. Classification Taxonomy

Found in src/components/meetings/index-rows.tsx lines 204 through 216, the CLASSIFICATION_OPTIONS array. Also reflected in the filter dropdown on the index page (src/app/meetings/page.tsx lines 496 through 506).

ClassificationWhat It MeansWhen to Use dealA real opportunity. Someone expressed interest in selling, buying, or exploring a transaction.The contact said something like "send me info," "interested," "open to selling," "schedule a meeting," or disclosed revenue/EBITDA figures. deal:<entity_id>Deal assigned to a specific entity (HR.com, Design Precast, or Capstone). Available in the "Assign to deal" optgroup.When you know which deal this call belongs to. voicemailYou reached a voicemail box. The system auto detects these (see below).The recording is a voicemail message, not a live conversation. no_answerPhone rang, nobody picked up.The disposition says "no answer," "left voicemail," "busy," or "no pickup" and no voicemail was detected. not_interestedContact explicitly declined.They said "not interested," "do not call," "already sold," "wrong number," or hung up. gatekeeperYou reached a receptionist or assistant, not the decision maker.The person who answered was not the target contact. They may have taken a message or transferred you. internal_trainingA team training call (not client facing).Fireflies recorded an internal session, roleplay, or team standup. revsupA RevsUp call that was mistakenly captured under the Next Chapter pipeline.The rep was working RevsUp business, not Next Chapter. privateHidden from the default view.Personal or sensitive calls that should not appear in the normal feed. otherDoes not fit any category above.Catch all. Use sparingly; most calls fit one of the categories above.

Auto classification logic (from autoClassifyFromDisposition() in derive.ts): If the Salesfinity disposition matches "no answer," "left voicemail," "busy," or "no pickup," the system checks for voicemail signals (callback request, phone number in text, greeting pattern, short duration). If 2 or more voicemail signals are present, it classifies as voicemail; otherwise no_answer. Negative dispositions ("not interested," "do not call," "bad number," "wrong contact," "already sold," "hang up," "deceased," "duplicate") auto classify as not_interested. If the disposition says "answered" or "connected," auto classification returns null, meaning Charlie must classify manually.

3. Charlie's Daily Workflow

Step 1: Open /meetings. The page loads both Fireflies meetings and Salesfinity calls. At the top: KPI cards showing counts for Awaiting Review, Salesfinity Calls, Fireflies Meetings, Opportunities, Processing, and total Library.

Step 2: Scan the KPI strip. The "AWAITING REVIEW" card shows how many meetings need classification or entity linking. The "OPPORTUNITIES" card shows how many calls have revenue signals detected. Click the green $ button or the "Revenue Opportunities" filter to show only calls where the classifier found buy signals.

Step 3: Use filters. The search bar, source dropdown (All / Fireflies only / Salesfinity only), classification dropdown, and the $ opportunity toggle narrow the list. Look for the $$ icon (revenue figure mentioned) and the lightning bolt icon (motivated seller detected) on each row.

Step 4: Click into a meeting or call. For Salesfinity calls, the detail page shows: contact info (left rail), call intelligence with structured notes (center, parsed from AI notes: outcome, key intel, objections, next steps, competitor mentions), transcript with signal highlighting (green for revenue/buy signals, red for rejections/objections, blue for callbacks), opportunity score with reasons, and a classification dropdown.

For Fireflies meetings, the detail page shows: recording player, attendee list, listener tone read (a single sentence summarizing the call), deal signals hero (revenue mentioned, EBITDA, prior broker history, sell urgency, next steps, seller commitment, objections), buying temperature (0 to 100 composite score), decisions/actions extracted from notes, and the full transcript with color coded marks (revenue, ebitda, urgency, broker, objection, next step, commitment).

Step 5: Classify. Use the dropdown (visible on both the list and the detail page). For Salesfinity calls, the classification saves to call_log.extra_fields.user_classification. For Fireflies meetings, it saves to meeting_notes.classification. Selecting "deal" triggers a best effort sync to the deals table via /api/meetings/sync-to-deal.

Step 6: If "deal," evaluate for promotion. Check the opportunity score (shown as OPP badge). Read the reasons bar. Verify revenue signals and motivated seller indicators. If everything checks out, the "Promote to Deal" button (on the Fireflies detail page) or escalation to Ewing is the next step.

4. What Makes a Good Classification

Classifying as "deal" requires at least one of these signals (from BUY_SIGNALS in derive.ts, lines 251 through 273):

Contact said "ready to sell," "wants to sell," "open to selling," "willing to sell" Contact asked to be sent information, an email, or details Contact expressed interest: "sounds good," "tell me more," "interested" Contact agreed to schedule a meeting or put something on the calendar M&A signals: "looking for a buyer," "no successor," "succession plan," "off market," "confidential" Financial disclosure: mentioned EBITDA, earnings, cash flow with a number Transaction language: "letter of intent," "LOI," "term sheet," "due diligence"

Classifying as "not_interested" when you see rejection signals (from REJECTION_SIGNALS, lines 275 through 283):

"Hung up," "take me off," "stop calling," "do not call" "Not interested," "no thanks" "Already sold," "already have a broker/advisor/buyer" "Not selling," "not for sale," "don't want to sell" "Wrong number," "wrong person"

"callback" vs "deal": If the contact said "call me back" or "give me a call at [number]" but did not express interest in selling or exploring a transaction, that is a callback, not a deal. The voicemail detector (detectVoicemail()) looks for: callback request, phone number in text, greeting pattern ("this is," "my name is," "leaving a message"), and short duration (under 90 seconds). Two or more of those signals means voicemail.

"gatekeeper" vs "no_answer": If a human answered but it was not the decision maker (receptionist, assistant, wrong department), that is gatekeeper. If nobody answered at all, that is no_answer.

5. The Promote Decision

Promotion is gated by isPromotable() in src/lib/handoff.ts (line 46). Charlie cannot press the button herself; meetings.promote-to-deal requires operator tier (Mark or Ewing). Charlie's job is to classify correctly and flag the call for Ewing.

Four required conditions (all must be true):

has_revenue_signal: The transcript or notes contain a dollar figure or revenue mention. opportunity_score >= 30: The composite classifier score is at least 30 out of 100. is_classified_as_deal: Charlie (or auto classification) set the classification to "deal." has_entity: The meeting is linked to an entity in the system.

Plus at least one optional signal:

has_motivated_seller: Urgency language detected ("ready to sell," "health issues," "tired of," "burned out," "retire") has_buyer_signal: Buy signals from the classifier (interest phrases, scheduling language, financial disclosure) has_meeting_set: A follow up meeting was scheduled has_info_requested: Contact asked for information to be sent

Auto vs review: promotionDecision() (line 63) returns "auto" if score >= 50 (high confidence, can promote without Ewing's explicit review), "review" if score is 30 to 49 (needs Ewing's eyes), or "skip" if not promotable.

Validation at execution time (validateHandoff(), line 78): The system also checks that the meeting exists in meeting_notes, has not already been promoted, the deal name is not empty, and the deal slug is not already taken. Warnings (not blockers) fire for: no entity linked, low opportunity score, no revenue signal detected.

What happens on promote: A new row inserts into the deals table with stage "worth_chasing," the meeting_notes row gets promoted_to_deal_id set, and a Slack notification fires. Bear then sees this deal on the Deals page.

6. Mistakes to Avoid

MistakeWhat HappensHow to Prevent Classifying a voicemail as "deal"The opportunity classifier gives it a score, Ewing wastes time reviewing a nonexistent lead.Check duration. If under 90 seconds and there is a callback request or greeting pattern, it is a voicemail. Classifying a gatekeeper call as "not_interested"The contact is lost. The gatekeeper said the owner is not available; that is not the same as the owner saying no.Read the transcript. Did the actual decision maker refuse? Or did a receptionist answer? Missing a motivated sellerRevenue signal lost. A contact who said "I'm tired of running this business" or "health issues" or "looking to retire" is extremely valuable. If you classify them as "other" or "no_answer," nobody follows up.Look for the lightning bolt icon on the list. Read the transcript for urgency language. Promoting a bad leadBear gets noise in his deal pipeline. He has to investigate, realize it is garbage, and close it. This wastes his time and pollutes the pipeline metrics.Promotion requires operator tier, so Charlie cannot promote directly. But if Charlie classifies a weak call as "deal," Ewing might promote it on her recommendation. Only flag calls where the contact expressed genuine interest and revenue is on the table. Leaving calls unclassifiedThe "Awaiting Review" count stays high. Ewing cannot see which calls matter. Revenue opportunities sit unnoticed.Classify every call the same day it comes in. Start with the $ opportunity filter to catch the highest value calls first. Ignoring the opportunity scoreA call with OPP 65 and three buy signals sits in the list while Charlie spends time on no_answer calls.Sort by opportunity. Green scores (50+) are high priority. Orange (30 to 49) need a closer look. Gray (below 30) are likely not deals.

S2. Blind spot

The autoClassifyFromDisposition() function returns null for "answered" or "connected" dispositions, which is correct (it defers to Charlie). But the Salesfinity disposition field is stored as JSON in some rows (with an external_name key) and plain text in others. The parseDispName() helper handles both, but if Salesfinity changes their payload shape, the auto classifier could misfire. No schema validation exists on the disposition field at ingestion time.

S3. Pattern

Pattern observed: the classification taxonomy lives in three places (the filter dropdown on the index page, the CLASSIFICATION_OPTIONS array in index-rows.tsx, and the auto classifier regex sets in derive.ts). These are not derived from a single source of truth. Adding a new classification (e.g. "callback" or "wrong_number") requires edits in all three locations. This is a consolidation candidate.

S6. One line takeaway

Charlie's job is triage: classify every call the same day, use the $ filter to catch revenue opportunities first, and flag deals for Ewing (she cannot promote directly because it requires operator tier).

Generated from 018__cold-call-workflow.md — do not edit this HTML directly.