T
Telephony SOPKnowledge Base
Search
← All topics

DataMap — Server-Side SWAIG API Passthrough

runnable

DataMap executes REST API calls on SignalWire's infrastructure — no webhook on your side. Variable substitution table, GET/POST/foreach patterns, expression-only matching, and the right use cases for DataMap vs a Python @tool handler.

signalwireswaigdatamapapi-integrationno-server
Agent trigger phrases: DataMap SignalWire · SWAIG without server · API passthrough SignalWire · DataMap foreach loop · DataMap expression match · ${args.param} substitution

DataMap — Server-Side SWAIG API Passthrough

DataMap is the "no-webhook" mode of SWAIG. Instead of you hosting a function handler, you describe the API call once and SignalWire executes it on its infrastructure. The response is templated into a FunctionResult and spoken to the caller.

Use DataMap when:

  • The integration is a straightforward HTTP call to a public or token-auth API.
  • You want zero servers.
  • The transformation from API response to spoken reply fits a template.

Skip DataMap when:

  • You need to mutate database state.
  • The handler needs custom auth flows (OAuth refresh, signed payloads).
  • The logic needs branching beyond simple expression() regex matches.

Variable substitution

| 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 (E.164 → human-readable) | | ${response.field} | API response field | | ${response.arr[0]} | Array element from response | | ${global_data.key} | Session-wide data | | ${this.field} | Current item inside a foreach loop |

Modifiers chain right-to-left. ${enc:lc:args.city} lowercases first, then URL-encodes.

GET request

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 loop

foreach iterates over an array in the response and accumulates a template into a single output variable.

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."))
)

foreach parameters:

| Key | Notes | |---|---| | input_key | Array path in the response JSON. | | output_key | Variable name where the concatenated string lands. | | max | Stop after this many items. | | append | Template per item, using ${this.field}. |

Expression-only (no HTTP)

For pure pattern matching with no API call, use .expression():

volume = (
    DataMap("set_volume")
    .description("Control audio 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"))
)

agent.register_swaig_function(volume.to_swaig_function())

Each .expression(value, regex, result) is evaluated in order. First match wins. Useful for IVR-like routing without a real backend.

Helper shortcuts

from signalwire import create_simple_api_tool, create_expression_tool

# Simple GET tool
weather_tool = create_simple_api_tool(
    name="get_weather",
    url="https://api.weather.com/v1/current?key=KEY&q=${args.location}",
    response_template="Weather in ${args.location}: ${response.current.condition.text}",
    parameters={"location": {"type": "string", "description": "City name", "required": True}},
)

# Pattern-matching tool
cmd_tool = create_expression_tool(
    name="playback_control",
    patterns={
        "${args.command}": (r"play.*", FunctionResult("Playing.")),
    },
    parameters={"command": {"type": "string", "description": "Command", "required": True}},
)

These wrap the full DataMap builder when you only need a single GET or single regex tool.

Auth patterns

| Auth style | How | |---|---| | API key in URL | Inline: https://api.example.com/?key=ABC&q=${enc:args.q} | | Bearer token | .webhook("GET", "...", headers={"Authorization": "Bearer TOKEN"}) | | Static header | Same headers={} pattern | | OAuth refresh / signed requests | Use a @tool handler, not DataMap |

Fallback behavior

.fallback_output(...) fires when:

  • The HTTP call returns a non-2xx status.
  • The response JSON doesn't contain the expected fields.
  • A timeout (default ~10s) elapses.

Always provide a fallback. Without it, a failed call returns nothing and the AI invents a response.

Anti-patterns

  • Putting secrets directly in headers and checking the SWML into git — secrets land in version control. Inject at runtime.
  • Using DataMap for mutation endpoints (POST that creates records) — race conditions, no retry strategy.
  • Foreach with max > 10 — spoken response gets unbearably long.
  • Skipping fallback_output — AI hallucinates results when the API errors.
  • Mixing %{...} (SWML) and ${...} (DataMap) prefixes — DataMap is ${...} only.

See also