T
Telephony SOPKnowledge Base
Search
← All topics

Sentiment Analysis Pipeline — Call to Outcome Classification

runnable

Process call recordings through transcribe → analyze → score → store pipeline. Transcription via AssemblyAI/Deepgram/SignalWire CI. Sentiment + outcome classification via Claude or GPT with structured JSON output. Extract: service_needed, urgency, intent, conversion_probability, sentiment per turn, key entities. Costs ~$0.01-0.05 per call. Saves structured data to lead pipeline for revenue attribution.

sentimentcall-analysisassemblyaiclaudelead-scoringpipeline
Agent trigger phrases: call sentiment analysis · call transcription pipeline · lead pipeline · call analysis Claude · AssemblyAI call analysis · outcome classification

Sentiment Analysis Pipeline

After a call ends, run the recording through a pipeline that produces structured analysis: sentiment, intent, urgency, conversion probability, extracted entities. This converts raw audio into queryable signal — the bridge between telephony and CRM/marketing systems.

The end-to-end pipeline

Call recorded (SignalWire) → Webhook fires recording.complete event
                                               ↓
                  Recording URL pulled, audio fetched
                                               ↓
                  Speech-to-text (AssemblyAI / Deepgram / SW CI)
                                               ↓
                  Transcript + speaker diarization + timestamps
                                               ↓
                  LLM analysis (Claude / GPT) with structured prompt
                                               ↓
                  Structured JSON output (sentiment, intent, outcome)
                                               ↓
                  Write to leads table → trigger CRM workflows → push to GA4 conversion

Total elapsed time: 30-90 seconds for a 5-minute call. Cost: ~$0.01-0.05 per call depending on length and tier.

Step 1: Transcription

Three primary options:

| Provider | Cost | Accuracy | Diarization | Notes | |---|---|---|---|---| | AssemblyAI | $0.00025/sec ($0.015/min) | Best for accent + jargon | Yes (built-in) | Audio Intelligence add-ons (sentiment, summary, topics) | | Deepgram | $0.0043/min (Nova-2) | Comparable | Yes | Fastest, real-time capable | | SignalWire Call Intelligence | Included in select plans | Good | Yes | No external API call, runs in-platform |

AssemblyAI example

import requests

def transcribe_with_assemblyai(audio_url):
    headers = {"authorization": ASSEMBLY_KEY}
    
    response = requests.post(
        "https://api.assemblyai.com/v2/transcript",
        headers=headers,
        json={
            "audio_url": audio_url,
            "speaker_labels": True,
            "sentiment_analysis": True,
            "entity_detection": True,
            "auto_highlights": True,
            "iab_categories": True,
        }
    )
    transcript_id = response.json()["id"]
    
    # Poll for completion
    while True:
        result = requests.get(
            f"https://api.assemblyai.com/v2/transcript/{transcript_id}",
            headers=headers
        ).json()
        if result["status"] == "completed":
            return result
        elif result["status"] == "error":
            raise Exception(result["error"])
        time.sleep(2)

AssemblyAI returns:

  • Full transcript with timestamps
  • Per-utterance speaker labels (Speaker A, B, C...)
  • Sentiment per sentence (positive, negative, neutral)
  • Entity detection (people, places, products, organizations)
  • IAB content categories (Auto, Health, Real Estate, etc.)
  • Auto-highlights (key phrases)

Deepgram example

from deepgram import DeepgramClient, PrerecordedOptions

dg = DeepgramClient(DEEPGRAM_KEY)

response = dg.listen.prerecorded.v("1").transcribe_url(
    {"url": audio_url},
    PrerecordedOptions(
        model="nova-2",
        smart_format=True,
        diarize=True,
        sentiment=True,
        intents=True,
        topics=True,
    )
)

Deepgram returns similar structured output with built-in sentiment and intent classification.

Step 2: LLM analysis with structured output

Even with AssemblyAI/Deepgram sentiment, the call-specific business logic (service requested, urgency, conversion intent) needs LLM analysis. Use Claude or GPT with a strict JSON-schema prompt.

import anthropic

def analyze_call(transcript):
    client = anthropic.Anthropic(api_key=ANTHROPIC_KEY)
    
    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=2000,
        system="""You analyze inbound phone calls for a service business and output structured JSON.
        
You MUST output ONLY valid JSON matching this schema:
{
  "service_needed": "string or null",
  "urgency_level": "low|medium|high|emergency",
  "call_outcome": "lead|customer|complaint|wrong_number|spam|inquiry_only",
  "conversion_probability": "0.0-1.0",
  "sentiment_overall": "positive|neutral|negative",
  "caller_intent_summary": "1-2 sentence summary",
  "key_topics": ["array", "of", "topics"],
  "entities_mentioned": {
    "addresses": [],
    "dates": [],
    "products": [],
    "competitor_mentions": []
  },
  "agent_quality_notes": "1-2 sentences",
  "recommended_followup": "string or null",
  "spam_likelihood": "0.0-1.0"
}""",
        messages=[{
            "role": "user",
            "content": f"Analyze this call transcript:\n\n{transcript}"
        }]
    )
    
    return json.loads(response.content[0].text)

The strict JSON schema in the system prompt makes Claude's output reliably parsable. For Claude specifically, use the tools mechanism for even higher reliability:

response = client.messages.create(
    model="claude-opus-4-5",
    max_tokens=2000,
    tools=[{
        "name": "save_call_analysis",
        "description": "Save the structured call analysis",
        "input_schema": {
            "type": "object",
            "properties": {
                "service_needed": {"type": "string"},
                "urgency_level": {"type": "string", "enum": ["low", "medium", "high", "emergency"]},
                "call_outcome": {"type": "string", "enum": ["lead", "customer", "complaint", "wrong_number", "spam", "inquiry_only"]},
                "conversion_probability": {"type": "number", "minimum": 0, "maximum": 1},
                # ... rest of schema
            },
            "required": ["service_needed", "urgency_level", "call_outcome", "conversion_probability"]
        }
    }],
    tool_choice={"type": "tool", "name": "save_call_analysis"},
    messages=[{"role": "user", "content": f"Analyze: {transcript}"}]
)

