Most CRM systems are excellent at storing what happened — call logged, email sent, stage updated — and poor at capturing what was learned. A sales call produces qualitative intelligence that is genuinely valuable for deal strategy: what objections surfaced, how strongly the prospect signaled interest, what next steps were agreed to, and what risk flags the conversation revealed. That intelligence almost never makes it into the CRM because it requires someone to spend 15 minutes synthesizing unstructured notes into structured fields.
Large language models change this equation. Given a call transcript, Claude can extract structured deal intelligence in seconds — categorizing sentiment, identifying specific objections, recommending stage movement, and flagging risk signals — with accuracy that equals or exceeds what a well-trained sales analyst would produce manually.
The data model centers on two tables. The deals table stores core deal attributes as a JSONB column, which allows flexible schema evolution without migrations as the intelligence fields change over time. The deal_activities table records each interaction — calls, emails, meetings — with the raw content in TEXT and the extracted intelligence in a separate JSONB column. A GIN index on both JSONB columns enables fast attribute queries across the deal pipeline.
CREATE TABLE deals (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
company TEXT NOT NULL,
contact TEXT,
stage TEXT,
attributes JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE deal_activities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
deal_id UUID REFERENCES deals(id),
activity_type TEXT,
raw_content TEXT,
intelligence JSONB,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX ON deals USING GIN (attributes);
CREATE INDEX ON deal_activities USING GIN (intelligence);
The DealIntelligence class wraps the Anthropic API call and parses the structured JSON response. The prompt instructs the model to return a fixed schema — sentiment, objections, next steps, deal signals, risk flags, recommended stage, and a summary — which can be validated and stored directly without post-processing.
import anthropic, json, psycopg2
from dataclasses import dataclass
from typing import Dict, List
@dataclass
class DealRecord:
deal_id: str
company: str
contact: str
stage: str
class DealIntelligence:
def __init__(self, db_conn: str):
self.client = anthropic.Anthropic()
self.db = psycopg2.connect(db_conn)
def analyze_transcript(self, transcript: str, deal: DealRecord) -> Dict:
prompt = f"""Analyze this sales call transcript for deal intelligence.
Return valid JSON only, with these exact keys:
- sentiment: "positive" | "neutral" | "negative"
- objections: list of specific objections raised
- next_steps: list of agreed action items
- deal_signals: list of positive buying signals
- risk_flags: list of concerns or blockers identified
- recommended_stage: suggested CRM stage based on this conversation
- summary: 2-3 sentence executive summary
Company: {deal.company}
Contact: {deal.contact}
Current Stage: {deal.stage}
Transcript:
{transcript}"""
response = self.client.messages.create(
model="claude-opus-4-7", max_tokens=1500,
messages=[{"role": "user", "content": prompt}]
)
return json.loads(response.content[0].text)
def draft_followup_email(self, intelligence: Dict, deal: DealRecord) -> str:
objections_text = ", ".join(intelligence.get('objections', []))
next_steps_text = "\n- ".join(intelligence.get('next_steps', []))
prompt = f"""Draft a professional follow-up email for this sales conversation.
Contact: {deal.contact} at {deal.company}
Call Summary: {intelligence.get('summary', '')}
Objections Raised: {objections_text}
Next Steps Agreed:
- {next_steps_text}
Write a concise, personalized follow-up that confirms next steps and addresses
the main objections without being pushy. Professional but warm tone."""
response = self.client.messages.create(
model="claude-opus-4-7", max_tokens=600,
messages=[{"role": "user", "content": prompt}]
)
return response.content[0].text
def save_activity(self, deal_id: str, transcript: str, intelligence: Dict):
with self.db.cursor() as cur:
cur.execute(
"""INSERT INTO deal_activities
(deal_id, activity_type, raw_content, intelligence)
VALUES (%s, 'call', %s, %s)""",
(deal_id, transcript, psycopg2.extras.Json(intelligence))
)
# Update stage if recommended stage differs
recommended = intelligence.get('recommended_stage')
if recommended:
cur.execute(
"UPDATE deals SET stage = %s WHERE id = %s",
(recommended, deal_id)
)
self.db.commit()
The follow-up email draft uses the intelligence output as its input — pulling objections, next steps, and the summary to generate a personalized message that is specific to the actual conversation rather than a generic template. The draft goes into a review queue rather than sending automatically. A human confirms and sends. This keeps the LLM in an assistive role and preserves the personal judgment that high-stakes business development requires.
Storing intelligence as JSONB rather than normalized columns is deliberate. The schema of deal intelligence evolves as the LLM prompt evolves — new keys appear, existing ones are renamed, arrays grow. JSONB absorbs these changes without schema migrations. The GIN index makes these fields queryable: finding all deals where a pricing objection was flagged is a straightforward WHERE intelligence @> '{"objections": ["pricing"]}' query.
The complete workflow is: transcript arrives (from a recording integration, a manual paste, or an email thread) → analyze_transcript() extracts structured intelligence → save_activity() persists it and updates the deal stage → draft_followup_email() generates a draft for human review. The entire flow completes in under 10 seconds per call, turning what was a 15-minute manual task into a background process that surfaces better intelligence than most teams produce by hand.
We build AI-augmented CRM systems that capture and structure deal intelligence from every conversation — automatically.
Talk to Our Team