Skip to main content

Full source code

Clone and run in minutes. Next.js + Browser Use SDK v3.
This tutorial walks through the chat-ui-example — a Next.js app that lets users chat with a Browser Use agent in real time. We focus on the SDK integration, not the UI components. The app has two pages:
  1. Home — the user types a task, the app creates a session and sends the task.
  2. Session — live browser preview, streaming messages, follow-ups, and recording download.
All SDK calls live in a single file: src/lib/api.ts.

Setup

api.ts
import { BrowserUse } from "browser-use-sdk/v3";

// Server-only — no NEXT_PUBLIC_ prefix, never exposed to the browser
const apiKey = process.env.BROWSER_USE_API_KEY ?? "";
export const client = new BrowserUse({ apiKey });
The API key uses BROWSER_USE_API_KEY (no NEXT_PUBLIC_ prefix) so it stays server-side. All SDK calls go through server actions — never call the SDK directly from client components.

1. Create a session

actions.ts
"use server";
import { client } from "./api";

export async function createSession() {
  const session = await client.sessions.create({
    keepAlive: true,
    enableRecording: true,
  });
  return { id: session.id, liveUrl: session.liveUrl, status: session.status };
}
  • keepAlive: true keeps the session open after each task so the user can send follow-ups (default is false).
  • enableRecording: true produces an MP4 video of the browser session.
  • liveUrl is returned immediately — no waiting or extra call needed.
The home page creates the session, navigates to the session page (passing liveUrl and the initial task via URL params), and the session page takes over from there:
page.tsx
async function handleSend(message: string) {
  const session = await createSession();

  router.push(
    `/session/${session.id}?liveUrl=${encodeURIComponent(session.liveUrl)}&task=${encodeURIComponent(message)}`
  );
}

2. Stream messages with for await

Instead of polling sessions.get() and sessions.messages() separately, use client.run() — it streams messages and resolves when the task completes:
session-context.tsx
const streamTask = useCallback(async (task: string) => {
  const run = client.run(task, { sessionId });

  for await (const msg of run) {
    setMessages((prev) => [...prev, msg]);
  }

  // Iterator done — task reached terminal state
  setSession(run.result);
}, [sessionId]);
The for await loop yields each message as it arrives. When the loop ends, run.result contains the final session state (status, output, etc.). No separate status polling needed. Wire it up in a useEffect to auto-run the initial task from URL params:
session-context.tsx
useEffect(() => {
  if (!initialTask) return;
  sendMessage(initialTask);
}, []);

3. Follow-up tasks

Follow-ups call the same streamTask function — the stream already includes the user message, so no optimistic insert is needed:
session-context.tsx
const sendMessage = useCallback(async (task: string) => {
  await streamTask(task);
}, [streamTask]);
The SDK auto-sets keepAlive: true when targeting an existing session, so follow-up tasks work without extra config.

4. Recording

Fetch the MP4 URL after the session ends (recording was enabled in step 1):
session-context.tsx
useEffect(() => {
  if (!isTerminal) return;

  client.sessions.waitForRecording(sessionId).then((urls) => {
    if (urls.length) setRecordingUrls(urls);
  });
}, [isTerminal, sessionId]);
waitForRecording polls for up to 15 seconds and returns presigned MP4 download URLs. Returns an empty array if the agent answered without opening a browser.

5. Stop a task

actions.ts
export async function stopTask(id: string) {
  await client.sessions.stop(id, { strategy: "task" });
}
Using strategy: "task" stops only the current task, keeping the session alive for follow-ups.

6. Session page

The session page consumes everything through a context provider:
session/[id]/page.tsx
function SessionPage() {
  const { session, turns, isBusy, isTerminal, recordingUrls, sendMessage, stopTask } =
    useSession();

  return (
    <div className="flex h-screen w-full overflow-hidden">
      {/* Chat column */}
      <div className="flex-1 flex flex-col min-w-0">
        <ChatMessages turns={turns} isBusy={isBusy} />
        <ChatInput
          onSend={sendMessage}
          onStop={stopTask}
          disabled={isTerminal}
        />
      </div>

      {/* Live browser view — liveUrl available from session creation */}
      <BrowserPanel liveUrl={session?.liveUrl} />
    </div>
  );
}

Summary

MethodPurpose
client.sessions.create()Create a session (returns liveUrl immediately)
client.run()Send a task and stream messages with for await
client.sessions.stop()Stop the current task
client.sessions.waitForRecording()Get MP4 recording URLs