A browser your voice agent can use mid-call.

The caller asks for a Tuesday slot. Your agent starts a lookup, keeps talking, and reads the open times back half a minute later. An API is still faster when the site has one.

@function_toolasync def check_slots(self, context: RunContext, business: str, date: str) -> str:    """Start looking up open appointment slots for a business. Returns at once."""    url = BOOKING_PAGES.get(business)    if url is None:        return f"I can only check: {', '.join(BOOKING_PAGES)}."    run = await client.runs.create(        f"Open {url} and list the open time slots on {date}. Do not book.",        output_schema=SLOTS,        max_cost_usd=0.5,    )    return f"Lookup started, id {run.id}. Call slot_results with this id."
receptionist.pyExcerpt of the LiveKit agent below, tested against Browser Use Cloud on Oct 20
for the tool call to return, so the agent keeps talking (4 test runs)
0.2-0.8 s
to read open slots off a Cal.com booking page, $0.004-0.006 per lookup
29-43 s
median browser ready across production runs (p95 2.4 s)
0.6 s

The whole agent, tested.

Both files ran against Browser Use Cloud on October 20 with LiveKit Agents 1.8.8 and browser-use-sdk 3.12.1: the agent's two tools called directly, not inside a live call, and the script on the same Cal.com page. The slot strings' format varied between agent runs; tighten the schema if you parse them.
import json from browser_use_sdk.v4 import AsyncBrowserUsefrom livekit.agents import Agent, RunContext, function_tool client = AsyncBrowserUse()  # reads BROWSER_USE_API_KEY SLOTS = {    "type": "object",    "properties": {"slots": {"type": "array", "items": {"type": "string"}}},    "required": ["slots"],} # Only pages you configure: the caller names a business, never a URL.BOOKING_PAGES = {"demo": "https://cal.com/rick/get-rick-rolled"}  class Receptionist(Agent):    def __init__(self) -> None:        super().__init__(            instructions=(                "You find appointment slots for callers. Checking a website takes a while: "                "say you are checking, keep talking, then call slot_results. "                "Read the slot back and get a yes before anything is booked."            )        )     @function_tool    async def check_slots(self, context: RunContext, business: str, date: str) -> str:        """Start looking up open appointment slots for a business. Returns at once."""        url = BOOKING_PAGES.get(business)        if url is None:            return f"I can only check: {', '.join(BOOKING_PAGES)}."        run = await client.runs.create(            f"Open {url} and list the open time slots on {date}. Do not book.",            output_schema=SLOTS,            max_cost_usd=0.5,        )        return f"Lookup started, id {run.id}. Call slot_results with this id."     @function_tool    async def slot_results(self, context: RunContext, run_id: str) -> str:        """Get the slots found by check_slots."""        run = await client.runs.get(run_id)        if run.status.value in ("queued", "dispatching", "running"):            return "Still checking."        if run.status.value != "completed":            return f"The lookup failed: {run.error}"        return json.dumps(run.output)
receptionist.pyPython · LiveKit Agents · Playwright over CDP

One booking, three moves

  1. 1

    Start the lookup

    The tool creates a run with an output schema and returns at once. Nothing blocks the conversation.
  2. 2

    Keep talking

    The agent tells the caller it is checking, asks for the missing details, then calls the second tool for the slots.
  3. 3

    Confirm, then book

    Read the slot back. Only after a yes, book it: through the business's booking API, or a follow-up run in the lookup's session. This example stops at the lookup.

API first, browser for the rest.

If the business books through a platform with an API, such as Cal.com or Square Bookings, call it from the tool directly. Use the browser for the sites with no API. For a site you call every day, script it with Playwright on a cloud browser: no model tokens. On the same Cal.com page the scripted version (the Known site tab) read every slot in 7-12 s, starting and stopping the browser included, against 29-43 s for the agent. Browsers are $0.02 per hour.

What does your voice agent need?

Find open slots
An agent run with an output schema. 29-43 s on a Cal.com page in our test, so start it in the background.
Book after the caller says yes
The business's booking API if it has one. Otherwise a follow-up run in the lookup's session, which reuses its browser for about 2 minutes, longer with keepAlive.
A site you call every day
Script it with Playwright on a cloud browser: 7-12 s in our test, no model. See Playwright.
Logins
A saved profile or secret bindings. See login automation.
A person takes over
Every browser has a live view URL an operator can open and click in.
Spend per call
Cap each run with max_cost_usd, as in the snippet.

Questions people ask

Is a browser fast enough for a live call?

Not inside one turn. In our test, reading open slots off a booking page took 29-43 s, while a caller notices a second or two of silence. So the tool starts the run and returns in 0.2-0.8 s, the agent says it is checking and keeps talking, and a second tool reads the result. Use a booking API instead when the business has one; it answers far faster than any browser.

Which voice frameworks does this work with?

The snippet is LiveKit Agents 1.8.8 with browser-use-sdk 3.12.1. We ran its two tools directly against Browser Use Cloud, not inside a live LiveKit call. Pipecat, Vapi and Retell call tools too; the same split works there: one tool starts the run and returns, another reads it.

How fast does the browser start?

Across production runs the browser is ready a median 0.6 s after the run starts (p95 2.4 s); in our 4 test lookups it was ready in 1.4-2.3 s. BrowserArena, a third-party benchmark, measures the whole session lifecycle: 493 ms median for Browser Use Cloud; Kernel is faster at 318 ms. For a lookup, the agent reading the page takes most of the time, not the start.

What does a lookup cost?

The run reported $0.004-0.006 per lookup in our 4 tests on the default model, plus browser time at $0.02 per hour. A scripted Playwright flow on a known site uses no model tokens, only browser time.

How do I stop it from booking the wrong slot?

Let the lookup only read: the task in the snippet says "Do not book", and the caller can only name a business you configured, never a URL. Read the slot back, and book only after a yes. The lookup's browser stays open about 2 minutes after it finishes; a follow-up run in its session after that gets a fresh browser, so set keep_alive (in seconds) on the lookup if callers take longer. Check for an existing booking before retrying a run that failed halfway.