Volume

Volumes: a persistent filesystem you mount into sandboxes and agents.

vol = noclick.Volume.from_name("datasets", create_if_missing=True)
vol.write("/raw/orders.csv", csv_bytes)
with noclick.Sandbox.create(volumes={"/data": vol}) as sb:
    sb.exec("python train.py --data /data/raw")

Paths start at the volume's root (/raw/orders.csv). Folders are real folders, made as files are written into them. What a sandbox or an agent's turn writes is saved every few seconds while it runs, and all of it when it ends; two writers to one file at once: the last one to finish wins.

class Volume

A persistent filesystem: files by path, in folders, that outlive the sandbox or agent turn that wrote them. Mount it with Sandbox.create(volumes={"/data": vol}) or Agent.from_name(..., volumes=...), or read and write it from your own code.

Attributes
idstr
namestr
size_bytesint | None
file_countint | None
mounted_bylist[dict[str, Any]]

The running sandboxes, the agents and the machines that mount it: kind (sandbox, agent or machine), id, name (an agent's or a machine's) and the mount path.

regionstr | None

Always None for now: volumes aren't placed by region yet (region= is in invite-only beta).

user_idstr | None
created_atAny
console_urlstr | None

This volume in the developer console (None off the hosted API).

createclassmethodasync

noclick.Volume.create(name, *, region=None, user=..., client=None) -> Volume

Create an empty volume called name (1-64 letters, digits and . _ -), unique to its user (a taken name raises noclick.Conflict with existing_id).

Invite-only beta: region (noclick.regions()), where its files are kept. The sandbox provider's volumes take no location yet, so passing one raises noclick.NotAvailable; a sandbox or agent in any region mounts a volume.

Parameters
namestrrequired
regionstr | Noneinvite-only betadefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Volume

The new, empty volume.

Usage

An empty volume:

cache = noclick.Volume.create("build-cache")
print(cache.id, cache.size_bytes)   # ... 0

One per user, for what their sandboxes build:

with noclick.as_user("user_42"):
    work = noclick.Volume.create("workbench")
    print(work.user_id)   # user_42

from_nameclassmethod

noclick.Volume.from_name(
    name, *, create_if_missing=False, region=None, user=..., client=None
) -> Volume

