> Fetch clean Markdown by appending `.md` to any page URL under https://signalwire.com/docs or requesting it with the HTTP header `Accept: text/markdown`. The root index at https://signalwire.com/docs/llms.txt lists the available documentation indexes. # Tool calling > Connect your AI agent to your backend with tool calls, so that your code owns business logic, live data, and decisions. [best-practices]: /docs/platform/ai/best-practices [prompt-engineering]: /docs/platform/ai/prompt-engineering [swaig-guide]: /docs/swml/guides/swaig [swaig-functions]: /docs/swml/reference/calling/ai/swaig/functions [ai-reference]: /docs/swml/reference/calling/ai [ai-languages]: /docs/swml/reference/calling/ai/languages [sdk-functions]: /docs/server-sdks/guides/defining-functions [sdk-swaig]: /docs/server-sdks/guides/swaig [sdk-datamap]: /docs/server-sdks/guides/data-map [swml-datamap]: /docs/swml/guides/data-map [result-actions]: /docs/server-sdks/guides/result-actions [state-management]: /docs/server-sdks/guides/state-management [contexts-workflows]: /docs/server-sdks/guides/contexts-workflows [toggle-functions]: /docs/swml/guides/toggle-functions [context-switch]: /docs/swml/guides/context-switch [swaig-webhook]: /docs/apis/rest/webhooks/ai-swaig-tool-webhook [swaig-includes]: /docs/swml/reference/calling/ai/swaig/includes [sdk-includes]: /docs/server-sdks/reference/python/agents/agent-base/add-function-include [signature-webhook]: /docs/apis/rest/webhooks/swaig-signature-request Ask a language model what a ride across town costs, with nothing else to go on, and it will give you a number. The number will sound right. That doesn't make it the fare you charge. A model without tools has no way to tell the difference: it will invent an answer rather than admit it doesn't know. Tool calling, also called function calling, closes that gap. A tool can look up or check something, such as the fare to an address or whether a driver is free, and a tool can do something, such as book the pickup, text the receipt, or transfer the call. Either way, the result goes back to the agent as context it can act on and talk about. On SignalWire, tool calls travel over **SWAIG**, the SignalWire AI Gateway, and the tools you define are **SWAIG functions**. SWAIG functions belong to the agent definition, not to a communication channel. The same functions can serve voice calls and text conversations. Channel-specific actions, such as transferring a call or playing audio, apply only when the active channel supports them. This guide follows one agent the whole way through: a dispatcher for a taxi company that quotes fares and books pickups. ## The agent is the front end Think about what you would ask of a new person on your dispatch desk. You would want them to listen well, get the caller to the point, and stay pleasant with someone who is running late. You would not ask them to memorize the fare table, or which drivers are free this afternoon, or how the airport surcharge is calculated. You would point them at the dispatch system and expect them to look it up, every time, because that is where the answer actually lives. An AI agent earns its keep the same way. Let it run the conversation, and leave prices, availability, and formulas in the code that owns them. Tool calling is how you point the agent at that code and tell it when to go looking. A prompt is a suggestion, and code is a constraint: "never give discounts" holds most of the time, while a pricing rule in your handler holds every time. Anything that must be exact, current, or enforced belongs behind a SWAIG function: | The prompt owns | Your code owns | | -------------------------------------------- | --------------------------------------------------------- | | Personality and tone | Business rules and policy | | Understanding what the caller wants | Prices, inventory, and availability | | Extracting details (names, dates, addresses) | Calculations and discounts | | Deciding when to reach for a function | Lookups and side effects (booking, texting, transferring) | The split also keeps rate tables, prices, and policies out of the prompt, where they go stale and get ignored. [Best practices][best-practices] and [prompt engineering][prompt-engineering] cover the prompt side. This guide covers the functions: how they work, how to build one, and how to keep them reliable on real calls. ## How a SWAIG function works A SWAIG function is a named capability you hand to the agent: a **name**, a **description**, and a JSON Schema describing its **parameters**. Each SWAIG request is one HTTP POST carrying a JSON body, answered by one JSON reply. Nothing stays open between requests, and nothing has to be installed on your server. > **Tip** > > SWAIG function descriptions are prompt engineering. > "Quote the fare from the caller's pickup address to their destination" tells the agent what the function does *and* what it needs before calling it. > When an agent picks the wrong function or calls it too early, the description is usually the first thing to fix. The round trip goes from the caller, through the agent, to your server and back: ```mermaid sequenceDiagram autonumber participant Caller participant Agent as AI agent participant Backend as Your backend Caller->>Agent: "How much to get to the airport
from 123 Gough Street?" Note over Agent: Chooses get_quote and extracts
the two addresses from the conversation Agent->>Backend: HTTP POST with the function name
and parsed arguments Note over Backend: Your code runs: geocode, distance
lookup, pricing engine Backend-->>Agent: response text, plus optional actions Agent-->>Caller: "That's $38.60,
and I can have someone there in ten minutes." ``` ### The request and the reply SignalWire POSTs the request to the function's `web_hook_url`. Every request names the function, carries the arguments the agent extracted, and identifies itself with `"content_type": "text/swaig"`; the fields around those describe the call and the session it belongs to. If `web_hook_url` carries credentials in `username:password@host` form, they arrive as HTTP basic authentication. The [AI SWAIG tool webhook][swaig-webhook] documents every field, and the [`SWAIG.functions` reference][swaig-functions] documents the same fields next to the configuration that declares them. A minimal reply: ```json { "response": "The fare to the airport is $38.60. Offer to book a pickup now." } ``` > **Note** > > The `response` is written to the AI, not to the caller. > It can carry data, and it can carry instructions about what the agent should do next. > A reply can also carry `action`, which changes what the conversation does next. See > [steering the conversation](#steering-the-conversation). ### Where the code lives Any endpoint that can host HTTP, accept JSON, and return JSON can serve a SWAIG function, which leaves you two ways to host yours: * **Your own webhook**: a route you write and host, reachable over HTTPS from the public internet — a Flask view, an Express handler, a serverless function behind its own URL. It reads the POST body, runs whatever your business does, and returns the JSON reply. You name it in `web_hook_url`, and nothing SignalWire-specific has to be installed for it to work. * **The Server SDK**: define the function and its handler in one class built on the SDK's Agents namespace (`AgentBase`), and the SDK serves the endpoint for you. See [SWAIG functions in the Server SDK][sdk-functions]. If the function is a straightforward REST call, DataMap describes the request and response mapping declaratively and SignalWire executes it server-side, with no server of yours at all. See DataMap in the [Server SDK][sdk-datamap] or in [SWML][swml-datamap]. ## Declaring your functions Wherever the code lives, the agent needs each function's name, description, and parameters before it can call anything. There are two ways to hand it that declaration: write it into the agent, or point the agent at a URL and let SignalWire fetch it while the agent loads. ### Declaring inline An inline declaration lives with the rest of the agent's configuration, so SignalWire has every signature the moment the agent loads and asks your server nothing. In SWML, each function is an entry in [`SWAIG.functions`][swaig-functions]. With the Server SDK, each is a `define_tool` call, which declares the function and registers the handler that runs it in one step. Declare inline when the functions belong to one application. The whole contract is readable where the agent is configured, and there is no second endpoint to keep available. #### Server SDK (Python) ```python self.define_tool( name="validate_trip", description="Confirm the pickup address and destination the caller gave", parameters={ "type": "object", "properties": { "pickup": { "type": "string", "description": "The pickup address, as the caller said it" }, "destination": { "type": "string", "description": "Where the caller is going, as they said it" } }, "required": ["pickup", "destination"] }, handler=self.validate_trip ) ``` #### SWML ```yaml SWAIG: functions: - function: validate_trip description: Confirm the pickup address and destination the caller gave parameters: type: object properties: pickup: type: string description: The pickup address, as the caller said it destination: type: string description: Where the caller is going, as they said it required: - pickup - destination web_hook_url: https://example.com/validate-trip ``` The [dispatch agent](#a-dispatch-agent) at the end of this guide declares its functions this way, in both forms. ### Declaring remotely A remote declaration leaves the agent holding only a URL. A [`SWAIG.includes`][swaig-includes] entry names that URL and the functions you want from it, and while the agent loads, SignalWire POSTs a [signature request][signature-webhook] there. Your server answers with the full definitions, so the agent learns the signatures from the same server that implements them. The point is that the asking happens up front rather than mid-call. The agent finishes loading knowing exactly what it can do, the same as if you had written the definitions in by hand — the list just arrived from somewhere else. That matters once one toolset serves more than one agent. Say the taxi company runs three: a main line, a corporate-accounts line, and an after-hours line, all of which quote and book. Declared inline, the same definitions are pasted into three configurations, and adding a `passenger_count` parameter means editing all three and redeploying each. Declared remotely, each agent names the dispatch server's URL, the parameter is added once where the handler already lives, and every agent picks it up the next time it loads. #### Server SDK (Python) ```python self.add_function_include( url="https://example.com/swaig", functions=["validate_trip", "book_ride"], meta_data={"fleet_id": "sf-01"} ) ``` #### SWML ```yaml SWAIG: includes: - url: https://example.com/swaig functions: ["validate_trip", "book_ride"] meta_data: fleet_id: sf-01 ``` Your server replies to the signature request with an array of definitions — the same fields an inline declaration carries, plus the `web_hook_url` each function should be called on: ```json [ { "function": "validate_trip", "description": "Confirm the pickup address and destination the caller gave", "parameters": { "type": "object", "properties": { "pickup": { "type": "string", "description": "The pickup address, as the caller said it" }, "destination": { "type": "string", "description": "Where the caller is going, as they said it" } }, "required": ["pickup", "destination"] }, "web_hook_url": "https://example.com/validate-trip" } ] ``` A definition registers only when it has all three of `function`, `description`, and a `web_hook_url`; one that omits any of them is skipped without an error, so check that a function you expected is actually being offered. A shared `web_hook_url` can go in a `defaults` object returned alongside the functions, and a definition that carries a `data_map` needs no URL at all. For every field an entry accepts, see [`SWAIG.includes`][swaig-includes] in SWML and [`add_function_include`][sdk-includes] in the Server SDK. From there the two paths converge. A remotely declared function receives the same request, returns the same `response` and `action`, and steers the call the same way; nothing downstream of the declaration can tell them apart. ## Steering the conversation Alongside `response`, your code can return **actions**: instructions the platform executes during the conversation. Some actions operate on either channel, while others require a live voice call: * **Update conversation state** (`global_data`) that later functions and the prompt can use. * **Move the conversation** to a different step or context, changing which functions are exposed. * **Send an SMS**, such as a confirmation, a link, or a receipt. * **Transfer the call** to a human, a queue, or another agent during a voice call. * **Play audio or execute calling SWML** during a voice call. Your backend decides, and the function result tells the agent what to do about it. When your code books the ride, it can return the pickup time for the agent to read out and an action that texts the caller the driver's name and plate. Every object `action` accepts is documented in the [`SWAIG.functions` reference][swaig-functions]. For the code side, see [`FunctionResult` actions][result-actions] in the Server SDK, and the SWML guides on [switching context][context-switch] and [toggling functions][toggle-functions]. ## Reliability patterns The failure modes below show up once real callers arrive. ### Validate in code Every argument the AI fills in was extracted from spoken, imperfect audio, so treat it as user input. Normalize and verify it in your handler: geocode the address, check the account number's format, ask "Portland, Oregon or Portland, Maine?" when it matters. When validation fails, return a `response` that tells the AI how to recover, such as "No match for that address. Ask the caller to repeat it, street first." ### Keep validated state in `global_data` Once your code has verified something, don't make the AI carry it. Write it to `global_data`, the conversation state that lives with the session, and let downstream functions take **no arguments at all**, reading the validated state instead. The dispatcher does this with a pair of handlers on the agent class. `validate_trip` takes the two addresses as the caller said them, geocodes both, and writes the results to `global_data`. `get_quote` declares no parameters at all and prices the trip from what is already there: ```python class DispatchAgent(AgentBase): # geocode() and price_ride() are the stand-ins defined with the complete agent below def validate_trip(self, args, raw_data): pickup = geocode(args.get("pickup", "")) destination = geocode(args.get("destination", "")) if not pickup: return FunctionResult( "No match for the pickup address. Ask the caller to repeat it, street first." ) if not destination: return FunctionResult( "No match for the destination. Ask the caller to say it another way." ) return FunctionResult( f"Trip confirmed: {pickup} to {destination}. Offer to quote the fare." ).update_global_data({"pickup": pickup, "destination": destination}) def get_quote(self, args, raw_data): state = raw_data.get("global_data", {}) pickup, destination = state.get("pickup"), state.get("destination") if not (pickup and destination): return FunctionResult("No confirmed trip yet. Ask for both addresses first.") fare = price_ride(pickup, destination) return FunctionResult(f"The fare is ${fare:.2f}. Ask if they'd like to book it.") ``` A function with no arguments has no arguments to get wrong. The quote is computed from addresses your code validated, so a creative caller can't talk the agent into a different pickup or a better price. The [dispatch agent](#a-dispatch-agent) at the end of this guide wires both functions into a complete Server SDK application and shows the equivalent SWML agent definition. See [state management][state-management] for the full `global_data` lifecycle. ### Scope functions to each step An agent with every function available at every moment will eventually call one at the wrong time, booking before it quotes or charging before it confirms. Structure multi-stage conversations into steps, and scope which functions are active in each one. The dispatcher can't call `book_ride` before `get_quote` if `book_ride` doesn't exist yet. For more on scoping functions to a step, see [contexts and workflows][contexts-workflows] in the Server SDK, or [toggling functions][toggle-functions] in SWML. ### Cover the wait on voice calls Most lookups take a noticeable moment, and callers hear silence as a dropped call. Give every function that leaves the conversation a filler phrase ("Let me work that out...") or hold audio, so the caller hears a working agent instead of dead air. Fillers play asynchronously; when your endpoint is fast, the caller may never hear them at all. Configure per-function `fillers`, as in the dispatch agent below, and agent-wide [`function_fillers`][ai-languages]. ### Read the post-prompt report Define a `post_prompt` and set a [`post_prompt_url`][ai-reference], and the platform delivers a report after each conversation ends: the summary, the full conversation log, and every function call with its timing. Study the turns around each function call. A mis-picked function or a guessed argument usually traces back to the function's description, which you can revise like any other interface copy. The open-source [post prompt viewer](https://github.com/signalwire/post_prompt_viewer) inspects these reports, with the transcript, telemetry, and latency in one place. In the Server SDK, see [post-prompt data][state-management]. ## A dispatch agent Everything above comes together in one agent. A fare depends on two addresses, the current rate card, and how busy the fleet is, so no prompt could hold it. The examples below confirm the addresses in code before anything is priced, keep them in `global_data` so the quote itself takes no arguments, cover both waits with fillers, and declare both functions inline. #### Server SDK (Python) ```python from signalwire import AgentBase, FunctionResult def geocode(address): # Stand-in for a real geocoding service known = { "123 gough street": "123 Gough St, San Francisco", "456 divisadero": "456 Divisadero St, San Francisco", "the airport": "San Francisco International Airport", } return known.get(address.lower().strip()) def price_ride(pickup, destination): # Stand-in for your pricing engine return 38.60 class DispatchAgent(AgentBase): def __init__(self): super().__init__(name="dispatch-agent") self.add_language("English", "en-US", "rime.spore:coda") self.prompt_add_section( "Role", "You are Ada, the dispatcher for Bayview Taxi. Ask where the caller is and " "where they're going, confirm both with validate_trip, then quote the fare " "with get_quote. Only quote a fare that get_quote returned." ) self.define_tool( name="validate_trip", description="Confirm the pickup address and destination the caller gave", parameters={ "type": "object", "properties": { "pickup": { "type": "string", "description": "The pickup address, as the caller said it" }, "destination": { "type": "string", "description": "Where the caller is going, as they said it" } }, "required": ["pickup", "destination"] }, handler=self.validate_trip, fillers={"en-US": ["Let me check those addresses..."]} ) # get_quote takes no arguments: it reads state your code already verified self.define_tool( name="get_quote", description="Quote the fare. Only call after validate_trip has confirmed both addresses.", parameters={"type": "object", "properties": {}}, handler=self.get_quote, fillers={"en-US": ["Let me work that out..."]} ) def validate_trip(self, args, raw_data): pickup = geocode(args.get("pickup", "")) destination = geocode(args.get("destination", "")) if not pickup: return FunctionResult( "No match for the pickup address. Ask the caller to repeat it, street first." ) if not destination: return FunctionResult( "No match for the destination. Ask the caller to say it another way." ) return FunctionResult( f"Trip confirmed: {pickup} to {destination}. Offer to quote the fare." ).update_global_data({"pickup": pickup, "destination": destination}) def get_quote(self, args, raw_data): state = raw_data.get("global_data", {}) pickup, destination = state.get("pickup"), state.get("destination") if not (pickup and destination): return FunctionResult("No confirmed trip yet. Ask for both addresses first.") fare = price_ride(pickup, destination) return FunctionResult( f"The fare from {pickup} to {destination} is ${fare:.2f}. " "Ask whether they'd like a pickup now." ) if __name__ == "__main__": agent = DispatchAgent() agent.run() ``` #### SWML ```yaml version: 1.0.0 sections: main: - ai: prompt: text: | You are Ada, the dispatcher for Bayview Taxi. Ask where the caller is and where they're going, confirm both with validate_trip, then quote the fare with get_quote. Only quote a fare that get_quote returned. SWAIG: functions: - function: validate_trip description: Confirm the pickup address and destination the caller gave parameters: type: object properties: pickup: type: string description: The pickup address, as the caller said it destination: type: string description: Where the caller is going, as they said it required: - pickup - destination fillers: en-US: - Let me check those addresses... web_hook_url: https://example.com/validate-trip - function: get_quote description: Quote the fare. Only call after validate_trip has confirmed both addresses. parameters: type: object properties: {} fillers: en-US: - Let me work that out... web_hook_url: https://example.com/get-quote ``` In SWML, `web_hook_url` names the server you run; with the Server SDK, the class *is* the server, and the SDK hosts the endpoint for you. The Server SDK tab is a complete local application. The SWML tab is the equivalent agent definition; replace its example webhook URLs with public endpoints that implement `validate_trip` and `get_quote` before you run it. For the SDK side, see [SWAIG functions in the Server SDK][sdk-functions] and [SWAIG request handling][sdk-swaig]. For SWML, see the [SWAIG guide][swaig-guide] and the [`SWAIG.functions` reference][swaig-functions]. `geocode` and `price_ride` stand in for the services you already run: an address lookup, a distance API, a rate table, a surge multiplier. Swap them out and nothing else changes. When an address doesn't resolve, the agent asks for it again, because that is what your code returned. You can exercise a function before any call. The SDK's [`swaig-test` CLI](/docs/server-sdks/reference/python/agents/cli/swaig-test) loads the agent file, executes a function with arguments you supply, and prints the exact response the agent would receive: ```bash swaig-test dispatch_agent.py --exec validate_trip --pickup "123 Gough Street" --destination "the airport" ``` To hear it on a real call, run the Python file and point a phone number at your agent (the [Server SDK quickstart](/docs/server-sdks/guides/quickstart) walks through it), or paste the SWML into your Dashboard as shown in the [AI quickstart](/docs/platform/ai/quickstart). ## Next steps #### [SWAIG guide](/docs/swml/guides/swaig) The request/response contract between the platform and your server. #### [SWAIG reference](/docs/swml/reference/calling/ai/swaig) Every SWAIG configuration option in SWML. #### [Server SDK functions](/docs/server-sdks/guides/defining-functions) Define SWAIG functions and handlers in the language of your choice. #### [DataMap](/docs/server-sdks/guides/data-map) Call REST APIs from SWAIG functions with no server of your own. > The AI runs the conversation. Your code runs the business.