> ## Documentation Index
> Fetch the complete documentation index at: https://docs.browserbase.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Write a Function

> Define Browserbase Functions, validate parameters, configure browser sessions, and publish multiple handlers.

Use `defineFn` from `@browserbasehq/sdk-functions` to register a named handler:

<Tabs>
  <Tab title="Stagehand">
    ```typescript Node.js theme={null}
    import { defineFn } from "@browserbasehq/sdk-functions";
    import { browserbase, Stagehand } from "@browserbasehq/stagehand";
    import { z } from "zod/v4";

    defineFn(
      "extract-title",
      async (context, params) => {
        const browser = await browserbase.connect({
          // The local dev server has no Secrets, so fall back to .env.
          apiKey: context.secrets.BROWSERBASE_API_KEY ?? process.env.BROWSERBASE_API_KEY!,
          sessionId: context.session.id,
        });
        // In this example, Stagehand uses the Model Gateway where Browserbase charges for the tokens
        const stagehand = await Stagehand.create({ browser });
        const page = (await browser.context.activePage())!;

        await page.goto(params.url);
        const { data } = await stagehand.extract(
          "Extract the main heading of the page",
          z.object({ heading: z.string() }),
        );

        await stagehand.close();
        return {
          sessionId: context.session.id,
          heading: data.heading,
        };
      },
      {
        parametersSchema: z.object({
          url: z.string().url(),
        }),
        sessionConfig: {
          // The ID from `browse cloud extensions upload`. Stagehand needs its extension in the session.
          extensionId: "your-extension-id",
          browserSettings: {
            solveCaptchas: true,
          },
        },
      },
    );
    ```
  </Tab>

  <Tab title="Playwright">
    ```typescript Node.js theme={null}
    import { defineFn } from "@browserbasehq/sdk-functions";
    import { chromium } from "playwright-core";
    import { z } from "zod";

    defineFn(
      "extract-title",
      async (context, params) => {
        const browser = await chromium.connectOverCDP(
          context.session.connectUrl,
        );
        const page = browser.contexts()[0]!.pages()[0]!;

        await page.goto(params.url);

        return {
          sessionId: context.session.id,
          title: await page.title(),
        };
      },
      {
        parametersSchema: z.object({
          url: z.string().url(),
        }),
        sessionConfig: {
          browserSettings: {
            solveCaptchas: true,
          },
        },
      },
    );
    ```
  </Tab>
</Tabs>

## `defineFn` arguments

`defineFn` accepts a name, a handler, and an optional configuration object.

### Name

The name identifies a Function within a Browserbase project. Each Function in an entrypoint must have a unique name.

Publishing another definition with the same name creates a new version of that Function. Callers continue to use the same Function ID.

### Handler

The async handler receives `context` and `params`.

`context` contains:

```typescript Node.js theme={null}
{
  session: {
    connectUrl: string;
    id: string;
  };
  secrets: Record<string, string>;
}
```

Stagehand connects to the browser that Browserbase created for the invocation with `session.id`. Playwright and Puppeteer connect with `session.connectUrl`. Use `session.id` to inspect the session or add it to your logs.

