
Subscribe
Tags
Developer Tutorials
Latency & Performance
AI Agents
Voice AI
SignalWire Server SDK · SWML · SWAIG
The agent does the talking. Your code runs the show.
A SignalWire AI agent works like a stage play. SignalWire is the theater, the AI agent is the actor, and your code is the director. This guide explains the whole production from the beginning. Each chapter opens with the idea in plain words, then shows the real documents and code that cross the wire, so you can stop at whatever depth you need.
- When a call comes in, SignalWire asks your code for a script (SWML) that says who the agent is, how it should behave, and which tools it may use.
- The agent talks with the caller, one scene (step) at a time.
- When the agent needs a fact or needs something done, SignalWire sends your code a note (a SWAIG request).
- Your code writes back a stage direction for the agent, plus actions for SignalWire to carry out.
- The agent reads your direction and says it to the caller in its own words, while the actions happen exactly as you wrote them.
Chapter 1 · The cast
Three players, with SignalWire in the middle
The caller is the audience. SignalWire is the theater: it owns the phone lines, the microphones and the speakers, and the actor lives there. The actor is the AI agent, who talks to the caller. Your code is the director. It never goes on stage, but it writes the script and makes every decision that matters.
RememberEverything passes through SignalWire. That is where the rules you write are enforced.
Chapter 2 · The script
SWML is the script, and your code writes it
Before the show starts, the theater asks the director, “What do I do with this call?” The director hands back a script. The script is a list of instructions read from top to bottom: pick up, record, start the agent with this personality, these scenes and these tools, then hang up. That script is called SWML.
A tiny script, line by line
Each instruction is called a verb. SignalWire starts at the top and works down.
{
"version": "1.0.0",
"sections": {
"main": [
Every script has a version and a main section. main is a list of verbs, and SignalWire starts at the top.
{ "answer": {} },
Pick up the phone.
{ "record_call": { "format": "wav" } },
Record the call. Leave this verb out and nothing is recorded.
{ "ai": {
"prompt": {
"text": "You are a support agent for Acme."
},
Start the AI agent. The prompt tells the actor who it is and how to behave.
"languages": [
{ "name": "English",
"code": "en-US",
"voice": "elevenlabs.rachel" }
],
Which language it speaks and which voice it uses.
"SWAIG": {
"defaults": {
"web_hook_url": "https://you.example/swaig"
},
"functions": [
{ "function": "lookup_order",
"description": "Look up an order by number" }
]
}
} },
The tools the actor may ask for, and the URL those requests go to. That URL is your own code, which is how the script and the backstage notes connect.
{ "hangup": {} }
]
}
}
End the call when the agent is finished.
SWML is more than AI
The ai verb is one verb among many. A plain phone menu with no AI at all is also SWML, and an AI agent can use the other verbs before, after, or in the middle of a conversation.
- Speak and listen
playpromptrecordPlay audio or speech, collect key presses or spoken answers, take a message.- Connect people
connecttransferRing one number, several at once, or one after another. Hand the call to a different script.- Make decisions
ifswitchcondgotoBranch on what the caller pressed or said. Loop back to a menu.- Reach other systems
requestsend_smsCall a web API and keep the result. Send a text message.- Run an AI agent
aiStart a governed conversation with a prompt, tools, steps and a voice.
It says what, not how
You list the steps. SignalWire handles the audio, the timing and the phone network.
It runs in order
Verbs run top to bottom. A script can also branch, loop, and call other sections like small subroutines.
The SignalWire Server SDK writes it for you
Your agent is a short Python class. The SignalWire Server SDK turns it into SWML each time SignalWire asks, so you rarely type SWML by hand.
The details
The full script for a real call, and the code that writes it
Meet Dana, who will call about order 8841 throughout this guide. Below is the complete SWML document SignalWire receives when Dana calls. Under it is the SignalWire Server SDK class that produces it and also answers every note in Chapter 6. You write the class. The SDK turns it into SWML fresh for every call.
{
"version": "1.0.0",
"sections": {
"main": [
{
"answer": {}
},
{
"record_call": {
"format": "wav",
"stereo": true
}
},
{
"ai": {
"prompt": {
"text": "You are Ava, a friendly support agent for Acme. Keep replies short and conversational. Only share order details that a tool returned.",
"contexts": {
"default": {
"steps": [
{
"name": "verify",
"text": "Greet the caller. Ask for their order number and ZIP code, then call verify_caller.",
"step_criteria": "verify_caller has confirmed the caller.",
"functions": [
"verify_caller"
],
"valid_steps": [
"help"
]
},
{
"name": "help",
"text": "The caller is ${global_data.caller_name}. Answer questions about their orders. Use lookup_order for any order status. For refunds, offer transfer_to_human.",
"step_criteria": "The caller has no more questions.",
"functions": [
"lookup_order",
"transfer_to_human"
],
"valid_steps": [
"wrap_up"
]
},
{
"name": "wrap_up",
"text": "Ask if there is anything else, then say goodbye.",
"functions": []
}
]
}
}
},
"post_prompt": {
"text": "Summarize the call as JSON with order_id and outcome."
},
"post_prompt_url": "https://user:pass@you.example/post_prompt",
"languages": [
{
"name": "English",
"code": "en-US",
"voice": "elevenlabs.rachel",
"function_fillers": [
"Let me check on that."
]
}
],
"hints": [
"Acme",
"UPS"
],
"global_data": {
"refund_limit": 50
},
"SWAIG": {
"defaults": {
"web_hook_url": "https://user:pass@you.example/swaig"
},
"functions": [
{
"function": "verify_caller",
"description": "Verify the caller with their order number and ZIP code. Call before sharing any order details.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "Order number, digits only"
},
"zip": {
"type": "string",
"description": "5-digit billing ZIP code"
}
},
"required": [
"order_id",
"zip"
]
}
},
{
"function": "lookup_order",
"description": "Look up an order's shipping status by order number.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "Order number, digits only"
}
},
"required": [
"order_id"
]
},
"meta_data_token": "orders",
"fillers": {
"en-US": [
"One moment while I pull that up."
]
}
},
{
"function": "transfer_to_human",
"description": "Transfer the caller to a billing specialist. Use for refunds or when the caller asks for a person.",
"parameters": {
"type": "object",
"properties": {}
}
}
]
}
}
},
{
"hangup": {}
}
]
}
}from signalwire import AgentBase, FunctionResult ACME_NUMBER = "+15125550100" # the number Acme texts from BILLING_NUMBER = "+15125550199" # where refund requests go # Stand-in for your order system ORDERS = { "8841": {"name": "Dana", "zip": "78701", "shipped": "Tuesday", "carrier": "UPS", "eta": "Thursday", "tracking_url": "https://acme.example/t/1Z999"}, } class AcmeSupport(AgentBase): def __init__(self): super().__init__(name="acme-support", route="/", record_call=True, record_format="wav", record_stereo=True) # Base prompt, voice, recognition hints, starting state, post-call summary self.set_prompt_text( "You are Ava, a friendly support agent for Acme. Keep replies short and " "conversational. Only share order details that a tool returned." ) self.add_language("English", "en-US", "elevenlabs.rachel", function_fillers=["Let me check on that."]) self.add_hints(["Acme", "UPS"]) self.set_global_data({"refund_limit": 50}) self.set_post_prompt("Summarize the call as JSON with order_id and outcome.") # Steps: each one lists exactly which tools exist while it is active ctx = self.define_contexts().add_context("default") ctx.add_step("verify") \ .set_text("Greet the caller. Ask for their order number and ZIP code, " "then call verify_caller.") \ .set_step_criteria("verify_caller has confirmed the caller.") \ .set_functions(["verify_caller"]) \ .set_valid_steps(["help"]) ctx.add_step("help") \ .set_text("The caller is ${global_data.caller_name}. Answer questions about " "their orders. Use lookup_order for any order status. For refunds, " "offer transfer_to_human.") \ .set_step_criteria("The caller has no more questions.") \ .set_functions(["lookup_order", "transfer_to_human"]) \ .set_valid_steps(["wrap_up"]) ctx.add_step("wrap_up") \ .set_text("Ask if there is anything else, then say goodbye.") \ .set_functions([]) # Tools: name, description and parameters become SWAIG.functions in the SWML self.define_tool( name="verify_caller", description="Verify the caller with their order number and ZIP code. " "Call before sharing any order details.", parameters={ "type": "object", "properties": { "order_id": {"type": "string", "description": "Order number, digits only"}, "zip": {"type": "string", "description": "5-digit billing ZIP code"}, }, "required": ["order_id", "zip"], }, handler=self.verify_caller, ) self.define_tool( name="lookup_order", description="Look up an order's shipping status by order number.", parameters={ "type": "object", "properties": { "order_id": {"type": "string", "description": "Order number, digits only"}, }, "required": ["order_id"], }, handler=self.lookup_order, fillers={"en-US": ["One moment while I pull that up."]}, meta_data_token="orders", ) self.define_tool( name="transfer_to_human", description="Transfer the caller to a billing specialist. Use for refunds " "or when the caller asks for a person.", parameters={"type": "object", "properties": {}}, handler=self.transfer_to_human, ) # ---- Handlers: each one receives the hop 3 POST and returns the hop 4 reply ---- def verify_caller(self, args, raw_data): order = ORDERS.get(args.get("order_id", "").strip()) if not order or order["zip"] != args.get("zip", "").strip(): return FunctionResult( "Those details don't match an order. Ask the caller to repeat " "the order number and ZIP code." ) return ( FunctionResult(f"Verified. The caller is {order['name']}. " "Ask how you can help with their order.") .update_global_data({"caller_name": order["name"], "verified": True}) .swml_change_step("help") ) def lookup_order(self, args, raw_data): if not raw_data.get("global_data", {}).get("verified"): return FunctionResult("The caller is not verified. Verify them first.") order_id = args.get("order_id", "").strip() order = ORDERS.get(order_id) if not order: return FunctionResult( f"No order {order_id} found. Ask the caller to read the number " "again, one digit at a time." ) return ( FunctionResult( f"Order {order_id} shipped {order['shipped']} by {order['carrier']} " f"and is due {order['eta']}. Tell the caller it shipped and when it " "arrives, and that a tracking link is on its way by text. " "Do not read the tracking number aloud." ) .update_global_data({"last_order": order_id}) .send_sms(to_number=raw_data.get("caller_id_num"), from_number=ACME_NUMBER, body=f"Track order {order_id}: {order['tracking_url']}") ) def transfer_to_human(self, args, raw_data): return ( FunctionResult("Transferring to a billing specialist. Tell the caller " "and say goodbye.", post_process=True) .connect(BILLING_NUMBER, final=True) ) # ---- Receives the post_prompt POST when the call ends ---- def on_summary(self, summary, raw_data=None): call_id = raw_data.get("call_id") if raw_data else "unknown" print(f"Call {call_id} summary: {summary}") if __name__ == "__main__": AcmeSupport().run()
Things to notice: each step lists only the tools it allows, so lookup_order doesn’t exist until the caller is verified. refund_limit sits in global_data, which your webhook receives but the agent never sees. caller_name isn’t known yet. verify_caller saves it, and the agent hears it only because the help step’s text expands ${global_data.caller_name}.
set_prompt_textai.prompt.textdefine_contexts().add_context().add_step()ai.prompt.contexts.default.stepsadd_language, add_hintsai.languages, ai.hintsset_global_dataai.global_datadefine_toolan entry in ai.SWAIG.functions, routed to the handlerset_post_prompt, on_summaryai.post_prompt and post_prompt_urlFunctionResult(...)the reply: response.update_global_data, .send_sms, .swml_change_step, .connectentries in actionpost_process=Truepost_process: trueThe constructor declares the prompt, voice, steps and tools. Each handler receives the hop 3 POST and returns the hop 4 reply. on_summary receives the post-call report. The SDK also serves the /swaig and /post_prompt endpoints, protects them with basic auth, and adds a per-call security token to every tool URL, so the SWML it actually renders carries those URLs and tokens where the SWML above shows placeholders.
Run the class with python acme_support.py and point a SignalWire phone number at its URL. To test a handler without placing a call, run swaig-test acme_support.py --exec lookup_order --order_id 8841 --custom-data '{"global_data": {"verified": true}}'. ORDERS and the phone numbers are stand-ins for your own systems.
Two habits from the walkthrough show up here. lookup_order checks verified in global_data even though the step rules already hide it, because args and steps are not a security boundary. And verify_caller moves the call to help with swml_change_step, so your code decides when verification has passed, not the agent.
RememberThe script is the whole plan for a call in one document. It is written fresh for every call, so one agent can treat each caller differently.
Chapter 3 · Two doors
Inbound and outbound calls enter the same room
Someone can call you, or your code can ask SignalWire to call them. Either way, SignalWire fetches the same script from the same place, and the show runs the same way from there.
RememberOnly the first ring is different.
Chapter 4 · Scenes
One scene on stage at a time, each with its own props
A play is split into scenes. In each scene the actor gets only that scene’s directions and only that scene’s props. If a prop isn’t in the scene, the actor can’t use it, no matter what the audience says. On SignalWire, scenes are called steps, and the props are tools.
- One step at a time. While Help is active, the agent has Help’s instructions and nothing else.
- Missing tools are absent, not locked. During Verify there is no order lookup for the agent to find or be talked into using.
- The agent moves only along paths you allowed. From Verify it can ask to go to Help. It cannot skip ahead.
- Your code can move the show anywhere. In Dana’s agent,
verify_callermoves the call to Help itself, so your code decides when verification has passed.
The details
In Dana’s SWML (Chapter 2), each step has text (its instructions), functions (the only tools that exist in it), valid_steps (where the agent may go next) and step_criteria (a plain-language hint about when it is done). In the SDK these are set_text, set_functions, set_valid_steps and set_step_criteria.
Two things catch people out. First, step_criteria steers the agent but is not a hard gate, so anything that must be true is checked in your code. Second, a step with no functions list does not get “no tools.” It keeps the previous step’s tools, so set functions on every step, even if it is an empty list.
RememberThe step rules limit the agent. They never limit your code.
Chapter 5 · The director’s clipboard
Your code keeps the facts and shows the agent only what it needs
The director carries a clipboard with private notes about this performance. The actor never reads the clipboard. The director decides which facts to pass on. On SignalWire the clipboard is called global_data, and it lasts for the whole call.
- caller_name
- Dana shown
- verified
- yes shown
- order
- #8841, shipped Tuesday shown
- risk_flag
- low backstage
- refund_limit
- $50 backstage
The caller is Dana and is verified. Talk about order #8841 only. It shipped Tuesday.
The agent never learns the risk flag or the refund limit, so it cannot repeat them or be argued out of them.
The details
- Starts in the script.
ai.global_datain SWML, orset_global_data()in the SDK. Dana’s starts withrefund_limit: 50. - Arrives with every note. Each tool request includes the current
global_data, so your code always has it (Chapter 6, hop 3). - Changes through actions. A reply can add or change values with
set_global_data, or.update_global_data()in the SDK. - Reaches the agent only when you say so. Writing
${global_data.caller_name}in a step’s text is how Dana’s name gets to the agent.
RememberThe agent can’t leak or invent what it never saw.
Chapter 6 · Notes passed backstage
SWAIG: how the agent asks your code for help
The actor is good with words but doesn’t know where anyone’s order is. When it needs a fact, it asks for one of the tools listed in the script. SignalWire writes that request on a note and passes it backstage to your code. Your code writes a note back. The caller never sees the notes. They just hear a short “one moment” while the notes travel. This note-passing system is called SWAIG, the SignalWire AI Gateway, and each tool is a SWAIG function.
Dana is verified and asks where their order is. One question makes six hops. Your server is contacted exactly once, in hops 3 and 4.
The details, hop by hop
-
1
The caller speaks
Caller → SignalWire“Hi, where’s my order? The number is eighty-eight forty-one.”
SignalWire handles the audio, detects when the caller has finished, and transcribes what they said.
-
2
The agent decides it needs your tool
inside SignalWireThe agent can’t know where an order is, so it reaches for a tool. It can only choose from the functions declared in the SWML from Chapter 2 that are active in the current step, here
lookup_orderandtransfer_to_humanin thehelpstep. The declaration is all it has to go on: thedescriptiontells it when to use the tool, andparameterstells it which arguments to pull from what the caller said.While the request goes out, SignalWire plays one of the
fillers(or await_file) so the caller doesn’t hear silence. -
3
SignalWire POSTs the tool call to your webhook
SignalWire → your webhookOne HTTP POST to the function’s
POST https://you.example/swaig · bodyweb_hook_url, with basic auth if credentials are set. One POST and one JSON reply. No connection stays open.{ "content_type": "text/swaig", "content_disposition": "SWAIG Function", "version": "2.0", "function": "lookup_order", "description": "Look up an order's shipping status by order number.", "argument": { "parsed": [ { "order_id": "8841" } ], "raw": "{\"order_id\":\"8841\"}" }, "argument_desc": { "type": "object", "properties": { "order_id": { "type": "string" } } }, "call_id": "3f1c…-call", "ai_session_id": "9a2e…-session", "app_name": "acme-support", "conversation_type": "voice", "channel_active": true, "channel_offhook": true, "channel_ready": true, "caller_id_name": "DANA R", "caller_id_num": "+15125550142", "project_id": "…", "space_id": "…", "global_data": { "caller_name": "Dana", "verified": true, "refund_limit": 50 }, "meta_data_token": "orders", "meta_data": {}, // only if swaig_post_conversation is on: "call_log": [ … redacted conversation so far … ], "raw_call_log": [ … unredacted … ], // only if swaig_post_swml_vars is on: "SWMLVars": { … }, "SWMLCall": { … } }The highlighted fields are the ones most handlers use: the arguments the agent pulled from the conversation, and the state your code saved earlier. Treat
Reading this POST in the SignalWire Server SDK (Python)argumentas untrusted input. It came from speech, and SignalWire does not enforce your schema on it. Theversionvalue is only an example.The SDK receives the POST, checks basic auth and the per-call token, and calls your handler with two arguments.
argsisargument.parsed[0], already pulled out.raw_datais the entire body above, as a Python dict.def lookup_order(self, args, raw_data): # The agent's arguments. The SDK pulls these out of argument.parsed[0] for you. order_id = args.get("order_id", "").strip() # "8841" raw_args = raw_data["argument"]["raw"] # '{"order_id":"8841"}' # Which call and session this is call_id = raw_data.get("call_id") # same on every POST for this call session = raw_data.get("ai_session_id") channel = raw_data.get("conversation_type") # "voice" or "chat" caller = raw_data.get("caller_id_num") # "+15125550142" call_is_up = raw_data.get("channel_active", False) # State your code saved earlier with update_global_data() state = raw_data.get("global_data", {}) if not state.get("verified"): return FunctionResult("The caller is not verified. Verify them first.") refund_limit = state.get("refund_limit") # 50, never shown to the agent # State shared only by functions with the same meta_data_token ("orders") orders_meta = raw_data.get("meta_data", {}) # Only present when you turn them on in the ai params history = raw_data.get("call_log", []) # swaig_post_conversation swml_vars = raw_data.get("SWMLVars", {}) # swaig_post_swml_vars ... # look up the order, then return the hop 4 reply
Use
.get()with a default for everything exceptfunctionandargument. Fields likecaller_id_num,global_dataandcall_logare optional and may be missing.If you prefer typed handlers, the SDK builds the parameter schema from the function signature and docstring. Each argument arrives as a keyword argument, and
raw_datais still available when you include it in the signature:@AgentBase.tool(name="lookup_order") def lookup_order(self, order_id: str, raw_data=None): """Look up an order's shipping status by order number. Args: order_id: Order number, digits only. """ state = (raw_data or {}).get("global_data", {}) ...
-
4
Your webhook replies: direction for the agent, actions for SignalWire
your webhook → SignalWireYour code does the real lookup and returns one JSON object. Every field is optional, so
JSON on the wire{}is a valid reply.{ "response": "Order 8841 shipped Tuesday by UPS and is due Thursday. Tell the caller it shipped and when it arrives, and that a tracking link is on its way by text. Do not read the tracking number aloud.", "action": [ { "set_global_data": { "last_order": "8841" } }, { "SWML": { "version": "1.0.0", "sections": { "main": [ { "send_sms": { "to_number": "+15125550142", "from_number": "+15125550100", "body": "Track order 8841: https://acme.example/t/1Z999" } } ] } } } ] // no "post_process" key: it is only sent when true }SignalWire Server SDK (Python)return ( FunctionResult( "Order 8841 shipped Tuesday by UPS and is due Thursday. Tell the caller " "it shipped and when it arrives, and that a tracking link is on its way " "by text. Do not read the tracking number aloud." ) .update_global_data({"last_order": "8841"}) # -> set_global_data action .send_sms(to_number="+15125550142", # -> SWML send_sms action from_number="+15125550100", body="Track order 8841: https://acme.example/t/1Z999") ) # post_process defaults to False
Read the
responseclosely. It holds a fact (shipped Tuesday, due Thursday), an instruction (say it shipped, mention the text) and a restriction (do not read the number). None of it is phrased as the sentence the caller will hear. -
5
SignalWire runs the actions, and the agent writes its reply
inside SignalWireWith
post_process: false, the actions run first.last_orderis merged intoglobal_dataand the SMS goes out. The agent has no say in either.Then the agent takes its turn. It reads your
responseas the tool’s result, together with:- the persona and rules in your prompt
- the instructions for the current step
- everything the caller has said so far, including their name, language and mood
From all of that it writes a reply. If your response points it at another tool, it can call that tool first, and hops 2 to 4 repeat before anything is spoken.
-
6
The caller hears the agent’s words
SignalWire → caller“Good news, Dana. Your order shipped on Tuesday and should be with you Thursday. I’ve just texted you a tracking link.”
Compare this to the
responsein hop 4. They share facts but almost no words. The gap between them is the agent at work.
RememberThe agent asks. Your code answers the agent. The agent answers the caller.
Chapter 7 · Stage directions, not lines
Your response is a note to the actor, not a line to read out
When the director passes a note back, it doesn’t say “read this out loud.” It says something like “It shipped Tuesday. Tell them, and keep it short.” The actor then says it in their own words, in the caller’s language, in the tone of the conversation. That is exactly how response works.
webhook returns response → SignalWire speaks it word for word
This would make your webhook the voice of the agent. It is not.
webhook returns response → the agent reads it as the tool’s result → the agent writes a reply in its own words → SignalWire speaks that reply
SignalWire’s docs say it directly: “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.”
Every reply has two slots
A note to the actor
“Order 8841 shipped Tuesday, due Thursday. Tell the caller, and mention the tracking text.”
Goes to the agent, which reads it and decides what to say.
What the theater does
Carried out by SignalWire exactly as your code wrote them. The agent has no say.
Proof: one response, different lines
{
"response": "Order 8841 shipped Tuesday, due Thursday. Tell the caller, and mention the tracking text."
}
SignalWire Server SDK (Python)
return FunctionResult( "Order 8841 shipped Tuesday, due Thursday. " "Tell the caller, and mention the tracking text." )
“Su pedido salió el martes y llega el jueves. Le acabo de enviar el enlace de seguimiento.”
“Shipped Tuesday, arriving Thursday. Tracking link’s in your texts.”
“It shipped Tuesday and arrives Thursday. Now, about changing the address…”
The details: writing a good response
A good response gives the agent facts it can rely on and tells it what to do with them. A plain string is passed to the agent as it is. The object form splits those two jobs. tool_result holds the factual status, and tool_prompt holds the instruction, written as “you” statements.
{ "response": {
"tool_result": "Order 8841: shipped Tuesday via UPS, ETA Thursday. Tracking SMS sent.",
"tool_prompt": "Tell the caller it shipped and when it arrives. Say a tracking link was texted. Do not read the tracking number."
} }
SignalWire Server SDK (Python)
return FunctionResult().set_tool_response( tool_result="Order 8841: shipped Tuesday via UPS, ETA Thursday. Tracking SMS sent.", tool_prompt="Tell the caller it shipped and when it arrives. " "Say a tracking link was texted. Do not read the tracking number.", )
Leave tool_prompt out for a silent result that only reports status.
{
"response": "Hello! Thank you so much for calling Acme! Your order has shipped!"
}
SignalWire Server SDK (Python)
return FunctionResult("Hello! Thank you so much for calling Acme! Your order has shipped!")
The agent rephrases it anyway, may greet the caller a second time, and has no arrival date to give if asked.
{
"response": "Shipped Tuesday, ETA Thursday. Confirm both to the caller. They are already greeted."
}
SignalWire Server SDK (Python)
return FunctionResult( "Shipped Tuesday, ETA Thursday. Confirm both to the caller. " "They are already greeted." )
Facts plus the next move. The persona in your prompt supplies the warmth.
{
"response": "{\"status\":\"S\",\"carrier\":\"UPS\",\"trk\":\"1Z999…\",\"eta\":1760054400,\"wh\":\"AUS-3\"}"
}
SignalWire Server SDK (Python)
order = orders_api.get("8841") return FunctionResult(json.dumps(order)) # raw record passed straight through
The agent has to guess what the codes mean, may misread the Unix time, and might say the warehouse ID out loud.
{
"response": "Shipped by UPS. Arrives Thursday, October 9."
}
SignalWire Server SDK (Python)
order = orders_api.get("8841") eta = datetime.fromtimestamp(order["eta"]).strftime("%A, %B %-d") return FunctionResult(f"Shipped by {order['carrier']}. Arrives {eta}.")
Translate codes in your own code. The agent can’t repeat anything it never received.
no usable response: the agent gets a generic failure
SignalWire Server SDK (Python)
order = orders_api.get(order_id) # raises: order system is down return FunctionResult(f"Order {order_id} shipped...") # never reached
The SignalWire Server SDK catches the exception and the agent gets a generic failure message, never the error text. It has to improvise an apology with no plan.
{
"response": "No order 8841 found. Ask the caller to read the number again, one digit at a time."
}
SignalWire Server SDK (Python)
order = ORDERS.get(order_id) if not order: return FunctionResult( f"No order {order_id} found. Ask the caller to read the number " "again, one digit at a time." )
Failures are directions too. Tell the agent what to do next.
RememberWrite response for the actor, not the audience: what is true, and what to do next.
Chapter 8 · Things the theater does
Actions make things happen, whatever the agent says
Some things are too important to leave to the actor: sending a text, moving to another scene, transferring the call, taking a payment. Your code puts those in the action list, and SignalWire does them exactly as written. A clumsy word from the agent can never turn into a wrong action.
- Remember things
- Save or remove facts on the clipboard.
update_global_data·remove_global_data·set_metadata - Change the scene
- Jump to any step or context, even ones the agent couldn’t reach itself.
swml_change_step·swml_change_context - Move the call
- Transfer, hand off to another agent, hold, or hang up.
connect·swml_transfer·sip_refer·hold·hangup - Sound and media
- Speak exact words, play audio, record.
say·play_background_file·record_call - Reach beyond the call
- Send a text, update an app the caller is looking at, run any SWML.
send_sms·swml_user_event·execute_swml - Take a payment
- Collect card details with the agent never seeing them.
pay - Tune the agent
- Turn tools on or off, adjust timing and recognition hints mid-call.
toggle_functions·set_end_of_speech_timeout·add_dynamic_hints
Names shown are SignalWire Server SDK FunctionResult methods. Each one adds an entry to the action list in the JSON reply.
The details: exact words
You can ask for specific wording in tool_prompt (“Read this exact amount: $38.60”), and the agent follows that well. It is still the agent choosing the words. When text must be spoken exactly as written, such as a legal disclosure, use the say action.
{ "response": "The recording disclosure was read to the caller. Ask if they agree before continuing.",
"action": [ { "say": "This call may be recorded for quality and training purposes." } ] }
SignalWire Server SDK (Python)
return ( FunctionResult("The recording disclosure was read to the caller. " "Ask if they agree before continuing.") .say("This call may be recorded for quality and training purposes.") )
Rule of thumbresponse tells the agent what happened and what to do. action makes things happen, including saying exact words.
The details: timing
By default, actions run as soon as your reply arrives, and the agent speaks afterward. With post_process: true, the agent speaks first and then the actions run. Use it when the caller should hear something before an action that changes the call, such as a transfer.
post_process: true, and the response “Transferring to billing. Tell the caller and say goodbye.” The caller hears the agent say goodbye, and then the transfer starts. Without post_process, the transfer can start before the agent has said anything.{
"response": "Transferring to a billing specialist. Tell the caller and say goodbye.",
"action": [
{
"SWML": {
"version": "1.0.0",
"sections": {
"main": [
{
"connect": {
"to": "+15125550199"
}
}
]
}
},
"transfer": "true"
}
],
"post_process": true
}
SignalWire Server SDK (Python)
return ( FunctionResult("Transferring to a billing specialist. Tell the caller " "and say goodbye.", post_process=True) .connect("+15125550199", final=True) )
Chapter 9 · The whole show
Dana’s call, start to finish
Here is every piece working together on one phone call.
- CallerThe phone ringsDana calls Acme’s support number.
- Your codeThe script is handed overSignalWire asks your code what to do. The SignalWire Server SDK returns Dana’s SWML (Chapter 2).
- SignalWireCurtain upSignalWire answers, starts recording, and starts the agent in the Verify step. The only tool that exists is
verify_caller. - Your codeVerificationDana gives their order number and ZIP code. The agent calls
verify_caller. Your code checks them, saves their name and “verified” on the clipboard, and moves the show to Help. - Your codeThe lookupDana asks where their order is. The agent calls
lookup_order. Your code replies with a direction (“shipped Tuesday, due Thursday, mention the text”) and two actions: savelast_orderand send the tracking text. - SignalWireWords and action togetherThe text goes out. The agent reads your direction and says, in its own words, that the order shipped Tuesday and the link is on its way.
- Your codeA decision the agent doesn’t makeDana asks for a refund. The agent calls
transfer_to_human. Your code replies withpost_processand a transfer, so the agent says goodbye first and then SignalWire transfers the call. - Your codeAfter the callSignalWire sends your code a summary of the conversation. In the SDK it arrives in
on_summary.
Chapter 10 · Reference
Every request, every field, every term
Every HTTP exchange an AI call can make
| When | Request | You return |
|---|---|---|
| Call starts | SignalWire requests your SWML URL, sending call details | The SWML document: prompt, steps, SWAIG.functions, webhook URLs |
| Agent loads | Signature request to each SWAIG.includes URL | Tool definitions (name, description, parameters, web_hook_url) |
| Each tool call | POST text/swaig to the function’s web_hook_url (hop 3) | response, action, post_process (hop 4) |
| DataMap tools | No request to you. SignalWire calls the API itself and builds the reply from your template | Nothing at call time. The template is in the SWML |
| Call ends | POST to post_prompt_url | Nothing required. The body holds the summary, the conversation log and every tool call with timing |
The tool webhook, field by field
| Field | Direction | What it is |
|---|---|---|
| Request: what the agent asked for | ||
function | in | Name of the function the agent called |
argument.parsed[0] | in | The agent’s arguments as an object. The SignalWire Server SDK passes this to your handler as args |
argument.raw | in | The arguments as the raw string the agent produced |
argument_desc, description | in | Your own schema and description, sent back to you |
| Request: which call and session | ||
call_id, ai_session_id | in | Stay the same for the whole call and AI session. On chat, call_id is the conversation ID |
conversation_type | in | voice or chat. Call-only actions do nothing on chat |
channel_active / offhook / ready | in | Whether the call is up, answered, and able to take actions |
caller_id_name / num, project_id, space_id, app_name | in | Who is calling, and which project and app this is |
| Request: state | ||
global_data | in | Current session state. The agent never sees it unless your prompt text expands a value |
meta_data, meta_data_token | in | State shared only among functions with the same token |
call_log, raw_call_log | in, opt-in | The conversation so far, redacted and unredacted. Needs swaig_post_conversation |
SWMLVars, SWMLCall | in, opt-in | SWML variables and call state. Needs swaig_post_swml_vars |
| Reply | ||
response | out, to the agent | The tool result the agent reads before writing its reply. A string, or {tool_result, tool_prompt}. Never spoken directly |
action | out, to SignalWire | One action or a list: set_global_data, say, SWML, hangup, hold, toggle_functions, step and context changes, and more |
post_process | out | Run the actions after the agent speaks instead of before. Default false |
Six habits for writing webhooks
- Write
responsefor the agent. Give facts and the next move. Leave out greetings and filler. - Translate before you reply. Turn codes, timestamps and IDs into plain facts in your own code. Don’t leave that to the agent.
- Return failures as directions. An error reaches the agent as a generic failure. A reply can tell it how to recover.
- Use actions for anything that must happen. SMS, transfers, state changes and exact wording don’t depend on the agent.
- Keep private values in
global_data. Your webhook receives them on every POST. The agent never sees them unless you expand them. - Keep handlers fast. The agent still has to write and speak its reply after yours arrives. Fillers cover the wait.
The playbill
| Real name | In the play | Plain English |
|---|---|---|
| SignalWire | The theater | Runs the call: phone lines, listening, speaking, turn-taking, and the agent itself. |
| AI agent | The actor | Talks with the caller. Knows only what the script and your notes tell it. |
| Your code | The director | Writes the script and answers notes. Usually a class built with the SignalWire Server SDK. |
| SWML | The script | SignalWire Markup Language. A JSON or YAML document that tells SignalWire how to run this call, one verb at a time. |
| Step / context | A scene | One part of the conversation, with its own instructions, tools and exits. |
| SWAIG function | A prop the actor can ask for | A tool with a name, description and parameters. Calling it sends a request to your code. |
| SWAIG request | A note passed backstage | The HTTP POST SignalWire sends your code when the agent calls a tool. |
response | A stage direction | What your code tells the agent: the facts and what to do next. The agent chooses the words. |
action | Stage machinery | Things SignalWire does because your code said so: text, transfer, save, say exact words, hang up. |
post_process | Cue timing | Whether actions happen before or after the agent speaks. |
global_data | The director’s clipboard | Facts kept for the call. Your code sees all of it. The agent sees only what you expand into its text. |
| Fillers | Stalling for time | Short phrases like “One moment” played while a note travels. |
| SignalWire Server SDK | The director’s toolkit | Writes the SWML for you, serves the endpoints, checks security, and hands each note to your handler. |
In one line each
The agent handles the conversation. Your code handles the truth. SignalWire handles everything in between.
The agent talks and your code decides. The agent can still pick a clumsy word, but it can’t take an action, change a fact, or leave a scene unless your code allows it.
SWML and webhook fields follow the SignalWire SWML ai reference, the SWAIG.functions reference and the AI SWAIG tool webhook reference. SDK code follows the SignalWire Server SDK (Python) reference and source. Dana, Acme, order 8841 and all IDs, numbers and URLs are made-up examples.
Related Resources






