Lets callers register multiple workers in a single call instead of awaiting add_worker() repeatedly. Updates all examples, docs, tests, and proxy worker docstrings to use the new API.
13 KiB
Pipecat Multi-Worker Examples
This directory contains example bots that use the multi-worker framework in pipecat.workers, pipecat.pipeline.runner (with add_workers()), and the WorkerBus. Each example shows a different cooperation pattern between workers: hand-off, parallel fan-out, remote workers, etc.
Setup
From the repo root:
uv sync --all-extras
source .venv/bin/activate
cd examples/multi-worker
Copy the env template and fill in your API keys:
cp env.example .env
Environment variables
| Variable | Required by |
|---|---|
OPENAI_API_KEY |
LLM workers |
DEEPGRAM_API_KEY |
STT |
CARTESIA_API_KEY |
TTS |
DAILY_API_KEY |
Optional: only with --transport daily |
Additional, example-specific variables are listed below.
Table of contents
Local (single process)
- Handoff between LLM workers
- Parallel debate
- Voice code assistant with Claude Agent SDK
- Sensor controller
Distributed (multi-process)
Local
Examples where all workers run in the same process on an AsyncQueueBus.
Handoff between LLM workers
Two LLM workers (greeter + support) that transfer control to each other during a voice conversation. A main worker owns the transport pipeline and bridges frames to the bus.
Running
uv run local-handoff/local-handoff-two-agents.py
Open http://localhost:7860/client in your browser to talk to your bot.
To use Daily transport:
uv run local-handoff/local-handoff-two-agents.py --transport daily
Overview
local-handoff-two-agents.py— Two LLM workers (greeter + support) that hand off viaactivate_worker(..., deactivate_self=True). The main worker owns STT, TTS, transport, and aBusBridgeProcessor.local-handoff-two-agents-tts.py— Same shape, but each child worker ships with its ownCartesiaTTSServicein a custom pipeline. The main worker has no TTS — audio comes from whichever child is active over the bus.
Parallel debate
Parallel fan-out using worker.job_group(...). A voice bot takes a topic from the user, kicks off three workers in parallel (advocate, critic, analyst), waits for all three to respond, and synthesizes a balanced answer. Each worker keeps its own LLM context across rounds.
Running
uv run parallel-debate/parallel-debate.py
Open http://localhost:7860/client in your browser to talk to your bot.
To use Daily transport:
uv run parallel-debate/parallel-debate.py --transport daily
Architecture
Main worker (transport + LLM + `debate` tool)
└── job_group(advocate, critic, analyst)
└── DebateWorker (LLMContextWorker, one per role)
- Main worker: transport (STT, TTS) + LLM moderator with a
debatedirect function that fans out viaworker.job_group(...). - Debate workers:
LLMContextWorkers spawned on the runner. Each keeps its ownLLMContextacross rounds and ships its completed turn back as a job response via the assistant-aggregator'son_assistant_turn_stoppedevent.
Voice code assistant
Talk to your codebase hands-free. Ask questions about code, project structure, or file contents and get spoken answers based on actual files. The Claude Agent SDK worker navigates the filesystem using Read, Bash, Glob, and Grep tools.
Additional environment variables
| Variable | Required by |
|---|---|
ANTHROPIC_API_KEY |
Code worker (Claude Agent SDK) |
PROJECT_PATH |
Optional, defaults to cwd |
Running
# Default: explores the current directory
uv run code-assistant/code-assistant.py
# Specify a project path
PROJECT_PATH=/path/to/your/project uv run code-assistant/code-assistant.py
Open http://localhost:7860/client in your browser to talk to your bot.
To use Daily transport:
uv run code-assistant/code-assistant.py --transport daily
Example questions
- "What does the main module do?"
- "Find all TODO comments in the project"
- "How is error handling implemented?"
- "What dependencies does this project use?"
- "Explain the test structure"
Architecture
Main worker (transport + LLM + `ask_code` tool)
└── job → CodeWorker (Claude Agent SDK)
code-assistant.py— Main worker: STT, LLM (with system prompt +ask_codedirect function), TTS, and transport. Theask_codetool dispatches a job to the worker viaworker.job("code_worker", payload=...).code_worker.py—CodeWorker: a bus-onlyBaseWorkerspawned on the runner. It accepts@job-style requests through the bus and runs them sequentially through a persistent Claude SDK session so follow-up questions share context.
Sensor controller
Two PipelineWorkers side by side, communicating only over job RPC. A voice agent has a single ask_controller(question) tool that forwards every temperature-related request to a worker; the worker owns a simulated thermometer and its own tool-calling LLM that decides how to answer (read the current value, inspect rolling stats, change the target, change the response rate). The worker is a plain PipelineWorker — it does not subclass LLMWorker and is not bridged.
Running
uv run sensor-controller/sensor-controller.py
Open http://localhost:7860/client in your browser to talk to your bot.
To use Daily transport:
uv run sensor-controller/sensor-controller.py --transport daily
Example questions
- "What's the temperature?"
- "Make it warmer."
- "Is it stable yet?"
- "Why is it slow?" / "Speed up the response."
- "What was the highest reading?"
Architecture
Voice agent (transport + STT + LLM + TTS, tool: ask_controller)
└── job → Controller (PipelineWorker)
└── SensorReader -> SensorStats -> user_agg -> llm -> assistant_agg
sensor-controller.py—build_sensor_controller()returns a plainPipelineWorker. Jobs arrive via@worker.event_handler("on_job_request"), the question is queued onto the worker LLM, and the LLM's reply is paired back to the job via the assistant aggregator'son_assistant_turn_stoppedevent.sensor.py— Two customFrameProcessorsubclasses:SensorReaderruns an autonomous tick loop that emits aSensorReadingFrameeach second (first-order lag toward target plus Gaussian noise; mutable target and response rate);SensorStatsmaintains rolling min/max/avg/trend.
Distributed
Examples where workers run across separate processes or machines.
Handoff via Redis
Same two-worker handoff as the local example, but each worker runs as a separate process connected via Redis pub/sub. Requires pip install pipecat-ai[redis].
Quick start (single machine, local Redis)
Terminal 1: start Redis
docker run --rm -p 6379:6379 redis:7
Terminal 2: start the greeter worker
uv run distributed-handoff/redis-handoff/llm.py greeter
Terminal 3: start the support worker
uv run distributed-handoff/redis-handoff/llm.py support
Terminal 4: start the main transport worker
uv run distributed-handoff/redis-handoff/main.py
All processes connect to redis://localhost:6379 by default.
Running across machines
Point each process at the same Redis instance:
Machine A
uv run distributed-handoff/redis-handoff/main.py --redis-url redis://your-redis-host:6379
Machine B
uv run distributed-handoff/redis-handoff/llm.py greeter --redis-url redis://your-redis-host:6379
Machine C
uv run distributed-handoff/redis-handoff/llm.py support --redis-url redis://your-redis-host:6379
Architecture
Machine A Redis Machine B
+------------+ +-------------+ +-------------+
| main.py | <----> | pub/sub | <----> | llm.py |
| (transport,| | channel: | | (greeter) |
| STT, TTS) | | pipecat:acme| +-------------+
+------------+ +-------------+ +-------------+
^ | llm.py |
+--------------> | (support) |
+-------------+
- main.py — Transport worker: Daily/WebRTC, Deepgram STT, Cartesia TTS, and a
BusBridgeProcessorover aRedisBus. - llm.py — LLM worker: runs either
greeterorsupportwith OpenAI behind a bridgedLLMWorker.
Handoff via PGMQ (Postgres)
Same shape as the Redis handoff, but the bus is backed by PGMQ on a shared Postgres database (e.g. Supabase). Requires pip install pipecat-ai[pgmq].
Additional environment variables
| Variable | Required by |
|---|---|
DATABASE_URL |
PostgreSQL DSN (e.g. Supabase pooled connection string) |
PGMQ_CHANNEL |
Optional, channel prefix for queue names. Defaults to pipecat_acme |
Quick start
Terminal 1: start the greeter worker
uv run distributed-handoff/pgmq-handoff/llm.py greeter --database-url $DATABASE_URL
Terminal 2: start the support worker
uv run distributed-handoff/pgmq-handoff/llm.py support --database-url $DATABASE_URL
Terminal 3: start the main transport worker
uv run distributed-handoff/pgmq-handoff/main.py --database-url $DATABASE_URL
You can also set DATABASE_URL in .env and omit the --database-url flag.
Architecture
Same as the Redis handoff above; the RedisBus is replaced by a PgmqBus, and the "pub/sub channel" is a set of PGMQ queues on the shared Postgres instance.
LLM worker via WebSocket proxy
Runs an LLM worker on a remote server, connected to the main transport worker via a WebSocket proxy. No shared bus required — the proxy workers forward bus messages point-to-point over the WebSocket.
Quick start (single machine)
Terminal 1: start the remote assistant server
uv run remote-proxy-assistant/assistant.py
Terminal 2: start the main transport worker
uv run remote-proxy-assistant/main.py --remote-url ws://localhost:8765/ws
Open http://localhost:7860/client in your browser to talk to the bot.
Running across machines
Server machine: start the assistant
uv run remote-proxy-assistant/assistant.py --host 0.0.0.0 --port 8765
Client machine: point at the server
uv run remote-proxy-assistant/main.py --remote-url ws://server-host:8765/ws
Architecture
+-------------+ +-------------+ +-------------+ +-----------------+
| | | | | | | |
| Main worker | | Proxy worker | <~~~~~> | Proxy worker | | Assistant worker |
| | | (client) | | (server) | | |
+-------------+ +-------------+ +-------------+ +-----------------+
messages messages messages messages
│ │ │ │
══════════╧═════════════════╧════════ ════════╧════════════════════╧═══════════
Task Bus Task Bus
═════════════════════════════════════ ═════════════════════════════════════════
- main.py — Transport worker with STT, TTS, and a
BusBridge. Spawns aWebSocketProxyClientTaskthat connects to the remote server and forwardsBusFrameMessages. - assistant.py — FastAPI server. Each WebSocket connection spawns a
WebSocketProxyServerTaskplus a bridgedAcmeAssistantLLM worker on a per-sessionPipelineRunner.
Security
The proxy workers filter messages by worker name:
- Only messages targeted at the remote worker cross the WebSocket
- Only messages targeted at the local worker are accepted from the WebSocket
- Broadcast messages never cross the WebSocket
Pass HTTP headers for authentication:
proxy = WebSocketProxyClientTask(
"proxy",
url="wss://server-host:8765/ws",
remote_worker_name="assistant",
local_worker_name="acme",
headers={"Authorization": "Bearer <token>"},
)