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

# Volumes

> Named persistent storage you attach to any deployment at any mount path. Data outlives the deployments that use it.

A volume is named storage. Create it once, mount it wherever you need it, and it
stays alive regardless of what gets deployed or deleted around it.

```python theme={null}
data = cycls.Volume("training-data")


@cycls.function(volumes={"/data": data})
def crunch(day):
    import pandas as pd
    return pd.read_parquet(f"/data/events-{day}.parquet").sum()
```

Inside the container, `/data` is the volume. Files written there survive calls,
instances and redeploys, and any other deployment mounting `training-data` sees
the same files.

## The mental model

A volume is shared files, not a database. Reads and writes are ordinary
filesystem operations, which has two consequences worth knowing up front:

* Concurrent writes to the same file are last write wins. There is no locking.
  Give concurrent writers distinct paths, such as one file per user or per run,
  and the problem disappears.
* A file written by one deployment becomes visible to others within seconds,
  not instantly.

## Creating and attaching

Referencing a volume creates it. The first deploy that mentions `training-data`
brings it into existence and says so in the deploy output, so a typo shows up as
a new volume rather than a silent empty one. To be explicit first:

```bash theme={null}
cycls volume create training-data
```

`volumes=` maps mount paths to volumes on any decorator:

```python theme={null}
state = cycls.Volume("staging-state")
prod = cycls.Volume("app-data")


@cycls.app(name="staging", volumes={
    "/workspace": state,
    "/prod": prod.read_only(),                # visible, not writable
    "/users": prod.sub_path("users/123"),     # one subtree only
})
def staging(port): ...
```

| Modifier           | Effect                       |
| ------------------ | ---------------------------- |
| `.read_only()`     | mount without write access   |
| `.sub_path("a/b")` | mount only that subdirectory |

## The workspace convention

`/workspace` is where apps and agents keep state: chats, per-user files,
credentials and the key-value store. Agents require a volume there and the
decorator raises without one. Apps need one only if they use `workspace` or `DB`.

```python theme={null}
@cycls.agent(volumes={"/workspace": cycls.Volume("my-agent")})
async def my_agent(context): ...
```

Nothing is created implicitly. Storage is exactly what your source declares, and
renaming a deployment changes nothing about its data, because the volume
reference is the identity.

<Note>
  `cycls run` ignores volumes. Locally your code sees the local filesystem, which
  keeps the development loop fast and offline.
</Note>

## Moving data in and out

The CLI talks to storage directly, so transfers do not proxy through the API and
file size is effectively unlimited.

```bash theme={null}
cycls volume put training-data ./model.bin models/model.bin
cycls volume get training-data outputs/result.parquet .
cycls volume ls training-data              # contents
cycls volume ls training-data models/      # contents under a prefix
cycls volume rm training-data models/old.bin
cycls volume ls                            # every volume, with attachments
```

This is how you seed a dataset before anything is deployed, and how you pull
results out without writing an endpoint.

## Sharing across deployments

Attachment is by name, so a family of deployments can work on one dataset.

```python theme={null}
shared = cycls.Volume("pipeline")


@cycls.function(volumes={"/pipe": shared})
def extract(day): ...        # writes /pipe/raw/<day>.json


@cycls.function(volumes={"/pipe": shared})
def transform(day): ...      # reads raw/, writes clean/


@cycls.app(name="dashboard", volumes={"/pipe": shared.read_only()})
def dashboard(port): ...     # serves clean/, cannot corrupt it
```

One volume, three deployments, no copying. Put a [schedule](/build/cron) on the
producer and the pipeline runs itself. For a worked version of this with Parquet
and DuckDB, see [Build a data warehouse](/guides/duckdb-warehouse).

## Lifecycle

* `cycls rm <deployment>` detaches volumes and leaves the data alone.
  Redeploying the same name reattaches them with files intact.
* `cycls volume delete <name>` is the only way to delete data. It refuses while
  any deployment has the volume attached, and names them in the error.
* Deleted volumes stay recoverable for seven days.

## Structured state on a volume

For per-user JSON rather than files, `cycls.DB` is a small key-value store that
writes to the workspace volume.

```python theme={null}
db = cycls.DB(context.workspace)

await db.put("usage/2026-09", {"count": 12})
entry = await db.get("usage/2026-09", {"count": 0})
async for key, value in db.items(prefix="usage/"):
    ...
await db.delete("usage/2026-08")
```

| Method                             | Meaning                                         |
| ---------------------------------- | ----------------------------------------------- |
| `await db.get(key, default=None)`  | read one key                                    |
| `await db.put(key, value)`         | write one key, atomic per key                   |
| `await db.delete(key)`             | remove a key, trailing slash clears a namespace |
| `db.items(prefix=, glob=, limit=)` | async iterator of key and value                 |
| `db.keys(prefix=)`                 | keys only                                       |
| `db.scan(prefix=)`                 | one round trip listing                          |

## Next

<Card title="Cron" icon="clock" href="/build/cron">
  Fire a deployed function on a schedule.
</Card>
