Skip to content

Add "Collect caller input" guide - #682

Merged
hey-august merged 22 commits into
mainfrom
august-20260909-route-control-calls
Nov 5, 2026
Merged

hey-august merged 22 commits into
mainfrom
august-20260909-route-control-calls

Conversation

@hey-august

@hey-august hey-august commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • New task-first capability guide Collect caller input (/docs/platform/voice/collect-input). It covers asking a question and branching on the answer via SWML, WebSocket (Relay) together with the REST calling.collect command, and Call Flow Builder; handling silence and unmatched speech; tracking what the flow heard in the Dashboard call log, a status_url webhook, and Relay events (two themed lifecycle diagrams with mermaid twins); and examples for keypad-plus-speech input and a bounded retry on each surface.
  • Narrowed from the earlier "Route calls" draft. The first-run answer-and-speak walkthrough, the Resource delivery steps, and the hand-off-to-a-phone example are gone; the guide links to Make and receive calls, Speak text on a call, and Forward calls for those instead.
  • Retires the SWML "Simple IVR" recipe: page deleted, /docs/swml/guides/ivr redirects to the new guide, the same treatment Devon/forward calls guide #793 gave the SWML forwarding recipe.
  • Voice overview: new card, and the speech-to-text FAQ now points at the guide. Root llms.txt: the existing "Collect caller input" bullet points at the guide. One-sentence backlink at the end of Speak text on a call.
  • Closes Collect caller input #896. Part of [epic] Platform capability guides #765.

Notes for reviewers

  • Installs are unpinned and the TypeScript REST sample passes its options through extras, following Install the latest Server SDK in guides instead of pinned versions #908 ahead of its merge. Both REST samples send status_url and start_input_timers through extras, because neither the Python (3.7.5) nor the TypeScript (3.7.1) REST collect() takes them as arguments. On the old 2.0.5 pin the TypeScript sample would send extras as a literal key, so the unpinning and this change go together.
  • The REST collect is sent after playback finishes with start_input_timers: true. The other order, collect first and calling.collect.start_input_timers on the play's finished event, is mentioned in prose only.
  • The Python builder still can't emit label on the latest SDK (the generated verb methods take keyword arguments only, and add_verb rejects a bare string), so the retry example keeps TypeScript, YAML, and JSON tabs and notes the gap in a code comment.
  • Dashboard labels used on the page (Click-to-Test, the Addresses & Phone Numbers tab, Primary Script URL, Hosted Script, External URL, the Logs timeline entries) were checked against the Dashboard's current UI strings, not clicked through on a live Space.
  • Not verified live: the Call Flow Builder node and connector labels, and a call through the three speech outcomes (match, no input, no match).
  • fern/assets/images/dashboard/phone-numbers/assign-resource-voice.png is unreferenced now that the recipe is gone; left in place.
  • The Dashboard quickstart card "Route + Control Calls" ("Forward, record and branch live calls") spans Forward calls, Record calls, and this guide, so repointing it is a separate follow-up, not a target for this page.

