# Run Jupyter notebooks in a sandbox for an AI agent Upload the .ipynb, run it with papermill or nbconvert --execute inside a microVM, and download the executed notebook and its outputs. **On Runtime, a notebook runs in its own Firecracker microVM with pandas, NumPy and matplotlib already installed**, so a notebook an agent wrote, or one a user uploaded, executes away from your servers and your keys. The sandbox bills the CPU the kernel uses: $0.03125 an hour for 2 vCPU and 4 GiB while cells wait, $0.08 with both CPUs busy ([pricing](/docs/pricing)). On 25 September 2026 PyPI's current releases were JupyterLab 4.6.4, Notebook 7.6.3, nbconvert 7.17.1, papermill 2.7.0 and ipykernel 7.3.0. ## Three ways to run notebook code | You have | Use | Needs an install | | ------------------------------------------ | ------------------------------------------------------------------- | ------------------------- | | Cells an agent writes one at a time | The sandbox's [code interpreter](/docs/javascript#code-interpreter) | No | | A whole `.ipynb` to run, maybe with inputs | papermill or `jupyter nbconvert --execute` | `pip install` one package | | A person who wants to open the notebook | JupyterLab behind a private preview | `pip install jupyterlab` | The interpreter keeps variables between calls and returns charts as PNG and data frames as tables, in Python, JavaScript, TypeScript, R, Java, Bash or Go. The rest of this page is the other two rows. ## Execute a notebook with parameters papermill runs every cell and writes a new notebook with the outputs. Values passed with `-p` go into a cell it inserts after the one tagged `parameters`. ```ts check import { Sandbox } from "withruntime"; await using sbx = await Sandbox.create({ timeoutSeconds: 1800 }); await sbx.exec("pip install --quiet papermill==2.7.0 nbconvert==7.17.1 ipykernel==7.3.0", { check: true, timeoutMs: 300_000, }); await sbx.files.upload("./report.ipynb", "/workspace/report.ipynb"); await sbx.files.upload("./sales.csv", "/workspace/sales.csv"); const run = await sbx.exec( "mkdir -p out && papermill report.ipynb out/report.ipynb -p region west -p year 2026", { timeoutMs: 1_200_000, onStderr: (text) => process.stderr.write(text) }, ); await sbx.exec("jupyter nbconvert --to html out/report.ipynb", { check: true, timeoutMs: 120_000 }); await sbx.files.download("/workspace/out", "./out"); process.exitCode = run.exitCode ?? 1; ``` ```python check import sys from withruntime import Sandbox with Sandbox.create(timeout_seconds=1800) as sbx: sbx.exec("pip install --quiet papermill==2.7.0 nbconvert==7.17.1 ipykernel==7.3.0", check=True, timeout_ms=300_000) sbx.files.upload("report.ipynb", "/workspace/report.ipynb") sbx.files.upload("sales.csv", "/workspace/sales.csv") run = sbx.exec("mkdir -p out && papermill report.ipynb out/report.ipynb -p region west -p year 2026", timeout_ms=1_200_000, on_stderr=sys.stderr.write) sbx.exec("jupyter nbconvert --to html out/report.ipynb", check=True, timeout_ms=120_000) sbx.files.download("/workspace/out", "out") print("exit", run.exit_code) ``` The exit code says whether every cell ran. The `out` folder comes back in one call: the executed `.ipynb`, its HTML rendering, and any file a cell saved there. Progress streams to your terminal while it runs. For a notebook with no parameters, nbconvert alone executes it: ```bash no-run jupyter nbconvert --to notebook --execute report.ipynb --output report.done.ipynb \ --ExecutePreprocessor.timeout=600 ``` nbconvert's documentation sets its per-cell timeout at 30 seconds by default; `-1` removes it. Set the sandbox's own `timeoutMs` above the whole run either way, since a command's default is 60 seconds. ## Open JupyterLab in your browser Start the server with `spawn` so it keeps running, and share its port as a preview. A preview is private: a browser needs its one-time link, and a script needs the token header. That link is the lock here, so Jupyter's own token is turned off. ```ts check import { Sandbox } from "withruntime"; await using sbx = await Sandbox.create({ timeoutSeconds: 3600, idlePauseSeconds: 900 }); await sbx.exec("pip install --quiet jupyterlab==4.6.4", { check: true, timeoutMs: 300_000 }); await sbx.spawn( "jupyter lab --no-browser --ip=127.0.0.1 --port=8888 " + "--IdentityProvider.token='' --ServerApp.allow_remote_access=True", ); const preview = await sbx.previews.create(8888); console.log("open:", preview.urlWithToken); ``` ```python check from withruntime import Sandbox sbx = Sandbox.create(timeout_seconds=3600, idle_pause_seconds=900) sbx.exec("pip install --quiet jupyterlab==4.6.4", check=True, timeout_ms=300_000) sbx.spawn("jupyter lab --no-browser --ip=127.0.0.1 --port=8888 " "--IdentityProvider.token='' --ServerApp.allow_remote_access=True") preview = sbx.previews.create(8888) print("open:", preview["urlWithToken"]) ``` - `allow_remote_access` lets Jupyter answer requests whose `Host` header is the preview's address rather than `localhost`, per Jupyter Server's options. - Kernels talk over WebSockets, which previews carry. - With `idlePauseSeconds`, the sandbox pauses after 15 quiet minutes. A pause keeps memory and processes, so the kernel and its variables are there when you come back, and the next visit wakes it with a short "Waking up" page ([pause and resume](/how-to/pause-and-resume-a-sandbox)). To use the notebook from VS Code instead, connect over SSH to `.runtime` and run Jupyter there ([SSH and editors](/docs/editors)). ## Start every sandbox with Jupyter ready A [custom image](/docs/images) installs the packages once: ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); await runtime.images.build({ name: "notebooks", recipe: { pip: ["jupyterlab==4.6.4", "papermill==2.7.0", "scikit-learn"] }, }); await using sbx = await runtime.sandboxes.create({ image: "notebooks" }); ``` ```python check from withruntime import Runtime runtime = Runtime() runtime.images.build(name="notebooks", recipe={"pip": ["jupyterlab==4.6.4", "papermill==2.7.0", "scikit-learn"]}) sbx = runtime.sandboxes.create(image="notebooks") ``` Building is free. The free trial stores three images free; after that an image is charged on its size. ## Keep an untrusted notebook contained - **No internet:** create the sandbox with `network: { internet: false }` after packages are installed, or baked into an image, and a notebook cannot send data anywhere ([turn off sandbox internet](/how-to/turn-off-sandbox-internet)). - **No keys inside:** pass what a notebook needs as `env` on the command, or store API keys as secrets the sandbox never sees ([security](/docs/security#secrets-sandboxes-never-see)). - **Memory:** a sandbox gets the memory you ask for with `memoryMiB`; the free trial allows up to 4 GiB. - **Disk:** the default 4 GiB disk had about 2.5 GiB free on 24 September 2026. Large datasets want `diskMiB`, or a [volume](/docs/javascript#volumes) that outlives the sandbox. For an agent that answers questions about uploaded data, see [a data analysis agent](/use-cases/data-analysis-agent); for charts and tables in a chat product, see [a code interpreter for chatbots](/use-cases/code-interpreter-for-chatbots). ## Sources - papermill, execute and parameterize, https://papermill.readthedocs.io/en/latest/usage-execute.html and https://papermill.readthedocs.io/en/latest/usage-parameterize.html, read 25 September 2026 - nbconvert, executing notebooks, https://nbconvert.readthedocs.io/en/latest/execute_api.html, read 25 September 2026 - Jupyter Server configuration, https://jupyter-server.readthedocs.io/en/latest/other/full-config.html, read 25 September 2026 - PyPI project pages for jupyterlab, notebook, nbconvert, papermill and ipykernel, https://pypi.org/, read 25 September 2026 Facts on this page were checked on 25 September 2026.