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