Mountable

Notebooks

Keep datasets, feature caches and checkpoints in one Mountable folder. The files stay when the kernel restarts. The same setup runs in Kaggle, Colab and Deepnote, and in Hugging Face Spaces and Marimo.

Mountable is a persistent filesystem for notebooks and AI agents. A notebook saves its files to one Mountable folder with the Python SDK. The files stay when the kernel restarts, the session times out, or you open the same folder from another notebook service.

Two runnable starters live in examples/notebooks in the public writeitai/mountable repository:

StarterWhat it showsOpen it
mountable_quickstart.ipynbA dataset saved once, a feature cache, and training that resumes from its last checkpointColab, Kaggle, Deepnote
mountable_marimo.pyExploring the CSV files in the folder, saving a summary for the next job, and a webhook that tells that jobmolab, or uvx marimo edit --sandbox mountable_marimo.py

Set up once

  1. In the console, create a filesystem and copy its ID.
  2. Create an API key under API keys. A key limited to that filesystem is the usual choice for a notebook.
  3. Add both values as secrets named MOUNTABLE_API_KEY and MOUNTABLE_FS_ID. Where they go depends on the platform:
PlatformWhere the secrets go
Google ColabSecrets, in the key icon in the left sidebar
Kaggle NotebooksAdd-ons → Secrets
DeepnoteEnvironment variables
Hugging Face SpacesSpace secrets
MarimoEnvironment variables, set before you start

Keep the API key in the platform's secret store. Do not paste it into a cell, a file in the folder, or a shared notebook.

Connect from any notebook

Install the SDK and open the folder. In Kaggle, Colab and Deepnote:

%pip install -q mountable-sdk

In a Gradio or Streamlit Space, add mountable-sdk to requirements.txt. Hugging Face installs the packages in that file for you. In a Docker Space, install the package in the image.

Space secrets reach the app as environment variables, so the secret() helper below still applies.

In Marimo, open the starter with uvx marimo edit --sandbox mountable_marimo.py. The starter declares mountable-sdk in its script dependencies. To add Mountable to another Marimo notebook, add the package to its script dependencies or the editor's package panel.

The connect code is then the same on every platform. The secret() helper below is copied from the quickstart's connect cell.

It reads the environment first. It then reads Colab Secrets and Kaggle Secrets. The notebook needs no change when you move it between those platforms:

import os
 
from mountable_sdk import Mountable
 
 
def secret(name):
    """Environment variables, then Colab Secrets, then Kaggle Secrets."""
    if os.environ.get(name):
        return os.environ[name]
    try:
        from google.colab import userdata
 
        return userdata.get(name)
    except ImportError:
        pass
    try:
        from kaggle_secrets import UserSecretsClient
 
        return UserSecretsClient().get_secret(name)
    except ImportError:
        return None
 
 
FS_ID = secret("MOUNTABLE_FS_ID")
if not FS_ID:
    raise ValueError("Add your filesystem ID as the secret MOUNTABLE_FS_ID")
client = Mountable(api_key=secret("MOUNTABLE_API_KEY"))
fs = client.filesystem(FS_ID, mode="rw")
 
fs.write("datasets/notes.txt", "saved from this notebook\n")
print(fs.read("datasets/notes.txt").data.decode())

In Colab, turn on Notebook access for MOUNTABLE_API_KEY and MOUNTABLE_FS_ID in the Secrets panel (the key icon) before you run the cell.

In Kaggle, tick the checkbox beside MOUNTABLE_API_KEY and MOUNTABLE_FS_ID under Add-ons → Secrets. The cell then loads them with UserSecretsClient.

In Deepnote, add an Environment variables integration from Integrations in the right-hand sidebar, then connect that integration to the project. The cell then reads both secrets from the environment.

Open the same filesystem with mode="ro" from another notebook to read results without being able to change them. The SDK reference, conditional writes, and listings are in Python and TypeScript SDKs.

Save once, resume after a restart

Write each stage's output to the folder and read it back before computing it again. A restart then costs a read, not a full rerun:

import json
 
from mountable_sdk import MountableError
 
 
def read_or_none(path):
    """The file's bytes, or None if it does not exist yet."""
    try:
        return fs.read(path).data
    except MountableError as e:
        if e.code == "not_found":
            return None
        raise
 
 
saved = read_or_none("checkpoints/latest.json")
state = json.loads(saved) if saved else {"epoch": 0}
print(f"Starting at epoch {state['epoch']}")
# Train from state, then save each epoch the same way:
fs.write("checkpoints/latest.json", json.dumps(state))

The quickstart uses this pattern three times: for the dataset in datasets/, the feature cache in cache/, and the checkpoint in checkpoints/. Run it, restart the kernel, and run it again. The second run reads the files it already made and resumes training from the last checkpoint.

Mount the same folder where FUSE is available

On a machine with /dev/fuse, such as your own Linux box or a GPU server, you can also mount the same folder. Libraries then read and write it with ordinary file paths.

This cell installs the CLI, creates a one-time ticket, and starts the mount with MOUNTABLE_API_KEY removed from its environment. It writes only the ticket to the mount's stdin. Wait for the mounted event before using the folder.

When you are done, run mountable unmount on that folder. The quickstart's mount cell does these steps, then prints the files it finds.

import json
import os
import subprocess
import sys
import sysconfig
 
if os.path.exists("/dev/fuse"):
    subprocess.run(
        [sys.executable, "-m", "pip", "install", "-q", "mountable"],
        check=True,
    )
    cli = os.path.join(sysconfig.get_path("scripts"), "mountable")
    MOUNT = os.path.expanduser("~/mountable")
    os.makedirs(MOUNT, exist_ok=True)
    ticket = client.create_mount_ticket(FS_ID, mode="rw")
    mount = subprocess.Popen(
        [cli, "mount", "--ticket-stdin", "--json", MOUNT],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        text=True,
        env={k: v for k, v in os.environ.items() if k != "MOUNTABLE_API_KEY"},
    )
    mount.stdin.write(ticket.ticket + "\n")
    mount.stdin.close()
    for line in mount.stdout:
        event = json.loads(line)
        if event["event"] != "mounted":
            raise RuntimeError(f"mount ended: {event}")
        break

Notebooks without /dev/fuse do not need the mount. The SDK cells above work without it. The full ticket rules are in Mount from a sandbox.

Tell the next job when a file lands

The Marimo starter saves its summary under artifacts/. It then shows how to create a signed file-change webhook on the /artifacts prefix. Set MOUNTABLE_ORG_ID (from mountable orgs list) and MOUNTABLE_WEBHOOK_URL, then click Create the webhook once.

After you create it, the webhook calls your next job's HTTPS endpoint for every file change under /artifacts. That covers a file created, written, renamed or deleted. A notebook, a mount, or your backend can cause the change.

The starter links to the signature check your endpoint runs.

Next steps