Skip to main content

Downloads

Kernel browsers run in fully sandboxed environments with writable filesystems. When your automation downloads a file, it’s saved inside the browser’s filesystem and can be retrieved using Kernel’s File I/O APIs.

Playwright

Playwright performs downloads via the browser itself, so there are a few steps:
  • Create a browser session
  • Configure where the browser saves downloads using CDP
  • Perform the download
  • Retrieve the file from the browser’s filesystem
The CDP downloadProgress event signals when the browser finishes writing a file, but there may be a brief delay before the file becomes available through Kernel’s File I/O APIs. This is especially true for larger downloads. We recommend polling listFiles to confirm the file exists before attempting to read it.
import Kernel from '@onkernel/sdk';
import { chromium } from 'playwright';
import fs from 'fs';
import pTimeout from 'p-timeout';

const DOWNLOAD_DIR = '/tmp/downloads';
const kernel = new Kernel();

// Poll listFiles until the expected file appears in the directory
async function waitForFile(
  sessionId: string,
  dir: string,
  filename: string,
  timeoutMs = 30_000
) {
  const start = Date.now();
  while (Date.now() - start < timeoutMs) {
    const files = await kernel.browsers.fs.listFiles(sessionId, { path: dir });
    if (files.some((f) => f.name === filename)) {
      return;
    }
    await new Promise((r) => setTimeout(r, 500));
  }
  throw new Error(`File ${filename} not found after ${timeoutMs}ms`);
}

async function main() {
  const kernelBrowser = await kernel.browsers.create();
  console.log('live view:', kernelBrowser.browser_live_view_url);

  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());

  const client = await context.newCDPSession(page);
  await client.send('Browser.setDownloadBehavior', {
    behavior: 'allow',
    downloadPath: DOWNLOAD_DIR,
    eventsEnabled: true,
  });

  // Set up CDP listeners to capture download filename and completion
  let downloadFilename: string | undefined;
  let downloadState: string | undefined;
  let downloadCompletedResolve!: () => void;
  const downloadCompleted = new Promise<void>((resolve) => {
    downloadCompletedResolve = resolve;
  });

  client.on('Browser.downloadWillBegin', (event) => {
    downloadFilename = event.suggestedFilename ?? 'unknown';
    console.log('Download started:', downloadFilename);
  });

  client.on('Browser.downloadProgress', (event) => {
    if (event.state === 'completed' || event.state === 'canceled') {
      downloadState = event.state;
      downloadCompletedResolve();
    }
  });

  console.log('Navigating to download test page');
  await page.goto('https://browser-tests-alpha.vercel.app/api/download-test');
  await page.getByRole('link', { name: 'Download File' }).click();

  try {
    await pTimeout(downloadCompleted, {
      milliseconds: 10_000,
      message: new Error('Download timed out after 10 seconds'),
    });
    console.log('Download completed');
  } catch (err) {
    console.error(err);
    throw err;
  }

  if (!downloadFilename) {
    throw new Error('Unable to determine download filename');
  }

  if (downloadState === 'canceled') {
    throw new Error('Download was canceled');
  }

  // Wait for the file to be available via Kernel's File I/O APIs
  console.log(`Waiting for file: ${downloadFilename}`);
  await waitForFile(kernelBrowser.session_id, DOWNLOAD_DIR, downloadFilename);

  const remotePath = `${DOWNLOAD_DIR}/${downloadFilename}`;
  console.log(`Reading file: ${remotePath}`);

  const resp = await kernel.browsers.fs.readFile(kernelBrowser.session_id, {
    path: remotePath,
  });

  const bytes = await resp.bytes();
  fs.mkdirSync('downloads', { recursive: true });
  const localPath = `downloads/${downloadFilename}`;
  fs.writeFileSync(localPath, bytes);
  console.log(`Saved to ${localPath}`);

  await kernel.browsers.deleteByID(kernelBrowser.session_id);
  console.log('Kernel browser deleted successfully.');
}

main();

We recommend using the list files API to poll for file availability before calling read file, as shown in the examples above. This approach ensures reliable downloads, especially for larger files. You can also use listFiles to enumerate and save all downloads at the end of a session.

Stagehand v3

When using Stagehand with Kernel browsers, you need to configure the download behavior in the localBrowserLaunchOptions:
const stagehand = new Stagehand({
  env: "LOCAL",
  verbose: 1,
  localBrowserLaunchOptions: {
    cdpUrl: kernelBrowser.cdp_ws_url,
    downloadsPath: DOWNLOAD_DIR, // Specify where downloads should be saved
    acceptDownloads: true, // Enable downloads
  },
});
Here’s a complete example:
import { Stagehand } from "@browserbasehq/stagehand";
import Kernel from "@onkernel/sdk";
import fs from "fs";

const DOWNLOAD_DIR = "/tmp/downloads";

