Skip to content

SWML messaging schema (TypeSpec)#315

Merged
Devon-White merged 12 commits into
Devon/swml-messagingfrom
Devon/swml-schema
May 13, 2026
Merged

SWML messaging schema (TypeSpec)#315
Devon-White merged 12 commits into
Devon/swml-messagingfrom
Devon/swml-schema

Conversation

@Devon-White
Copy link
Copy Markdown
Collaborator

@Devon-White Devon-White commented May 12, 2026

Splits the SWML TypeSpec schema into SWML.Calling (the existing schema, relocated under specs/swml/calling/) and SWML.Messaging (new — covers reply, receive, request, transfer, execute, goto, switch, label, return).

REST consumers (calling-api/calls, fabric-api/swml-scripts, fabric-api/ai-agent) now embed SWML.Calling.SWMLObject or SWML.Messaging.SWMLObject where appropriate. The SWML inbound message webhook (@webhook("inboundMessageWebhook", ...)) lands on top of the calling sibling that shipped with the decorator PR.

Base branch

This PR targets Devon/swml-messaging — the integration branch — not main. Its sibling, the SWML reference docs PR #316, also targets the integration branch. When both merge in, the integration → main PR ships them atomically.

Drive-along refactors (kept in scope)

  • UrlMethodTypeRequestUrlMethodType rename (it only ever described request URLs).
  • SWML_CONTENTS_EXAMPLE + SWML_MESSAGING_CONTENTS_EXAMPLE literal-object constants for @example(...).

Verification

```bash
cd specs && yarn build:all
```

TypeSpec 1.11.0 compiles all four projects (signalwire-rest, compatibility-api, swml-calling, swml-messaging) cleanly. The regenerated `fern/apis/signalwire-rest/openapi.yaml` exposes `inboundCallWebhook` and `inboundMessageWebhook` under `webhooks:`.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 12, 2026

…saging

Splits the SWML TypeSpec schema into two sibling namespaces:

* `SWML.Calling` — the existing schema, relocated under `specs/swml/calling/`.
  Every method TSP file moves from `specs/swml/Methods/<method>/` to
  `specs/swml/calling/Methods/<method>/` (139 renames). Behavior unchanged.
* `SWML.Messaging` — new namespace under `specs/swml/messaging/` covering
  inbound SMS/MMS handlers. Methods: execute, goto, label, receive, reply,
  request, return, switch, transfer. `SWMLObject` is a `@oneOf` union of a
  full document (top-level `sections`) and a light document (single `reply`
  or `receive`, including the empty `{}` shorthand).

`specs/package.json` replaces the single `build:swml` script with
`build:swml-calling` and `build:swml-messaging` (both wired into
`build:schema`). The legacy `specs/swml/tsp-output/.../SWMLObject.json` file
is kept in place — consumers may have hardcoded that URL path; the new
calling-only schema lives at `specs/swml/calling/tsp-output/...`.

REST consumers that embed SWML documents are updated to use the new
namespaced types:
* `signalwire-rest/calling-api/calls/models/{requests,examples}.tsp` —
  `SWML.Calling.SWMLObject` for call-control SWML payloads.
* `signalwire-rest/fabric-api/swml-scripts/models/{core,requests}.tsp` —
  splits the `contents` field across `SWML.Calling.SWMLObject` and
  `SWML.Messaging.SWMLObject` based on script type.
* `signalwire-rest/fabric-api/ai-agent/models/{core,ai/main}.tsp` — pulls
  AI method types from `SWML.Calling`.

Adds the second SWML inbound webhook to the SWML Webhooks namespace:
`@webhook("inboundMessageWebhook", InboundMessageWebhookPayload, ...)` with
companion `InboundMessageMediaItem`, `InboundMessageContext`, and
`InboundMessageWebhookPayload` models in
`signalwire-rest/fabric-api/swml-webhook/models/webhooks.tsp`. The Calling
sibling `@webhook("inboundCallWebhook", ...)` already shipped with the
decorator PR; this PR completes the pair. `fern/products/apis/apis.yml`
gains the matching nav entry.

Drive-along refactors that landed naturally with this work:
* `uuid` and `jwt` scalars moved from
  `specs/signalwire-rest/types/scalar-types/main.tsp` up to
  `specs/_shared/types/main.tsp` so compatibility-api can reach them.
  `@format("uuid") + string` pairs across ~135 REST + compat spec files
  collapse to the bare `uuid` scalar (TypeSpec carries the format).
* `UrlMethodType` (which actually only describes request URLs) renames to
  `RequestUrlMethodType`; `cxml-webhooks/models/requests.tsp` propagates
  the new name.
* `signalwire-rest/fabric-api/_shared/const.tsp` adds tuple-literal
  `SWML_CONTENTS_EXAMPLE` and `SWML_MESSAGING_CONTENTS_EXAMPLE` constants
  for use in `@example(...)` decorators.
* `signalwire-rest/message-api/messages/models/webhooks.tsp` clarifies in
  prose that the message status callback shape is also fired by SWML
  messaging `reply.status_url`.

Targets the SWML messaging integration branch (`Devon/swml-messaging`).
The matching SWML reference MDX docs land as a sibling PR.

Verified by `yarn build:all` from `specs/`: TypeSpec 1.11.0 compiles
signalwire-rest, compatibility-api, swml-calling, and swml-messaging
without errors. Generated `fern/apis/signalwire-rest/openapi.yaml`
exposes both `inboundCallWebhook` and `inboundMessageWebhook` under
`webhooks:`.
InboundMessageWebhookPayload
- Add optional `vars` field carrying propagated runtime variables on
  transfer-driven fetches; absent on the initial inbound fetch
- `body` typed as nullable (provider intakes leave it null on media-only
  MMS with no carrier-supplied text)
- Loosen from/to descriptions to forward-compatible wording
- Tighten timestamp/media descriptions

Reply method
- ReplyInlineSwitch.case and default accept `string | ReplyPlan` so each
  branch can carry its own body/media/to/from/status_url
- Require `body` be non-empty (@minlength(1)) on both ReplyWithBody and
  ReplyWithMedia, with matching doc text
- status_url cross-links to the message status callback payload

Request method
- Rewrite description with the four request_result values, the soft vs
  hard failure semantics, and the response variables (request_response,
  request_response_code, request_response_body)

Transfer method
- Description now references the inbound message webhook payload and
  explains what message/params/vars carry on the transfer fetch

SwmlScriptCreate/UpdateRequest
- script_type optional with per-branch default; structural oneOf still
  discriminates via the contents schema. Update docs to call out the
  recommended-but-optional path.
Move the two receive-shape types out of main.tsp into receive/main.tsp
so all receive-related definitions live together. SWMLLightDocument
stays in main.tsp since it's a document-level union; it still composes
LightReceive via the Methods import.

Generated JSON Schema is unchanged.
@Devon-White Devon-White marked this pull request as ready for review May 12, 2026 15:09
@Devon-White Devon-White requested a review from cassieemb May 12, 2026 15:18
Response side — both fields are emitted as parsed JSON objects by Rails:
- CallFlowSerializer / CallFlowVersionSerializer call
  RelayBins::ParseJsonOrYaml.call(raw_contents).as_json for relayml
- flow_data is a jsonb column that the serializer passes through directly

Spec previously typed both as `string`, which would reject the real
responses. Updated CallFlow (core), CallFlowVersion, CallFlowVersionDeployResponse,
and the dead-but-present CallFlowVersionResponse:
- relayml: SWML.Calling.SWMLObject
- flow_data: opaque object (Record<unknown>)

Request side — CallFlowCreateRequest and CallFlowUpdateRequest were
missing the relayml and flow_data fields entirely. Rails contracts
require both (with create allowing both to be omitted together to
get SignalWire defaults; update requires both present along with
document_version). Added the missing fields with required/optional
flags matching the Rails contracts.

Generated REST OpenAPI regenerated.
Devon-White added a commit that referenced this pull request May 12, 2026
- reply.mdx: inline switch case/default values are now string OR full reply
  object (body/media/to/from/status_url). Updated the ParamField descriptions
  and the trailing constraint paragraph, and added a per-case-routing example.
- transfer.mdx: payload section now lists the three keys (message/params/vars)
  and embeds the inboundMessageWebhook payload snippet. Removed the inaccurate
  'same shape as the initial fetch' note.
- overview.mdx: tighten the reply summary (inline switch branches the reply,
  not just the body); replace the over-simplified 'request failures are soft'
  blurb with the precise hard-vs-soft split, surfacing the four request_result
  values.
Devon-White added a commit that referenced this pull request May 12, 2026
The Fern URL slug for the message-logs operation is built from its OpenAPI
tag "Message Logs" -> kebab "message-logs", not the parent "Logs" nav
section (which uses `skip-slug: true`). Updated both occurrences to point
at /docs/apis/rest/message-logs/list-message-logs.

The inbound-message-webhook links in variables.mdx are left alone; that
target page is generated by the schema PR (#315), and the links will
resolve once both PRs land on the Devon/swml-messaging integration branch.
The TypeSpec @doc strings emit to JSON Schema descriptions and ultimately
into customer-facing surfaces (REST API reference, SDK docs). They should
say the same thing as the MDX reference docs and stay focused on purpose
and behavior — type/required/default info already lives in the type
signature and ParamField attributes.

Changes per method (eight files, ~17 description updates):

* execute — clarify that completion happens via `return` or end-of-section,
  and that URLs/inline documents are not accepted in the messaging context
  (matches the doc intro that already calls this out).

* goto — promote `goto.max` description to "section ends without running
  further steps" (matches Rails behavior in section_executor.rb; the prior
  "stopping execution" was vague). Add a goto-scoped @doc on the `label`
  field instead of inheriting Label.label's @doc which describes labels
  from the label's perspective, not from goto's.

* label — note that label names must be unique within the section.

* reply — append "reply does not end execution; subsequent steps continue"
  to the method @doc, swap the Ruby-resource framing on `from` ("PhoneRoute
  or ShortCode") for user-facing language ("owned by your project and have
  messaging capability"), and document the inline-switch no-match failure
  behavior on `default`.

* request — drop redundant "Default X" prose from `method`, `timeout`, and
  `save_variables` @docs (already declared via `= default` on the type),
  swap "Hashes" → "Objects" on `request.body` for non-Ruby-flavored docs,
  add the publicly-reachable URL constraint to `request.url`, and add the
  "map of header name to value" clarification to `request.headers`.

* return — note the `return: null` shorthand for returning without a value.

* switch — extend the method @doc with use-case framing ("useful for
  keyword-driven inbound message handling"), document key/value semantics
  on `case`, and document the no-match failure behavior on `default`.

* transfer — note the URL-only constraint in the method @doc, document
  embedded basic-auth syntax on `dest`, drop redundant "Default `POST`"
  prose on `method`, and append the runtime semantic on `params`
  ("available as `params.*` in the transferred document").

Regenerated specs/swml/messaging/tsp-output/.../SWMLObject.json is the
mechanical byproduct of these source edits.

Verified by `yarn build:swml-messaging` — compiles clean under TypeSpec
1.11.0 with no warnings or errors.
Copy link
Copy Markdown
Contributor

@cassieemb cassieemb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really good! I left a few questions about areas I'm unsure about

Comment thread specs/signalwire-rest/fabric-api/swml-scripts/models/core.tsp Outdated
Comment thread specs/signalwire-rest/fabric-api/swml-scripts/models/requests.tsp Outdated
used_for: "calling" | "messaging";

@doc("Primary request url of the SWML Webhook.")
@doc("Primary URL SignalWire fetches the SWML document from when the webhook fires. The webhook payload depends on `used_for`: for `calling`, see the [SWML inbound call webhook](/docs/apis/rest/swml-webhook/webhooks/inbound-call-webhook); for `messaging`, see the [SWML inbound message webhook](/docs/apis/rest/swml-webhook/webhooks/inbound-message-webhook).")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this true for calling? I tried assigning a SWML webhook as a call handler and I didn't get a posted body to the webhook URL. Prime doesn't handle inbound SWML for calling, so this isn't a pattern I've seen before we added it for messaging. Maybe I tested incorrectly though, and this is something that FS currently handles?

primary_request_method: RequestUrlMethodType;

@doc("Fallback request url of the SWML Webhook.")
@doc("Fallback URL SignalWire fetches the SWML document from if the primary URL fails. Receives the same payload as `primary_request_url` — see the [SWML inbound call webhook](/docs/apis/rest/swml-webhook/webhooks/inbound-call-webhook) or [SWML inbound message webhook](/docs/apis/rest/swml-webhook/webhooks/inbound-message-webhook) depending on `used_for`.")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here - I'm not sure if this is something FS already handles for calling, but Prime does not.

@Devon-White Devon-White merged commit d2a00b2 into Devon/swml-messaging May 13, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants