Browser, shell and files together
Download a file in the browser, work on it in the shell, and use the result back in the browser, all in one session: from your code, by the AI agent, and as a script.
A session with shell: true is one machine with a browser and a shell on the same disk. So the browser can download a file, the shell can work on it, and the browser can use the result, without the file ever leaving the machine.
One disk for everything
| Part | How it uses /workspace |
|---|---|
| Browser | Every download lands in /workspace/downloads, whoever clicks: your code, the agent, or a person in the live view. The upload action puts a file from the workspace into a form. |
| Shell | Starts in /workspace. downloads/report.csv is the file the browser just saved. |
| Files API | Reads, writes, lists and waits for the same files, from your own code. See File uploads and downloads. |
The examples below download a small revenue report, add up its revenue column with Python, keep the total in output/total.txt (5460), and type it into a form. They run as they are: the report is a demo file and the form is httpbin.org’s demo form. For your own site, open your report page and your form instead. The shell machine already has Python with pandas, Node, ffmpeg, ImageMagick and poppler.
1. From your code
Playwright drives the browser over the session’s connectUrl. The files API waits for the download, exec runs Python in the shell, the files API reads the result, and the actions API fills in the form. The curl version uses actions instead of Playwright for the browser part.
# 1. A session with a browser and a shell.curl -X POST https://api.boxline.dev/v1/sessions \ -H "x-api-key: $BOXLINE_API_KEY" \ -H "content-type: application/json" \ -d '{"shell": true, "keepAlive": true}'# 2. Browser: put a download link on the page and click it (on your own site: goto your report page).curl -X POST https://api.boxline.dev/v1/sessions/$SESSION_ID/browser/actions \ -H "x-api-key: $BOXLINE_API_KEY" \ -H "content-type: application/json" \ -d @- <<'JSON'{"actions": [ {"action": "evaluate", "expression": "document.body.innerHTML = '<a download=\"report.csv\" href=\"data:text/csv,month%2Crevenue%0AJan%2C1200%0AFeb%2C1850%0AMar%2C2410\">Download report</a>'"}, {"action": "click", "selector": "text=Download report"}]}JSON# 3. Files: wait for the download in /workspace/downloads.curl -G https://api.boxline.dev/v1/sessions/$SESSION_ID/files/wait \ -H "x-api-key: $BOXLINE_API_KEY" \ --data-urlencode "pattern=downloads/*.csv"# 4. Shell: add it up with Python, on the same machine.curl -X POST https://api.boxline.dev/v1/sessions/$SESSION_ID/shell/exec \ -H "x-api-key: $BOXLINE_API_KEY" \ -H "content-type: application/json" \ -d @- <<'JSON'{"command": "mkdir -p output && python3 -c \"import pandas as pd; print(pd.read_csv('downloads/report.csv')['revenue'].sum())\" > output/total.txt"}JSON# 5. Files: read the result (5460).curl -G https://api.boxline.dev/v1/sessions/$SESSION_ID/files \ -H "x-api-key: $BOXLINE_API_KEY" \ --data-urlencode "path=output/total.txt"# 6. Browser: type it into the form and send it.curl -X POST https://api.boxline.dev/v1/sessions/$SESSION_ID/browser/actions \ -H "x-api-key: $BOXLINE_API_KEY" \ -H "content-type: application/json" \ -d '{"actions": [ {"action": "goto", "url": "https://httpbin.org/forms/post"}, {"action": "fill", "selector": "input[name=custname]", "value": "5460"}, {"action": "click", "selector": "form button"}, {"action": "content", "format": "text"} ]}'import { Boxline } from "@boxline/sdk";import { chromium } from "playwright";const bx = new Boxline();const session = await bx.sessions.create({ shell: true, keepAlive: true });try { // Browser: download a report. Downloads land in /workspace/downloads. // (A demo page; on your own site, page.goto() your report page instead.) const browser = await chromium.connectOverCDP(session.connectUrl!); const context = browser.contexts()[0]; const page = context.pages()[0] ?? (await context.newPage()); await page.setContent( `<a download="report.csv" href="data:text/csv,${encodeURIComponent("month,revenue\nJan,1200\nFeb,1850\nMar,2410")}">Download report</a>`, ); await page.getByRole("link", { name: "Download report" }).click(); const csv = await session.files.waitFor("downloads/*.csv"); console.log("downloaded", csv.path, csv.size); // Shell: add up the revenue column with Python, on the same machine. const run = await session.exec(`mkdir -p output && python3 -c "import pandas as pd; print(pd.read_csv('downloads/report.csv')['revenue'].sum())" > output/total.txt`); if (run.exitCode !== 0) throw new Error(run.stderr); // Files: read the result. const total = (await session.files.readText("output/total.txt")).trim(); // "5460" // Actions: type it into the form and send it. const results = await session.actions([ { action: "goto", url: "https://httpbin.org/forms/post" }, { action: "fill", selector: "input[name=custname]", value: total }, { action: "click", selector: "form button" }, { action: "content", format: "text" }, ]); console.log(results.at(-1)?.value?.content); // what the form received await browser.close();} finally { await session.stop();}import urllib.parsefrom boxline import Boxlinefrom playwright.sync_api import sync_playwrightbx = Boxline()with bx.sessions.create(shell=True, keep_alive=True) as session: # Browser: download a report. Downloads land in /workspace/downloads. # (A demo page; on your own site, page.goto() your report page instead.) with sync_playwright() as p: browser = p.chromium.connect_over_cdp(session.connect_url) context = browser.contexts[0] page = context.pages[0] if context.pages else context.new_page() csv_url = "data:text/csv," + urllib.parse.quote("month,revenue\nJan,1200\nFeb,1850\nMar,2410") page.set_content(f'<a download="report.csv" href="{csv_url}">Download report</a>') page.get_by_role("link", name="Download report").click() csv = session.files.wait_for("downloads/*.csv") print("downloaded", csv["path"], csv["size"]) # Shell: add up the revenue column with Python, on the same machine. run = session.exec("""mkdir -p output && python3 -c "import pandas as pd; print(pd.read_csv('downloads/report.csv')['revenue'].sum())" > output/total.txt""") if run["exitCode"] != 0: raise RuntimeError(run["stderr"]) # Files: read the result. total = session.files.read_text("output/total.txt").strip() # "5460" # Actions: type it into the form and send it. results = session.actions([ {"action": "goto", "url": "https://httpbin.org/forms/post"}, {"action": "fill", "selector": "input[name=custname]", "value": total}, {"action": "click", "selector": "form button"}, {"action": "content", "format": "text"}, ]) print(results[-1]["value"]["content"]) # what the form received2. With the AI agent
Give the run a session with a shell (session: { shell: true }) and a goal that needs both. The agent uses its browser tools for the web pages and bash for the calculation.
curl -X POST https://api.boxline.dev/v1/agent/runs \ -H "x-api-key: $BOXLINE_API_KEY" \ -H "content-type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "task": "Download the report from https://httpbin.org/base64/bW9udGgscmV2ZW51ZQpKYW4sMTIwMApGZWIsMTg1MApNYXIsMjQxMAo= (a CSV file). In the shell, add up its revenue column with Python and save the total, and nothing else, to output/total.txt. Then open https://httpbin.org/forms/post, type the total into the first text box of the form, submit it, and tell me what the page says.", "session": {"shell": true} }'# Then poll GET /v1/agent/runs/$RUN_ID until status is "completed" or "failed".import { Boxline } from "@boxline/sdk";const bx = new Boxline();const { id } = await bx.agent.run({ task: "Download the report from https://httpbin.org/base64/bW9udGgscmV2ZW51ZQpKYW4sMTIwMApGZWIsMTg1MApNYXIsMjQxMAo= (a CSV file). In the shell, add up its revenue column with Python and save the total, and nothing else, to output/total.txt. Then open https://httpbin.org/forms/post, type the total into the first text box of the form, submit it, and tell me what the page says.", session: { shell: true },});const run = await bx.agent.wait(id);console.log(run.status, run.result);// Each tool call, with the agent's thought behind it.for (const s of run.steps) if (s.type === "tool") console.log(s.name, JSON.stringify(s.input), s.thought ?? "");from boxline import Boxlinebx = Boxline()started = bx.agent.run( task="Download the report from https://httpbin.org/base64/bW9udGgscmV2ZW51ZQpKYW4sMTIwMApGZWIsMTg1MApNYXIsMjQxMAo= (a CSV file). In the shell, add up its revenue column with Python and save the total, and nothing else, to output/total.txt. Then open https://httpbin.org/forms/post, type the total into the first text box of the form, submit it, and tell me what the page says.", session={"shell": True},)run = bx.agent.wait(started["id"])print(run["status"], run["result"])for s in run["steps"]: if s["type"] == "tool": print(s["name"], s.get("input"), s.get("thought", "")) # with the agent's thoughtboxline run 'Download the report from https://httpbin.org/base64/bW9udGgscmV2ZW51ZQpKYW4sMTIwMApGZWIsMTg1MApNYXIsMjQxMAo= (a CSV file). In the shell, add up its revenue column with Python and save the total, and nothing else, to output/total.txt. Then open https://httpbin.org/forms/post, type the total into the first text box of the form, submit it, and tell me what the page says.' --shellThe run’s steps show the switch from browser to shell and back. An example, shortened (each run picks its own way):
[ { "type": "tool", "name": "browser_navigate", "input": { "url": "https://httpbin.org/base64/bW9udGgs…" } }, { "type": "tool", "name": "bash", "input": { "command": "curl -s -o downloads/report.csv 'https://httpbin.org/base64/bW9udGgs…'" } }, { "type": "tool", "name": "bash", "input": { "command": "python3 -c \"import csv; …\" > output/total.txt && cat output/total.txt" }, "output": "5460" }, { "type": "tool", "name": "browser_navigate", "input": { "url": "https://httpbin.org/forms/post" } }, { "type": "tool", "name": "browser_type", "input": { "element": 1, "text": "5460" } }, { "type": "tool", "name": "browser_click", "input": { "element": 12 } }, { "type": "text", "text": "The total revenue is 5460. I saved it to output/total.txt and submitted the form; the page shows custname: 5460." }]Each tool step also has output, ms and at. See Follow a run.
3. In a script
A script runs with Node inside the session, next to the browser. Besides Playwright, it can require() Node’s own modules: child_process to run shell commands and fs for the workspace. step() handles the clicks you would rather describe than select, and useModel() picks its model.
// Download a CSV, add it up with Python in the shell, and// type the total into a form. One AI step; the rest is code.const { execSync } = require("child_process");const fs = require("fs");useModel("claude-haiku-5-5"); // the model step() uses// 1. The report (a demo page; on your own site, page.goto() it).const csv = "month,revenue\nJan,1200\nFeb,1850\nMar,2410";await page.setContent(`<h1>Quarterly report</h1> <a download="report.csv" href="data:text/csv,${encodeURIComponent(csv)}">Download report</a>`);const [download] = await Promise.all([ page.waitForEvent("download"), page.getByRole("link", { name: /download/i }).first().click(),]);fs.mkdirSync("downloads", { recursive: true });const file = `downloads/${download.suggestedFilename()}`;await download.saveAs(file);console.log("Downloaded", file);// 2. Add up the revenue column with Python, in the shell.fs.mkdirSync("output", { recursive: true });execSync( `python3 - "${file}" <<'PY'import csv, sysrows = csv.DictReader(open(sys.argv[1]))total = sum(float(r["revenue"]) for r in rows)open("output/total.txt", "w").write(f"{total:g}")PY`, { stdio: "inherit" },);const total = fs.readFileSync("output/total.txt", "utf8").trim();console.log("Total revenue:", total);// 3. The AI types it into the form; code submits it.await page.goto("https://httpbin.org/forms/post");await step(`type ${total} into the first text box of the form`);const before = page.url();await page.locator("form button, form [type=submit]").first().click();await page.waitForURL((u) => u.href !== before);return (await page.locator("body").innerText()).slice(0, 400);# The body is JSON: put the script (total.js, above) in "code".jq -n --rawfile code total.js '{code: $code, timeoutMs: 300000}' | curl -N -X POST https://api.boxline.dev/v1/sessions/$SESSION_ID/browser/script \ -H "x-api-key: $BOXLINE_API_KEY" \ -H "content-type: application/json" \ -d @-import { readFile } from "node:fs/promises";import { Boxline } from "@boxline/sdk";const bx = new Boxline();const session = await bx.sessions.create({ shell: true, keepAlive: true });const result = await session.runScript(await readFile("total.js", "utf8"), { timeoutMs: 300_000, onData: (_stream, text) => process.stdout.write(text),});console.log("exit code", result.exitCode);await session.stop();from boxline import Boxlinebx = Boxline()with bx.sessions.create(shell=True, keep_alive=True) as session: code = open("total.js").read() # the script above with session.run_script(code, timeout_ms=300_000) as script: for stream, text in script: print(text, end="") print("exit code", script.result["exitCode"])See Scripts for what else is in scope.
More examples
Tested examples that use the browser, the shell and the files together, each with its result check:
- Download and add up: this job as a complete program, in Node and Python.
- Video clip with yt-dlp and ffmpeg.
- PDF to summary with pdftotext.
- Screenshots to a PDF report with Pillow.
- Every other one: All examples.
Good to know
- Read what you want to keep before you delete the session: its files go with it. Stopping keeps them, and you can still read them or download a whole folder afterwards. See Stop and resume.
- To use the browser’s login from the shell (for
curlorwget), export its cookies. The file holds live credentials: delete it when you are done. - Packages you install with
setupare installed again after a resume. Only/workspaceis saved. See Install packages. - Shell sessions are billed by their machine size, not as a browser session. See Pricing.
The same flow from Python with boxline or Node with Boxline: the Node SDK and Python SDK pages list every method.
These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.