Skip to main content
Pipecat ships a built-in development runner (pipecat.runner.run) that handles the server-side glue most bots need during development: creating Daily rooms, accepting WebRTC offers, terminating telephony WebSockets, and serving a prebuilt UI to talk to your bot. You write the bot; the runner does everything around it. This page covers the canonical shape: a bot file with a single async entry point, run via pipecat.runner.run.main().
The development runner is a local development tool — it isn’t built or supported for production use. See The runner is a development tool below, and Running bots in production for what to reach for instead.

The bot entry point

Every bot is a Python file with an async bot() function that takes a RunnerArguments. The runner calls this function once per session, passing in everything the bot needs to connect (room URL, token, WebRTC connection, WebSocket, etc.) as fields on the argument object.
bot.py
The bot is encapsulated: it doesn’t know how the request to start a session arrived, who authenticated it, or whether it’s running locally or in production. Everything it needs to do its job is on runner_args. This is the property that makes the same bot file portable across the development runner, Pipecat Cloud, and most production self-hosting setups.

Choosing a transport

RunnerArguments has transport-specific subclasses; the runner instantiates the right one based on how the session was initiated. A typical multi-transport bot pattern-matches:
A bot can support a single transport or many. In both the local development runner and Pipecat Cloud, the client chooses how to start the session by sending a transport value in the /start request (webrtc, daily, twilio, telnyx, plivo, exotel, or websocket); -t/--transport only restricts the local runner to one transport and sets that default. For working files demonstrating each transport, see examples/runner-examples/ — the 01- through 04- files step from a single-transport bot through to a factory-driven multi-transport setup.

Installing and running

Run the bot file directly:
By default, the development runner starts a local server on localhost:7860, serves the prebuilt client UI at /client, and lets clients start sessions through POST /start. Clients can request the transport in that start request; -t/--transport is available when you want to restrict the local runner to one transport. For the full /start request shape, transport-specific behavior, and CLI flags, see the Development Runner guide.

What the runner does for you

Behind uv run bot.py the runner is doing a fair amount of work depending on the transport the client requests:
  • WebRTC (smallwebrtc) — mounts a prebuilt UI at /client, accepts POST /api/offer (with ICE candidates via PATCH /api/offer), and bridges the resulting SmallWebRTCConnection to your bot() function. Also exposes a POST /start endpoint that returns a sessionId, mimicking Pipecat Cloud’s start API.
  • Daily — calls Daily’s REST API to create a room and issue tokens, then either redirects the browser to the room (via GET /daily) or returns the room URL + token via POST /start.
  • Telephony — returns the carrier-specific XML stub (TwiML for Twilio, the equivalent for Telnyx/Plivo/Exotel) on POST /, then accepts the bidirectional media WebSocket at /ws and hands it to your bot wrapped in a WebSocketRunnerArguments.
  • Plain WebSocket — for non-telephony clients (e.g. browser apps using protobuf framing). POST /start with "transport": "websocket" returns a wsUrl for the /ws-client endpoint; the bot receives a WebSocketRunnerArguments with transport_type="websocket".
  • Daily PSTN dial-in (--dialin) — handles Daily’s pinless dial-in webhook, creates a SIP-enabled room, and dispatches the bot with full dial-in context.
GET / redirects to the prebuilt client UI at /client/, and GET /status reports which transports the running instance accepts. The POST /start and /sessions/{id}/... endpoints exposed by the runner are deliberately shaped the same way as Pipecat Cloud’s session API. That means a client built against the development runner works against PCC unchanged, and a custom production dispatcher can offer the same contract if you want clients to remain portable.

Adding your own routes

The runner exports its FastAPI app as a module-level attribute, so you can add custom routes before calling main():
This is the simplest way to extend the runner — for things like health checks, internal status endpoints, or accepting metadata alongside session-start requests — without forking the dispatcher itself.

The runner is a development tool, not a production server

The development runner is exactly that — a tool for local development. It’s deliberately simple: its POST /start endpoint accepts session-start requests from anyone, some transport webhooks (like Daily’s dial-in webhook) are likewise unauthenticated, and there’s no rate limiting or backpressure anywhere. It’s also a single process with no lifecycle management. Those are the right tradeoffs on your own machine, and the wrong ones on a public address, where a single request can spin up a bot and create paid rooms on your account. For anything real, put a proper dispatcher or a managed runtime in front of your bots instead of exposing the runner. The next page, Running bots in production, walks through those options — including how to package the bot into a container image — and, if you only need something reachable for a prototype or alpha test, the minimum you’d want in front of the runner and where that stops being enough.