T
Telephony SOPKnowledge Base
Search
← All topics

SWAIG Functions — Live AI Tool Calls

runnable

SignalWire AI Gateway functions — JSON schema definitions, the webhook contract, FunctionResult return type, secure tokens, native functions, and the DataMap server-side passthrough alternative.

signalwireswaigai-toolsswmlfunction-calling
Agent trigger phrases: what is SWAIG · SignalWire AI function · register SWAIG function · SWAIG webhook payload · DataMap vs SWAIG · FunctionResult · AI tool call in SWML

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_url cascading — set it once at SWAIG.defaults, override only where needed.
  • Setting secure: false for any function that reads or mutates user data — kills the signed-token defense.
  • Using DataMap for anything stateful or auth-complex — switch to @tool and own the call.
  • Hand-rolling the JSON Schema when type hints would infer it — let the decorator do it.

See also