SWML ai Verb
The ai verb spins up an AI voice agent inside a SignalWire call. ASR (automatic speech recognition) + LLM + TTS all in one block, with a function-call layer (SWAIG) for tool use during the conversation.
Minimum viable ai
version: 1.0.0
sections:
main:
- answer: {}
- ai:
prompt:
text: |
You are a friendly receptionist for Acme Plumbing.
Greet the caller, ask how you can help, and book an appointment.
post_prompt:
text: |
Return JSON only:
{ "caller_intent": string, "appointment_booked": boolean }
post_prompt_url: "https://your.api/webhooks/post-prompt"
That's the complete shape. Everything else below is optional.
Top-level ai properties
| Property | Type | Notes |
|---|---|---|
| prompt | object (required) | Persona, goals, instructions. |
| post_prompt | object | Final instructions sent after the call ends. Best used to extract structured JSON. |
| post_prompt_url | string | Webhook to receive the post-call payload. |
| params | object | AI behavior tuning. See params table below. |
| languages | object[] | Supported languages and TTS voices. |
| hints | string[] or object[] | Boost ASR recognition on specific words. |
| pronounce | object[] | Override pronunciation of specific words. |
| SWAIG | object | Tool-call functions. See SWAIG functions. |
| global_data | object | Session-wide data, accessible via ${global_data.key}. |
prompt configuration
prompt.text is the persona block. LLMs respond well to markdown headers, so structure it.
ai:
prompt:
text: |
## Role
You are a receptionist for Acme Plumbing.
## Guidelines
- Be concise.
- Never quote prices.
- If unsure, transfer to a human.
## Tools
- book_appointment: Use when caller asks to schedule.
- transfer_to_human: Use when caller asks for a person.
temperature: 0.7
top_p: 0.9
confidence: 0.6
presence_penalty: 0.0
frequency_penalty: 0.0
| Field | Range | Notes |
|---|---|---|
| temperature | 0.0 - 1.5 | Higher = more random. Default 1.0. |
| top_p | 0.0 - 1.0 | Alternative to temperature. Lower = less random. Default 1.0. |
| confidence | 0.0 - 1.0 | Speech-detect end-of-utterance threshold. Lower = quicker turn-end but more false positives. |
| presence_penalty | -2.0 - 2.0 | Positive = more new topics. |
| frequency_penalty | -2.0 - 2.0 | Positive = less repetition. |
params — runtime behavior
Everything in params is optional. Most useful ones:
| Param | Type | Notes |
|---|---|---|
| end_of_speech_timeout | int (ms) | Silence after caller speech before AI replies. Default 1000. |
| attention_timeout | int (ms) | Idle-caller reminder timeout. Default 10000. |
| max_speech_timeout | int (ms) | Max single utterance length. Default 30000. |
| hard_stop_time | string | Hard cap on session duration, e.g. "30m". |
| asr_diarize | bool | Speaker labels in transcript. |
| asr_smart_format | bool | Clean number/date formatting in transcripts. |
| save_conversation | bool | Auto-send conversation summary to post_prompt_url. |
| energy_level | int 0-100 | Mic sensitivity. |
| debug_webhook_url | string | Streams each AI turn in real time. |
| debug_webhook_level | 0|1|2 | 0 off, 1 basic, 2 verbose. |
languages and hints
ai:
languages:
- name: English
code: en-US
voice: rime.spore
- name: Spanish
code: es-MX
voice: rime.luna
hints:
- SignalWire
- SWAIG
- HVAC
- { hint: "Tony", pattern: "Toni", replace: "Tony", ignore_case: true }
pronounce:
- { replace: "GHL", with: "G H L" }
- { replace: "SWML", with: "swimmel" }
Hint objects let you regex-rewrite ASR mis-hears. Pronounce objects fix TTS mispronunciations.
post_prompt and post_prompt_url — structured extraction
post_prompt runs after the call ends. Use it to coerce a JSON object out of the conversation.
ai:
post_prompt:
text: |
Analyze the call. Return ONLY valid JSON, no prose:
{
"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"
}
temperature: 0.2
post_prompt_url: "https://your.api/webhooks/post-prompt"
post_prompt_url payload contract
SignalWire POSTs JSON with these key fields:
| Field | Notes |
|---|---|
| action | Always "post_conversation" |
| ai_session_id | UUID for this AI session |
| ai_start_date, ai_end_date | Unix timestamps |
| call_id | Call ID |
| call_start_date, call_answer_date, call_end_date | Unix timestamps |
| caller_id_num, caller_id_name | Caller info |
| call_log | Full role/content log of the conversation |
| post_prompt_data.raw | The AI's full response to post_prompt.text |
| post_prompt_data.parsed | If valid JSON was detected, it's parsed here |
| swaig_log | Every SWAIG function called during the call |
The handler should look at post_prompt_data.parsed first; fall back to parsing post_prompt_data.raw only if the AI included non-JSON prose.
global_data — session-wide state
ai:
global_data:
campaign: spring-sale
agent_id: vox-001
prompt:
text: |
Campaign code: ${global_data.campaign}.
Reference number: ${global_data.agent_id}.
SWAIG handlers can mutate it via FunctionResult().update_global_data({...}). The new values are visible to subsequent prompts and SWAIG calls.
Full example — recording + AI + post-prompt extraction
version: 1.0.0
sections:
main:
- answer: {}
- record_call:
format: mp3
stereo: true
status_url: "https://your.api/webhooks/recording"
- ai:
prompt:
text: |
## Role
You are an HVAC dispatcher for Acme HVAC.
Greet, identify problem, get address, offer same-day or next-day.
temperature: 0.7
params:
end_of_speech_timeout: 700
asr_diarize: true
asr_smart_format: true
save_conversation: true
hard_stop_time: "20m"
languages:
- { name: English, code: en-US, voice: rime.spore }
post_prompt:
text: |
Return ONLY JSON:
{ "intent": string, "address": string, "scheduled": boolean, "urgency": "high|normal|low" }
temperature: 0.2
post_prompt_url: "https://your.api/webhooks/post-prompt"
SWAIG:
defaults:
web_hook_url: "https://your.api/webhooks/swaig"
functions:
- function: book_appointment
description: Schedule an appointment slot
parameters:
type: object
properties:
service: { type: string }
date: { type: string }
time_window: { type: string }
required: [service, date, time_window]
Anti-patterns
- Putting unstructured paragraphs in
prompt.text— LLMs follow markdown structure much better. - Setting
confidencetoo low (< 0.4) — the agent will cut callers off mid-sentence. - Forgetting
temperatureinpost_prompt— leave it ≤ 0.3 to get reliable JSON. - Not validating
post_prompt_data.parsedexists before reading — sometimes the AI ignores the JSON instruction. Always fall back toraw. - Skipping
languages— defaults to a low-quality voice. Always specify. - Setting
save_conversation: trueAND a custompost_promptthat returns structured JSON — the two payloads collide. Pick one strategy.