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.

1import { Bctrl } from "@bctrl/sdk";
2import { chromium } from "playwright";
3
4const bctrl = new Bctrl({ apiKey: process.env.BCTRL_API_KEY! });
5
6const runtime = await bctrl.runtimes.create({ type: "browser", name: "downloads-recipe" });
7const { connectUrl, runId } = await bctrl.runtimes.start(runtime.id);
8
9const browser = await chromium.connectOverCDP(connectUrl);
10const context = browser.contexts()[0] ?? (await browser.newContext());
11const page = context.pages()[0] ?? (await context.newPage());
12
13// Trigger a download like a user would.
14await page.goto("https://acme.com/reports");
15await page.click("text=Download CSV");
16await page.waitForTimeout(5_000); // let the download finish
17
18await browser.close();
19await bctrl.runtimes.stop(runtime.id);
20
21// See what the run produced...
22for await (const file of bctrl.runs.files.iter(runId)) {
23 console.log(file.name, file.sizeBytes, file.contentType);
24}
25
26// ...and export the downloads as one archive.
27const archive = await bctrl.runs.files.export(runId, {
28 name: "acme-reports.zip", // must end in .zip
29 type: ["download"],
30});
31
32console.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 (see Idempotency).

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:

1await bctrl.runtimes.files.upload(runtime.id, {
2 file: new Blob(["name,email\n[email protected]"]),
3 runtimePath: "/work/input.csv",
4});

Next