// Poll listFiles until any file appears in the directory
async function waitForFile(
    kernel: Kernel,
    sessionId: string,
    dir: string,
    timeoutMs = 30_000
) {
    const start = Date.now();
    while (Date.now() - start < timeoutMs) {
        const files = await kernel.browsers.fs.listFiles(sessionId, { path: dir });
        if (files.length > 0) {
            return files[0];
        }
        await new Promise((r) => setTimeout(r, 500));
    }
    throw new Error(`No files found in ${dir} after ${timeoutMs}ms`);
}

async function main() {
    const kernel = new Kernel();

    console.log("Creating browser via Kernel...");
    const kernelBrowser = await kernel.browsers.create({
        stealth: true,
    });

    console.log(`Kernel Browser Session Started`);
    console.log(`Session ID: ${kernelBrowser.session_id}`);
    console.log(`Watch live: ${kernelBrowser.browser_live_view_url}`);

    // Initialize Stagehand with Kernel's CDP URL and download configuration
    const stagehand = new Stagehand({
        env: "LOCAL",
        verbose: 1,
        localBrowserLaunchOptions: {
            cdpUrl: kernelBrowser.cdp_ws_url,
            downloadsPath: DOWNLOAD_DIR,
            acceptDownloads: true,
        },
    });

    await stagehand.init();

    const page = stagehand.context.pages()[0];

    await page.goto("https://browser-tests-alpha.vercel.app/api/download-test");

    // Use Stagehand to click the download button
    await stagehand.act("Click the download file link");
    console.log("Download triggered");

    // Wait for the file to be fully available via Kernel's File I/O APIs
    console.log("Waiting for file to appear...");
    const downloadedFile = await waitForFile(
        kernel,
        kernelBrowser.session_id,
        DOWNLOAD_DIR
    );
    console.log(`File found: ${downloadedFile.name}`);

    const remotePath = `${DOWNLOAD_DIR}/${downloadedFile.name}`;
    console.log(`Reading file from: ${remotePath}`);

    // Read the file from Kernel browser's filesystem
    const resp = await kernel.browsers.fs.readFile(kernelBrowser.session_id, {
        path: remotePath,
    });

    // Save to local filesystem
    const bytes = await resp.bytes();
    fs.mkdirSync("downloads", { recursive: true });
    const localPath = `downloads/${downloadedFile.name}`;
    fs.writeFileSync(localPath, bytes);
    console.log(`Saved to ${localPath}`);

    // Clean up
    await stagehand.close();
    await kernel.browsers.deleteByID(kernelBrowser.session_id);
    console.log("Browser session closed");
}

main().catch((err) => {
    console.error(err);
    process.exit(1);
});

Browser Use

Browser Use handles downloads automatically when configured properly. Documentation for Browser Use downloads coming soon.

Uploads

You can upload from your local filesystem into the browser directly using Playwright’s file input helpers.
import Kernel from '@onkernel/sdk';
import { chromium } from 'playwright';
import { config } from 'dotenv';

config();

const REMOTE_DIR = '/tmp/downloads';
const FILENAME = 'Kernel-Logo_Accent.png';
const IMAGE_URL = 'https://www.onkernel.com/brand_assets/Kernel-Logo_Accent.png';

const kernel = new Kernel();

async function main() {
    // 1. Create Kernel browser session
    const kernelBrowser = await kernel.browsers.create();
    console.log('Live view:', kernelBrowser.browser_live_view_url);

    // 2. Fetch the image from URL
    console.log(`Fetching image from ${IMAGE_URL}`);
    const response = await fetch(IMAGE_URL);
    if (!response.ok) {
        throw new Error(`Failed to fetch image: ${response.status}`);
    }
    const imageBlob = await response.blob();

    // 3. Write the fetched image to the remote browser's filesystem
    const remotePath = `${REMOTE_DIR}/${FILENAME}`;
    console.log(`Writing to remote browser at ${remotePath}`);
    await kernel.browsers.fs.writeFile(kernelBrowser.session_id, imageBlob, {
        path: remotePath,
    });
    console.log('File written to remote browser');

    // 4. Connect Playwright and navigate to upload test page
    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());

    console.log('Navigating to upload test page');
    await page.goto('https://browser-tests-alpha.vercel.app/api/upload-test');

    // 5. Upload the file using Playwright's file input helper
    console.log(`Uploading ${remotePath} via file input`);
    const remoteFile = await kernel.browsers.fs.readFile(kernelBrowser.session_id, { path: remotePath });
    const fileBuffer = Buffer.from(await remoteFile.bytes());
    await page.locator('#fileUpload').setInputFiles([{
        name: FILENAME,
        mimeType: 'image/png',
        buffer: fileBuffer,
    }]);
    console.log('Upload completed');

    await kernel.browsers.deleteByID(kernelBrowser.session_id);
    console.log('Browser deleted');

    return null;
}

main();