Transform a file where it lives
Derive a file where it already is: four encodings from one render capture, Pillow in a code-interpreter sandbox, and a routine step on that sandbox.
When a file is already on the node, do the conversion there and move only the result. Three places a file lives, and the call that transforms it in each:
- A render capture. Ask for
variantsin the screenshot request. The page is loaded and painted once and every encoding comes back as its own artifact: here a full-size PNG, a WebP, an 800 px JPEG and a 400 px thumbnail. There is no second capture and no download to re-encode on your side. Each variant bills one morerender.screenshot. - A sandbox. A file an agent downloaded or produced sits in its sandbox. Pillow is installed in the
code-interpretertemplate, so onerun_codecall shrinks a 4000x3000 JPEG to a 1,600 px WebP in the kernel and you read back only that. The sandbox hasegress: deny: the transform needs no network. - A routine step.
sandboxes/exec@1withsandboxruns a command in an existing sandbox, so the same script can make a 400 px thumbnail on a schedule or after another step, with no process of yours waiting on it.
The script both the kernel and the routine run. Pillow's draft mode decodes a JPEG at 1/2, 1/4 or 1/8 scale when the target is that much smaller, so a thumbnail never decodes all 12 megapixels:
python"""Shrink an image to a width, as WebP next to the original. Prints one JSON line."""import jsonimport osimport sysimport timefrom PIL import Imagedef shrink(src, width, quality=82): t0 = time.perf_counter() with Image.open(src) as im: im.draft("RGB", (width, width * im.height // im.width)) out = im.convert("RGB") out.thumbnail((width, out.height), Image.LANCZOS) dst = f"{os.path.splitext(src)[0]}-{width}.webp" out.save(dst, "WEBP", quality=quality) return { "width": out.width, "height": out.height, "bytes": os.path.getsize(dst), "path": dst, "ms": round((time.perf_counter() - t0) * 1000, 1), }if __name__ == "__main__": print(json.dumps(shrink(sys.argv[1], int(sys.argv[2]))))
Verified 2026-11-03: this recipe's last run passed against https://beta-api.b3os.org, label live-recorded (recorded from production), from commit e1617763. The recipe harness runs it again, and a failed run takes this page out of the cookbook.
Run it
Save the script above as thumb.py. The sandbox needs a file to work on: the recipe makes a 4000x3000 stand-in
photo inside it; to try it on your own, write one with PUT /v1/sandboxes/{id}/files or download it from inside a
sandbox that allows the host.
bashAPI=https://beta-api.b3os.orgAUTH="Authorization: Bearer $B3OS_API_KEY"post() { curl -s "$API$1" -H "$AUTH" -H "Content-Type: application/json" -d @-; }# 1. render: one capture, four encodingspost /v1/render/screenshots <<'EOF' | jq '.artifacts[] | {content_type, width, bytes}'{"url": "https://beta.b3os.org/demo/billing/", "viewport": {"width": 1440, "height": 900}, "format": "png", "variants": [{"format": "webp", "quality": 85}, {"format": "jpeg", "width": 800, "quality": 80}, {"format": "webp", "width": 400, "quality": 80}]}EOF# 2. sandboxes: shrink the file in the kernel, read back only the resultSBX=$(echo '{"template": "code-interpreter", "timeout_s": 300, "egress": {"mode": "deny"}}' | post /v1/sandboxes | jq -r .id)curl -s -X PUT "$API/v1/sandboxes/$SBX/files?path=/workspace/thumb.py" -H "$AUTH" \ -H "Content-Type: application/octet-stream" --data-binary @thumb.py > /dev/nullcurl -s -X PUT "$API/v1/sandboxes/$SBX/files?path=/workspace/photo.jpg" -H "$AUTH" \ -H "Content-Type: application/octet-stream" --data-binary @photo.jpg > /dev/nullecho '{"code": "import json, sys\nsys.path.insert(0, \"/workspace\")\nfrom thumb import shrink\nprint(json.dumps(shrink(\"/workspace/photo.jpg\", 1600)))"}' \ | post "/v1/sandboxes/$SBX/run_code" | jq -r .stdoutcurl -s "$API/v1/sandboxes/$SBX/files?path=/workspace/photo-1600.webp" -H "$AUTH" -o photo-1600.webp# 3. routines: the same script as a step on the sandbox that holds the filecurl -s -X PUT "$API/v1/routines/thumbnail" -H "$AUTH" -H "Content-Type: application/json" -d @- <<'EOF' > /dev/null{"name": "thumbnail", "inputs": {"type": "object", "required": ["sandbox"], "properties": {"sandbox": {"type": "string"}}}, "steps": [{"id": "thumb", "uses": "sandboxes/exec@1", "with": {"sandbox": "${{ inputs.sandbox }}", "cmd": ["python3", "/workspace/thumb.py", "/workspace/photo.jpg", "400"]}}], "output": {"stdout": "${{ steps.thumb.outputs.stdout }}"}}EOFjq -n --arg sbx "$SBX" '{input: {sandbox: $sbx}}' | post "/v1/routines/thumbnail/runs?wait=60" | jq -r .result.stdoutcurl -s -X DELETE "$API/v1/routines/thumbnail" -H "$AUTH" > /dev/nullcurl -s -X DELETE "$API/v1/sandboxes/$SBX" -H "$AUTH" | jq .status
tsimport { readFileSync, writeFileSync } from "node:fs";const API = "https://beta-api.b3os.org";const auth = { authorization: `Bearer ${process.env.B3OS_API_KEY}` };const call = async (method: string, path: string, body?: unknown): Promise<any> => ( await fetch(`${API}${path}`, { method, headers: { ...auth, "content-type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), }) ).json();const put = (path: string, file: string) => fetch(`${API}${path}`, { method: "PUT", headers: { ...auth, "content-type": "application/octet-stream" }, body: readFileSync(file), });// 1. render: one capture, four encodingsconst shot = await call("POST", "/v1/render/screenshots", { url: "https://beta.b3os.org/demo/billing/", viewport: { width: 1440, height: 900 }, format: "png", variants: [ { format: "webp", quality: 85 }, { format: "jpeg", width: 800, quality: 80 }, { format: "webp", width: 400, quality: 80 }, ],});for (const a of shot.artifacts) console.log(a.content_type, a.width, a.bytes);// 2. sandboxes: shrink the file in the kernel, read back only the resultconst sbx = await call("POST", "/v1/sandboxes", { template: "code-interpreter", timeout_s: 300, egress: { mode: "deny" } });try { await put(`/v1/sandboxes/${sbx.id}/files?path=/workspace/thumb.py`, "thumb.py"); await put(`/v1/sandboxes/${sbx.id}/files?path=/workspace/photo.jpg`, "photo.jpg"); const cell = await call("POST", `/v1/sandboxes/${sbx.id}/run_code`, { code: 'import json, sys\nsys.path.insert(0, "/workspace")\nfrom thumb import shrink\nprint(json.dumps(shrink("/workspace/photo.jpg", 1600)))', }); const made = JSON.parse(cell.stdout); const file = await fetch(`${API}/v1/sandboxes/${sbx.id}/files?path=${encodeURIComponent(made.path)}`, { headers: auth }); writeFileSync("photo-1600.webp", Buffer.from(await file.arrayBuffer())); // 3. routines: the same script as a step on the sandbox that holds the file await call("PUT", "/v1/routines/thumbnail", { name: "thumbnail", inputs: { type: "object", required: ["sandbox"], properties: { sandbox: { type: "string" } } }, steps: [ { id: "thumb", uses: "sandboxes/exec@1", with: { sandbox: "${{ inputs.sandbox }}", cmd: ["python3", "/workspace/thumb.py", "/workspace/photo.jpg", "400"] }, }, ], output: { stdout: "${{ steps.thumb.outputs.stdout }}" }, }); const run = await call("POST", "/v1/routines/thumbnail/runs?wait=60", { input: { sandbox: sbx.id } }); console.log(run.status, run.result.stdout); await call("DELETE", "/v1/routines/thumbnail");} finally { await call("DELETE", `/v1/sandboxes/${sbx.id}`);}
pythonimport jsonimport osimport requestsAPI = "https://beta-api.b3os.org"s = requests.Session()s.headers["Authorization"] = f"Bearer {os.environ['B3OS_API_KEY']}"# 1. render: one capture, four encodingsshot = s.post(f"{API}/v1/render/screenshots", json={ "url": "https://beta.b3os.org/demo/billing/", "viewport": {"width": 1440, "height": 900}, "format": "png", "variants": [{"format": "webp", "quality": 85}, {"format": "jpeg", "width": 800, "quality": 80}, {"format": "webp", "width": 400, "quality": 80}],}).json()for a in shot["artifacts"]: print(a["content_type"], a["width"], a["bytes"])# 2. sandboxes: shrink the file in the kernel, read back only the resultsbx = s.post(f"{API}/v1/sandboxes", json={ "template": "code-interpreter", "timeout_s": 300, "egress": {"mode": "deny"}}).json()try: for name in ("thumb.py", "photo.jpg"): with open(name, "rb") as f: s.put(f"{API}/v1/sandboxes/{sbx['id']}/files", params={"path": f"/workspace/{name}"}, data=f, headers={"Content-Type": "application/octet-stream"}) cell = s.post(f"{API}/v1/sandboxes/{sbx['id']}/run_code", json={"code": ( 'import json, sys\nsys.path.insert(0, "/workspace")\nfrom thumb import shrink\n' 'print(json.dumps(shrink("/workspace/photo.jpg", 1600)))')}).json() made = json.loads(cell["stdout"]) with open("photo-1600.webp", "wb") as f: f.write(s.get(f"{API}/v1/sandboxes/{sbx['id']}/files", params={"path": made["path"]}).content) # 3. routines: the same script as a step on the sandbox that holds the file s.put(f"{API}/v1/routines/thumbnail", json={ "name": "thumbnail", "inputs": {"type": "object", "required": ["sandbox"], "properties": {"sandbox": {"type": "string"}}}, "steps": [{"id": "thumb", "uses": "sandboxes/exec@1", "with": { "sandbox": "${{ inputs.sandbox }}", "cmd": ["python3", "/workspace/thumb.py", "/workspace/photo.jpg", "400"]}}], "output": {"stdout": "${{ steps.thumb.outputs.stdout }}"}, }) run = s.post(f"{API}/v1/routines/thumbnail/runs", params={"wait": 60}, json={"input": {"sandbox": sbx["id"]}}).json() print(run["status"], run["result"]["stdout"]) s.delete(f"{API}/v1/routines/thumbnail")finally: s.delete(f"{API}/v1/sandboxes/{sbx['id']}")
On the remote B3OS MCP server, ask your agent:
textTake one screenshot of https://beta.b3os.org/demo/billing/ at 1440x900 as PNG with three variants: WebP, an 800 pxJPEG and a 400 px WebP, and list each artifact's size. Then create a code-interpreter sandbox with egress deny,write thumb.py and photo.jpg to /workspace, run shrink("/workspace/photo.jpg", 1600) from thumb.py in the kernel,and show me the JSON it prints. Delete the sandbox when you are done.
The agent calls render_screenshot, sandbox_create, sandbox_write_file, sandbox_run_code and
sandbox_delete.
Which one to use
- The file came from render: ask for
variantsin the same request, up to 4 per capture (limits). - The file is in an agent's sandbox: run the tool there with
run_codeorexecand read back the result. - The transform should run without you: put the command in a routine as
sandboxes/exec@1withsandbox, or withtemplatefor a sandbox made for the step (definitions). - The file is on your laptop: send it where it will be used next. Converting it on the node first only adds a hop.
The recipe's own run makes the stand-in photo with numpy and Pillow inside the sandbox, so no photo is uploaded.
Owner: Docs. Products: Render, Sandboxes, Routines.
Output of the last green run
json{ "page": "https://beta.b3os.org/demo/billing/", "encodings": [ { "content_type": "image/png", "width": 1440, "height": 900, "bytes": 269361 }, { "content_type": "image/webp", "width": 1440, "height": 900, "bytes": 48378 }, { "content_type": "image/jpeg", "width": 800, "height": 500, "bytes": 32757 }, { "content_type": "image/webp", "width": 400, "height": 250, "bytes": 6718 } ], "render_call_ms": 714, "render_server_ms": 427.177, "create_ms": 80.7, "photo": { "width": 4000, "height": 3000, "bytes": 1772041 }, "kernel": { "width": 1600, "height": 1200, "bytes": 18462, "transform_ms": 196.8, "kernel_ms": 205.041, "run_code_ms": 985.8, "read_ms": 59 }, "routine": { "run_status": "succeeded", "step_status": "succeeded", "step_ms": 236.567, "width": 400, "height": 300, "bytes": 2486, "transform_ms": 52.1, "run_ms": 843.7 }, "deleted": true, "total_ms": 5023.9}
bash$ node run.mjs# exit 0
The outputs this recipe publishes (its recipe.json publish.outputs), with workspace identifiers redacted. The run's stdout, stderr and other outputs are not published.
Captured 2026-11-03 from commit
e1617763
, label
live-recorded
(recorded from production). Case
recipes/transform-file-in-place
.