Verification

  • yarn fern-md-check: all 2830 MDX files valid.
  • yarn fern-check: passes apart from the redirects check, which needs FERN_TOKEN locally.
  • Every fenced block extracted: complete Python samples compile, JSON and bash parse, the TypeScript REST sample parses under Node. REST collect() signatures and the builder's label handling checked against the signalwire-sdk 3.7.4 and @signalwire/sdk 3.7.1 sources.
  • Review fixes checked against the Relay engine (no_match only when the recognizer returns no text, start_input_timers default, play events on the prompt's status_url) and the Dashboard's call log timeline.
  • Every internal link resolved to a page slug; anchors checked by hand against the target headings.

Preview:

@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

@hey-august

Copy link
Copy Markdown
Collaborator Author

Consider a rename to "Call logic" to better reflect content

@hey-august
hey-august marked this pull request as draft September 24, 2026 15:52
@hey-august hey-august changed the title Add "Route and control calls" guide Add "Collect caller input" guide Oct 29, 2026
@hey-august
hey-august marked this pull request as ready for review October 30, 2026 17:07

@Devon-White Devon-White left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I checked the guide against the SWML engine, the Relay and REST collect handlers, Rails webhook delivery, and the Dashboard voice log. Then I ran every SDK call in the samples against signalwire-sdk 3.4.1 and 3.7.5 and @signalwire/sdk 2.0.5 and 3.7.1. A customer following the SWML and Relay paths gets a working menu. The REST path and several explanations need fixes first.

Must change before merge

  1. The REST collect never starts its no-input clock. start_input_timers defaults to false, so silence isn't reported as no_input the way the page says. Send the collect after playback finishes, with start_input_timers: true. Suggestions are on all three REST samples.
  2. The TypeScript REST sample drops status_url on the current SDK. 3.7.1 silently omits unknown option keys and fails tsc --strict, so it has to go through extras. This is tied to the unpinning in #908, which this page should follow (install lines, plus the "3.4.1" code comment).
  3. no_match is described as "speech that matched nothing". It means the recognizer returned no text. An off-menu reply comes back as recognized speech and is caught by your own comparison. The samples already handle it right; the prose, the matrix row, and the compare table need correcting.
  4. "Including partial ones" is wrong. play_and_collect doesn't accept partial_results, so no partials arrive.
  5. The SWML status_url also receives calling.call.play events. Readers need to filter on event_type.
  6. Call Flow Builder exposes the text as %{prompt_value}, per its Gather Input reference. The compare table says it isn't exposed.
  7. The Dashboard's Collect entry shows neither the outcome nor the text. Line 773 says it shows the outcome, which also contradicts lines 613-614.

Authoring rules

  • Per-language method names and SDK internals in prose (lines 281-286 and 1092-1097). For the retry, building it from sections instead of label lets both builders emit it.
  • The first-run verification re-teaches SWML delivery and its troubleshooting. Prepare does too, with the ngrok and basic-auth items. Link Make and receive calls instead.

Nits (not blocking)

  • Link the webhook body to the REST reference's collect status callback page (/docs/apis/rest/webhooks/collect-status-callback, from collectStatusCallback in specs/signalwire-rest/webhooks/calling/main.tsp), next to the prompt reference link at line 688 and the Calling API link at line 492.
  • The Gather Input reference already has a bounded retry ("Input with infinite loop prevention": Set Variables plus Conditions). Adding a #### Ask again when the caller says nothing via Call Flow Builder would keep the examples parallel with the other two tasks.
  • error result type, the webhook diagram's ordering, and the relay-client link (all inline).

What I verified as correct

  • Prompt input selection: any of speech_hints, speech_language, speech_timeout, or speech_end_timeout switches prompt to speech only, and adding max_digits enables both (create_speech_from_object / create_digits_from_object in swml.c).
  • Prompt variables: prompt_value speech normalization (swml_normalize_speech_result_text), the prompt_result values, and the 5-second initial_timeout default.
  • Branching: switch resolves a bare variable name with an exact, case-sensitive key and falls back to default. transfer to a section name doesn't return. goto when/max and a bare-string label work.
  • Spoken text: the JavaScript ternary inside say: works, because play strings go through resolve_swml_var_string.
  • Webhook payload: the shape matches what the engine builds (event_type, event_channel, timestamp, project_id, space_id, params with control_id, call_id, node_id, result, and no result.params on no_input/no_match). Rails posts it minus private.
  • Relay result fields: speech → text and confidence; digit → digits and terminator.
  • REST commands: calling.collect accepts status_url (relay_apis.c whitelist), and REST has no play_and_collect (it's absent from the spec, and the injected async fails its whitelist).
  • Python SDK: play_and_collect(media, collect) resolves only on collect events, not play events. play(on_completed=…), call.on, client.run(), and the REST collect(extras=…) merge behave as the samples assume.
  • Builder output: I rendered the Python and TypeScript builder chains, and they produce the same documents as the JSON tabs.
  • Dashboard labels: Collect, Play, Script Warning, and Error match the timeline titles.
  • Links: every internal link resolves to a page slug:, and every anchor matches a heading or step id.
  • Removed page: the redirect /docs/swml/guides/ivr → /docs/platform/voice/collect-input is in place. Nothing else links the deleted recipe, the SWML recipes nav is folder-driven, and the llms.txt bullet copies the frontmatter description. CI passes and the branch merges cleanly with main.
  • Changelog: leaving out a changelog entry is right.

Couldn't verify

  • The Call Flow Builder node and connector labels in the live UI. They match the Gather Input reference page.
  • Click-to-Test and the Addresses & Phone Numbers tab wording.
  • Whether the speech engine returns empty text, and so no_match, for low-confidence audio. play_and_collect sends a 0.60 confidence threshold by default.
  • How the speech engine applies start_input_timers. Only the digit path is visible in source.
  • That event_channel is swml:<uuid> for SWML calls.

@Devon-White
Devon-White requested review from Devon-White and removed request for Devon-White November 3, 2026 13:01

@Devon-White Devon-White left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I re-checked 3918334 against current mod_infrastructure master (c80bf95), current prime-rails master, and the latest SDKs (signalwire-sdk 3.7.5, @signalwire/sdk 3.7.1). #908 is still open.

Resolved

  • "including partial ones" is gone.
  • The Call Flow Builder row now uses %{prompt_value}.
  • The Dashboard sentence now says the call log shows neither the outcome nor the text.

New text checks out. SWML can hand off to your server with transfer or execute to a URL. On c80bf95, swml_fetch POSTs get_swml_script_state (call, vars, envs), and prompt stores prompt_value in vars, so the bullet is accurate. The replacement matrix row (only Relay delivers the reply to your process without a webhook) is also correct.

Still open before merge

  1. The REST collect still doesn't set start_input_timers, in the prose, the Python sample, the TypeScript sample, or the cURL sample.
  2. no_match is still described as "matched nothing": line 570, the matrix row at line 48, and the compare table.
  3. The SWML status_url paragraph still leaves out the calling.call.play events.
  4. Language-specific prose remains at line 287 (cond) and lines 1094-1096 (label).
  5. The first run still re-teaches SWML delivery troubleshooting.

Tied to #908: the install pins and the TypeScript REST status_url → extras change. Whichever PR merges second carries them.

Nits, not blocking: the error result type, the webhook diagram's ordering, and the relay-client link.

@Devon-White
Devon-White self-requested a review November 3, 2026 19:47
@hey-august

Copy link
Copy Markdown
Collaborator Author

@Devon-White thanks for the thorough pass, both rounds. c850c50 addresses everything, including the nits:

  • REST collect sends start_input_timers: true in the prose and all three samples; the TypeScript sample now goes through extras.
  • Installs are unpinned on all 13 lines and the 3.4.1 comment is gone. I went ahead of Install the latest Server SDK in guides instead of pinned versions #908 rather than waiting, since a reader installing the latest SDK today gets a collect with no webhook. Nothing here conflicts if Install the latest Server SDK in guides instead of pinned versions #908 merges first.
  • no_match is corrected everywhere it appeared: matrix row, Handle silence intro, SWML and Relay subsections, compare table.
  • calling.call.play on status_url, the error result type, %{prompt_value} for Call Flow Builder, and the call log sentence are in.
  • Per-language prose is out in both places; the label gap lives in a code comment on the TypeScript retry sample.
  • Delivery troubleshooting and the Prepare bullet now point at Answer a call via SWML.
  • Nits: collect status callback reference linked in both spots, Relay bullet links the Server SDKs landing page, the webhook diagram no longer implies the flow waits on the 200, and the retry example gained a Call Flow Builder subsection.

Three places I went a different way:

  • Kept label + goto for the retry instead of the section-based rewrite. That construct is the point of the example, and I confirmed the gap still exists on 3.7.4 (the generated verb methods are kwargs-only and add_verb rejects a bare string), so there's no Python tab.
  • Kept the Click-to-Test symptom in the first-run list. Make and receive calls doesn't mention Click-to-Test, so there's nowhere to point it.
  • Followed your "collect after playback finishes" order, with the alternative (collect first, then calling.collect.start_input_timers on the play's finished event) left as one sentence for readers who need barge-in.

@hey-august
hey-august merged commit 62d0e1d into main Nov 5, 2026
2 checks passed
@hey-august
hey-august deleted the august-20260909-route-control-calls branch November 5, 2026 22:20
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.

Collect caller input

2 participants