Skip to main content
Kernel browsers run in fully sandboxed environments with a writable filesystem that you control. Anything your automation downloads during a session is saved inside this filesystem and can be retrieved directly while the session is running.

Downloads

Playwright performs downloads via the browser itself, so there are a few steps:
  • Create a browser session
  • Configure where the browser saves downloads
  • 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.

Uploads

You can upload from your local filesystem into the browser directly using Playwright’s file input helpers.
const localPath = '/path/to/a/file.txt';

console.log(`Uploading ${localPath}...`);
await page.locator('#fileUpload').setInputFiles(localPath);
console.log('Upload completed');