SignalWire Call Intelligence Pipeline
The canonical pattern for building a call-sentiment dashboard, lead-intelligence pipeline, or any system that turns calls into structured data.
The full flow
- Inbound call arrives → SWML script executes.
record_callstarts background stereo recording.live_transcribestreams real-time transcript to your webhook.aiverb runs the agent with apost_promptthat extracts structured JSON.- Call ends →
post_prompt_urlreceives the payload: transcript, sentiment, intent, CRM fields. - Backend parses and writes to DB.
- Dashboard queries DB and renders sentiment scores, summaries, extracted data.
Key principle: record_call, live_transcribe, and ai are non-blocking background verbs. They run concurrently.
Production SWML — concurrent record + transcribe + AI
version: 1.0.0
sections:
main:
- answer: {}
- record_call:
format: mp3
stereo: true # required for per-speaker analysis
direction: both
max_length: 3600
status_url: "https://your.api/webhooks/recording"
- live_transcribe:
action:
start:
webhook: "https://your.api/webhooks/transcript"
lang: en
live_events: true
ai_summary: true
direction: [remote-caller, local-caller]
speech_engine: deepgram
vad_silence_ms: 300
- ai:
prompt:
text: |
## Role
You are a sales call assistant for Acme Corp.
Greet the caller, identify their needs, guide them to the right product.
temperature: 0.7
params:
end_of_speech_timeout: 700
asr_diarize: true
asr_smart_format: true
save_conversation: true
hard_stop_time: "30m"
languages:
- { name: English, code: en-US, voice: rime.spore }
global_data:
campaign: spring-sale
agent_id: hawkeye-001
post_prompt:
text: |
Analyze the conversation. Return ONLY valid JSON, no prose:
{
"sentiment": "positive|neutral|negative",
"sentiment_score": 0.0,
"caller_intent": "string",
"key_topics": ["string"],
"outcome": "sold|not_sold|follow_up|transferred|other",
"follow_up_required": true,
"caller_name": "string or null",
"caller_email": "string or null",
"summary": "2-3 sentence summary"
}
temperature: 0.2
post_prompt_url: "https://your.api/webhooks/post-prompt"
Relay SDK equivalent — call.ai() with full control
For programmatic control (start AI mid-call, swap prompts, inject context) use the Relay SDK.
from signalwire.relay import RelayClient
client = RelayClient(
project="your-project-id",
token="your-api-token",
host="your-space.signalwire.com",
contexts=["default"],
)
@client.on_call
async def handle_call(call):
await call.answer()
action = await call.ai(
prompt={
"text": "You are a sales call assistant for Acme Corp...",
"temperature": 0.7,
"top_p": 0.9,
},
post_prompt={
"text": """Return ONLY JSON:
{
"sentiment": "positive|neutral|negative",
"sentiment_score": 0.0,
"caller_intent": "string",
"outcome": "sold|not_sold|follow_up|transferred|other",
"follow_up_required": true,
"caller_email": "string or null",
"summary": "2-3 sentence summary"
}"""
},
post_prompt_url="https://your.api/webhooks/post-prompt",
post_prompt_auth_user="webhook_user",
post_prompt_auth_password="webhook_pass",
hints=["Acme", "product names", "pricing tiers"],
global_data={"campaign": "spring-sale"},
ai_params={
"asr_diarize": True,
"asr_smart_format": True,
"end_of_speech_timeout": 700,
"energy_level": 52,
"attention_timeout": 10000,
"hard_stop_time": "30m",
"save_conversation": True,
"debug_webhook_url": "https://your.api/webhooks/debug",
"debug_webhook_level": 2,
},
on_completed=lambda event: print("AI session ended"),
)
await action.wait()
client.run()
Pause and resume the AI mid-call
ai_hold + ai_unhold pause the AI agent while keeping the call alive. Use for supervisor barge-in, CRM lookup, or compliance pause.
# Pause the AI; speak a hold message; auto-resume after timeout.
await call.ai_hold(
prompt="One moment while I check your account.",
timeout="60",
)
# Do backend work...
account = await fetch_account(caller_id)
# Inject context as a system message before resuming.
await call.ai_message(
message_text=f"Customer status: {account['status']}. Balance: ${account['balance']}.",
role="system",
)
await call.ai_unhold(
prompt="Thanks for holding. I've pulled up your account."
)
Inject context, simulate input, or reset
| Action | Method | Purpose |
|---|---|---|
| Inject system instruction (invisible to caller) | call.ai_message(role="system", message_text=...) | Mid-call CRM data, supervisor note. |
| Update session data | call.ai_update_global_data({...}) | Add caller_name, account_id after lookup. |
| Simulate user input | call.ai_simulate_input(...) | Testing or IVR-style forced injection. |
| Reset conversation | call.ai_reset_conversation() | Start over after handoff. |
Post-prompt webhook payload
When the call ends and post_prompt_url is set, SignalWire POSTs:
{
"action": "post_conversation",
"ai_session_id": "uuid",
"ai_start_date": 1640000000,
"ai_end_date": 1640000900,
"call_id": "uuid",
"call_start_date": 1640000000,
"call_answer_date": 1640000003,
"call_end_date": 1640000900,
"caller_id_num": "+15551234567",
"caller_id_name": "Jane Smith",
"call_log": [
{ "role": "system", "content": "You are a sales call assistant..." },
{ "role": "user", "content": "Hi, I'm interested in pricing..." },
{ "role": "assistant", "content": "Happy to help..." }
],
"post_prompt_data": {
"raw": "{ \"sentiment\": \"positive\", ... }",
"parsed": {
"sentiment": "positive",
"sentiment_score": 0.85,
"caller_intent": "pricing inquiry",
"outcome": "follow_up",
"follow_up_required": true,
"summary": "Caller wants pricing for enterprise tier..."
}
},
"swaig_log": [
{ "function": "lookup_account", "args": {...}, "result": {...} }
]
}
Read post_prompt_data.parsed first. Fall back to parsing raw only if the AI included prose.
Database schema for a call-intelligence dashboard
Minimum useful columns:
| Column | Source |
|---|---|
| call_id | call_id |
| ai_session_id | ai_session_id |
| from_number, to_number | caller_id_num, fetched DID |
| started_at, ended_at | call_start_date, call_end_date |
| duration_seconds | derived |
| recording_url | from status_url webhook |
| transcript_url | from live_transcribe summary webhook |
| sentiment, sentiment_score | post_prompt_data.parsed.sentiment |
| caller_intent, outcome | post_prompt_data.parsed.* |
| follow_up_required | bool from parsed |
| summary | text from parsed |
| raw_payload | jsonb of the full POST body |
| swaig_calls | jsonb of swaig_log |
Anti-patterns
- Relying on
live_transcribefinal transcript for analytics — userecord_call+ post-call transcription (orpost_promptsummary).live_transcribeis for real-time UI, not source-of-truth. - Not setting
asr_diarize: truewhen you need per-speaker sentiment. - Putting your post-prompt JSON schema in plain prose — LLMs follow code-fenced JSON examples much more reliably.
- Skipping
post_prompt_auth_user/password— anyone who guesses the webhook URL can spoof call results. - Storing only the parsed JSON — keep the raw payload in
jsonbfor re-analysis later.