Skip to main content
If your telephony already runs on FreeSWITCH, you can drop an Omni agent onto any channel without leaving your dialplan. FreeSWITCH forks the call’s audio to a small WebSocket bridge, the bridge relays it to Omni and plays the agent’s reply back into the channel, and you drive call control (barge-in, transfer, DTMF) over the Event Socket.
Run the complete example. Scaffold this guide’s full bridge in one command, no clone:
Or browse it: freeswitch-omni-voice-agent.

How it fits together

mod_audio_stream (or mod_audio_fork) forks channel audio as L16, signed linear 16-bit PCM, at 16 kHz, and plays audio you send back into the same channel. L16/16 kHz is byte-for-byte the same as Omni’s PCM16 at 16 kHz, so run Omni with ?rate=16000 and the audio path is a straight passthrough, no resampling. Call control rides a separate ESL (Event Socket) connection.
This guide is correct on transport, the fork codec/rate, and the Omni event behaviors. The exact JSON payloads of Omni events (notably the dtmf frame you forward) are defined by the Omni wire protocol, they live in one helper so updating them is a one-line change.

Prerequisites

1

FreeSWITCH with a fork module

mod_audio_stream (bidirectional; recommended) or mod_audio_fork loaded and in modules.conf.xml. Confirm with fs_cli -x "module_exists mod_audio_stream".
2

Event Socket access

The inbound Event Socket enabled (default 127.0.0.1:8021, password ClueCon). Lock this down to localhost or your bridge host.
3

A key

A pyai_test_ key, supplied to the bridge as an environment variable. Omni is zero-state, no agent to create; optionally pick a session_label to tag each call in your kb_endpoint.

Project layout

package.json

Build it

1

Dialplan: fork the channel to your bridge

Answer the call, then start mod_audio_stream toward your bridge at 16 kHz mono, and park the channel so it stays up while audio streams. Pass the channel UUID on the URL so the bridge can issue ESL commands for it.
dialplan/omni-agent.xml
With mod_audio_fork the app is uuid_audio_fork/audio_fork; the arguments (mix type mono, rate 16k) are the same. Keep the rate at 16k so it lines up with Omni’s rate=16000 and no resampling is needed.
2

Bridge: relay fork audio ↔ Omni

FreeSWITCH connects to /fork and streams binary L16 frames. Forward them to Omni untouched, and stream Omni’s binary frames straight back into the channel.
server.js (audio bridge)
L16 and PCM16 are both signed 16-bit, but endianness must match. If you hear static or white noise, your fork is emitting big-endian, byte-swap each 16-bit sample (buf.swap16()) before forwarding, and again on the way back.
3

ESL: barge-in, transfer, and DTMF

One Event Socket connection drives every call. Subscribe to DTMF events and forward digits to Omni; react to Omni events with uuid_break (barge-in) and uuid_transfer (escalate to a human).
server.js (control plane)
The inbound event names (flush, transfer_to_human, send_dtmf, play_hold, collect, end_call, session_end) are stable (server frames are keyed on event); your outbound configure / dtmf frames are keyed on type. The call-control verbs are emitted by Omni but executed here, they only happen because this switch handles them. Exact payload fields come from the Omni wire protocol §4.1. forwardDtmf is the only spot that constructs an Omni control frame.

Run it

Reload the dialplan (fs_cli -x "reloadxml"), set bridge_host to where your Node process is reachable, then dial extension 9000. The agent should greet the caller within a second. Talk over it to confirm uuid_break cuts the agent off; press a DTMF key and watch it reach Omni; trigger a transfer to confirm the channel reaches the human extension.

Codec & rate notes

  • No resampling at the fork. Fork at 16k and run Omni at rate=16000, L16/16 kHz ↔ PCM16/16 kHz is a passthrough. If the caller’s leg is 8 kHz, FreeSWITCH transcodes it to 16 kHz for the fork (8000 → 16000 is a 2:1 upsample done inside FreeSWITCH), so Omni always sees a clean 16 kHz stream.
  • Endianness, not rate, is the usual culprit for distorted audio, see the warning above.
  • park keeps the channel alive. Without it the call can tear down before the fork is established.

Troubleshooting

Next steps

Omni wire protocol

Exact event payloads and close codes.

Browser voice agent

The same agent in the browser via WebRTC.

Phone agent with Twilio

Media Streams bridge at μ-law 8 kHz.

Errors & limits

Rate limits, concurrency, and retries.