Skip to content

SWML connect: undocumented call: destination, nonexistent transfer_after_bridge property, and destination-format precision gaps #547

Description

@briankwest

Summary

I audited the connect.to destination formats documented on the SWML connect reference against the implementation in mod_infrastructure (master @ a8ba2dc, 2026-08-05). The five documented formats are all correct, but the audit turned up one entirely undocumented destination type, one documented parameter that does not exist and hard-fails any script that uses it, and a set of precision gaps that are currently guesswork for users.

Filing this as one ticket since it's all the same page/spec pair.

Files in scope

  • fern/products/swml/pages/reference/methods/calling/connect/index.mdx
  • specs/swml/calling/Methods/connect/main.tsp
  • fern/apis/signalwire-rest/openapi.yaml (generated — transfer_after_bridge appears at ~35588, 35758, 35928, 36104, 36805)

Implementation references (repo: signalwire/mod_infrastructure)

  • swml.c:4206-4362create_connect_device(), the destination dispatcher
  • relay_apis.c:642-757 — the prefix predicates
  • swml_schema.c:1036-1118is_valid_stream_url, is_connect_to_endpoint_uri, is_endpoint_uri
  • relay.c:5605, relay.c:5821, relay.c:7441 — call/queue intercept path

1. Confirmed correct (no action needed)

All five documented formats match the implementation:

Documented format Detection Resulting device
Phone number, E.164 fallback branch (swml.c:4355) phone / to_number
sip:alice@example.com relay_apis.c:642 sip / to
/public/test_room relay_apis.c:702 fabric / to
queue:support relay_apis.c:718 queue / queue_name
stream:wss://example.com/audio relay_apis.c:750 + swml_schema.c:1036 stream / url

2. Missing: the call:<uuid> destination is undocumented

  • Document call: as a sixth connect.to destination type

connect accepts to: "call:<call-uuid>" to bridge the current call directly to an existing in-progress call, interrupting whatever that call is doing. It is accepted by the same validator as every other format (swml_schema.c:1054), translated at swml.c:4297, and executed by the relay_intercept app (relay.c:5821). It appears nowhere in the docs or the TypeSpec.

version: 1.0.0
sections:
  main:
    - connect:
        to: "call:ba79c846-4834-42b9-9cd3-2f87c30f2972"

Behavior worth documenting:

  • The call ID must be a well-formed 36-character UUID (relay_apis.c:1038-1040). A malformed value passes script validation but fails at execution time, so the error surfaces later than users expect.
  • The target call is located across the cluster via the registrar; if it lives on another node the bridge is set up over an internal link (relay.c:5570, relay.c:5917). Cross-node is transparent to the user.
  • Constraints (each is a distinct, user-visible failure):
    • Only one destination is allowed — a parallel/serial fan-out containing a call: (or queue:) target is rejected with Only 1 device allowed when connecting to call or queue (relay.c:7643).
    • A call cannot connect to itself — checked against both call_id and segment_id (relay.c:7666).
    • The target must belong to the same project (relay.c:5617).
    • The target must not be cXML/LaML-controlled (relay.c:5626) — returns busy.
    • call_state_url / call_state_events are rejected at validation time for call: targets (swml_schema.c:1098), and unlike queue and stream there is no alternative webhook.
  • Failure surfaces in connect_failed_reason as NOT_FOUND (target gone), BUSY (LaML target), or error.
  • Default post-bridge behavior is that the target leg parks and returns to script control (relay.c:5655).

3. Incorrect: connect.transfer_after_bridge does not exist and breaks scripts

  • Remove connect.transfer_after_bridge from all four tabs and from the TypeSpec
  • Document connect.execute_after_queue in its place

transfer_after_bridge is documented as a connect property in all four tabs (index.mdx:125, :356, :566, :778) and in main.tsp:161, with the note "Required when connecting to a queue" (main.tsp:158).

The connect method's accepted-parameter list (swml_schema.c:1645) does not include transfer_after_bridge. Unknown parameters are a hard script error, not a silent no-op — check_unknown_object_params (swml_schema.c:872-884) returns SWML_METHOD_PARAMETER_UNDEFINED. So anyone who follows this documentation gets a failed script, not a working one.

The real parameter is execute_after_queue (swml_schema.c:1645, translated at swml.c:4292), and it is optional, not required. transfer_after_bridge is the internal name this maps to downstream (relay.c:7446), which is presumably how it leaked into the docs. Note that transfer_after_bridge is correct for the enter_queue method — that page is fine; only connect is wrong.

One caveat for whoever writes the replacement: execute_after_queue is accepted at the verb level only. It is not in the per-destination schema (swml_schema.c:1597), so putting it inside a serial/parallel entry is rejected as an unknown parameter. Since a queue target must be the only destination anyway, the verb-level form is the only one that makes sense — but that should be stated rather than left to be discovered.


4. Precision gaps

  • E.164 requires a leading +. The validator (cjson_check.c:261) requires +, digits only after it, and total length 5–19. A bare 15552345678 is not treated as a phone number — it matches no prefix, fails the destination validator, and the script errors. "E.164 format" is technically sufficient but is a common trip-up worth one explicit sentence.
  • sips: is accepted. relay_apis.c:648 accepts both sip: and sips: (case-insensitively). sips: appears nowhere in the docs.
  • queue: and stream: prefixes are case-sensitive. They use strncmp (relay_apis.c:724, :756), so Queue:support is rejected. sip:/sips: and the wss:// portion of a stream URL are case-insensitive.
  • stream: requires wss://ws:// is rejected. swml_schema.c:1036-1050 enforces TLS. The docs show stream:wss:// in examples but never state that plaintext ws:// is a validation error.
  • from accepts a narrower set than to. from is validated by is_endpoint_uri (swml_schema.c:1115): phone, SIP, or Resource Address only. queue:, call:, and stream: are not valid from values. The docs describe from only as "Caller ID number", which doesn't convey that it shares a validator with to.
  • call_state_url / call_state_events are rejected for queue, call, and stream destinations. Not ignored — a validation error (swml_schema.c:1066-1113). The docs list both as general connect properties with no such caveat. The error messages already point users at the right alternative (status_url on enter_queue for queues, the stream's own status_url for streams); the docs should say the same thing up front.
  • ringback also accepts a boolean. Documented as string[] (index.mdx:105, main.tsp:101-103), but connect accepts a boolean: ringback: false suppresses synthetic ringback and passes the B-leg's early media through to the A-leg (swml.c:4586-4600). This is a genuinely useful and completely undocumented capability. Note it is connect-onlydial accepts only the play form.
  • A stream: destination must be the only destination. Same one-destination rule as call/queue (relay.c:7474, relay.c:7698).
  • status_url and queue destinations. The status_url description enumerates behavior for phone/SIP/Resource Address and for stream, but not for queue. Queue lifecycle webhooks go to the status_url configured on enter_queue, not to connect.

5. Related: dial accepts to values it cannot execute

  • Document the dial destination set, or track the underlying validator mismatch

The SWML dial method validates to with the same permissive validator as connect (swml_schema.c:1655, :1689), but the execution layer for dial accepts only phone, sip, and fabric devices (relay_apis.c:1021-1025). A dial with a queue:, call:, or stream: target therefore passes script validation and fails at execution.

This is arguably an implementation bug rather than a docs bug, and I'm happy to file it against mod_infrastructure instead if that's preferred. Flagging it here because whatever the docs currently imply about dial destinations is wrong either way — the two methods do not accept the same destination types in practice.

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions