---
title: "Configuring LiveKit Agents at dispatch with metadata"
date: "2026-06-23"
authors: "Darryn Campbell"
tags: "guide"
---

# Configuring LiveKit Agents at dispatch with metadata

Say you're building a restaurant agent that serves many restaurants, each with different opening hours, a different address, and a different menu. You could deploy a separate agent per restaurant, but it's more efficient to handle all of them with a single generic agent that you customize at runtime, at the moment it's dispatched.

LiveKit supports [job metadata](https://docs.livekit.io/agents/server/agent-dispatch/#job-metadata), a JSON object you pass alongside the dispatch. The metadata can hold whatever you like, but in the restaurant example it's enough to pass the restaurant's ID; the agent looks up everything else from there.

This post shows how to build and dispatch one agent that serves six restaurants, specializes into the one the user picks, and answers questions about its opening hours and menu. You can find the full source code for this demo on [GitHub](https://github.com/darryncampbell/livekit-demo-single-agent-multiple-restaurants).

## Use cases

But it doesn't have to be restaurants. Consider a support line that answers for a different company on every call, a booking assistant that represents a different clinic or hotel, or a marketplace where each seller gets the same agent configured with their own catalog. In each case you run one deployment and select the right configuration when the client connects.

You can use the same technique to tailor the experience to the user, not just the business. A companion or coaching app greets the caller by name and looks up their history and preferences at the start of the call. Based on the user's region, you might also select the language, voice, and other attributes, so the same agent greets one caller in English and the next in Japanese. On security: most clients are verified with [tokens](https://docs.livekit.io/frontends/reference/tokens-grants/#overview), but for SIP clients see this post on [layered caller verification](./verify-sip-caller-identity).

The common thread is that the *configuration* varies per session, but the *code* does not.

## How it works

Your production solution has at least three components:

| Component | Description                                                                                                                                                                                                                                                                         |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Frontend  | The client your end user interacts with when they choose to 'talk to an agent'.                                                                                                                                                                                                     |
| Backend   | Your business logic. It issues [LiveKit tokens](https://docs.livekit.io/frontends/reference/tokens-grants/) that grant the user permission to join a room, and dispatches [an agent](https://docs.livekit.io/agents/server/agent-dispatch/) to that room with the correct metadata. |
| Agent     | Your generic agent, with all the capabilities it needs such as function tools or [external data](https://docs.livekit.io/agents/logic/external-data/), configured per calling client.                                                                                               |

The metadata can be any JSON object; in this demo it carries just the restaurant's ID.

Dispatching an agent and a user to the same room, based on a provided ID, looks like this:

<Mermaid
  chart={`flowchart TD
FE["Frontend"] -->|"{restaurant_id}"| BE["Backend"]
BE -->|"dispatch agent (metadata)"| LK["LiveKit"]
LK -->|"dispatch agent (metadata)"| AG["Agent (restaurant_id)"]
BE -->|"token"| FE
FE -->|"joins room (token)."| LK

classDef key fill:#1FD5F9,stroke:#00BFFF,color:#1a1a1a;
class AG key;
`}
/>

## Code walkthrough

For the restaurant use case described above, where a single agent serves six restaurants, the code looks like this:

### Step 1: the frontend sends an ID

When the user picks a restaurant, the frontend POSTs that restaurant's ID to the backend and gets back everything it needs to join the room.

```tsx
// frontend/components/restaurant-session.tsx
const res = await fetch(`${BACKEND_URL}/api/connection-details`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ restaurant_id: restaurant.id }),
});
const { serverUrl, participantToken } = await res.json();
```

Once it has `serverUrl` and `participantToken`, the demo uses the [LiveKitRoom](https://docs.livekit.io/reference/components/react/component/livekitroom/) component to join the room with that token.

```tsx
<LiveKitRoom
  serverUrl={connection.serverUrl}
  token={connection.participantToken}
  connect={true}
  audio={true}
  video={false}
  onDisconnected={onClose}
>
  <AgentView restaurant={restaurant} />
  <VoiceAssistantControlBar />
  <RoomAudioRenderer />
</LiveKitRoom>
```

### Step 2: the backend dispatches the agent with the ID as metadata

In response to the client's request, the backend:

- [Explicitly dispatches](https://docs.livekit.io/reference/agents/agent-dispatch-service-api/) the agent into a fresh room, with the `restaurant_id` as job metadata.
- [Mints a token](https://docs.livekit.io/frontends/build/authentication/) so the browser can join that same room.

```ts
const AGENT_NAME = 'restaurant-agent';

const dispatchClient = new AgentDispatchClient(LIVEKIT_URL, 
  LIVEKIT_API_KEY, LIVEKIT_API_SECRET);

app.post('/api/connection-details', async (req, res) => {
  const { restaurant_id } = req.body ?? {};

  // A unique room per session keeps each dispatch isolated.
  const roomName = `restaurant-${restaurant_id}-${randomUUID()}`;

  // 1) Explicitly dispatch the agent, handing it the id as ctx.job.metadata.
  //    createDispatch creates the room if it does not already exist.
  await dispatchClient.createDispatch(roomName, AGENT_NAME, {
    metadata: JSON.stringify({ restaurant_id }),
  });

  // 2) Mint a short-lived token so the diner can join that same room.
  const at = new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET, {
    identity: `diner-${randomUUID()}`,
    ttl: '15m',
  });
  at.addGrant({ roomJoin: true, room: roomName, 
    canPublish: true, canSubscribe: true });

  res.json({ serverUrl: LIVEKIT_URL, participantToken: await at.toJwt() });
});
```

The room name in this demo is unique per session, which keeps each diner's dispatch cleanly isolated from the rest. The agent name, which is the name your agent workers register under, stays constant.

Note that to make the backend easy to run locally, it uses a wide-open `cors()`, which you should not enable in production.

### Step 3: the agent reads the ID and configures itself

For [each job](https://docs.livekit.io/agents/server/job/), the agent reads the metadata and configures its instructions, data, and [tools](https://docs.livekit.io/agents/logic/tools/definition/) as needed. For simplicity this demo hardcodes the restaurant information, but in production you'd load it from an external source so it stays up to date.

```python
@server.rtc_session(agent_name="restaurant-agent")
async def entrypoint(ctx: agents.JobContext) -> None:
    # Extract restaurant_id from the JSON metadata
    restaurant_id = parse_restaurant_id(ctx.job.metadata)
    restaurant = get_restaurant(restaurant_id)
    logger.info("Starting session for restaurant_id=%r -> %s", restaurant_id, restaurant.name)

    session = AgentSession(
    ...
    )

    await session.start(agent=RestaurantAgent(restaurant), room=ctx.room, room_options=...)
    await ctx.connect()
    await session.say(f"Welcome to {restaurant.name}. How can I help you?")
```

```python
class RestaurantAgent(Agent):
    def __init__(self, restaurant: Restaurant) -> None:
        self.restaurant = restaurant
        super().__init__(
            instructions=build_instructions(restaurant),
            ...
        )

    @function_tool
    async def get_opening_hours(self, context: RunContext) -> str:
        """Return the restaurant's opening hours for every day of the week."""
        lines = [f"{days}: {hours}" for days, hours in 
          self.restaurant.opening_hours.items()]
        return f"Opening hours for {self.restaurant.name}:\n" + "\n".join(lines)

    # get_address and get_menu follow the same shape...
```

That's the entire pattern. To configure on *users* instead of restaurants, you change almost nothing: carry a `user_id` in the metadata instead of a `restaurant_id`, and change `get_restaurant` to `get_user_profile`.

## Pitfalls to watch for

This pattern is common, so it's worth calling out a few pitfalls developers often hit:

**Avoid (or explicitly handle) duplicate room names.** If you dispatch the agent by embedding `RoomConfiguration` in the access token, that dispatch happens only when the room is first created. If the room already exists, the token's dispatch configuration is silently ignored and your agent never joins. This demo sidesteps the issue by using unique room names and the explicit dispatch API.

**The dispatch name must match exactly.** The agent registers with `@server.rtc_session(agent_name="restaurant-agent")` and the backend dispatches to `"restaurant-agent"`. If these drift, the dispatch is created but no agent worker ever picks it up, so keep the two values in sync.

**Metadata is a string, always.** `ctx.job.metadata` is a string, not a parsed object. You must `json.loads` it yourself and handle the case where it's empty or not valid JSON.

**Console mode has no metadata.** Running `lk agent console` is the fastest way to talk to your agent, but it only simulates an agent joining a room, so there's no metadata. To test with metadata, use the [Agent Console](https://docs.livekit.io/testing/agent-console/), which supports job metadata.

## Wrapping up

Configuring a LiveKit agent per session is a common need. Follow the recommendations in this article and you'll have a robust way to customize a single agent for many different scenarios.

The complete restaurant example is at [`livekit-demo-single-agent-multiple-restaurants`](https://github.com/darryncampbell/livekit-demo-single-agent-multiple-restaurants). There's no hosted version, but the README has full setup instructions.