Skip to main content
The repo ships two example projects so you can see a full working setup - an agent and the bridge - before wiring your own. A LiveKit call needs both: your agent runs as a worker, and the bridge dispatches it into a per-call room. Budget about 15 minutes once the Teams setup is done.

examples/agents

Two ready-to-run LiveKit agents: a minimal voice pipeline (voice_agent.py) and a bitHuman avatar (avatar_agent.py).

examples/basic-bridge

Embed the bridge in your own Node project: loadConfig() + startServer(), env-file config, graceful shutdown.

What you need first

  • Node.js >= 20 (bridge) and Python 3.10+ with uv or pip (agent).
  • A LiveKit server - a LiveKit Cloud project or self-hosted - with its URL, API key, and API secret.
  • An OpenAI API key (the example agent uses OpenAI STT/LLM/TTS).
  • A StandIn identity with its shared secret (from pairing or the dashboard). The sandbox works too if you have no Teams bot yet.

Set up the LiveKit project and agent

Unlike a hosted agent, a LiveKit “agent” is a worker you run - the example in examples/agents is a complete, ready-to-run one. You need two things: a LiveKit project for it to connect to, and an agent name so the bridge can dispatch it.

Create a LiveKit project

Sign in to LiveKit Cloud (or run a self-hosted server) and create a project. From its settings, copy three values - they go in both the agent’s .env (step 1) and the bridge (step 2):
ValueEnv variable
Project URL (wss://<project>.livekit.cloud)LIVEKIT_URL
API keyLIVEKIT_API_KEY
API secretLIVEKIT_API_SECRET
The agent worker and the bridge must point at the same LiveKit project. If they use different projects, dispatch never reaches the agent and the caller hears silence - the single most common setup mistake.

The agent name is the contract

The bridge dispatches by agent name (explicit dispatch). The example voice agent registers itself as teams-voice-agent:
cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint, agent_name="teams-voice-agent"))
That name must equal the bridge’s LIVEKIT_AGENT_NAME (step 2). Nothing else about the agent is Teams-specific - and unlike a hosted agent, there is no audio-format setting to get right, because the bridge and the LiveKit SDK resample to 16 kHz for you.

Use your own agent instead

Any existing LiveKit agent works - give its worker an agent_name, point it at the same project, and run it in place of the example:
cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint, agent_name="my-teams-agent"))
Read the caller’s details from ctx.job.metadata and react to the teams.context / teams.goodbye data topics if you want (all shown in the example agents). See Agents and dispatch.

1. Run the voice agent

Clone the repo and enter the agents example:
git clone https://github.com/komaa-com/livekit-msteams-bridge
cd livekit-msteams-bridge/examples/agents
cp .env.example .env
Fill in the agent’s .env:
VariableWhat to put there
LIVEKIT_URLYour LiveKit server, e.g. wss://<project>.livekit.cloud.
LIVEKIT_API_KEY / LIVEKIT_API_SECRETThe key pair from your LiveKit project.
OPENAI_API_KEYKey for the example’s STT/LLM/TTS stack.
Then install and start the worker:
uv sync
uv run voice_agent.py download-files
uv run voice_agent.py dev
(Plain pip works too: pip install -r requirements.txt && python voice_agent.py dev. Use start instead of dev in production.) The worker registers with your LiveKit server under the agent name teams-voice-agent and waits for dispatch - it will not join anything until the bridge creates a room.

2. Run the bridge

In a second terminal, point the bridge at the same LiveKit project and at that agent name:
LIVEKIT_URL=wss://your-project.livekit.cloud \
LIVEKIT_API_KEY=API... \
LIVEKIT_API_SECRET=... \
LIVEKIT_AGENT_NAME=teams-voice-agent \
WORKER_SHARED_SECRET=... \
npx @komaa/livekit-msteams-bridge
LIVEKIT_AGENT_NAME must equal the agent_name the worker registers with (the examples use teams-voice-agent and teams-avatar-agent). A mismatch is the classic silent failure: the room is created, the caller hears nothing, and the worker never gets a job.
The bridge listens on ws://<host>:8080/voice/msteams/stream.

3. Expose port 8080

StandIn connects from the internet, so the port needs a public wss:// URL. Any tunnel works:
tailscale funnel --bg --https=8080 8080
Your URL: wss://<machine>.<tailnet>.ts.net:8080/voice/msteams/stream

4. Connect it to StandIn and call

  1. In your StandIn dashboard, set the identity’s Agent voice URL to the wss:// URL from step 3.
  2. Make sure the identity’s shared secret equals WORKER_SHARED_SECRET.
  3. Call your Teams bot (or join the sandbox meeting). StandIn joins, connects to the bridge, the bridge creates a room and dispatches the agent, and the agent answers.

Swap in the avatar agent

avatar_agent.py is the same pipeline plus a lip-synced bitHuman avatar on the bot’s video tile. Two extra variables in the agent’s .env:
VariableWhat to put there
BITHUMAN_API_SECRETYour bitHuman API secret.
BITHUMAN_MODEL_PATHPath to the downloaded .imx avatar model.
Run it the same way (uv run avatar_agent.py dev) and restart the bridge with LIVEKIT_AGENT_NAME=teams-avatar-agent.

How the pieces talk

Your agent needs no Teams-specific code, but three integration points are available:
  • agent_name in WorkerOptions - must match the bridge’s LIVEKIT_AGENT_NAME for explicit dispatch.
  • ctx.job.metadata (JSON) - per-call context: source, caller_name, tenant_id, call_direction, and user_id (AAD id when Teams provides one).
  • Data topics - teams.context (participant count, DTMF) and teams.goodbye (the governor’s goodbye line; have your handler speak it and interrupt the current turn).
Details: Agent dispatch and Agents and dispatch on the project site.

Docker

Each agent has a Dockerfile; pass the environment at runtime rather than baking it in:
docker build -f Dockerfile.voice_agent -t teams-voice-agent .
docker run --env-file .env teams-voice-agent
The avatar image additionally needs the model mounted: docker run --env-file .env -v ./avatar.imx:/models/avatar.imx teams-avatar-agent.

From example to your own project

Embed the bridge with npm install @komaa/livekit-msteams-bridge and start from examples/basic-bridge (loadConfig() + startServer()); keep your own agent worker as is - any LiveKit agent works unchanged. The library API documents the programmatic surface.

If something does not work

  • WebSocket rejected with 401 - WORKER_SHARED_SECRET does not match the secret in StandIn.
  • Room is created but no agent joins - LIVEKIT_AGENT_NAME does not match the worker’s agent_name, or the worker is not running / registered with a different LiveKit project.
  • Bot joins the call but stays silent - the Agent voice URL is unreachable from the internet; re-check the tunnel.
  • More: Troubleshooting.

Examples README

Configuration

Library API