`secrets` maps the name of each [Secret attached to the Function](#use-secrets) to its value. If you haven't attached any Secrets, `secrets` is an empty object. The local development server always passes an empty object.

`params` contains the object supplied in the invoke request. The request can contain up to 64 KB of serialized JSON.

The handler can return a JSON-serializable string, number, boolean, array, or object. Browserbase stores non-empty return values in the invocation's `results` field. A handler that returns `null` or `undefined` completes without `results`.

### Configuration

The optional third argument supports:

* `parametersSchema` to validate invocation parameters with Zod.
* `sessionConfig` to set the default browser session configuration for every invocation.

## Validate parameters

Define `parametersSchema` with Zod:

```typescript Node.js theme={null}
import { defineFn } from "@browserbasehq/sdk-functions";
import { z } from "zod";

defineFn(
  "search",
  async (_context, params) => {
    return { query: params.query, limit: params.limit };
  },
  {
    parametersSchema: z.object({
      query: z.string().min(1),
      limit: z.number().int().min(1).max(20),
    }),
  },
);
```

Use a schema for every Function that accepts external input. It documents the expected shape and rejects invalid values before your browser logic uses them.

## Configure the browser session

Set `sessionConfig` when every invocation needs the same browser settings:

```typescript Node.js theme={null}
defineFn("authenticated-task", handler, {
  sessionConfig: {
    browserSettings: {
      context: {
        id: "YOUR_CONTEXT_ID",
        persist: true,
      },
    },
    proxies: true,
    timeout: 600,
  },
});
```

Functions support most [Create a Session](/reference/api/create-a-session) options. They don't support `region` or `keepAlive`. The invocation timeout must be between 60 and 900 seconds.

A Function that uses Stagehand also sets `extensionId` in `sessionConfig`. See [Connect a browser library](#connect-a-browser-library).

Callers can override supported defaults for one invocation with [`sessionCreateParams`](/platform/functions/invoke#override-session-settings).

## Connect a browser library

<Tabs>
  <Tab title="Stagehand">
    Stagehand needs its extension in the browser session, and a session cannot get an extension after it starts. Upload the extension one time for your project, then use the `id` from the output as `extensionId`:

    ```bash theme={null}
    browse cloud extensions upload node_modules/@browserbasehq/stagehand/dist/assets/stagehand-extension.zip
    ```

    Connect Stagehand to the Function's session by its ID:

    ```typescript Node.js theme={null}
    import { defineFn } from "@browserbasehq/sdk-functions";
    import { browserbase, Stagehand } from "@browserbasehq/stagehand";

    defineFn(
      "stagehand-task",
      async (context) => {
        const browser = await browserbase.connect({
          // The local dev server has no Secrets, so fall back to .env.
          apiKey: context.secrets.BROWSERBASE_API_KEY ?? process.env.BROWSERBASE_API_KEY!,
          sessionId: context.session.id,
        });
        // In this example, Stagehand uses the Model Gateway where Browserbase charges for the tokens
        const stagehand = await Stagehand.create({ browser });
        const page = (await browser.context.activePage())!;

        await page.goto("https://example.com");
        await stagehand.act("Click the More information link");

        await stagehand.close();
        return { url: await page.url() };
      },
      { sessionConfig: { extensionId: "your-extension-id" } },
    );
    ```

    Use `browserbase.connect()` with the session ID. Don't use `localBrowser.connect()` with `context.session.connectUrl`. That call loads the extension from your local disk, and the Function's browser runs on Browserbase.

    The initializer installs the zod version that Stagehand uses. In an existing project, install it yourself. If your project has a different zod version, the schemas that you give to `extract()` don't type-check:

    ```bash theme={null}
    pnpm add @browserbasehq/stagehand "zod@$(npm view @browserbasehq/stagehand dependencies.zod)"
    ```
  </Tab>

  <Tab title="Playwright">
    Connect Playwright over CDP:

    ```typescript Node.js theme={null}
    import { defineFn } from "@browserbasehq/sdk-functions";
    import { chromium } from "playwright-core";

    defineFn("playwright-task", async (context) => {
      const browser = await chromium.connectOverCDP(
        context.session.connectUrl,
      );
      const page = browser.contexts()[0]!.pages()[0]!;

      await page.goto("https://example.com");

      return { title: await page.title() };
    });
    ```
  </Tab>

  <Tab title="Puppeteer">
    Connect Puppeteer with the browser WebSocket endpoint:

    ```typescript Node.js theme={null}
    import { defineFn } from "@browserbasehq/sdk-functions";
    import puppeteer from "puppeteer-core";

    defineFn("puppeteer-task", async (context) => {
      const browser = await puppeteer.connect({
        browserWSEndpoint: context.session.connectUrl,
      });
      const page = (await browser.pages())[0]!;

      await page.goto("https://example.com");

      return { title: await page.title() };
    });
    ```
  </Tab>
</Tabs>

<Note>
  A Function can also run a custom browser agent loop. Connect its browser tool to `context.session.connectUrl`. If you don't need to own the model loop, use [Browserbase Agents](/platform/agents/overview).
</Note>

## Use Secrets

Keep API keys in encrypted project Secrets, not in your code. A Function reads each attached Secret from `context.secrets`. The Stagehand examples read `BROWSERBASE_API_KEY` this way.

Stagehand doesn't need a model API key. Without a `model` option, Stagehand sends model calls through the [Browserbase Model Gateway](https://docs.browserbase.com/platform/model-gateway/overview#model-gateway), which picks a model for each call. Browserbase charges for the tokens.

Create each Secret one time for your project. The name of the Secret is its key in `context.secrets`:

```bash theme={null}
browse cloud secrets create BROWSERBASE_API_KEY --env BROWSERBASE_API_KEY
```

The CLI encrypts each value on your machine before it sends it. After you [publish](/platform/functions/deploy#deploy-from-the-cli), attach the Secrets to the Function:

```bash theme={null}
browse functions secrets attach FUNCTION_ID SECRET_ID
browse functions secrets list FUNCTION_ID
```

Use the Function ID from `builtFunctions[].id` in the publish output. Attached Secrets take effect on the next invocation.

The local development server doesn't pass Secrets. The Stagehand examples fall back to `process.env`, so export the same values in your shell.

## Publish multiple Functions

The publish command follows the entrypoint's import graph. Each `defineFn` call in that graph becomes a Function.

For a single file, define each Function in the entrypoint:

```typescript Node.js theme={null}
import { defineFn } from "@browserbasehq/sdk-functions";

defineFn("first-task", async () => ({ task: 1 }));
defineFn("second-task", async () => ({ task: 2 }));
```

For multiple files, import every file that registers a Function:

```typescript Node.js theme={null}
// index.ts
import "./functions/extract-title.js";
import "./functions/take-screenshot.js";
```

Then [deploy from the CLI](/platform/functions/deploy#deploy-from-the-cli).

## Handle errors and logs

An unhandled error marks the invocation as failed. Catch an error only when your Function can add useful context or return a valid partial result.

Use `console.log`, `console.warn`, and `console.error` for runtime logs. Browserbase captures this output in the [invocation logs](/reference/api/get-invocation-logs).

Browserbase closes the browser session when the handler completes. You don't need to close it in your code.

<CardGroup cols={2}>
  <Card title="Deploy Functions" icon="cloud-arrow-up" href="/platform/functions/deploy">
    Publish from the CLI or the Playground.
  </Card>

  <Card title="Invoke a Function" icon="terminal" href="/platform/functions/invoke">
    Pass parameters and retrieve asynchronous results.
  </Card>

  <Card title="Functions limits" icon="gauge" href="/platform/functions/limits">
    Check bundle, timeout, storage, and region constraints.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.