> ## Documentation Index
> Fetch the complete documentation index at: https://tbd-6fc993ce-hypeship-docs-website-deploy-hook.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Laminar

[Laminar](https://www.laminar.sh/) is an open-source observability and evaluation platform for autonomous AI agents. You can create a cloud account or self-host Laminar for your infrastructure. By integrating Laminar with Kernel, you can trace and monitor your browser automations with full visibility into LLM calls, browser actions, session recordings, and performance metrics.

## Why use Laminar with Kernel?

* **No local browser management**: Run automations in the cloud while maintaining full observability
* **Scalability**: Launch multiple browser sessions with independent traces
* **Debugging**: Use Kernel's [live view](/browsers/live-view) during development and Laminar's session recordings for post-execution analysis
* **Cost optimization**: Track LLM costs across all your browser automations
* **Performance tuning**: Identify slow operations and optimize your agent workflows

## Prerequisites

Before integrating Laminar with Kernel, you'll need:

1. A [Kernel account](https://dashboard.onkernel.com/sign-up) with a Kernel API Key
2. A [Laminar account](https://www.laminar.sh/) and project
3. Your Laminar project API key from the Project Settings page

## Installation

<CodeGroup>
  ```bash npm theme={null}
  npm install @lmnr-ai/lmnr @onkernel/sdk
  ```

  ```bash python theme={null}
  uv pip install --upgrade 'lmnr[all]' kernel
  ```
</CodeGroup>

## Getting your Laminar API key

1. Log in to your [Laminar dashboard](https://www.laminar.sh/)
2. Navigate to **Project Settings**
3. Generate a new API key in your project
4. Copy your **Project API Key**
5. Set it as an environment variable:

```bash theme={null}
export LMNR_PROJECT_API_KEY=your_api_key_here
```

<Info>
  You will also need to generate a `KERNEL_API_KEY` from your [Kernel dashboard](https://dashboard.onkernel.com/api-keys) to authenticate with Kernel's browser infrastructure.
</Info>

## Browser Agent Framework Examples

Select your browser automation framework to enable Laminar tracing with Kernel:

* [Playwright](#playwright)
* [Browser Use](#browser-use)
* [Stagehand](#stagehand)

<Info>
  Always call `Laminar.flush()` or ensure your traced functions complete to submit traces to Laminar.
</Info>

### Playwright

Playwright is a popular low-level browser automation framework. Here's how to use it with Laminar and Kernel:

<Info>
  The Playwright examples include `waitForTimeout()` calls to help ensure Laminar traces populate properly for these short, fast code snippets.
</Info>

<CodeGroup>
  ```javascript Typescript/Javascript theme={null}
  import { Laminar, observe } from '@lmnr-ai/lmnr';
  import Kernel from '@onkernel/sdk';
  import { chromium } from 'playwright';

  // Initialize Laminar with Playwright instrumentation
  Laminar.initialize({
    projectApiKey: process.env.LMNR_PROJECT_API_KEY,
    instrumentModules: {
      playwright: {
        chromium
      },
      kernel: Kernel,
    }
  });

  // Initialize Kernel and create a cloud browser
  const kernel = new Kernel();

  const main = async () => observe({ name: 'main' }, async () => {
    const kernelBrowser = await kernel.browsers.create({
      stealth: true
    });

    console.log("Live view url:", kernelBrowser.browser_live_view_url);

    // Connect Playwright to Kernel's browser via CDP
    const browser = await chromium.connectOverCDP(kernelBrowser.cdp_ws_url);
    const context = browser.contexts()[0] || (await browser.newContext());
    const page = context.pages()[0] || (await context.newPage());

    // Wait for 3 second
    await page.waitForTimeout(3000);

    // Your automation code
    await page.goto('https://www.onkernel.com/docs');

    // Wait for 2 second
    await page.waitForTimeout(2000);

    // Navigate to careers page
    await page.goto('https://www.onkernel.com/docs/careers/intro');

    // Extract all job URLs from the ul next to #open-roles
    const jobLinks = await page.locator('#open-roles + ul a').evaluateAll((links) => {
      const baseUrl = 'https://www.onkernel.com';
      return links
        .map(link => {
          const href = link.getAttribute('href');
          if (!href) return null;
          // Convert relative URLs to absolute URLs
          return href.startsWith('http') ? href : baseUrl + href;
        })
        .filter(href => href !== null);
    });

    console.log('Job URLs found:', jobLinks);
    console.log(`Total jobs: ${jobLinks.length}`);

    // Wait for 3 seconds
    await page.waitForTimeout(3000);

    // Clean up the browser
    await browser.close();
    // Delete the browser for those who left open the live view url
    await kernel.browsers.deleteByID(kernelBrowser.session_id);
  });

  await main();
  await Laminar.flush();
  ```

  ```python Python theme={null}
  import os
  from lmnr import Laminar, observe
  from playwright.sync_api import sync_playwright
  from kernel import Kernel

  # Initialize Laminar
  Laminar.initialize(project_api_key=os.environ["LMNR_PROJECT_API_KEY"])

  # Use @observe decorator to create a trace
  @observe()
  def run_automation():
      # Initialize Kernel
      client = Kernel()
      kernel_browser = client.browsers.create(stealth=True)

      print(f"Live view url: {kernel_browser.browser_live_view_url}")

      # Connect Playwright to Kernel's browser
      with sync_playwright() as p:
          browser = p.chromium.connect_over_cdp(kernel_browser.cdp_ws_url)
          context = browser.contexts[0] if browser.contexts else browser.new_context()
          page = context.pages[0] if context.pages else context.new_page()

          # Wait for 3 seconds
          page.wait_for_timeout(3000)

          # Your automation code
          page.goto('https://www.onkernel.com/docs')

          # Wait for 3 seconds
          page.wait_for_timeout(3000)

          # Navigate to careers page
          page.goto('https://www.onkernel.com/docs/careers/intro')
          page.wait_for_timeout(3000)  # Wait 2 seconds

          # Extract all job URLs from the ul next to #open-roles
          job_links = page.locator('#open-roles + ul a').evaluate_all("""
              (links) => {
                  const baseUrl = 'https://www.onkernel.com';
                  return links
                      .map(link => {
                          const href = link.getAttribute('href');
                          if (!href) return null;
                          // Convert relative URLs to absolute URLs
                          return href.startsWith('http') ? href : baseUrl + href;
                      })
                      .filter(href => href !== null);
              }
          """)

          print(f'Job URLs found: {job_links}')
          print(f'Total jobs: {len(job_links)}')

          # Wait for 3 seconds
          page.wait_for_timeout(3000)

          # Clean up the browser
          browser.close()

      # Flush traces to Laminar
      Laminar.flush()

      # Delete the browser for those who left open the Kernel live view url
      client.browsers.delete_by_id(kernel_browser.session_id)

  # Run the automation
  run_automation()
  ```
</CodeGroup>

### Browser Use

[Browser Use](https://github.com/browser-use/browser-use) is an AI browser agent framework. Here's how to integrate it with Laminar and Kernel:

```python python theme={null}
import os
import asyncio
from lmnr import Laminar, observe
from browser_use import Agent, Browser, ChatOpenAI
from kernel import Kernel

# Initialize Laminar
Laminar.initialize(project_api_key=os.environ["LMNR_PROJECT_API_KEY"])

@observe()
async def main():
    # Initialize Kernel and create a browser
    client = Kernel()
    kernel_browser = client.browsers.create(stealth=True, viewport={'width': 1920, 'height': 1080})

    print(f"Live view url: {kernel_browser.browser_live_view_url}")

    # Configure Browser Use with Kernel's CDP URL
    browser = Browser(
        cdp_url=kernel_browser.cdp_ws_url,
        headless=False,
        window_size={'width': 1920, 'height': 1080},
        viewport={'width': 1920, 'height': 1080},
        device_scale_factor=1.0
    )

    # Initialize the model
    llm = ChatOpenAI(
        model="gpt-4.1",
    )

    # Create and run the agent with job extraction task
    agent = Agent(
        task="""1. Go to https://www.onkernel.com/docs
        2. Navigate to the main Jobs page
        3. Extract all the job posting URLs. List each URL you find.""",
        llm=llm,
        browser_session=browser
    )

    result = await agent.run()
    print(f"Job URLs found:\n{result.final_result()}")

    # Flush traces to Laminar
    Laminar.flush()

    # Delete the browser for those who left open the live view url
    client.browsers.delete_by_id(kernel_browser.session_id)

asyncio.run(main())
```

### Stagehand

[Stagehand](https://github.com/browserbase/stagehand) is an AI browser automation framework. Here's how to use it with Laminar and Kernel:

<Info>
  Stagehand v3 requires `@lmnr-ai/lmnr@0.7.11` or later.
</Info>

<CodeGroup>
  ```javascript Stagehand v3 theme={null}
  import { Laminar, observe } from '@lmnr-ai/lmnr';
  import { Stagehand } from '@browserbasehq/stagehand';
  import Kernel from '@onkernel/sdk';
  import { z } from 'zod';

  // Initialize Laminar with Stagehand instrumentation
  Laminar.initialize({
    projectApiKey: process.env.LMNR_PROJECT_API_KEY,
    instrumentModules: {
      stagehand: Stagehand,
      kernel: Kernel,
    },
  });

  // Initialize Kernel and create a browser
  const kernel = new Kernel();

  const main = async () => observe({ name: 'main' }, async () => {
    // Create browser
    const kernelBrowser = await kernel.browsers.create({
      stealth: true,
    });

    console.log("Live view url:", kernelBrowser.browser_live_view_url);

    // Configure Stagehand to use Kernel's browser
    const stagehand = new Stagehand({
      env: "LOCAL",
      localBrowserLaunchOptions: {
        cdpUrl: kernelBrowser.cdp_ws_url,
      },
      model: "openai/gpt-4.1",
      apiKey: process.env.OPENAI_API_KEY,
      verbose: 1,
      domSettleTimeout: 30_000
    });
    await stagehand.init();

    // Your automation code
    const page = stagehand.context.pages()[0];
    if (!page) {
      throw new Error('No page available');
    }
    await page.goto("https://www.ycombinator.com/companies");

    await stagehand.act("Type in Kernel into the search box");
    await stagehand.act("Click on the first search result");

    // Extract team size from the YC startup page
    const output = await stagehand.extract(
      "Extract the team size (number of employees) shown on this Y Combinator company page.",
      z.object({
        teamSize: z.string(),
      })
    );

    console.log("Team size:", output.teamSize);

    // Cleanup
    await stagehand.close();
    await kernel.browsers.deleteByID(kernelBrowser.session_id);
  });

  await main();
  await new Promise(resolve => setTimeout(resolve, 5000));
  await Laminar.flush();
  ```

  ```javascript Stagehand v2 theme={null}
  import { Laminar, observe } from '@lmnr-ai/lmnr';
  import { Stagehand } from '@browserbasehq/stagehand';
  import Kernel from '@onkernel/sdk';
  import { z } from 'zod';

  // Initialize Laminar with Stagehand instrumentation
  Laminar.initialize({
    projectApiKey: process.env.LMNR_PROJECT_API_KEY,
    instrumentModules: {
      stagehand: Stagehand,
      kernel: Kernel,
    },
  });

  // Initialize Kernel and create a browser
  const kernel = new Kernel();

  const main = async () => observe({ name: 'main' }, async () => {
    const kernelBrowser = await kernel.browsers.create({ stealth: true });
    
    console.log("Live view url:", kernelBrowser.browser_live_view_url);
    
    // Configure Stagehand to use Kernel's browser
    const stagehand = new Stagehand({
      env: 'LOCAL',
      localBrowserLaunchOptions: {
        cdpUrl: kernelBrowser.cdp_ws_url
      }
      verbose: 1,
      domSettleTimeoutMs: 30_000,
      modelName: 'openai/gpt-4.1',
      modelClientOptions: {
        apiKey: process.env.OPENAI_API_KEY
      }
    });
    await stagehand.init();

    // Your automation code
    const page = stagehand.page;
    await page.goto('https://www.onkernel.com/docs');

    // Navigate to careers page
    await page.goto('https://www.onkernel.com/docs/careers/intro');

    // Extract all job URLs
    const output = await page.extract({
      instruction: 'Extract all job posting URLs from the Open Roles section.',
      schema: z.object({
        jobUrls: z.array(z.string()).describe('Array of job posting URLs')
      })
    });

    console.log('Job URLs found:', output.jobUrls);
    console.log(`Total jobs: ${output.jobUrls.length}`);

    // Clean up and flush traces to Laminar
    await stagehand.close();

    // Delete the browser for those who left open the live view url
    await kernel.browsers.deleteByID(kernelBrowser.session_id);
  });

  await main();
  await new Promise(resolve => setTimeout(resolve, 5000));
  await Laminar.flush();
  ```
</CodeGroup>

## Tracing Kernel Apps & Computer Controls

When you use Kernel's [App platform](/apps/develop) or [Computer controls](/browsers/computer-controls), Laminar will automatically trace the computer and process interactions.

In addition, you don't have to manually `observe` your kernel `app.actions` or worry about manual trace flushing inside your Kernel apps.

<Tip>
  Laminar will take care of trace lifecycle automatically for Kernel apps.
</Tip>

### Example Kernel app with Laminar tracing

To deploy this example on Kernel, follow the steps in [Kernel's app deployment guide](/apps/deploy).

<CodeGroup>
  ```javascript Typescript/Javascript theme={null}
  import { Laminar } from '@lmnr-ai/lmnr';
  import { chromium } from 'playwright';
  import { config } from 'dotenv';
  import Kernel, { type KernelContext } from "@onkernel/sdk";

  config();

  Laminar.initialize({
    instrumentModules: {
      playwright: {
        chromium,
      },
      kernel: Kernel,
    }
  });

  const kernel = new Kernel();
  const app = kernel.app('my-app-name');

  app.action('my-action', async (ctx: KernelContext, payload) => {
    const kernelBrowser = await kernel.browsers.create({
      invocation_id: ctx.invocation_id,
    });

    const browser = await chromium.connectOverCDP(kernelBrowser.cdp_ws_url);
    const context = browser.contexts[0] || (await browser.newContext());
    const page = context.pages[0] || (await context.newPage());

    // Your automation code
    await page.goto('https://www.duckduckgo.com/')
    await page.waitForTimeout(2000)
    await page.goto('https://www.github.com/trending');

    // Wait for 2 seconds
    await page.waitForTimeout(2000);

    // Extract all trending repos
    const trendingRepos = await page.locator("h2 a.Link").evaluateAll((repos) =>
      repos.map((repo) => repo.textContent.trim().replace(/[\n\s]/g, ''))
    );

    console.log('Trending repos:', trendingRepos);

    await page.waitForTimeout(2000);

    // Computer tool call
    await kernel.browsers.computer.captureScreenshot(
      kernelBrowser.session_id,
    );

    // Process tool call
    await kernel.browsers.process.exec(
      kernelBrowser.session_id,
      {
        command: 'ls',
        args: ['-la'],
      }
    )

    await browser.close();
    await kernel.browsers.deleteByID(kernelBrowser.session_id);

    return { success: true, result: "Action completed successfully" };
  });
  ```

  ```python Python theme={null}
  from lmnr import Laminar
  from playwright.async_api import async_playwright
  from kernel import App, Kernel, KernelContext

  # Initialize Laminar
  Laminar.initialize()

  kernel_client = Kernel()
  app = App("my-app-name")

  @app.action(name="my-action")
  async def run_automation(ctx: KernelContext):
      # Connect Playwright to Kernel's browser
      async with async_playwright() as p:
          kernel_browser = kernel_client.browsers.create(
              invocation_id=ctx.invocation_id
          )
          browser = await p.chromium.connect_over_cdp(kernel_browser.cdp_ws_url)
          context = (
              browser.contexts[0]
              if browser.contexts
              else await browser.new_context()
          )
          page = context.pages[0] if context.pages else await context.new_page()

          # Your automation code
          await page.goto('https://www.duckduckgo.com/')
          await page.wait_for_timeout(2000)
          await page.goto('https://www.github.com/trending')
          await page.wait_for_timeout(2000)

          # Extract all trending repos
          trending_repos = await page.locator("h2 a.Link").evaluate_all(r"""
              (repos) => 
                  repos.map((repo) => 
                      repo.textContent.trim().replace(/[\n\s]/g, '')
                  )
              """)

          print('Trending repos:', trending_repos)

          # Process tool call
          kernel_client.browsers.process.exec(
              id=kernel_browser.session_id,
              command="ls",
              args=["-la"],
          )

          # Computer tool call
          kernel_client.browsers.computer.move_mouse(
              id=kernel_browser.session_id,
              x=100,
              y=100,
          )

          # Wait for 3 seconds
          await page.wait_for_timeout(3000)

          # Clean up the browser
          await browser.close()

      # Delete the browser for those who left open the Kernel live view url
      kernel_client.browsers.delete_by_id(kernel_browser.session_id)

      print("Done")
  ```
</CodeGroup>

## Viewing traces in Laminar

View your traces in the Laminar UI's traces tab to see synchronized browser session recordings and agent execution steps.

After running your automation:

1. Log in to your [Laminar dashboard](https://www.lmnr.ai/)
2. Navigate to the **Traces** tab
3. Find your recent trace to view:
   * Full execution timeline
   * LLM calls and responses
   * Browser session recordings
   * Computer and process interactions (for Kernel apps)
   * Token usage and costs
   * Latency metrics

Timeline highlights indicate which step your agent is currently executing, making it easy to debug and optimize your automations.

## Next steps

* Explore [Laminar's tracing structure](https://docs.lmnr.ai/tracing/structure/overview) to understand how traces are organized
* Learn about [Laminar's evaluations](https://docs.lmnr.ai/evaluations/introduction) for validating and testing your AI application outputs
* Learn about [stealth mode](/browsers/bot-detection/stealth) for avoiding detection
* Learn how to [deploy your app](/apps/deploy) to Kernel's platform
