> ## Documentation Index
> Fetch the complete documentation index at: https://docs.beta.b3os.org/llms.txt
> Use this file to discover all available pages before exploring further.

# 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 `variants` in 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 more
  `render.screenshot`.
- **A sandbox.** A file an agent downloaded or produced sits in its sandbox. Pillow is installed in the
  `code-interpreter` template, so one `run_code` call shrinks a 4000x3000 JPEG to a 1,600 px WebP in the kernel and
  you read back only that. The sandbox has `egress: deny`: the transform needs no network.
- **A routine step.** `sandboxes/exec@1` with `sandbox` runs 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 json
import os
import sys
import time

from PIL import Image


def 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.

**curl**

```bash
API=https://beta-api.b3os.org
AUTH="Authorization: Bearer $B3OS_API_KEY"
post() { curl -s "$API$1" -H "$AUTH" -H "Content-Type: application/json" -d @-; }

# 1. render: one capture, four encodings
post /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 result
SBX=$(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/null
curl -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/null
echo '{"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 .stdout
curl -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 file
curl -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 }}"}}
EOF
jq -n --arg sbx "$SBX" '{input: {sandbox: $sbx}}' | post "/v1/routines/thumbnail/runs?wait=60" | jq -r .result.stdout

curl -s -X DELETE "$API/v1/routines/thumbnail" -H "$AUTH" > /dev/null
curl -s -X DELETE "$API/v1/sandboxes/$SBX" -H "$AUTH" | jq .status
```
**TypeScript**

```ts
import { 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 encodings
const 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 result
const 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}`);
}
```
**Python**

```python
import json
import os
import requests

API = "https://beta-api.b3os.org"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['B3OS_API_KEY']}"

# 1. render: one capture, four encodings
shot = 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 result
sbx = 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']}")
```
**MCP**

On the remote [B3OS MCP server](/sdks/mcp), ask your agent:

```text
Take one screenshot of https://beta.b3os.org/demo/billing/ at 1440x900 as PNG with three variants: WebP, an 800 px
JPEG 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 `variants` in the same request, up to 4 per capture
  ([limits](/render/limits)).
- The file is in an agent's sandbox: run the tool there with `run_code` or `exec` and read back the result.
- The transform should run without you: put the command in a routine as `sandboxes/exec@1` with `sandbox`, or with
  `template` for a sandbox made for the step ([definitions](/routines/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

**Outputs**

```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
}
```
**Command**

```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`.