Agent dispatch
Agent dispatch
Your token endpoint works, but there is a gap: how does LiveKit know which agent to put in the room? Right now it uses automatic dispatch — every new room gets the same agent. That is fine for a single-agent project, but the moment you have two agents (say a receptionist and a billing specialist), you need explicit control. This chapter covers how agent dispatch works and how to configure it from both the agent server and the frontend.
What you'll learn
- The difference between automatic and explicit agent dispatch
- How to register an agent with a name on the server side
- How to dispatch a specific agent from your token endpoint
- How to pass metadata to the agent through dispatch
- How to dispatch multiple agents into a single room
- When to use API dispatch vs. token-based dispatch
How dispatch works
When a participant connects to a room, LiveKit server needs to decide which agent server should handle the session. This process is called dispatch. LiveKit optimizes it for high concurrency — hundreds of thousands of connections per second with dispatch latency under 150 ms.
There are two modes:
| Mode | How it works | Best for |
|---|---|---|
| Automatic | Every new room gets the agent. No configuration needed. | Single-agent projects, prototyping |
| Explicit | You specify which agent(s) to dispatch by name. | Multi-agent projects, production apps |
Think of automatic dispatch like a restaurant with one chef — every order goes to the same person. Explicit dispatch is like a restaurant with a sushi chef and a pastry chef. You need to tell the kitchen which chef should handle which order. The dispatch system is that routing layer.
Automatic dispatch: the default
If you followed Course 1.1, your agent already uses automatic dispatch. When you define an entrypoint without an agent_name, LiveKit dispatches it to every new room:
from livekit.agents import AgentSession, Agent, RtcSession
@server.rtc_session()
async def entrypoint(ctx: RtcSession):
agent = Agent(instructions="You are a helpful dental receptionist.")
session = AgentSession()
await session.start(agent=agent, room=ctx.room)This is zero-configuration and convenient for getting started. But the moment you deploy a second agent in the same LiveKit project, both agents would try to join every room. That is not what you want.
Explicit dispatch: naming your agent
To switch to explicit dispatch, give your agent a name. This single change tells LiveKit: "do not auto-dispatch this agent — wait until someone asks for it by name."
@server.rtc_session(agent_name="dental-receptionist")
async def entrypoint(ctx: RtcSession):
agent = Agent(instructions="You are a helpful dental receptionist.")
session = AgentSession()
await session.start(agent=agent, room=ctx.room)const server = new AgentServer({
agentName: "dental-receptionist",
});Setting agent_name disables automatic dispatch
Once you set agent_name, the agent will not be dispatched to any room unless explicitly requested. If you deploy this change without updating your token endpoint, your frontend will connect to an empty room with no agent.
Dispatching from your token endpoint
The most common way to dispatch an agent is through the participant's access token. When the token includes a RoomConfiguration with agent dispatch information, LiveKit dispatches the named agent as soon as the participant connects.
Update the token endpoint you built in the previous chapter:
import { AccessToken } from "livekit-server-sdk";
import { RoomAgentDispatch, RoomConfiguration } from "@livekit/protocol";
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const roomName = request.nextUrl.searchParams.get("roomName");
const participantName = request.nextUrl.searchParams.get("participantName");
if (!roomName || !participantName) {
return NextResponse.json(
{ error: "Missing roomName or participantName" },
{ status: 400 }
);
}
const at = new AccessToken(
process.env.LIVEKIT_API_KEY,
process.env.LIVEKIT_API_SECRET,
{
identity: participantName,
ttl: "10m",
}
);
at.addGrant({
roomJoin: true,
room: roomName,
canPublish: true,
canSubscribe: true,
});
// Dispatch the dental receptionist agent when the participant connects
at.roomConfig = new RoomConfiguration({
agents: [
new RoomAgentDispatch({
agentName: "dental-receptionist",
}),
],
});
const token = await at.toJwt();
return NextResponse.json({ token });
}The Python equivalent for a FastAPI backend:
from livekit.api import AccessToken, VideoGrants, RoomAgentDispatch, RoomConfiguration
def create_token(room_name: str, participant_name: str) -> str:
token = (
AccessToken()
.with_identity(participant_name)
.with_grants(VideoGrants(room_join=True, room=room_name))
.with_room_config(
RoomConfiguration(
agents=[
RoomAgentDispatch(agent_name="dental-receptionist")
],
),
)
.to_jwt()
)
return tokenToken-based dispatch is the recommended approach for frontend applications. The dispatch configuration travels with the token, so the agent is assigned the moment the participant connects — no extra API calls needed. The Session API on the frontend does not need any changes; it just works.
Passing metadata to the agent
Explicit dispatch lets you pass a metadata string to the agent. This metadata is available in the agent's RtcSession context, making it a clean way to send user-specific information without an extra API call.
at.roomConfig = new RoomConfiguration({
agents: [
new RoomAgentDispatch({
agentName: "dental-receptionist",
metadata: JSON.stringify({
userId: "user-123",
userName: "Jane Doe",
appointmentId: "apt-456",
}),
}),
],
});On the agent side, read the metadata from the job context:
import json
@server.rtc_session(agent_name="dental-receptionist")
async def entrypoint(ctx: RtcSession):
metadata = json.loads(ctx.job.metadata or "{}")
user_name = metadata.get("userName", "there")
agent = Agent(
instructions=f"You are a dental receptionist. The caller's name is {user_name}."
)
session = AgentSession()
await session.start(agent=agent, room=ctx.room)Use JSON for structured metadata
The metadata field is a plain string. LiveKit recommends JSON so both sides can serialize and deserialize structured data reliably. Keep it small — this is for routing context, not large payloads.
Dispatching multiple agents
A single token can dispatch multiple agents into the same room. This is useful when you want a primary conversational agent plus a background agent for tasks like note-taking or real-time translation.
at.roomConfig = new RoomConfiguration({
agents: [
new RoomAgentDispatch({
agentName: "dental-receptionist",
metadata: JSON.stringify({ role: "primary" }),
}),
new RoomAgentDispatch({
agentName: "note-taker",
metadata: JSON.stringify({ role: "observer" }),
}),
],
});Each agent receives its own job with its own metadata. They join the same room as separate participants and can interact with the user and with each other through tracks and data channels.
Dispatch via API
Sometimes you need to dispatch an agent without a participant connecting first — for example, when making an outbound phone call or pre-warming a room. The AgentDispatchService API handles this:
from livekit import api
async def dispatch_agent_to_room():
lkapi = api.LiveKitAPI()
dispatch = await lkapi.agent_dispatch.create_dispatch(
api.CreateAgentDispatchRequest(
agent_name="dental-receptionist",
room="outbound-call-room-123",
metadata='{"phone": "+15551234567"}',
)
)
print(f"Dispatched agent: {dispatch.agent_name} to {dispatch.room}")
await lkapi.aclose()import { AgentDispatchClient } from "livekit-server-sdk";
async function dispatchAgentToRoom() {
const client = new AgentDispatchClient(
process.env.LIVEKIT_URL!,
process.env.LIVEKIT_API_KEY!,
process.env.LIVEKIT_API_SECRET!
);
const dispatch = await client.createDispatch(
"outbound-call-room-123",
"dental-receptionist",
{ metadata: JSON.stringify({ phone: "+15551234567" }) }
);
console.log("Dispatched agent:", dispatch.agentName);
}The room is automatically created if it does not already exist. This is the pattern used for outbound calling — dispatch the agent first, then dial the phone number into the same room.
API dispatch and token dispatch are not mutually exclusive. You might use token dispatch for web users (the agent joins when they connect) and API dispatch for outbound calls (the agent joins before the callee picks up). Both use the same agent server infrastructure — the only difference is who triggers the dispatch.
Choosing a dispatch strategy
| Scenario | Strategy | Why |
|---|---|---|
| Single agent, all rooms | Automatic | Zero configuration, simplest setup |
| Web frontend, 1:1 sessions | Token dispatch | Agent arrives with the user, no extra API calls |
| Multiple agents per room | Token dispatch (multiple entries) | All agents dispatched in one token |
| Outbound phone calls | API dispatch | Agent must be in the room before the call connects |
| Background job / no participant | API dispatch | No participant token to carry dispatch info |
| Dynamic agent selection at runtime | API dispatch | Server logic decides which agent based on business rules |
Test your knowledge
Question 1 of 3
What happens if you set agent_name on your agent server but do not include dispatch configuration in the participant's token?
What you learned
- Automatic dispatch sends the same agent to every room — simple but inflexible
- Explicit dispatch requires setting
agent_nameon the agent server, which disables automatic dispatch - Token-based dispatch embeds
RoomAgentDispatchin the access token'sRoomConfiguration— the recommended approach for frontends - Dispatch metadata passes user context (IDs, names, preferences) to the agent without extra API calls
- Multiple agents can be dispatched to the same room from a single token
- API dispatch via
AgentDispatchServiceis used when no participant triggers the connection (outbound calls, background jobs)
Next up
Your agent is dispatched and in the room. In the next chapter, you will track its lifecycle — connecting, listening, thinking, speaking — and build UI that responds to every state change.