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