A lazy reference to the volume called name. Acting for a user, that user's own volume is found first, then the account's (every user's sandboxes and agents can mount those). With create_if_missing=True the current (or given) user's is created on first use. Invite-only beta: region (see create).

Parameters
namestrrequired
create_if_missingbooldefault False
regionstr | Noneinvite-only betadefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Volume

A volume that loads (or is created) the first time it's used. Nothing is sent yet.

Usage

Your code is the definition; the first run creates it:

vol = noclick.Volume.from_name("datasets", create_if_missing=True)
vol.write("/raw/orders.csv", b"id,total\no_1,42\n")

A volume per user, for what their sandboxes build:

with noclick.as_user("user_42"):
    work = noclick.Volume.from_name("workbench", create_if_missing=True)
    print(work.listdir("/"))

from_idclassmethod

noclick.Volume.from_id(volume_id, *, client=None, user=...) -> Volume

A lazy reference to a volume by id.

Parameters
volume_idstrrequired
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
Volume

A volume that loads the first time you read a field. Nothing is sent yet.

Usage
vol = noclick.Volume.from_id("4c8e1f0a-6b3d-4a92-9e57-1d2f8b0c7a36")
data = vol.read("/raw/orders.csv")

getclassmethodasync

noclick.Volume.get(volume_id, *, user=..., client=None) -> Volume

Load a volume by id now; an unknown id raises noclick.NotFound.

Parameters
volume_idstrrequired
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Volume

The volume, loaded: its size, how many files it holds, and who mounts it.

Usage
vol = noclick.Volume.get("4c8e1f0a-6b3d-4a92-9e57-1d2f8b0c7a36")
print(vol.name, vol.size_bytes, vol.file_count, vol.mounted_by)

listclassmethodasync

noclick.Volume.list(
    *, limit=None, user_filter=None, user=..., client=None
) -> list[Volume]

The current user's volumes and the account's (or those user_filter covers).

Parameters
limitint | None

Return at most this many.

(default None)
user_filterAny

Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
list[Volume]

Every matching volume, newest first, fetched page by page.

Usage
for vol in noclick.Volume.list():
    print(vol.name, vol.size_bytes, vol.user_id)   # user_id is None for the account's

everyone = noclick.Volume.list(user_filter=noclick.ALL_USERS)   # every user's, plus your account's

list_pageclassmethodasync

noclick.Volume.list_page(
    *, cursor=None, limit=None, user_filter=None, user=..., client=None
) -> Page

One page of volumes; pass next_cursor back as cursor.

Parameters
cursorstr | None

The next_cursor of the page before; omit it for the first page.

(default None)
limitint | None

Return at most this many.

(default None)
user_filterAny

Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Page

A page whose data holds the volumes and whose next_cursor is None on the last page.

Usage
page = noclick.Volume.list_page(limit=50)
while True:
    for vol in page.data:
        print(vol.name, vol.file_count)
    if page.next_cursor is None:
        break
    page = noclick.Volume.list_page(limit=50, cursor=page.next_cursor)

writeasync

vol.write(path, data) -> VolumeEntry

Write data (bytes, text as UTF-8, or a binary file) to path, replacing the file there and making any folders it needs. It's saved at once: a sandbox or agent that mounts the volume after this sees it. One write carries up to 100 MB; make bigger files inside a sandbox that mounts the volume.

Parameters
pathstrrequired
dataDatarequired
Returns
VolumeEntry

The file as written, with its size.

Usage

Put a dataset where your sandboxes will read it:

vol = noclick.Volume.from_name("datasets", create_if_missing=True)
entry = vol.write("/raw/orders.csv", b"id,total\no_1,42\n")
print(entry.path, entry.size)

Upload a file from disk:

vol = noclick.Volume.from_name("datasets")
with open("model.bin", "rb") as f:
    vol.write("/models/model.bin", f)

readasync

vol.read(path) -> bytes

Read the file at path, as the volume last saved it, up to 100 MB; a missing file raises noclick.NotFound, a folder or a bigger file noclick.InvalidRequest.

Parameters
pathstrrequired
Returns
bytes

The file's bytes, exactly as stored; decode text yourself.

Usage
vol = noclick.Volume.from_name("datasets")
report = vol.read("/out/report.csv").decode()
print(report.splitlines()[0])

listdirasync

vol.listdir(path='/', *, recursive=False, limit=None) -> list[VolumeEntry]

What's in the folder at path, in path order: its files and folders, or with recursive=True everything below it. A path that doesn't exist raises noclick.NotFound.

Parameters
pathstrdefault '/'
recursivebooldefault False
limitint | None

Return at most this many.

(default None)
Returns
list[VolumeEntry]

Every entry (or the first limit), fetched page by page.

Usage

One folder, like ls:

vol = noclick.Volume.from_name("datasets")
for entry in vol.listdir("/raw"):
    print(entry.path, entry.type, entry.size)

Every file under a folder:

vol = noclick.Volume.from_name("datasets")
files = [e for e in vol.listdir("/", recursive=True) if e.type == "file"]
print(len(files), sum(e.size or 0 for e in files))

listdir_pageasync

vol.listdir_page(path='/', *, recursive=False, cursor=None, limit=None) -> Page

One page of listdir; pass next_cursor back as cursor.

Parameters
pathstrdefault '/'
recursivebooldefault False
cursorstr | None

The next_cursor of the page before; omit it for the first page.

(default None)
limitint | None

Return at most this many.

(default None)
Returns
Page

A page whose data holds VolumeEntrys and whose next_cursor is None on the last page.

Usage
vol = noclick.Volume.from_name("datasets")
page = vol.listdir_page("/raw", limit=100)
while True:
    for entry in page.data:
        print(entry.path, entry.size)
    if page.next_cursor is None:
        break
    page = vol.listdir_page("/raw", limit=100, cursor=page.next_cursor)

removeasync

vol.remove(path, *, recursive=False) -> None

Delete the file or folder at path. A folder that isn't empty needs recursive=True (otherwise noclick.Conflict); a path that isn't there does nothing.

Parameters
pathstrrequired
recursivebooldefault False
Usage
vol = noclick.Volume.from_name("datasets")
vol.remove("/raw/orders.csv")
vol.remove("/tmp", recursive=True)

copyasync

vol.copy(src, dst) -> VolumeEntry

Copy the file or folder at src to dst in this volume (a folder with everything in it), replacing what dst held.

Parameters
srcstrrequired
dststrrequired
Returns
VolumeEntry

The copy at dst.

Usage
vol = noclick.Volume.from_name("datasets")
vol.copy("/raw/orders.csv", "/archive/2026-09/orders.csv")

exportasync

vol.export() -> Operation

Export every file, so the volume can move anywhere: a zip holding them in their folders (under volumes/<name>/files/), with index.jsonl listing each path, its size and when it last changed.

Returns
Operation

An operation whose wait() gives an ExportBundle: the zip at url, valid 7 days.

Usage
vol = noclick.Volume.from_name("datasets")
bundle = vol.export().wait()
print(bundle.url, bundle.expires_at)

deleteasync

vol.delete() -> TrashItem

Delete the volume and every file in it: it goes to the trash, restorable for 30 days (its files stored, and billed, until it's purged). While a running sandbox, an agent or a machine mounts it (mounted_by), this raises noclick.Conflict: take it out of the agent's or machine's volumes and let the sandboxes end first. A user can't delete the account's volumes (noclick.PermissionDenied).

Returns
TrashItem

Its TrashItem: restore() puts it back.

Usage
noclick.Volume.from_name("build-cache").delete()

VolumeEntry

class VolumeEntry

A file or folder in a volume: its path from the volume's root, its type (file or directory), a file's size in bytes (None for a folder) and when it last changed (updated_at).

Attributes
pathstr
typestr
sizeint | None
updated_atAny
rawdict[str, Any]

The response JSON exactly as the API sent it.