Skip to content
Expedify
Actions — making things happen

Module · Voice

Voice Response

Lesson 11 of 21 · 7 min

Some answers have to be heard. A confirmation read out on a call, a prompt in a phone menu, a message left on someone's voicemail — none of those are text, and none of them can wait for the person to open an app.

Voice Response is the node that speaks. It takes text — usually text some earlier node produced — hands it to a speech provider, and gives you back audio. In shape it is Text Response with a voice attached: same templated field, same place at the end of a chain.

In practice it has one decision that the rest of the node hangs on, and the default for that decision is wrong for every real workflow. Start there.

The fields

text

What it holds
Text to convert to speech (supports template variables) Defaults to {{output}}.

synthesizer_source

What it holds
Source for synthesis service One of: direct · voice_agent · none Defaults to none.

synthesizer_integration_id

What it holds
Text-to-speech integration to use (when source is 'direct')

output_format

What it holds
Audio output format One of: mp3 · wav · ogg · m4a Defaults to mp3.

output_destination

What it holds
Where to send the audio output One of: file · stream · phone · webrtc Defaults to file.

quality

What it holds
Audio quality setting One of: low · medium · high Defaults to medium.

The default synthesizer_source is none, and none means no speech is produced. It is not “use the default provider” — it is “do not start one”. The node then reports a successful run with no audio attached. If you take one thing from this lesson, take this: set synthesizer_source to direct and name an integration.

The three sources, plainly:

  • direct — you name a synthesizer integration in synthesizer_integration_id. This is the one you want. Which voice, from which provider, decided here.
  • voice_agent — borrow the settings from a Voice Agent node in the same workflow, so the spoken reply matches the agent's voice. Only sensible when there is such a node.
  • none — no synthesizer. The node still runs and still reports success.

A worked example

A confirmation, spoken. The text is built from what the workflow found, and the synthesizer is named rather than assumed.

Text in, an audio file out

synthesizer_source is direct, and the integration is named. That is the difference between audio and silence.

Scroll for all 3 steps →

The text field templates exactly like Text Response, with the same rule about only seeing what is wired directly into it. Everything else on that node is about the audio rather than the words: the format, where it goes, how hard the provider should work.

Write for the ear. This is the part no configuration helps with. A sentence that reads well can be unlistenable — a URL, a reference number, a bulleted list. Say “we'll email you the link” rather than reading the link. Break long numbers into groups. Read your text aloud before you save it; if you stumble, so will the synthesiser.

Where the audio goes

Choose file and hand audio_url to whatever needs to play it.

file

What happens
An audio file you can fetch from the node's audio_url. The one that works today, and the one to build on.

phone · webrtc · stream

What happens
Named in the panel, and not implemented. Selecting one logs a line and does nothing — the audio is still synthesised, it just is not delivered anywhere.

That is worth being blunt about because the dropdown reads like four working options. If you need audio on a live call, the node that owns the call places it there — see the next three lessons. Voice Response's job is to produce the audio.

What comes back

{{alias.audio_url}}

What you get
Where the audio is. The field everything downstream actually needs — and the field to check when you suspect nothing was produced.

{{alias.response_text}}

What you get
The text that was spoken, after templating. Useful for logging what the caller heard.

{{alias.synthesis_metadata}}

What you get
Duration, character count, voice settings, provider. Also where the error goes when synthesis fails.

What breaks

A failed synthesis is reported as a successful node. If the synthesiser cannot start — no source configured, or the integration is wrong — the failure is caught inside the node, recorded in synthesis_metadata and then discarded. The node returns success and leaves down its normal path with audio_url set to nothing. Nothing in the execution log looks wrong. The test is always the same: read audio_url, not the node's status.

Empty text is the one thing it does treat as an error. If text resolves to nothing — usually because the default {{output}} found nothing upstream — the node fails properly and leaves down its error path. So the two failure modes look opposite: no words is loud, no voice is silent.

Long text costs real money and real seconds. Synthesis is billed by the character and takes time proportional to length. A node that speaks a whole AI answer is a node that occasionally speaks four hundred words. Cap it upstream — ask the model for one or two sentences — rather than discovering the bill.

Try it

  1. Add a Voice Response with a fixed sentence in text and leave everything else alone. Run it and read {{alias.audio_url}} — it will be empty, and the run will be green. That is the default.
  2. Set synthesizer_source to direct, pick an integration, and run it again. Fetch the URL and play it.
  3. Change voice_settings speed to 0.8 and again to 1.3, and listen to both. Pick the one you would want on a phone call — it is usually slower than you expect.
  4. Clear text entirely and run it. This time the node fails and takes the error path, which is the opposite of the first step and worth feeling the difference.

Next: Voice Inbound Call — what happens when the phone rings and the workflow, rather than a person, decides what to do about it.