{"slug":"swaig-functions","title":"SWAIG Functions — Live AI Tool Calls","tags":["signalwire","swaig","ai-tools","swml","function-calling"],"agent_summary":"SignalWire AI Gateway functions — JSON schema definitions, the webhook contract, FunctionResult return type, secure tokens, native functions, and the DataMap server-side passthrough alternative.","trigger_phrases":["what is SWAIG","SignalWire AI function","register SWAIG function","SWAIG webhook payload","DataMap vs SWAIG","FunctionResult","AI tool call in SWML"],"runnable":true,"markdown":"\n# SWAIG Functions\n\n**SignalWire AI Gateway** (SWAIG) functions are the tool-call layer of a SignalWire AI agent. While the call is live, the LLM decides when to invoke a function, SignalWire POSTs a JSON payload to your webhook, your handler returns a `FunctionResult`, and the AI speaks or acts on it.\n\nThere are three ways to define a SWAIG function. Pick one per function.\n\n| Method | When to use |\n|---|---|\n| `@tool` decorator (Python Agents SDK) | Default. Local handler, fluent return type, type-inferred schema. |\n| `SWAIGFunction` class | Same as decorator, but for manual registration in advanced cases. |\n| `DataMap` builder | Call an external HTTP API directly from SignalWire infrastructure — no webhook on your side required. |\n\n## SWAIG schema (raw SWML form)\n\nWhen written directly inside a SWML `ai` block, a SWAIG function looks like this.\n\n```yaml\nai:\n  SWAIG:\n    defaults:\n      web_hook_url: https://your.api/webhook\n    functions:\n      - function: lookup_order\n        description: Look up an order by ID\n        parameters:\n          type: object\n          properties:\n            order_id:\n              type: string\n              description: The order ID\n          required: [order_id]\n        fillers:\n          en-US:\n            - One moment, let me check.\n        secure: true\n```\n\n`defaults.web_hook_url` cascades to every function unless an individual function overrides with its own `web_hook_url`. Set `secure: false` only for non-sensitive read-only calls — `secure: true` (default) makes SignalWire token-sign every webhook.\n\n## The webhook contract\n\nSignalWire POSTs JSON to `web_hook_url`. Body fields:\n\n| Field | Type | Notes |\n|---|---|---|\n| `function` | string | Name of the called function |\n| `argument` | object | `{ \"parsed\": [{...}], \"raw\": \"...\" }` — `parsed[0]` is the parsed args |\n| `call_id` | string | The active call ID |\n| `ai_session_id` | string | UUID for the AI session |\n| `caller_id_num` | string | The caller's number |\n| `caller_id_name` | string | Caller name (if available) |\n| `meta_data` | object | Per-function scratch data (function-scoped) |\n| `global_data` | object | Session-wide data (set via `update_global_data`) |\n| `argument_desc` | object | Echo of the function schema |\n\nThe handler returns a `FunctionResult`. The minimum response body is `{\"response\": \"...\"}` — but `FunctionResult` builds this for you, including action chains.\n\n## Defining a function via the @tool decorator\n\nThe Python SDK does the heavy lifting. See the [Python Agents SDK gem](/topic/signalwire-python-agents-sdk) for full setup.\n\n```python\nfrom signalwire import AgentBase, FunctionResult\n\nagent = AgentBase(name=\"support\")\n\n@agent.tool(\n    name=\"get_appointment_slots\",\n    description=\"Get the next 3 available appointment slots\",\n    parameters={\n        \"type\": \"object\",\n        \"properties\": {\n            \"service\": {\"type\": \"string\", \"description\": \"Service type\"},\n            \"zip_code\": {\"type\": \"string\", \"description\": \"Caller ZIP\"},\n        },\n        \"required\": [\"service\"],\n    },\n    fillers={\"en-US\": [\"Checking the schedule one second...\"]},\n)\ndef get_appointment_slots(args, raw_data=None):\n    service = args.get(\"service\")\n    slots = my_scheduler.next_three(service, args.get(\"zip_code\"))\n    return FunctionResult(f\"I have {slots[0]}, {slots[1]}, or {slots[2]}. Which works?\")\n```\n\nThe `fillers` map plays while the handler executes — keeps the call from sounding dead.\n\n## Returning actions, not just text\n\n`FunctionResult` chains call-control actions. The AI speaks the body first, then SignalWire executes the actions.\n\n```python\n@agent.tool(name=\"transfer_to_billing\", description=\"Transfer caller to billing\")\ndef transfer_to_billing(args, raw_data):\n    return (\n        FunctionResult(\"I'll transfer you to billing. Anything else first?\", post_process=True)\n        .update_global_data({\"transferred\": True})\n        .send_sms(\n            to_number=\"+15551234567\",\n            from_number=\"+15559876543\",\n            body=\"You are being transferred to billing.\",\n        )\n        .connect(\"+15551234567\", final=True)\n    )\n```\n\n`post_process=True` lets the AI take one more turn before actions fire — useful when the caller might say \"actually wait.\"\n\n## DataMap — call APIs without a webhook\n\nDataMap executes REST calls on SignalWire's infrastructure. No server on your side required. Good for straightforward integrations.\n\n### Variable substitution table\n\n| Pattern | Expands to |\n|---|---|\n| `${args.param}` | Function argument value |\n| `${enc:args.param}` | URL-encoded argument |\n| `${lc:args.param}` | Lowercased argument |\n| `${fmt_ph:args.phone}` | Formatted phone number |\n| `${response.field}` | API response field |\n| `${response.arr[0]}` | Array element in response |\n| `${global_data.key}` | Global session data |\n| `${this.field}` | Current item in a `foreach` loop |\n\nModifiers chain right-to-left: `${enc:lc:args.param}` lowercases then URL-encodes.\n\n### GET DataMap\n\n```python\nfrom signalwire import DataMap, FunctionResult\n\nweather = (\n    DataMap(\"get_weather\")\n    .description(\"Get current weather for a city\")\n    .parameter(\"city\", \"string\", \"City name\", required=True)\n    .webhook(\"GET\", \"https://api.weatherapi.com/v1/current.json?key=API_KEY&q=${enc:args.city}\")\n    .output(FunctionResult(\n        \"Weather in ${args.city}: ${response.current.condition.text}, ${response.current.temp_f}F\"\n    ))\n    .fallback_output(FunctionResult(\"Sorry, couldn't get weather for ${args.city}.\"))\n)\n\nagent.register_swaig_function(weather.to_swaig_function())\n```\n\n### POST with body and foreach\n\n```python\nsearch_docs = (\n    DataMap(\"search_docs\")\n    .purpose(\"Search documentation\")\n    .parameter(\"query\", \"string\", \"Search query\", required=True)\n    .webhook(\"POST\", \"https://api.docs.example.com/search\",\n             headers={\"Authorization\": \"Bearer TOKEN\"})\n    .body({\"query\": \"${args.query}\", \"limit\": 3})\n    .foreach({\n        \"input_key\": \"results\",\n        \"output_key\": \"formatted_results\",\n        \"max\": 3,\n        \"append\": \"- ${this.title}: ${this.summary}\\n\",\n    })\n    .output(FunctionResult(\"Found:\\n${formatted_results}\"))\n    .fallback_output(FunctionResult(\"Search unavailable.\"))\n)\n```\n\n### Expression-only DataMap (no HTTP)\n\nFor pure pattern matching with no API call:\n\n```python\nvolume = (\n    DataMap(\"set_volume\")\n    .parameter(\"level\", \"string\", \"Volume level\", required=True)\n    .expression(\"${args.level}\", r\"high|loud|up\",   FunctionResult(\"Volume increased\"))\n    .expression(\"${args.level}\", r\"low|quiet|down\", FunctionResult(\"Volume decreased\"))\n    .expression(\"${args.level}\", r\"mute|off\",       FunctionResult(\"Audio muted\"))\n)\n```\n\n## Native functions — built-in to the platform\n\nEnable a curated list of platform-provided tools without writing handlers.\n\n```python\nagent = AgentBase(name=\"receptionist\", native_functions=[\"check_time\"])\n```\n\nCurrent native functions include `check_time` (returns current time in caller's timezone).\n\n## Anti-patterns\n\n- Returning plain strings from a handler — must be `FunctionResult`.\n- Mixing `web_hook_url` cascading — set it once at `SWAIG.defaults`, override only where needed.\n- Setting `secure: false` for any function that reads or mutates user data — kills the signed-token defense.\n- Using DataMap for anything stateful or auth-complex — switch to `@tool` and own the call.\n- Hand-rolling the JSON Schema when type hints would infer it — let the decorator do it.\n\n## See also\n\n- [Python Agents SDK](/topic/signalwire-python-agents-sdk)\n- [SWML AI verb](/topic/swml-ai-verb)\n- [DataMap deep-dive (Agents SDK)](/topic/swml-data-map)\n- [MCP Gateway federation](/topic/agents-mcp-gateway)\n","html":"<h1>SWAIG Functions</h1>\n<p><strong>SignalWire AI Gateway</strong> (SWAIG) functions are the tool-call layer of a SignalWire AI agent. While the call is live, the LLM decides when to invoke a function, SignalWire POSTs a JSON payload to your webhook, your handler returns a <code>FunctionResult</code>, and the AI speaks or acts on it.</p>\n<p>There are three ways to define a SWAIG function. Pick one per function.</p>\n<p>| Method | When to use |\n|---|---|\n| <code>@tool</code> decorator (Python Agents SDK) | Default. Local handler, fluent return type, type-inferred schema. |\n| <code>SWAIGFunction</code> class | Same as decorator, but for manual registration in advanced cases. |\n| <code>DataMap</code> builder | Call an external HTTP API directly from SignalWire infrastructure — no webhook on your side required. |</p>\n<h2>SWAIG schema (raw SWML form)</h2>\n<p>When written directly inside a SWML <code>ai</code> block, a SWAIG function looks like this.</p>\n<pre><code class=\"language-yaml\">ai:\n  SWAIG:\n    defaults:\n      web_hook_url: https://your.api/webhook\n    functions:\n      - function: lookup_order\n        description: Look up an order by ID\n        parameters:\n          type: object\n          properties:\n            order_id:\n              type: string\n              description: The order ID\n          required: [order_id]\n        fillers:\n          en-US:\n            - One moment, let me check.\n        secure: true\n</code></pre>\n<p><code>defaults.web_hook_url</code> cascades to every function unless an individual function overrides with its own <code>web_hook_url</code>. Set <code>secure: false</code> only for non-sensitive read-only calls — <code>secure: true</code> (default) makes SignalWire token-sign every webhook.</p>\n<h2>The webhook contract</h2>\n<p>SignalWire POSTs JSON to <code>web_hook_url</code>. Body fields:</p>\n<p>| Field | Type | Notes |\n|---|---|---|\n| <code>function</code> | string | Name of the called function |\n| <code>argument</code> | object | <code>{ \"parsed\": [{...}], \"raw\": \"...\" }</code> — <code>parsed[0]</code> is the parsed args |\n| <code>call_id</code> | string | The active call ID |\n| <code>ai_session_id</code> | string | UUID for the AI session |\n| <code>caller_id_num</code> | string | The caller's number |\n| <code>caller_id_name</code> | string | Caller name (if available) |\n| <code>meta_data</code> | object | Per-function scratch data (function-scoped) |\n| <code>global_data</code> | object | Session-wide data (set via <code>update_global_data</code>) |\n| <code>argument_desc</code> | object | Echo of the function schema |</p>\n<p>The handler returns a <code>FunctionResult</code>. The minimum response body is <code>{\"response\": \"...\"}</code> — but <code>FunctionResult</code> builds this for you, including action chains.</p>\n<h2>Defining a function via the @tool decorator</h2>\n<p>The Python SDK does the heavy lifting. See the <a href=\"/topic/signalwire-python-agents-sdk\">Python Agents SDK gem</a> for full setup.</p>\n<pre><code class=\"language-python\">from signalwire import AgentBase, FunctionResult\n\nagent = AgentBase(name=\"support\")\n\n@agent.tool(\n    name=\"get_appointment_slots\",\n    description=\"Get the next 3 available appointment slots\",\n    parameters={\n        \"type\": \"object\",\n        \"properties\": {\n            \"service\": {\"type\": \"string\", \"description\": \"Service type\"},\n            \"zip_code\": {\"type\": \"string\", \"description\": \"Caller ZIP\"},\n        },\n        \"required\": [\"service\"],\n    },\n    fillers={\"en-US\": [\"Checking the schedule one second...\"]},\n)\ndef get_appointment_slots(args, raw_data=None):\n    service = args.get(\"service\")\n    slots = my_scheduler.next_three(service, args.get(\"zip_code\"))\n    return FunctionResult(f\"I have {slots[0]}, {slots[1]}, or {slots[2]}. Which works?\")\n</code></pre>\n<p>The <code>fillers</code> map plays while the handler executes — keeps the call from sounding dead.</p>\n<h2>Returning actions, not just text</h2>\n<p><code>FunctionResult</code> chains call-control actions. The AI speaks the body first, then SignalWire executes the actions.</p>\n<pre><code class=\"language-python\">@agent.tool(name=\"transfer_to_billing\", description=\"Transfer caller to billing\")\ndef transfer_to_billing(args, raw_data):\n    return (\n        FunctionResult(\"I'll transfer you to billing. Anything else first?\", post_process=True)\n        .update_global_data({\"transferred\": True})\n        .send_sms(\n            to_number=\"+15551234567\",\n            from_number=\"+15559876543\",\n            body=\"You are being transferred to billing.\",\n        )\n        .connect(\"+15551234567\", final=True)\n    )\n</code></pre>\n<p><code>post_process=True</code> lets the AI take one more turn before actions fire — useful when the caller might say \"actually wait.\"</p>\n<h2>DataMap — call APIs without a webhook</h2>\n<p>DataMap executes REST calls on SignalWire's infrastructure. No server on your side required. Good for straightforward integrations.</p>\n<h3>Variable substitution table</h3>\n<p>| Pattern | Expands to |\n|---|---|\n| <code>${args.param}</code> | Function argument value |\n| <code>${enc:args.param}</code> | URL-encoded argument |\n| <code>${lc:args.param}</code> | Lowercased argument |\n| <code>${fmt_ph:args.phone}</code> | Formatted phone number |\n| <code>${response.field}</code> | API response field |\n| <code>${response.arr[0]}</code> | Array element in response |\n| <code>${global_data.key}</code> | Global session data |\n| <code>${this.field}</code> | Current item in a <code>foreach</code> loop |</p>\n<p>Modifiers chain right-to-left: <code>${enc:lc:args.param}</code> lowercases then URL-encodes.</p>\n<h3>GET DataMap</h3>\n<pre><code class=\"language-python\">from signalwire import DataMap, FunctionResult\n\nweather = (\n    DataMap(\"get_weather\")\n    .description(\"Get current weather for a city\")\n    .parameter(\"city\", \"string\", \"City name\", required=True)\n    .webhook(\"GET\", \"https://api.weatherapi.com/v1/current.json?key=API_KEY&#x26;q=${enc:args.city}\")\n    .output(FunctionResult(\n        \"Weather in ${args.city}: ${response.current.condition.text}, ${response.current.temp_f}F\"\n    ))\n    .fallback_output(FunctionResult(\"Sorry, couldn't get weather for ${args.city}.\"))\n)\n\nagent.register_swaig_function(weather.to_swaig_function())\n</code></pre>\n<h3>POST with body and foreach</h3>\n<pre><code class=\"language-python\">search_docs = (\n    DataMap(\"search_docs\")\n    .purpose(\"Search documentation\")\n    .parameter(\"query\", \"string\", \"Search query\", required=True)\n    .webhook(\"POST\", \"https://api.docs.example.com/search\",\n             headers={\"Authorization\": \"Bearer TOKEN\"})\n    .body({\"query\": \"${args.query}\", \"limit\": 3})\n    .foreach({\n        \"input_key\": \"results\",\n        \"output_key\": \"formatted_results\",\n        \"max\": 3,\n        \"append\": \"- ${this.title}: ${this.summary}\\n\",\n    })\n    .output(FunctionResult(\"Found:\\n${formatted_results}\"))\n    .fallback_output(FunctionResult(\"Search unavailable.\"))\n)\n</code></pre>\n<h3>Expression-only DataMap (no HTTP)</h3>\n<p>For pure pattern matching with no API call:</p>\n<pre><code class=\"language-python\">volume = (\n    DataMap(\"set_volume\")\n    .parameter(\"level\", \"string\", \"Volume level\", required=True)\n    .expression(\"${args.level}\", r\"high|loud|up\",   FunctionResult(\"Volume increased\"))\n    .expression(\"${args.level}\", r\"low|quiet|down\", FunctionResult(\"Volume decreased\"))\n    .expression(\"${args.level}\", r\"mute|off\",       FunctionResult(\"Audio muted\"))\n)\n</code></pre>\n<h2>Native functions — built-in to the platform</h2>\n<p>Enable a curated list of platform-provided tools without writing handlers.</p>\n<pre><code class=\"language-python\">agent = AgentBase(name=\"receptionist\", native_functions=[\"check_time\"])\n</code></pre>\n<p>Current native functions include <code>check_time</code> (returns current time in caller's timezone).</p>\n<h2>Anti-patterns</h2>\n<ul>\n<li>Returning plain strings from a handler — must be <code>FunctionResult</code>.</li>\n<li>Mixing <code>web_hook_url</code> cascading — set it once at <code>SWAIG.defaults</code>, override only where needed.</li>\n<li>Setting <code>secure: false</code> for any function that reads or mutates user data — kills the signed-token defense.</li>\n<li>Using DataMap for anything stateful or auth-complex — switch to <code>@tool</code> and own the call.</li>\n<li>Hand-rolling the JSON Schema when type hints would infer it — let the decorator do it.</li>\n</ul>\n<h2>See also</h2>\n<ul>\n<li><a href=\"/topic/signalwire-python-agents-sdk\">Python Agents SDK</a></li>\n<li><a href=\"/topic/swml-ai-verb\">SWML AI verb</a></li>\n<li><a href=\"/topic/swml-data-map\">DataMap deep-dive (Agents SDK)</a></li>\n<li><a href=\"/topic/agents-mcp-gateway\">MCP Gateway federation</a></li>\n</ul>\n"}