Volumes
A persistent filesystem you mount into sandboxes and agents, for datasets, caches and build output.
A volume is a filesystem that outlives the machine that used it. Mount one into a sandbox or an agent at a path such as /data, and the code there reads and writes ordinary files: a dataset to train on, a cache that makes the next build fast, output that the next run picks up. Your own code can read and write it too, without starting anything.
Pick a volume when code needs a folder. For files your app hands out by URL, use a bucket; for small shared values such as counters and locks, a dict.
An app doesn't bind a volume. Its code runs at the edge or in a container on Google Cloud Run, and neither can mount the sandbox provider's volumes, so an app could only reach one file by file over the API, which a bucket does better: it serves its files by URL, and an app's preview gets an overlay of it, its own writes over production's files. Give an app a bucket; an agent keeps its files in one too (Storage).
Create a volume
import noclick
vol = noclick.Volume.from_name("datasets", create_if_missing=True)from_name with create_if_missing=True creates the volume on first use and finds it after that. create makes one outright and raises noclick.Conflict if the name is taken. A name is 1 to 64 letters, digits and . _ -. A new volume is empty.
A name is unique for each user, and for your account. Created inside as_user, a volume belongs to that user and bills to it. A user's sandboxes and agents can mount that user's volumes and your account's, never another user's; acting for a user, from_name finds the user's own volume first, then the account's. See Users.
Volumes aren't placed by region yet: they live in the sandbox provider's volume storage, which takes no location, so region= is in invite-only beta, and without access passing it raises noclick.NotAvailable. A sandbox or agent in any region mounts any volume.
Mount it in a sandbox
Pass volumes when you create the sandbox, mapping a path to a volume (a Volume, its id or its name):
import noclick
data = noclick.Volume.from_name("datasets", create_if_missing=True)
with noclick.Sandbox.create(volumes={"/data": data}, timeout_s=1800) as sb:
sb.exec("python train.py --data /data/raw --out /data/models")The volume is mounted read-write. A mount path is absolute, such as /data or /workspace/; it can't be /, /workspace itself, or a system folder (/bin, /boot, /dev, /etc, /lib, /lib64, /proc, /run, /sbin, /sys, /usr, /var), and one mount can't sit inside another. A sandbox mounts up to 5 volumes. Its volumes lists them, {path: volume id}.
A cache is the same idea: mount it where your tools keep one, and the next sandbox starts warm.
cache = noclick.Volume.from_name("pip-cache", create_if_missing=True)
with noclick.Sandbox.create(volumes={"/root/.cache/pip": cache}, network=True) as sb:
sb.exec("pip install -r requirements.txt")Mount it in an agent
Give an agent volumes and its shell sees the files at that path in every conversation:
import noclick
analyst = noclick.Agent.from_name(
"analyst",
create_if_missing=True,
instructions="You answer questions about our orders. The data is in /data; save charts to /data/charts.",
volumes={"/data": noclick.Volume.from_name("datasets", create_if_missing=True)},
)
reply = analyst.message("Which venue had the most bookings last month?")volumes is part of the agent's definition: with create_if_missing=True, from_name keeps it in step with your code like the rest, and a change reaches its next turn. Tell the agent in its instructions what's there, since it can't guess. An agent's mount can't be its conversation's workspace (/workspace or anything in it) or its home folder (/root), where its harness keeps its own files. A user's agent mounts that user's volumes and your account's.
An account agent is shared by every user who messages it, and so is its volume: every conversation, with anyone, reads and writes the same files. Keep a user's files in their own place: a user's own agent with that user's own volume, or the agent's per-conversation workspace.
Read and write from your code
A path starts at the volume's root. write stores bytes, text or an open binary file, replacing what was there and making any folders it needs; read gives the bytes back:
import noclick
vol = noclick.Volume.from_name("datasets", create_if_missing=True)
vol.write("/raw/orders.csv", b"id,total\no_1,42\n")
with open("venues.json", "rb") as f:
vol.write("/raw/venues.json", f)
report = vol.read("/out/report.csv").decode()One write or read carries up to 100 MB. For bigger files, make them inside a sandbox that mounts the volume. read raises noclick.NotFound for a path that isn't there.
List, copy and remove files
listdir shows what's in a folder, like ls; recursive=True walks everything below it. Each entry has a path, a type (file or directory), a file's size and updated_at:
for entry in vol.listdir("/raw"):
print(entry.path, entry.type, entry.size)
files = [e for e in vol.listdir("/", recursive=True) if e.type == "file"]listdir fetches every page for you (pass limit to stop early); listdir_page gives one page at a time. Copy and remove work on files and whole folders:
vol.copy("/raw/orders.csv", "/archive/2026-09/orders.csv")
vol.remove("/archive/2026-09/orders.csv")
vol.remove("/tmp", recursive=True)Removing a folder that still holds files needs recursive=True, or it raises noclick.Conflict. Removing a path that isn't there does nothing.
When writes are saved
- A sandbox sees the volume as it was when the sandbox started. What it writes is saved to the volume in the background every few seconds while it runs, and all of it when it ends, whether you terminate it or it runs out of time: your code can read a running sandbox's writes within seconds, but only once it has ended is everything it wrote certain to be there.
- An
sdkagent's shell works the same way per turn: each turn starts from the volume as last saved, its writes are saved every few seconds while the turn runs, and all of them when it ends. - An agent on a coding harness keeps its sandbox up between turns: it sees the volume as it was when the sandbox started, its writes are saved every few seconds, and all of them when the sandbox stops, a few minutes after its last turn.
write,copyandremovefrom your code are saved at once.- Changes saved after a sandbox started don't reach it; start a new one to see them.
- Two writers to the same file at once: the one that finishes last wins, for that file. Writers that touch different files never get in each other's way.
So a volume suits a job that writes its own output folder, and a pipeline where one step finishes before the next starts. For many writers updating shared values, use a dict or a database instead.
See who uses it
A volume's mounted_by lists the agents and machines that mount it and the sandboxes running with it, each with its mount path:
import noclick
vol = noclick.Volume.from_name("datasets")
print(vol.size_bytes, vol.file_count)
for mount in vol.mounted_by:
print(mount["kind"], mount["name"] or mount["id"], mount["path"]) # agent assistant /dataExport and delete
export is an operation that gives an export bundle: a zip, at a link valid 7 days, of every file in its folders under volumes/<name>/files/, with index.jsonl listing each path, its size and when it last changed. What a running sandbox hasn't saved yet isn't in it.
bundle = vol.export().wait()
print(bundle.url)delete removes the volume and every file in it. While an agent or a machine mounts it, or a running sandbox does, it raises noclick.Conflict: take it out of the agent's or machine's volumes and let the sandboxes end first. Only the account deletes the account's volumes; a user's call raises noclick.PermissionDenied.
noclick.Volume.from_name("build-cache").delete()Limits and billing
| Limit | Value |
|---|---|
| One volume | 100 GB (your write and copy stop there; what a sandbox writes past it is kept and billed) |
One write or read | 100 MB |
| Volumes per sandbox or agent | 5 |
A volume is billed for what it stores, $0.09 per GB-month, charged by the hour. NoClick measures it within a minute or so of a change (your write, copy or remove, or a sandbox or turn that mounted it ending) and at least every hour; size_bytes and file_count are that measurement. Reading and writing it cost nothing extra, from your code or a mount. Spend counts toward the cap of the user the volume belongs to.
Errors
| Error | When |
|---|---|
NotFound | no file or folder at that path, or no volume by that name or id for the user the call acts for |
Conflict | the name is taken; remove on a folder with files and no recursive=True; a write to a folder's path; delete while it's mounted |
PermissionDenied | a user's call deleting the account's volume |
InvalidRequest | read on a folder or a file over 100 MB, a write over 100 MB, a mount path that isn't absolute, is reserved or sits inside another, more than 5 mounts |
NotAvailable | region=, until volumes are placed by region |
QuotaExceeded | a write would take the volume past 100 GB |
Every method is in the Volume reference: write, read, listdir, copy and the rest. The volumes option is on Sandbox.create and Agent.from_name.