Skip to navigation

Collect Downloads & Artifacts

View as Markdown

Files downloaded in a Runtime don’t land on your machine - they land on the Run, alongside screenshots and agent outputs. When the Run ends, export them as a single archive with a download URL.

import { Bctrl } from "@bctrl/sdk";
import { chromium } from "playwright";
const bctrl = new Bctrl({ apiKey: process.env.BCTRL_API_KEY! });
const runtime = await bctrl.runtimes.create({ type: "browser", name: "downloads-recipe" });
const { connection, runId } = await bctrl.runtimes.start(runtime.id);
const browser = await chromium.connectOverCDP(connection.cdpUrl);
const context = browser.contexts()[0] ?? (await browser.newContext());
const page = context.pages()[0] ?? (await context.newPage());
// Trigger a download like a user would.
await page.goto("https://acme.com/reports");
await page.click("text=Download CSV");
await page.waitForTimeout(5_000); // let the download finish
await browser.close();
await bctrl.runtimes.stop(runtime.id);
// See what the run produced...
for await (const file of bctrl.runs.files.iter(runId)) {
console.log(file.name, file.sizeBytes, file.contentType);
}
// ...and export the downloads as one archive.
const archive = await bctrl.runs.files.export(runId, {
name: "acme-reports.zip", // must end in .zip
type: ["download"],
});
console.log(archive.downloadUrl); // fetch this to get the ZIP

Export is idempotent - pass an idempotency key so a retried job doesn’t create duplicate archives.

Feeding files in

The reverse direction works while the runtime is live: upload pushes local bytes into the browser’s workspace, stage copies a durable file into it - useful when the automation needs an input CSV or a document to submit:

await bctrl.runtimes.files.upload(runtime.id, {
file: new Blob(["name,email\nada@acme.com"]),
runtimePath: "/work/input.csv",
});

Next