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:
| Starter | What it shows | Open it |
|---|---|---|
mountable_quickstart.ipynb | A dataset saved once, a feature cache, and training that resumes from its last checkpoint | Colab, Kaggle, Deepnote |
mountable_marimo.py | Exploring the CSV files in the folder, saving a summary for the next job, and a webhook that tells that job | molab, or uvx marimo edit --sandbox mountable_marimo.py |
Set up once
- In the console, create a filesystem and copy its ID.
- Create an API key under API keys. A key limited to that filesystem is the usual choice for a notebook.
- Add both values as secrets named
MOUNTABLE_API_KEYandMOUNTABLE_FS_ID. Where they go depends on the platform:
| Platform | Where the secrets go |
|---|---|
| Google Colab | Secrets, in the key icon in the left sidebar |
| Kaggle Notebooks | Add-ons → Secrets |
| Deepnote | Environment variables |
| Hugging Face Spaces | Space secrets |
| Marimo | Environment 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-sdkIn 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}")
breakNotebooks 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
- Python and TypeScript SDKs: read, write, list and follow a filesystem's files. Create mount tickets.
- Webhooks: react when files change, without polling.
- Limits per end user: give each end user their own filesystem with a hard monthly spend cap.