> ## Documentation Index
> Fetch the complete documentation index at: https://plivo.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Cloudflare Agents

> Connect a voice agent built on Cloudflare Workers to phone calls with Plivo Audio Streaming

[Cloudflare Agents](https://developers.cloudflare.com/agents/) is a framework for building voice agents that run on Cloudflare Workers. Plivo Audio Streaming bridges a phone call to one of these agents over a WebSocket, using the [Plivo adapter](https://github.com/cloudflare/agents/tree/main/voice-providers/plivo).

The Voice Agent pipeline is composed of independent stages instead of a single hosted API. The speech-to-text, response text generation, and text-to-speech stages each run on a [Workers AI model](https://developers.cloudflare.com/workers-ai/models/) or a [third-party provider model](https://developers.cloudflare.com/ai/models/?providers=third-party) through [AI Gateway](https://developers.cloudflare.com/ai-gateway/), so quality, latency, and cost can be tuned independently per stage.

The agent itself runs on Cloudflare Workers, requiring no server infrastructure to provision or scale.

***

## How it works

<div className="sipflow sipflow--vertical">
  <div className="sipflow__head">
    <span>Pipeline</span>

    <span className="sipflow__head-right">
      <span className="sipflow__led" aria-hidden="true" />

      Cloudflare Agents
    </span>
  </div>

  <ol className="sipflow__body">
    <li>
      <div className="sipflow__node">
        <span className="sipflow__node-body">
          <span className="sipflow__title">Phone Call</span>
        </span>
      </div>

      <div className="sipflow__link sipflow__link--bi" aria-hidden="true">
        <span className="sipflow__arrow sipflow__arrow--back" />

        <span className="sipflow__rail" />

        <span className="sipflow__arrow" />
      </div>

      <div className="sipflow__node">
        <span className="sipflow__node-body">
          <span className="sipflow__title">Plivo</span>
          <span className="sipflow__sub">Audio Stream</span>
        </span>
      </div>

      <div className="sipflow__link sipflow__link--bi">
        <span className="sipflow__arrow sipflow__arrow--back" aria-hidden="true" />

        <span className="sipflow__rail" aria-hidden="true" />

        <span className="sipflow__edge-label">WebSocket</span>
        <span className="sipflow__edge-label sipflow__edge-label--return">WebSocket server</span>

        <span className="sipflow__arrow" aria-hidden="true" />
      </div>

      <div className="sipflow__node">
        <span className="sipflow__node-body">
          <span className="sipflow__title">Cloudflare Worker</span>
          <span className="sipflow__sub">VoiceAgent</span>

          <span className="sipflow__chips">
            <span className="sipflow__chip">STT</span>
            <span className="sipflow__chip">LLM</span>
            <span className="sipflow__chip">TTS</span>
          </span>
        </span>
      </div>
    </li>
  </ol>
</div>

The Cloudflare Worker runs a WebSocket server. On an inbound call, Plivo opens a bidirectional WebSocket to it and streams the caller's audio in both directions.

The [Plivo adapter](https://github.com/cloudflare/agents/tree/main/voice-providers/plivo) accepts the connection and bridges the caller and the `VoiceAgent`.  It converts between Plivo's 8 kHz mulaw and the agent's 16 kHz PCM, relays call and DTMF events, and handles barge-in when the caller speaks over the agent.

The Plivo adapter routes the call to the `VoiceAgent`, which transcribes the caller's speech, generates a response with the model, and synthesizes the reply as audio.

***

## Prerequisites

| Service        | Requirement                                                                                    |
| -------------- | ---------------------------------------------------------------------------------------------- |
| **Plivo**      | Auth ID, Auth Token, and a voice-enabled number from the [Plivo console](https://cx.plivo.com) |
| **Cloudflare** | An account with [Workers AI](https://developers.cloudflare.com/workers-ai/) enabled            |
| **Wrangler**   | The Cloudflare CLI, signed in with `npx wrangler login`                                        |
| **Node.js**    | Node 18 or newer                                                                               |

***

## Quick start

Deploy the ready example to Cloudflare and connect the Plivo number to it.

<Steps>
  <Step title="Clone and build the repo">
    ```bash theme={null}
    git clone https://github.com/cloudflare/agents.git
    cd agents
    pnpm install && pnpm run build
    ```
  </Step>

  <Step title="Set up the example">
    Copy the example env file to `.env`, and add the Plivo Auth ID, Auth Token, and phone number to it.

    ```bash theme={null}
    cd examples/plivo-voice-agent
    cp .env.example .env
    ```
  </Step>

  <Step title="Deploy">
    ```bash theme={null}
    pnpm run deploy
    ```

    `pnpm run deploy` deploys the Worker to Cloudflare, where it runs at a stable `workers.dev` URL.

    The deploy script then creates a Plivo application, connects it to the deployed Worker, and assigns the Plivo number to that application.
  </Step>

  <Step title="Test the call">
    Dial the Plivo number to reach the agent.
  </Step>
</Steps>

***

## Local development

Run the Worker locally.

```bash theme={null}
cd examples/plivo-voice-agent
pnpm run dev
```

`pnpm run dev` opens a [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) tunnel that exposes the local Worker to a temporary public URL, then connects the Plivo number to that tunnel. Calls now reach the locally running Cloudflare Worker.

To connect the Plivo number to the deployed Cloudflare Worker, run `pnpm run deploy`.

***

## Set up the integration

To add the Plivo Adapter to an existing Cloudflare Worker.

<Steps>
  <Step title="Install the adapter">
    Install the adapter along with the agent, voice, and Workers AI packages.

    ```bash theme={null}
    pnpm add @cloudflare/voice-plivo @cloudflare/voice agents ai workers-ai-provider
    ```
  </Step>

  <Step title="Add the adapter to the Worker">
    The `withVoice` function takes Cloudflare's base `Agent` class and returns a voice-enabled version, `VoiceAgent`. Create a class that extends `VoiceAgent` and sets the three stages of the pipeline:

    * `transcriber` for [STT](https://developers.cloudflare.com/workers-ai/models/?tasks=Automatic+Speech+Recognition)
    * `tts` for [TTS](https://developers.cloudflare.com/workers-ai/models/?tasks=Text-to-Speech)
    * `onTurn` for the [LLM](https://developers.cloudflare.com/workers-ai/models/?tasks=Text+Generation)

    Then add two routes to the Worker.

    * `/answer` returns the Plivo answer XML that opens the audio stream
    * `/plivo` is where the audio stream connects, and it hands the call to the adapter

    ```typescript theme={null}
    import { Agent, routeAgentRequest } from "agents";
    import { withVoice, WorkersAIFluxSTT, type TTSProvider, type VoiceTurnContext } from "@cloudflare/voice";
    import { PlivoAdapter } from "@cloudflare/voice-plivo";
    import { createWorkersAI } from "workers-ai-provider";
    import { streamText } from "ai";

    const VoiceAgent = withVoice(Agent);

    class PCMTTS implements TTSProvider {
      constructor(private ai: Ai) {}

      async synthesize(text: string, signal?: AbortSignal): Promise<ArrayBuffer | null> {
        const res = (await this.ai.run(
          "@cf/deepgram/aura-2-en",
          { text, speaker: "asteria", encoding: "linear16", sample_rate: 16000, container: "none" },
          { returnRawResponse: true, ...(signal ? { signal } : {}) }
        )) as Response;
        if (!res.ok) return null;
        return res.arrayBuffer();
      }
    }

    export class MyVoiceAgent extends VoiceAgent<Env> {
      transcriber = new WorkersAIFluxSTT(this.env.AI);
      tts = new PCMTTS(this.env.AI);

      async onTurn(transcript: string, context: VoiceTurnContext) {
        const workersAi = createWorkersAI({ binding: this.env.AI });
        const result = streamText({
          model: workersAi("@cf/moonshotai/kimi-k2.6"),
          messages: [...context.messages, { role: "user", content: transcript }]
        });
        return result.textStream;
      }
    }

    export default {
      async fetch(request: Request, env: Env) {
        const url = new URL(request.url);

        if (url.pathname === "/answer") {
          const wsUrl = `wss://${url.host}/plivo`;
          const xml = `<Response><Stream keepCallAlive="true" bidirectional="true" contentType="audio/x-mulaw;rate=8000">${wsUrl}</Stream></Response>`;
          return new Response(xml, { headers: { "Content-Type": "application/xml" } });
        }

        if (url.pathname === "/plivo") {
          return PlivoAdapter.handleRequest(request, env, "MyVoiceAgent");
        }

        return (await routeAgentRequest(request, env)) ?? new Response("Not found", { status: 404 });
      }
    };
    ```

    <Note>
      The TTS model should return PCM instead of MP3 since the adapter needs the audio format in 16 kHz `linear16` and does not decode MP3.

      The built-in `WorkersAITTS` returns MP3, hence `PCMTTS`, the custom class wraps the `@cf/deepgram/aura-2-en` model to request `linear16` output.
    </Note>
  </Step>

  <Step title="Add tool calling (optional)">
    `onTurn` runs the LLM model through the [`ai`](https://www.npmjs.com/package/ai) SDK. The tools are added with the SDK's `tools` option.

    The model calls a tool, the SDK runs it, and the result feeds back into the reply the agent speaks.

    ```typescript theme={null}
    import { streamText, tool } from "ai";
    import { z } from "zod";

    async onTurn(transcript: string, context: VoiceTurnContext) {
      const workersAi = createWorkersAI({ binding: this.env.AI });
      const result = streamText({
        model: workersAi("@cf/moonshotai/kimi-k2.6"),
        messages: [...context.messages, { role: "user", content: transcript }],
        tools: {
          get_current_time: tool({
            description: "Get the current date and time.",
            inputSchema: z.object({}),
            execute: async () => new Date().toISOString()
          })
        }
      });
      return result.textStream;
    }
    ```
  </Step>

  <Step title="Handle interruptions (optional)">
    The adapter lets the caller interrupt the Voice Agent. When it detects the caller speak while the agent is talking, it stops the current reply.

    It ignores short backchannels and background noise so a stray sound does not cut the agent off.

    Override `onInterrupt` to run custom logic when an interrupt happens.

    ```typescript theme={null}
    import type { Connection } from "agents";

    async onInterrupt(connection: Connection) {
      console.log("caller barged in");
    }
    ```
  </Step>

  <Step title="Deploy and connect Plivo to the Worker">
    The Worker must be publicly reachable for Plivo to connect to it.

    `wrangler deploy` builds and uploads it to Cloudflare and prints its `workers.dev` URL. No separate build command is needed.

    Create a deploy script named `deploy.ts`. It runs `wrangler deploy`, reads the deployed Worker URL from the output, and passes it to `setupPlivoApplication`, which finds or creates a Plivo application, sets its answer URL to the deployed Worker's `/answer` endpoint, and assigns the Plivo number to it.

    Placing it in a `scripts` folder at the project root (`scripts/deploy.ts`) is the recommended structure.

    ```typescript theme={null}
    import { execSync } from "node:child_process";
    import { setupPlivoApplication } from "@cloudflare/voice-plivo";

    const out = execSync("wrangler deploy", { encoding: "utf8" });
    const workerUrl = out.match(/https:\/\/[^\s]+\.workers\.dev/)[0];

    await setupPlivoApplication({
      authId: process.env.PLIVO_AUTH_ID,
      authToken: process.env.PLIVO_AUTH_TOKEN,
      phoneNumber: process.env.PLIVO_PHONE_NUMBER,
      answerUrl: `${workerUrl}/answer`
    });
    ```

    Add it to `package.json` as a `deploy` script.

    ```json theme={null}
    "scripts": {
      "deploy": "tsx scripts/deploy.ts"
    }
    ```

    Then deploy and connect Plivo in one command.

    ```bash theme={null}
    pnpm run deploy
    ```

    `setupPlivoApplication` is idempotent, so it is safe to run on every deploy.
  </Step>

  <Step title="Test the call">
    Dial the Plivo number to reach the agent.
  </Step>
</Steps>

***

## Choose your models

### Workers AI Models

[Workers AI](https://developers.cloudflare.com/workers-ai/models/) models are hosted by Cloudflare and called through the `env.AI` binding.

Browse the [Workers AI catalog](https://developers.cloudflare.com/workers-ai/models/), or the models directory in the Cloudflare dashboard, and swap in whichever fits the language, latency, and cost requirements.

The example above uses these Workers AI models:

* STT Provider - [`@cf/deepgram/flux`](https://developers.cloudflare.com/workers-ai/models/flux/)
* Text Generation Model - [`@cf/moonshotai/kimi-k2.6`](https://developers.cloudflare.com/workers-ai/models/kimi-k2.6/)
* TTS Provider - [`@cf/deepgram/aura-2-en`](https://developers.cloudflare.com/workers-ai/models/aura-2-en/)

<Note>
  The TTS model must return raw PCM, and not MP3. See [Set up the integration](#set-up-the-integration) for the encoding settings that produce it.
</Note>

### Third Party Providers

Cloudflare also lists [third-party models](https://developers.cloudflare.com/ai/models/?providers=third-party) from outside providers. These are reached through [AI Gateway](https://developers.cloudflare.com/ai-gateway/) instead of the `env.AI` binding.

Each provider has its own API, so integrating these models for any stage of the pipeline is a provider-specific implementation. [AI Gateway](https://developers.cloudflare.com/ai-gateway/) gives a consistent way to route and authenticate the request, but the endpoint, parameters, and output format follow the provider.

The snippets below show how to implement a third-party provider model for each stage:

#### Response-Text-Generation Stage

Route the response text generation stage through AI Gateway with the [`ai-gateway-provider`](https://www.npmjs.com/package/ai-gateway-provider) package, which plugs into the same `ai` SDK that `onTurn` already uses.

```typescript theme={null}
import { streamText } from "ai";
import { createAiGateway } from "ai-gateway-provider";
import { createOpenAI } from "@ai-sdk/openai";

async onTurn(transcript: string, context: VoiceTurnContext) {
  const aigateway = createAiGateway({ binding: this.env.AI.gateway("my-gateway") });
  const openai = createOpenAI({ apiKey: this.env.OPENAI_API_KEY });

  const result = streamText({
    model: aigateway([openai("gpt-4o-mini")]),
    messages: [...context.messages, { role: "user", content: transcript }]
  });
  return result.textStream;
}
```

#### Text-to-Speech Stage

The `tts` stage takes a `TTSProvider`, an interface from `@cloudflare/voice` with one method, `synthesize`, that returns the spoken audio.

To use a third-party TTS model, implement `TTSProvider` in a small class whose `synthesize` method calls the model through AI Gateway and returns raw PCM.

Requesting a `pcm_16000` output format returns the 16 kHz linear16 the adapter needs directly, the same way the example's `PCMTTS` class, requests `linear16` from the Workers AI model `@cf/deepgram/aura-2-en`.

```typescript theme={null}
import type { TTSProvider } from "@cloudflare/voice";

class GatewayTTS implements TTSProvider {
  constructor(private env: Env) {}

  async synthesize(text: string, signal?: AbortSignal): Promise<ArrayBuffer | null> {
    const res = await fetch(
      `https://gateway.ai.cloudflare.com/v1/${this.env.CF_ACCOUNT_ID}/my-gateway/elevenlabs/v1/text-to-speech/${this.env.TTS_VOICE_ID}?output_format=pcm_16000`,
      {
        method: "POST",
        headers: {
          "xi-api-key": this.env.ELEVENLABS_API_KEY,
          "Content-Type": "application/json"
        },
        body: JSON.stringify({ text, model_id: "eleven_multilingual_v2" }),
        signal
      }
    );
    if (!res.ok) return null;
    return res.arrayBuffer();
  }
}
```

Set it as the agent's `tts` stage.

```typescript theme={null}
tts = new GatewayTTS(this.env);
```

#### Speech-to-Text Stage

The `transcriber` stage takes a [`Transcriber`](https://github.com/cloudflare/agents/blob/main/packages/voice/src/types.ts), a streaming interface from `@cloudflare/voice` that runs a live session and returns transcripts as the caller speaks.

The built-in [`WorkersAIFluxSTT`](https://github.com/cloudflare/agents/blob/main/packages/voice/src/workers-ai-providers.ts) implements this interface for the Workers AI models and can be used as a reference. A third-party provider follows the same structure, implemented as a custom class that connects to the provider's realtime STT API.

***

## Related

<CardGroup cols={2}>
  <Card title="Audio Streaming" icon="audio-lines" href="/docs/voice-agents/audio-streaming/overview">
    The Plivo streaming API this integration builds on
  </Card>

  <Card title="Stream XML" icon="file-code" href="/docs/voice-agents/audio-streaming/xml/stream">
    The `<Stream>` element the answer URL returns
  </Card>

  <Card title="Cloudflare Agents docs" icon="cloudflare" href="https://developers.cloudflare.com/agents/" arrow={true}>
    The Cloudflare Agents platform documentation
  </Card>

  <Card title="Cloudflare Agents repo" icon="github" href="https://github.com/cloudflare/agents" arrow={true}>
    The Cloudflare Agents source and examples
  </Card>
</CardGroup>
