{"slug":"sentiment-analysis-pipeline","title":"Sentiment Analysis Pipeline — Call to Outcome Classification","tags":["sentiment","call-analysis","assemblyai","claude","lead-scoring","pipeline"],"agent_summary":"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.","trigger_phrases":["call sentiment analysis","call transcription pipeline","lead pipeline","call analysis Claude","AssemblyAI call analysis","outcome classification"],"runnable":true,"markdown":"\n# Sentiment Analysis Pipeline\n\nAfter 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.\n\n## The end-to-end pipeline\n\n```\nCall recorded (SignalWire) → Webhook fires recording.complete event\n                                               ↓\n                  Recording URL pulled, audio fetched\n                                               ↓\n                  Speech-to-text (AssemblyAI / Deepgram / SW CI)\n                                               ↓\n                  Transcript + speaker diarization + timestamps\n                                               ↓\n                  LLM analysis (Claude / GPT) with structured prompt\n                                               ↓\n                  Structured JSON output (sentiment, intent, outcome)\n                                               ↓\n                  Write to leads table → trigger CRM workflows → push to GA4 conversion\n```\n\nTotal elapsed time: 30-90 seconds for a 5-minute call. Cost: ~$0.01-0.05 per call depending on length and tier.\n\n## Step 1: Transcription\n\nThree primary options:\n\n| Provider | Cost | Accuracy | Diarization | Notes |\n|---|---|---|---|---|\n| AssemblyAI | $0.00025/sec ($0.015/min) | Best for accent + jargon | Yes (built-in) | Audio Intelligence add-ons (sentiment, summary, topics) |\n| Deepgram | $0.0043/min (Nova-2) | Comparable | Yes | Fastest, real-time capable |\n| SignalWire Call Intelligence | Included in select plans | Good | Yes | No external API call, runs in-platform |\n\n### AssemblyAI example\n\n```python\nimport requests\n\ndef transcribe_with_assemblyai(audio_url):\n    headers = {\"authorization\": ASSEMBLY_KEY}\n    \n    response = requests.post(\n        \"https://api.assemblyai.com/v2/transcript\",\n        headers=headers,\n        json={\n            \"audio_url\": audio_url,\n            \"speaker_labels\": True,\n            \"sentiment_analysis\": True,\n            \"entity_detection\": True,\n            \"auto_highlights\": True,\n            \"iab_categories\": True,\n        }\n    )\n    transcript_id = response.json()[\"id\"]\n    \n    # Poll for completion\n    while True:\n        result = requests.get(\n            f\"https://api.assemblyai.com/v2/transcript/{transcript_id}\",\n            headers=headers\n        ).json()\n        if result[\"status\"] == \"completed\":\n            return result\n        elif result[\"status\"] == \"error\":\n            raise Exception(result[\"error\"])\n        time.sleep(2)\n```\n\nAssemblyAI returns:\n- Full transcript with timestamps\n- Per-utterance speaker labels (Speaker A, B, C...)\n- Sentiment per sentence (positive, negative, neutral)\n- Entity detection (people, places, products, organizations)\n- IAB content categories (Auto, Health, Real Estate, etc.)\n- Auto-highlights (key phrases)\n\n### Deepgram example\n\n```python\nfrom deepgram import DeepgramClient, PrerecordedOptions\n\ndg = DeepgramClient(DEEPGRAM_KEY)\n\nresponse = dg.listen.prerecorded.v(\"1\").transcribe_url(\n    {\"url\": audio_url},\n    PrerecordedOptions(\n        model=\"nova-2\",\n        smart_format=True,\n        diarize=True,\n        sentiment=True,\n        intents=True,\n        topics=True,\n    )\n)\n```\n\nDeepgram returns similar structured output with built-in sentiment and intent classification.\n\n## Step 2: LLM analysis with structured output\n\nEven 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.\n\n```python\nimport anthropic\n\ndef analyze_call(transcript):\n    client = anthropic.Anthropic(api_key=ANTHROPIC_KEY)\n    \n    response = client.messages.create(\n        model=\"claude-opus-4-5\",\n        max_tokens=2000,\n        system=\"\"\"You analyze inbound phone calls for a service business and output structured JSON.\n        \nYou MUST output ONLY valid JSON matching this schema:\n{\n  \"service_needed\": \"string or null\",\n  \"urgency_level\": \"low|medium|high|emergency\",\n  \"call_outcome\": \"lead|customer|complaint|wrong_number|spam|inquiry_only\",\n  \"conversion_probability\": \"0.0-1.0\",\n  \"sentiment_overall\": \"positive|neutral|negative\",\n  \"caller_intent_summary\": \"1-2 sentence summary\",\n  \"key_topics\": [\"array\", \"of\", \"topics\"],\n  \"entities_mentioned\": {\n    \"addresses\": [],\n    \"dates\": [],\n    \"products\": [],\n    \"competitor_mentions\": []\n  },\n  \"agent_quality_notes\": \"1-2 sentences\",\n  \"recommended_followup\": \"string or null\",\n  \"spam_likelihood\": \"0.0-1.0\"\n}\"\"\",\n        messages=[{\n            \"role\": \"user\",\n            \"content\": f\"Analyze this call transcript:\\n\\n{transcript}\"\n        }]\n    )\n    \n    return json.loads(response.content[0].text)\n```\n\nThe strict JSON schema in the system prompt makes Claude's output reliably parsable. For Claude specifically, use the `tools` mechanism for even higher reliability:\n\n```python\nresponse = client.messages.create(\n    model=\"claude-opus-4-5\",\n    max_tokens=2000,\n    tools=[{\n        \"name\": \"save_call_analysis\",\n        \"description\": \"Save the structured call analysis\",\n        \"input_schema\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"service_needed\": {\"type\": \"string\"},\n                \"urgency_level\": {\"type\": \"string\", \"enum\": [\"low\", \"medium\", \"high\", \"emergency\"]},\n                \"call_outcome\": {\"type\": \"string\", \"enum\": [\"lead\", \"customer\", \"complaint\", \"wrong_number\", \"spam\", \"inquiry_only\"]},\n                \"conversion_probability\": {\"type\": \"number\", \"minimum\": 0, \"maximum\": 1},\n                # ... rest of schema\n            },\n            \"required\": [\"service_needed\", \"urgency_level\", \"call_outcome\", \"conversion_probability\"]\n        }\n    }],\n    tool_choice={\"type\": \"tool\", \"name\": \"save_call_analysis\"},\n    messages=[{\"role\": \"user\", \"content\": f\"Analyze: {transcript}\"}]\n)\n\nanalysis = response.content[0].input\n```\n\nThe tool-use pattern is the most reliable way to get structured JSON from Claude — schema validation is built in.\n\n## Step 3: Persistence\n\n```sql\nCREATE TABLE call_analysis (\n    id UUID PRIMARY KEY,\n    call_sid TEXT REFERENCES calls(call_sid),\n    transcript TEXT,\n    analysis JSONB,\n    sentiment_overall TEXT,\n    call_outcome TEXT,\n    conversion_probability NUMERIC,\n    service_needed TEXT,\n    urgency_level TEXT,\n    transcript_cost_cents INT,\n    llm_cost_cents INT,\n    analyzed_at TIMESTAMPTZ DEFAULT now()\n);\n\nCREATE INDEX idx_outcome ON call_analysis(call_outcome);\nCREATE INDEX idx_service ON call_analysis(service_needed);\nCREATE INDEX idx_analyzed_at ON call_analysis(analyzed_at DESC);\n```\n\nIndex on the most commonly queried fields. The full JSON sits in `analysis` for ad-hoc queries.\n\n## Step 4: Trigger downstream actions\n\nBased on the analysis, fire workflows:\n\n```python\ndef trigger_actions(call_id, analysis):\n    if analysis[\"call_outcome\"] == \"lead\" and analysis[\"conversion_probability\"] > 0.5:\n        push_to_crm(call_id, analysis, priority=\"high\")\n    \n    if analysis[\"urgency_level\"] == \"emergency\":\n        sms_alert_owner(f\"Emergency call: {analysis['caller_intent_summary']}\")\n    \n    if analysis[\"call_outcome\"] == \"complaint\":\n        create_helpdesk_ticket(call_id, analysis)\n    \n    if analysis[\"spam_likelihood\"] > 0.8:\n        add_to_blocklist(call_caller)\n    \n    push_to_ga4_conversion_if_appropriate(call_id, analysis)\n```\n\n## SignalWire Call Intelligence integration\n\nSignalWire'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:\n\n```yaml\n- ai:\n    prompt:\n      text: \"You are a customer service agent. Help the caller.\"\n    post_prompt:\n      text: |\n        Analyze this conversation and output JSON:\n        {\n          \"service_needed\": string,\n          \"urgency_level\": \"low|medium|high|emergency\",\n          \"call_outcome\": \"lead|customer|complaint\",\n          \"conversion_probability\": 0.0-1.0,\n          \"summary\": string\n        }\n    post_prompt_url: https://your.api/call-analysis-webhook\n```\n\nWhen 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.\n\n**When to use Call Intelligence vs DIY pipeline:**\n\n| Factor | Call Intelligence | DIY pipeline |\n|---|---|---|\n| Setup complexity | Low | High |\n| Cost per call | Bundled in SW pricing | $0.01-0.05 per call |\n| Customization | Limited to prompt | Full pipeline control |\n| Diarization quality | Good | AssemblyAI/Deepgram are tops |\n| Latency to result | At call end | At call end + transcription time |\n| Best for | Standard cases, faster setup | High-volume, custom analysis |\n\n## Cost projections\n\nFor 1000 calls/day, average 4 minutes each:\n\n| Component | Cost per call | Daily cost |\n|---|---|---|\n| AssemblyAI transcription | $0.06 | $60 |\n| Claude analysis (Opus) | $0.04 | $40 |\n| Total | $0.10 | $100 |\n\nFor lower cost:\n- AssemblyAI Nano model: $0.0125/min ($0.05/call) → ~$50/day\n- Claude Sonnet instead of Opus: $0.01/call → ~$10/day\n- Combined: $60/day for 1000 calls\n\nFor volume above 5K calls/day, consider Deepgram Nova-2 (cheaper) + Claude Haiku for cost optimization.\n\n## Real-time vs post-call analysis\n\nThis topic focuses on post-call analysis. For real-time (during the call):\n\n- AssemblyAI Streaming for live transcription\n- Deepgram real-time for sub-200ms transcription\n- Live LLM analysis on partial transcripts (for agent assist — see [agent assist](/topic/agent-assist))\n\nPost-call analysis is cheaper and more accurate. Use real-time only when there's a clear action requirement during the live conversation.\n\n## Error handling\n\n| Failure | Cause | Recovery |\n|---|---|---|\n| Recording URL 404 | Recording deleted (retention exceeded) | Fall back to \"no transcript available\" |\n| Transcription returns empty | Silent call, very short | Skip analysis, mark as `inquiry_only` |\n| LLM returns invalid JSON | Edge case in prompt | Retry once with strict schema, fall back to manual review |\n| Webhook delivery fails | Network issue | Retry with exponential backoff (15s, 1m, 5m, 30m, give up) |\n| Caller speaks unknown language | Transcript is garbled | Detect via language ID, route to human review |\n\n## Common pitfalls\n\n- **Trusting LLM categorization without spot-checking** — sample 1% manually for the first 1000 calls to verify the prompt is producing useful output.\n- **Cost runaway from Opus on long calls** — set a max-token budget. Calls > 30 minutes should chunk-then-summarize.\n- **No retention policy on transcripts** — sensitive content (PII, payment info) accumulating indefinitely. Set 90-day retention by default.\n- **PHI in healthcare calls** — see [call recording compliance](/topic/call-recording-compliance). Use BAA-eligible vendors only (AssemblyAI BAA available enterprise tier).\n- **Webhook timeout** — analysis pipeline can take 60-90 seconds. Webhook receiver must respond fast and process async.\n\n## Related patterns\n\n- [AssemblyAI transcription](/topic/assemblyai-transcription) — full AssemblyAI reference\n- [SignalWire Call Intelligence](/topic/signalwire-call-intelligence) — platform-native pipeline\n- [Call recording compliance](/topic/call-recording-compliance) — what you can store and for how long\n- [Call attribution GA4 GHL](/topic/call-attribution-ga4-ghl) — push analysis to revenue systems\n- [Agent assist](/topic/agent-assist) — real-time variant\n\n## References\n\n- AssemblyAI API documentation — transcript and audio intelligence\n- Deepgram API documentation — Nova-2 model\n- Anthropic Claude API — tool use for structured output\n- SignalWire SWML post_prompt — platform-native analysis\n","html":"<h1>Sentiment Analysis Pipeline</h1>\n<p>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.</p>\n<h2>The end-to-end pipeline</h2>\n<pre><code>Call recorded (SignalWire) → Webhook fires recording.complete event\n                                               ↓\n                  Recording URL pulled, audio fetched\n                                               ↓\n                  Speech-to-text (AssemblyAI / Deepgram / SW CI)\n                                               ↓\n                  Transcript + speaker diarization + timestamps\n                                               ↓\n                  LLM analysis (Claude / GPT) with structured prompt\n                                               ↓\n                  Structured JSON output (sentiment, intent, outcome)\n                                               ↓\n                  Write to leads table → trigger CRM workflows → push to GA4 conversion\n</code></pre>\n<p>Total elapsed time: 30-90 seconds for a 5-minute call. Cost: ~$0.01-0.05 per call depending on length and tier.</p>\n<h2>Step 1: Transcription</h2>\n<p>Three primary options:</p>\n<p>| Provider | Cost | Accuracy | Diarization | Notes |\n|---|---|---|---|---|\n| AssemblyAI | $0.00025/sec ($0.015/min) | Best for accent + jargon | Yes (built-in) | Audio Intelligence add-ons (sentiment, summary, topics) |\n| Deepgram | $0.0043/min (Nova-2) | Comparable | Yes | Fastest, real-time capable |\n| SignalWire Call Intelligence | Included in select plans | Good | Yes | No external API call, runs in-platform |</p>\n<h3>AssemblyAI example</h3>\n<pre><code class=\"language-python\">import requests\n\ndef transcribe_with_assemblyai(audio_url):\n    headers = {\"authorization\": ASSEMBLY_KEY}\n    \n    response = requests.post(\n        \"https://api.assemblyai.com/v2/transcript\",\n        headers=headers,\n        json={\n            \"audio_url\": audio_url,\n            \"speaker_labels\": True,\n            \"sentiment_analysis\": True,\n            \"entity_detection\": True,\n            \"auto_highlights\": True,\n            \"iab_categories\": True,\n        }\n    )\n    transcript_id = response.json()[\"id\"]\n    \n    # Poll for completion\n    while True:\n        result = requests.get(\n            f\"https://api.assemblyai.com/v2/transcript/{transcript_id}\",\n            headers=headers\n        ).json()\n        if result[\"status\"] == \"completed\":\n            return result\n        elif result[\"status\"] == \"error\":\n            raise Exception(result[\"error\"])\n        time.sleep(2)\n</code></pre>\n<p>AssemblyAI returns:</p>\n<ul>\n<li>Full transcript with timestamps</li>\n<li>Per-utterance speaker labels (Speaker A, B, C...)</li>\n<li>Sentiment per sentence (positive, negative, neutral)</li>\n<li>Entity detection (people, places, products, organizations)</li>\n<li>IAB content categories (Auto, Health, Real Estate, etc.)</li>\n<li>Auto-highlights (key phrases)</li>\n</ul>\n<h3>Deepgram example</h3>\n<pre><code class=\"language-python\">from deepgram import DeepgramClient, PrerecordedOptions\n\ndg = DeepgramClient(DEEPGRAM_KEY)\n\nresponse = dg.listen.prerecorded.v(\"1\").transcribe_url(\n    {\"url\": audio_url},\n    PrerecordedOptions(\n        model=\"nova-2\",\n        smart_format=True,\n        diarize=True,\n        sentiment=True,\n        intents=True,\n        topics=True,\n    )\n)\n</code></pre>\n<p>Deepgram returns similar structured output with built-in sentiment and intent classification.</p>\n<h2>Step 2: LLM analysis with structured output</h2>\n<p>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.</p>\n<pre><code class=\"language-python\">import anthropic\n\ndef analyze_call(transcript):\n    client = anthropic.Anthropic(api_key=ANTHROPIC_KEY)\n    \n    response = client.messages.create(\n        model=\"claude-opus-4-5\",\n        max_tokens=2000,\n        system=\"\"\"You analyze inbound phone calls for a service business and output structured JSON.\n        \nYou MUST output ONLY valid JSON matching this schema:\n{\n  \"service_needed\": \"string or null\",\n  \"urgency_level\": \"low|medium|high|emergency\",\n  \"call_outcome\": \"lead|customer|complaint|wrong_number|spam|inquiry_only\",\n  \"conversion_probability\": \"0.0-1.0\",\n  \"sentiment_overall\": \"positive|neutral|negative\",\n  \"caller_intent_summary\": \"1-2 sentence summary\",\n  \"key_topics\": [\"array\", \"of\", \"topics\"],\n  \"entities_mentioned\": {\n    \"addresses\": [],\n    \"dates\": [],\n    \"products\": [],\n    \"competitor_mentions\": []\n  },\n  \"agent_quality_notes\": \"1-2 sentences\",\n  \"recommended_followup\": \"string or null\",\n  \"spam_likelihood\": \"0.0-1.0\"\n}\"\"\",\n        messages=[{\n            \"role\": \"user\",\n            \"content\": f\"Analyze this call transcript:\\n\\n{transcript}\"\n        }]\n    )\n    \n    return json.loads(response.content[0].text)\n</code></pre>\n<p>The strict JSON schema in the system prompt makes Claude's output reliably parsable. For Claude specifically, use the <code>tools</code> mechanism for even higher reliability:</p>\n<pre><code class=\"language-python\">response = client.messages.create(\n    model=\"claude-opus-4-5\",\n    max_tokens=2000,\n    tools=[{\n        \"name\": \"save_call_analysis\",\n        \"description\": \"Save the structured call analysis\",\n        \"input_schema\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"service_needed\": {\"type\": \"string\"},\n                \"urgency_level\": {\"type\": \"string\", \"enum\": [\"low\", \"medium\", \"high\", \"emergency\"]},\n                \"call_outcome\": {\"type\": \"string\", \"enum\": [\"lead\", \"customer\", \"complaint\", \"wrong_number\", \"spam\", \"inquiry_only\"]},\n                \"conversion_probability\": {\"type\": \"number\", \"minimum\": 0, \"maximum\": 1},\n                # ... rest of schema\n            },\n            \"required\": [\"service_needed\", \"urgency_level\", \"call_outcome\", \"conversion_probability\"]\n        }\n    }],\n    tool_choice={\"type\": \"tool\", \"name\": \"save_call_analysis\"},\n    messages=[{\"role\": \"user\", \"content\": f\"Analyze: {transcript}\"}]\n)\n\nanalysis = response.content[0].input\n</code></pre>\n<p>The tool-use pattern is the most reliable way to get structured JSON from Claude — schema validation is built in.</p>\n<h2>Step 3: Persistence</h2>\n<pre><code class=\"language-sql\">CREATE TABLE call_analysis (\n    id UUID PRIMARY KEY,\n    call_sid TEXT REFERENCES calls(call_sid),\n    transcript TEXT,\n    analysis JSONB,\n    sentiment_overall TEXT,\n    call_outcome TEXT,\n    conversion_probability NUMERIC,\n    service_needed TEXT,\n    urgency_level TEXT,\n    transcript_cost_cents INT,\n    llm_cost_cents INT,\n    analyzed_at TIMESTAMPTZ DEFAULT now()\n);\n\nCREATE INDEX idx_outcome ON call_analysis(call_outcome);\nCREATE INDEX idx_service ON call_analysis(service_needed);\nCREATE INDEX idx_analyzed_at ON call_analysis(analyzed_at DESC);\n</code></pre>\n<p>Index on the most commonly queried fields. The full JSON sits in <code>analysis</code> for ad-hoc queries.</p>\n<h2>Step 4: Trigger downstream actions</h2>\n<p>Based on the analysis, fire workflows:</p>\n<pre><code class=\"language-python\">def trigger_actions(call_id, analysis):\n    if analysis[\"call_outcome\"] == \"lead\" and analysis[\"conversion_probability\"] > 0.5:\n        push_to_crm(call_id, analysis, priority=\"high\")\n    \n    if analysis[\"urgency_level\"] == \"emergency\":\n        sms_alert_owner(f\"Emergency call: {analysis['caller_intent_summary']}\")\n    \n    if analysis[\"call_outcome\"] == \"complaint\":\n        create_helpdesk_ticket(call_id, analysis)\n    \n    if analysis[\"spam_likelihood\"] > 0.8:\n        add_to_blocklist(call_caller)\n    \n    push_to_ga4_conversion_if_appropriate(call_id, analysis)\n</code></pre>\n<h2>SignalWire Call Intelligence integration</h2>\n<p>SignalWire's Call Intelligence is the platform-native version of this pipeline. The SWML <code>record</code> verb with <code>post_prompt</code> runs transcription + an LLM analysis in-platform:</p>\n<pre><code class=\"language-yaml\">- ai:\n    prompt:\n      text: \"You are a customer service agent. Help the caller.\"\n    post_prompt:\n      text: |\n        Analyze this conversation and output JSON:\n        {\n          \"service_needed\": string,\n          \"urgency_level\": \"low|medium|high|emergency\",\n          \"call_outcome\": \"lead|customer|complaint\",\n          \"conversion_probability\": 0.0-1.0,\n          \"summary\": string\n        }\n    post_prompt_url: https://your.api/call-analysis-webhook\n</code></pre>\n<p>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.</p>\n<p><strong>When to use Call Intelligence vs DIY pipeline:</strong></p>\n<p>| Factor | Call Intelligence | DIY pipeline |\n|---|---|---|\n| Setup complexity | Low | High |\n| Cost per call | Bundled in SW pricing | $0.01-0.05 per call |\n| Customization | Limited to prompt | Full pipeline control |\n| Diarization quality | Good | AssemblyAI/Deepgram are tops |\n| Latency to result | At call end | At call end + transcription time |\n| Best for | Standard cases, faster setup | High-volume, custom analysis |</p>\n<h2>Cost projections</h2>\n<p>For 1000 calls/day, average 4 minutes each:</p>\n<p>| Component | Cost per call | Daily cost |\n|---|---|---|\n| AssemblyAI transcription | $0.06 | $60 |\n| Claude analysis (Opus) | $0.04 | $40 |\n| Total | $0.10 | $100 |</p>\n<p>For lower cost:</p>\n<ul>\n<li>AssemblyAI Nano model: $0.0125/min ($0.05/call) → ~$50/day</li>\n<li>Claude Sonnet instead of Opus: $0.01/call → ~$10/day</li>\n<li>Combined: $60/day for 1000 calls</li>\n</ul>\n<p>For volume above 5K calls/day, consider Deepgram Nova-2 (cheaper) + Claude Haiku for cost optimization.</p>\n<h2>Real-time vs post-call analysis</h2>\n<p>This topic focuses on post-call analysis. For real-time (during the call):</p>\n<ul>\n<li>AssemblyAI Streaming for live transcription</li>\n<li>Deepgram real-time for sub-200ms transcription</li>\n<li>Live LLM analysis on partial transcripts (for agent assist — see <a href=\"/topic/agent-assist\">agent assist</a>)</li>\n</ul>\n<p>Post-call analysis is cheaper and more accurate. Use real-time only when there's a clear action requirement during the live conversation.</p>\n<h2>Error handling</h2>\n<p>| Failure | Cause | Recovery |\n|---|---|---|\n| Recording URL 404 | Recording deleted (retention exceeded) | Fall back to \"no transcript available\" |\n| Transcription returns empty | Silent call, very short | Skip analysis, mark as <code>inquiry_only</code> |\n| LLM returns invalid JSON | Edge case in prompt | Retry once with strict schema, fall back to manual review |\n| Webhook delivery fails | Network issue | Retry with exponential backoff (15s, 1m, 5m, 30m, give up) |\n| Caller speaks unknown language | Transcript is garbled | Detect via language ID, route to human review |</p>\n<h2>Common pitfalls</h2>\n<ul>\n<li><strong>Trusting LLM categorization without spot-checking</strong> — sample 1% manually for the first 1000 calls to verify the prompt is producing useful output.</li>\n<li><strong>Cost runaway from Opus on long calls</strong> — set a max-token budget. Calls > 30 minutes should chunk-then-summarize.</li>\n<li><strong>No retention policy on transcripts</strong> — sensitive content (PII, payment info) accumulating indefinitely. Set 90-day retention by default.</li>\n<li><strong>PHI in healthcare calls</strong> — see <a href=\"/topic/call-recording-compliance\">call recording compliance</a>. Use BAA-eligible vendors only (AssemblyAI BAA available enterprise tier).</li>\n<li><strong>Webhook timeout</strong> — analysis pipeline can take 60-90 seconds. Webhook receiver must respond fast and process async.</li>\n</ul>\n<h2>Related patterns</h2>\n<ul>\n<li><a href=\"/topic/assemblyai-transcription\">AssemblyAI transcription</a> — full AssemblyAI reference</li>\n<li><a href=\"/topic/signalwire-call-intelligence\">SignalWire Call Intelligence</a> — platform-native pipeline</li>\n<li><a href=\"/topic/call-recording-compliance\">Call recording compliance</a> — what you can store and for how long</li>\n<li><a href=\"/topic/call-attribution-ga4-ghl\">Call attribution GA4 GHL</a> — push analysis to revenue systems</li>\n<li><a href=\"/topic/agent-assist\">Agent assist</a> — real-time variant</li>\n</ul>\n<h2>References</h2>\n<ul>\n<li>AssemblyAI API documentation — transcript and audio intelligence</li>\n<li>Deepgram API documentation — Nova-2 model</li>\n<li>Anthropic Claude API — tool use for structured output</li>\n<li>SignalWire SWML post_prompt — platform-native analysis</li>\n</ul>\n"}