Capability
Independent storage volumes
A volume is independent, dynamically-provisioned block storage with its own lifecycle — create it once, mount it into any number of sandboxes you create afterward, and it survives past any single sandbox session being destroyed. This is a different thing from the workspace/uploads/outputs storage every sandbox already gets automatically: those are per-session and synced to object storage; a volume is a standing resource you explicitly create, mount, and eventually delete.
Opt-in, and the underlying provisioner isn't wired to a live cluster yet
This capability is off by default (BOXKITE_VOLUMES_ENABLED=false) — every /v1/volumes route 404s until an operator explicitly opts in. The create/provision/delete dispatch logic is implemented and covered by tests, but real Kubernetes PersistentVolumeClaim provisioning (K8sVolumeProvisioner.provision) does not yet run against a live cluster — same status as the declarative builder's own KanikoJobBuildRunner.run_build.
Read docs/EXTERNAL-STORAGE-MOUNTING-DESIGN.md's Volume addendum before relying on this. Don't confuse it with that same document's FUSE object-storage mounting section — that's a different, unrelated (and much less finished) capability for mounting an existing S3/GCS/Azure bucket.
Create a volume
size_gb is required. The build is always asynchronous — the call below returns immediately with a queued volume, not a ready one.
volume = client.create_volume(label="model-weights", size_gb=20)
print(volume["id"], volume["status"]) # "vol_...", "queued"Poll status, then mount it into a sandbox
status progresses queued → creating → ready (or failed at any stage). Once ready, pass a {volume_id: mount_path} mapping into sandbox creation. A volume that isn't owned by your account or isn't yet ready404s the sandbox-create call rather than silently omitting the mount — you can never mistakenly believe a volume is attached when it isn't. mount_path must be an absolute path outside the sandbox's existing typed roots (/workspace, /mnt/*, /tmp).
volume = client.get_volume(volume["id"])
if volume["status"] == "ready":
sandbox = client.create_sandbox(
label="training-job",
volume_mounts={volume["id"]: "/data"},
)List and delete
Deleting a volume removes the control-plane's bookkeeping row and its underlying PersistentVolumeClaim — it does not retroactively unmount it from any already-running sandbox session; Kubernetes itself keeps a bound PVC alive until the pod using it is gone.
client.list_volumes()
client.delete_volume(volume["id"])