Move files between your machine and a cloud browser
Use the Steel Files API with Playwright to automate file uploads and downloads in the cloud.
Scaffolds a starter project locally. Requires the Steel CLI.
Every Steel session ships with a scoped filesystem inside the session VM. client.sessions.files exposes methods to move bytes across the boundary between your machine and that sandbox. This recipe uses upload to push a local CSV into the session, hands the resulting path to a remote <input type="file"> over CDP, and lets the browser render a chart against it. It's one of Steel's session APIs.
const uploadedFile = await client.sessions.files.upload(session.id, {file,});
file is a Web File built from fs.readFileSync("./assets/stock.csv"). What comes back is a record whose path is a handle inside the session VM (something like stock.csv at the sandbox root). That path is not valid on your laptop, and paths on your laptop are not valid inside the session. The whole recipe hinges on keeping that distinction straight.
Wiring a remote file into a DOM input
page.setInputFiles("./local.csv") resolves paths on the machine running Playwright. Since Chromium lives on a Steel VM, you need to resolve the path there instead. The main function drops down to raw CDP:
const cdpSession = await currentContext.newCDPSession(page);const document = await cdpSession.send("DOM.getDocument");const inputNode = await cdpSession.send("DOM.querySelector", {nodeId: document.root.nodeId,selector: "#load-file",});await cdpSession.send("DOM.setFileInputFiles", {files: [uploadedFile.path],nodeId: inputNode.nodeId,});
DOM.setFileInputFiles runs browser-side, so uploadedFile.path resolves against the session VM, which is exactly where upload() wrote the bytes. After that, it's plain Playwright: wait for svg.main-svg, scroll into view, screenshot to stock.png on your local disk.
Run it
cd examples/files-tscp .env.example .env # set STEEL_API_KEYnpm installnpm start
Get a key at app.steel.dev/settings/api-keys. The script prints a session viewer URL as it starts. Open it in another tab to watch the upload land and the chart render.
Your output varies. Structure looks like this:
Steel + Files API Starter============================================================Creating Steel session...Steel Session created!View session at https://app.steel.dev/sessions/ab12cd34...Uploading CSV file to the Steel session...CSV file uploaded successfully!File path on Steel session: stock.csvConnected to browser via PlaywrightReleasing session...Session releasedDone!
stock.png lands in the recipe folder. It's the rendered chart, captured server-side after the CSV was parsed remotely, then saved locally. A run takes ~15 seconds.
The rest of the surface
The recipe touches upload and nothing else, but client.sessions.files has more:
list(sessionId): returns every file in the session namespace with{ path, size, lastModified }. Useful after the browser triggers a download and you need to find the new file.download(sessionId, path): pulls a single file back out. Stream the response body to disk.downloadArchive(sessionId): zips the whole namespace into one response. One call instead of N.delete(sessionId, path)anddeleteAll(sessionId): explicit cleanup. Releasing the session also clears storage.
Browser-initiated downloads (PDF exports, file-save dialogs) land in the same namespace automatically, so the inverse of this recipe is: drive the page to export, then list() and download() what showed up.
There's also client.files (without .sessions), an organization-scoped store that persists across sessions. Same method shape. Useful for fixtures and assets you don't want to re-upload every run.
Make it yours
- Upload from a URL. Pass a string instead of a
File:client.sessions.files.upload(session.id, { file: "https://example.com/report.pdf" }). Steel fetches it server-side and drops it in the session namespace, skipping your machine entirely. - Harvest generated files. Swap the
csvplot.comflow for a site that exports. After the download fires, calllist()to discover the new path, thendownload()it back. - Target a nested path.
upload()accepts apathargument to control where the file lands inside the sandbox. Default is the filename at root; passpath: "inputs/stock.csv"to nest.
Related
Credentials for auth tokens kept out of the filesystem. Auth context for cookies and storage state. Profiles for persistent user-data directories across runs. Extensions for loading unpacked Chrome extensions into a session.
Scaffolds a starter project locally. Requires the Steel CLI.
client.sessions.files moves bytes between your machine and the filesystem that lives inside a Steel session VM. This recipe uploads a local CSV into the session, hands the path the upload returns to a remote <input type="file"> over raw CDP, and screenshots the chart the page renders from it. The whole thing turns on one fact: a file you push over the API lands at a path the browser can read, and that path means nothing back on your laptop.
Shaping the upload
The Python SDK speaks multipart/form-data, so the file argument takes the same tuple shape as requests or the OpenAI client: (filename, content, content_type).
csv_bytes = (Path(__file__).parent / "assets" / "stock.csv").read_bytes()uploaded = client.sessions.files.upload(session.id,file=("stock.csv", csv_bytes, "text/csv"),)
uploaded.path comes back as a handle inside the session sandbox (typically just stock.csv at the root). Pass a URL string instead of the tuple and Steel fetches the file server-side, so the bytes never touch your machine at all.
Reaching the input over CDP
page.set_input_files("./stock.csv") resolves paths on the host running Playwright. The browser is on a Steel VM, so the file has to be resolved there. That means dropping under Playwright's locators to the Chrome DevTools Protocol, which new_cdp_session exposes as a send(method, params) call:
cdp = current_context.new_cdp_session(page)document = cdp.send("DOM.getDocument")input_node = cdp.send("DOM.querySelector",{"nodeId": document["root"]["nodeId"], "selector": "#load-file"},)cdp.send("DOM.setFileInputFiles",{"files": [uploaded.path], "nodeId": input_node["nodeId"]},)
send returns plain dicts, so the node ids are read with subscript access. Because DOM.setFileInputFiles runs browser-side, uploaded.path resolves against the VM, exactly where upload wrote it. After that it is ordinary Playwright: wait for svg.main-svg, scroll it into view, and screenshot it to stock.png on your local disk.
Run it
cd examples/files-pycp .env.example .env # set STEEL_API_KEYuv run main.py
Grab a key at app.steel.dev/settings/api-keys. uv sync runs automatically on first uv run, so there is no separate install step. The script prints a session viewer URL as it starts. Open it in another tab to watch the upload land and the chart render.
Your output varies. Structure looks like this:
Steel + Files API Starter============================================================Creating Steel session...Steel Session created!View session at https://app.steel.dev/sessions/ab12cd34...Uploading CSV file to the Steel session...CSV file uploaded successfully!File path on Steel session: stock.csvConnected to browser via PlaywrightReleasing session...Session releasedDone!
stock.png lands in the recipe folder: the chart, parsed and drawn remotely, captured server-side, then saved to your disk. A run takes about 15 seconds.
Make it yours
- Skip your machine. Pass a URL string for
fileinstead of the tuple, and Steel downloads it into the session directly. - Nest the upload.
uploadtakes apathargument that sets where the file lands in the sandbox. Default is the filename at root; passpath="inputs/stock.csv"to nest it. - Pull files back out.
client.sessions.files.list(session.id)enumerates the namespace, anddownload(session.id, path)returns the bytes. Browser-initiated downloads land in the same namespace, so the inverse recipe is: drive the page to export, then list and download what appeared.
Related
TypeScript version covers the same flow with the Web File API. Go version and Rust version build the upload from typed structs. The CDP calls map to Playwright's new_cdp_session; the protocol methods are in the DOM domain reference.
Scaffolds a starter project locally. Requires the Steel CLI.
Each Steel session owns a scoped filesystem inside its VM, and client.sessions().files() moves bytes across the boundary between your machine and that sandbox. This recipe reads a local CSV, uploads it with upload, captures the path the file landed at inside the session, and hands that path to a remote <input type="file"> so csvplot.com renders a chart against bytes that never touched the browser's own disk.
let uploaded = client.sessions().files().upload(session_id,SessionFileUploadParams {file: FileUpload::new("stock.csv", bytes).with_content_type("text/csv"),path: None,},).await?;
FileUpload::new takes a filename and the raw bytes; with_content_type is the builder step for the MIME type. What comes back is a File whose path is a handle inside the session VM (for this asset, stock.csv at the sandbox root). That path is meaningful to the browser running on Steel, not to your laptop, and keeping those two namespaces straight is the whole point of the recipe.
Driving a file input over raw CDP
chromiumoxide's typed helpers resolve file paths on the machine running your code, which is the wrong filesystem here. The fix is to issue the DOM commands yourself. chromiumoxide re-exports the generated CDP types under chromiumoxide::cdp::browser_protocol, and page.execute(...) sends any of them and deserializes the typed reply:
let document = page.execute(GetDocumentParams::default()).await?;let input = page.execute(QuerySelectorParams::new(document.root.node_id, "#load-file")).await?;page.execute(SetFileInputFilesParams {files: vec![uploaded.path.clone()],node_id: Some(input.node_id),backend_node_id: None,object_id: None,}).await?;
DOM.setFileInputFiles runs browser-side, so uploaded.path resolves against the session VM, which is exactly where upload wrote the bytes. After that it is ordinary automation: poll for svg.main-svg, scroll it into view, and screenshot the element to stock.png on your local disk.
Run it
cd examples/files-rscp .env.example .env # set STEEL_API_KEYcargo run
Get a key at app.steel.dev/settings/api-keys. The program prints a session viewer URL as it starts. Open it in another tab to watch the upload land and the chart render.
Your output varies. Structure looks like this:
Creating Steel session...Session live at https://app.steel.dev/sessions/ab12cd34...Uploading stock.csv (5488 bytes) to the session...Uploaded. Path inside the session VM: stock.csvConnected over CDP, opening csvplot.com...Setting the uploaded file on the page's #load-file input...Saved stock.png (48213 bytes)Releasing session...Session released
stock.png lands in the recipe folder: the rendered chart, captured server-side after the CSV was parsed remotely, then saved locally.
Make it yours
- Upload from a URL.
FileUploadcarries the bytes here, but the underlying endpoint also accepts a URL it fetches server-side, so you can skip reading the file locally for large fixtures. - Harvest generated files. Swap the csvplot flow for a site that exports. After the download fires, call
files().list(session_id)to discover the new path, thenfiles().download(session_id, &path)to pull the bytes back. - Target a nested path. Set
path: Some("inputs/stock.csv".into())onSessionFileUploadParamsto control where the file lands inside the sandbox instead of the default filename at root.
Related
files-ts, files-py, and files-go are the same recipe in other languages. The chromiumoxide docs cover page.execute and the generated CDP command types under chromiumoxide::cdp::browser_protocol.
Scaffolds a starter project locally. Requires the Steel CLI.
A Steel session carries its own filesystem inside the session VM. client.Sessions.Files moves bytes across the boundary between your machine and that sandbox. This recipe reads a local CSV, uploads it with Upload, then hands the returned server-side path to a remote <input type="file"> so csvplot.com can render a chart against bytes that never lived on the browser host's local disk.
The upload is a plain Go value, not an io.Reader or a multipart form you assemble yourself:
uploaded, err := client.Sessions.Files.Upload(ctx, sess.ID, steel.SessionFileUploadParams{File: steel.FileUpload{Name: "stock.csv",Content: csvBytes,ContentType: "text/csv",},})
Content is the raw []byte you got from os.ReadFile. What comes back is a *steel.File whose Path is a handle inside the session VM (typically stock.csv at the sandbox root). That path is meaningless on your laptop, and your laptop's paths are meaningless inside the session. Keeping that distinction straight is the whole point.
Wiring a remote file into a DOM input
chromedp's chromedp.SetUploadFiles resolves paths on the machine running chromedp, which is your laptop. The file we want lives on the Steel VM, so we drop to raw CDP from github.com/chromedp/cdproto/dom instead. DOM.setFileInputFiles runs browser-side, so uploaded.Path resolves against the session VM, exactly where Upload wrote the bytes. setRemoteFileInput wraps the three CDP calls in a chromedp.ActionFunc so it slots into a normal chromedp.Run task list:
func setRemoteFileInput(selector, remotePath string) chromedp.Action {return chromedp.ActionFunc(func(ctx context.Context) error {root, err := dom.GetDocument().Do(ctx)if err != nil {return err}nodeID, err := dom.QuerySelector(root.NodeID, selector).Do(ctx)if err != nil {return err}return dom.SetFileInputFiles([]string{remotePath}).WithNodeID(nodeID).Do(ctx)})}
After that it is ordinary chromedp: WaitVisible("svg.main-svg"), then FullScreenshot to stock.png on your local disk.
Run it
cd examples/files-gocp .env.example .env # set STEEL_API_KEYgo mod tidygo run .
Get a key at app.steel.dev/settings/api-keys. The program prints a session viewer URL as it starts. Open it in another tab to watch the upload land and the chart render.
Your output varies. Structure looks like this:
Creating Steel session...Session created. Watch it live at https://app.steel.dev/sessions/ab12cd34...Uploading stock.csv to the session...Uploaded. Path inside the session VM: stock.csvLoading csvplot.com and feeding it the uploaded file...Saved chart to stock.pngReleasing session...
stock.png lands in the recipe folder. It is the rendered chart, captured server-side after the CSV was parsed remotely, then saved locally.
Make it yours
- Upload from a URL.
steel.FileUploadcarries bytes, but the underlying API also accepts a URL string for the file field. Fetch a report server-side and skip your machine entirely. - Harvest generated files. Swap the csvplot.com flow for a site that exports. After the download fires, call
client.Sessions.Files.List(ctx, sess.ID)to discover the new path, thenclient.Sessions.Files.Download(ctx, sess.ID, path)to pull it back as anio.ReadCloser. - Target a nested path.
SessionFileUploadParamshas an optionalPathfield. The default is the filename at the sandbox root; setPathto a pointer to nest the upload, for example underinputs/.
Related
files-ts and files-py and files-rs for the same recipe in other languages. chromedp and its cdproto/dom package for the raw CDP surface used here.
Related recipes
Upload and run browser extensions
Use the Steel Extensions API with Playwright to upload and run browser extensions.
Scrape a page to Markdown, screenshot, and PDF
Use the Steel TypeScript SDK's direct API to scrape a page to clean Markdown for LLM context, plus screenshot and PDF, with no browser library.
Watch Claude pricing for divergent A/B variants
Convex cron plus two parallel Steel proxy probes against claude.com/pricing. Stores per-tier per-region snapshots and surfaces tiers where the probes disagree.