analysis = response.content[0].input

The tool-use pattern is the most reliable way to get structured JSON from Claude — schema validation is built in.

Step 3: Persistence

CREATE TABLE call_analysis (
    id UUID PRIMARY KEY,
    call_sid TEXT REFERENCES calls(call_sid),
    transcript TEXT,
    analysis JSONB,
    sentiment_overall TEXT,
    call_outcome TEXT,
    conversion_probability NUMERIC,
    service_needed TEXT,
    urgency_level TEXT,
    transcript_cost_cents INT,
    llm_cost_cents INT,
    analyzed_at TIMESTAMPTZ DEFAULT now()
);

CREATE INDEX idx_outcome ON call_analysis(call_outcome);
CREATE INDEX idx_service ON call_analysis(service_needed);
CREATE INDEX idx_analyzed_at ON call_analysis(analyzed_at DESC);

Index on the most commonly queried fields. The full JSON sits in analysis for ad-hoc queries.

Step 4: Trigger downstream actions

Based on the analysis, fire workflows:

def trigger_actions(call_id, analysis):
    if analysis["call_outcome"] == "lead" and analysis["conversion_probability"] > 0.5:
        push_to_crm(call_id, analysis, priority="high")
    
    if analysis["urgency_level"] == "emergency":
        sms_alert_owner(f"Emergency call: {analysis['caller_intent_summary']}")
    
    if analysis["call_outcome"] == "complaint":
        create_helpdesk_ticket(call_id, analysis)
    
    if analysis["spam_likelihood"] > 0.8:
        add_to_blocklist(call_caller)
    
    push_to_ga4_conversion_if_appropriate(call_id, analysis)

SignalWire Call Intelligence integration

SignalWire's Call Intelligence is the platform-native version of this pipeline. The SWML record verb with post_prompt runs transcription + an LLM analysis in-platform:

- ai:
    prompt:
      text: "You are a customer service agent. Help the caller."
    post_prompt:
      text: |
        Analyze this conversation and output JSON:
        {
          "service_needed": string,
          "urgency_level": "low|medium|high|emergency",
          "call_outcome": "lead|customer|complaint",
          "conversion_probability": 0.0-1.0,
          "summary": string
        }
    post_prompt_url: https://your.api/call-analysis-webhook

When the call ends, SignalWire transcribes, runs the post_prompt as a final LLM turn, and POSTs the JSON to your webhook. Single pipeline call, no external transcription provider.

When to use Call Intelligence vs DIY pipeline:

| Factor | Call Intelligence | DIY pipeline | |---|---|---| | Setup complexity | Low | High | | Cost per call | Bundled in SW pricing | $0.01-0.05 per call | | Customization | Limited to prompt | Full pipeline control | | Diarization quality | Good | AssemblyAI/Deepgram are tops | | Latency to result | At call end | At call end + transcription time | | Best for | Standard cases, faster setup | High-volume, custom analysis |

Cost projections

For 1000 calls/day, average 4 minutes each:

| Component | Cost per call | Daily cost | |---|---|---| | AssemblyAI transcription | $0.06 | $60 | | Claude analysis (Opus) | $0.04 | $40 | | Total | $0.10 | $100 |

For lower cost:

  • AssemblyAI Nano model: $0.0125/min ($0.05/call) → ~$50/day
  • Claude Sonnet instead of Opus: $0.01/call → ~$10/day
  • Combined: $60/day for 1000 calls

For volume above 5K calls/day, consider Deepgram Nova-2 (cheaper) + Claude Haiku for cost optimization.

Real-time vs post-call analysis

This topic focuses on post-call analysis. For real-time (during the call):

  • AssemblyAI Streaming for live transcription
  • Deepgram real-time for sub-200ms transcription
  • Live LLM analysis on partial transcripts (for agent assist — see agent assist)

Post-call analysis is cheaper and more accurate. Use real-time only when there's a clear action requirement during the live conversation.

Error handling

| Failure | Cause | Recovery | |---|---|---| | Recording URL 404 | Recording deleted (retention exceeded) | Fall back to "no transcript available" | | Transcription returns empty | Silent call, very short | Skip analysis, mark as inquiry_only | | LLM returns invalid JSON | Edge case in prompt | Retry once with strict schema, fall back to manual review | | Webhook delivery fails | Network issue | Retry with exponential backoff (15s, 1m, 5m, 30m, give up) | | Caller speaks unknown language | Transcript is garbled | Detect via language ID, route to human review |

Common pitfalls

  • Trusting LLM categorization without spot-checking — sample 1% manually for the first 1000 calls to verify the prompt is producing useful output.
  • Cost runaway from Opus on long calls — set a max-token budget. Calls > 30 minutes should chunk-then-summarize.
  • No retention policy on transcripts — sensitive content (PII, payment info) accumulating indefinitely. Set 90-day retention by default.
  • PHI in healthcare calls — see call recording compliance. Use BAA-eligible vendors only (AssemblyAI BAA available enterprise tier).
  • Webhook timeout — analysis pipeline can take 60-90 seconds. Webhook receiver must respond fast and process async.

Related patterns

References

  • AssemblyAI API documentation — transcript and audio intelligence
  • Deepgram API documentation — Nova-2 model
  • Anthropic Claude API — tool use for structured output
  • SignalWire SWML post_prompt — platform-native analysis