diff --git a/EXAMPLES.md b/EXAMPLES.md
index a036aa01d..562ad8b21 100644
--- a/EXAMPLES.md
+++ b/EXAMPLES.md
@@ -10,6 +10,7 @@ Runnable examples live in [`examples/`](./examples).
- [Blueprint with Build Context](#blueprint-with-build-context)
- [Devbox From Blueprint (Run Command, Shutdown)](#devbox-from-blueprint-lifecycle)
- [Devbox Snapshot and Resume](#devbox-snapshot-resume)
+- [Devbox Snapshots (Suspend, Resume, Restore, Delete)](#devbox-snapshots)
- [Devbox Tunnel (HTTP Server Access)](#devbox-tunnel)
- [MCP Hub + Claude Code + GitHub](#mcp-github-tools)
- [Secrets with Devbox (Create, Inject, Verify, Delete)](#secrets-with-devbox)
@@ -106,6 +107,37 @@ uv run pytest -m smoketest tests/smoketests/examples/
**Source:** [`examples/devbox_snapshot_resume.py`](./examples/devbox_snapshot_resume.py)
+
+## Devbox Snapshots (Suspend, Resume, Restore, Delete)
+
+**Use case:** Upload a file to a devbox, preserve it across suspend and resume, create a disk snapshot, restore multiple devboxes from that snapshot, mutate each copy independently, and delete the snapshot when finished.
+
+**Tags:** `devbox`, `snapshot`, `suspend`, `resume`, `files`, `cleanup`
+
+### Workflow
+- Create a source devbox
+- Upload a file and mutate it into a shared baseline
+- Suspend and resume the source devbox
+- Create a disk snapshot from the resumed devbox
+- Restore two additional devboxes from the same snapshot baseline
+- Mutate the same file differently in each devbox to prove isolation
+- Shutdown the devboxes and delete the snapshot
+
+### Prerequisites
+- `RUNLOOP_API_KEY`
+
+### Run
+```sh
+uv run python -m examples.devbox_snapshots
+```
+
+### Test
+```sh
+uv run pytest -m smoketest tests/smoketests/examples/
+```
+
+**Source:** [`examples/devbox_snapshots.py`](./examples/devbox_snapshots.py)
+
## Devbox Tunnel (HTTP Server Access)
diff --git a/README.md b/README.md
index 09aeb9016..9bdf991b4 100644
--- a/README.md
+++ b/README.md
@@ -91,6 +91,7 @@ Functionality between the synchronous and asynchronous clients is otherwise iden
## Examples
Workflow-oriented runnable examples are documented in [`EXAMPLES.md`](./EXAMPLES.md).
+For a suspend/resume plus shared snapshot restore workflow, see [`examples/devbox_snapshots.py`](./examples/devbox_snapshots.py).
`EXAMPLES.md` is generated from metadata in `examples/*.py` and should not be edited manually.
Regenerate it with:
diff --git a/examples/devbox_snapshots.py b/examples/devbox_snapshots.py
new file mode 100644
index 000000000..bcb9c4539
--- /dev/null
+++ b/examples/devbox_snapshots.py
@@ -0,0 +1,245 @@
+#!/usr/bin/env -S uv run python
+"""
+---
+title: Devbox Snapshots (Suspend, Resume, Restore, Delete)
+slug: devbox-snapshots
+use_case: Upload a file to a devbox, preserve it across suspend and resume, create a disk snapshot, restore multiple devboxes from that snapshot, mutate each copy independently, and delete the snapshot when finished.
+workflow:
+ - Create a source devbox
+ - Upload a file and mutate it into a shared baseline
+ - Suspend and resume the source devbox
+ - Create a disk snapshot from the resumed devbox
+ - Restore two additional devboxes from the same snapshot baseline
+ - Mutate the same file differently in each devbox to prove isolation
+ - Shutdown the devboxes and delete the snapshot
+tags:
+ - devbox
+ - snapshot
+ - suspend
+ - resume
+ - files
+ - cleanup
+prerequisites:
+ - RUNLOOP_API_KEY
+run: uv run python -m examples.devbox_snapshots
+test: uv run pytest -m smoketest tests/smoketests/examples/
+---
+"""
+
+from __future__ import annotations
+
+import tempfile
+from pathlib import Path
+
+from runloop_api_client import AsyncRunloopSDK
+from runloop_api_client.lib.polling import PollingConfig
+from runloop_api_client.sdk.async_devbox import AsyncDevbox
+from runloop_api_client.sdk.async_snapshot import AsyncSnapshot
+
+from ._harness import run_as_cli, unique_name, wrap_recipe
+from .example_types import ExampleCheck, RecipeOutput, RecipeContext
+
+FILE_PATH = "/tmp/snapshot-demo.txt"
+POLLING_CONFIG = PollingConfig(timeout_seconds=120.0, interval_seconds=5.0)
+
+
+async def read_file_contents(devbox: AsyncDevbox) -> str:
+ """Read the shared demo file from a devbox."""
+ return await devbox.file.read(file_path=FILE_PATH)
+
+
+async def recipe(ctx: RecipeContext) -> RecipeOutput:
+ """Demonstrate suspend/resume and shared snapshot restoration with isolated mutations."""
+ cleanup = ctx.cleanup
+ sdk = AsyncRunloopSDK()
+
+ resources_created: list[str] = []
+
+ source_devbox: AsyncDevbox | None = None
+ clone_a: AsyncDevbox | None = None
+ clone_b: AsyncDevbox | None = None
+ snapshot: AsyncSnapshot | None = None
+ local_file_path: Path | None = None
+
+ source_needs_cleanup = False
+ clone_a_needs_cleanup = False
+ clone_b_needs_cleanup = False
+ snapshot_needs_cleanup = False
+
+ async def cleanup_source() -> None:
+ if source_needs_cleanup and source_devbox is not None:
+ await source_devbox.shutdown()
+
+ async def cleanup_clone_a() -> None:
+ if clone_a_needs_cleanup and clone_a is not None:
+ await clone_a.shutdown()
+
+ async def cleanup_clone_b() -> None:
+ if clone_b_needs_cleanup and clone_b is not None:
+ await clone_b.shutdown()
+
+ async def cleanup_snapshot() -> None:
+ if snapshot_needs_cleanup and snapshot is not None:
+ await snapshot.delete()
+
+ def cleanup_local_file() -> None:
+ if local_file_path is not None:
+ local_file_path.unlink(missing_ok=True)
+
+ # Cleanup runs in LIFO order, so register these handlers up front in reverse
+ # dependency order: clones, then source devbox, then snapshot, then local file.
+ cleanup.add("local-file:snapshot-demo", cleanup_local_file)
+ cleanup.add("snapshot:baseline", cleanup_snapshot)
+ cleanup.add("devbox:source", cleanup_source)
+ cleanup.add("devbox:clone-a", cleanup_clone_a)
+ cleanup.add("devbox:clone-b", cleanup_clone_b)
+
+ uploaded_contents = "uploaded-from-local-file"
+ baseline_contents = "baseline-after-upload-and-mutation"
+ source_contents = "source-devbox-after-isolated-mutation"
+ clone_a_contents = "clone-a-after-isolated-mutation"
+ clone_b_contents = "clone-b-after-isolated-mutation"
+
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") as tmp_file:
+ tmp_file.write(uploaded_contents)
+ local_file_path = Path(tmp_file.name)
+
+ source_devbox = await sdk.devbox.create(
+ name=unique_name("snapshot-source"),
+ launch_parameters={
+ "resource_size_request": "X_SMALL",
+ },
+ )
+ source_needs_cleanup = True
+ resources_created.append(f"devbox:{source_devbox.id}")
+
+ await source_devbox.file.upload(path=FILE_PATH, file=local_file_path)
+ uploaded_readback = await read_file_contents(source_devbox)
+
+ await source_devbox.file.write(file_path=FILE_PATH, contents=baseline_contents)
+
+ suspend_response = await source_devbox.suspend()
+ suspended_info = suspend_response
+ if suspended_info.status != "suspended":
+ suspended_info = await source_devbox.await_suspended(polling_config=POLLING_CONFIG)
+
+ resumed_info = await source_devbox.resume(polling_config=POLLING_CONFIG)
+ resumed_readback = await read_file_contents(source_devbox)
+
+ snapshot = await source_devbox.snapshot_disk(
+ name=unique_name("snapshot-baseline"),
+ commit_message="Capture the shared baseline after suspend and resume.",
+ polling_config=POLLING_CONFIG,
+ )
+ snapshot_needs_cleanup = True
+ resources_created.append(f"snapshot:{snapshot.id}")
+
+ clone_a = await snapshot.create_devbox(
+ name=unique_name("snapshot-clone-a"),
+ launch_parameters={
+ "resource_size_request": "X_SMALL",
+ },
+ )
+ clone_a_needs_cleanup = True
+ resources_created.append(f"devbox:{clone_a.id}")
+
+ clone_b = await sdk.devbox.create_from_snapshot(
+ snapshot.id,
+ name=unique_name("snapshot-clone-b"),
+ launch_parameters={
+ "resource_size_request": "X_SMALL",
+ },
+ )
+ clone_b_needs_cleanup = True
+ resources_created.append(f"devbox:{clone_b.id}")
+
+ clone_a_baseline_readback = await read_file_contents(clone_a)
+ clone_b_baseline_readback = await read_file_contents(clone_b)
+
+ await source_devbox.file.write(file_path=FILE_PATH, contents=source_contents)
+ await clone_a.file.write(file_path=FILE_PATH, contents=clone_a_contents)
+ await clone_b.file.write(file_path=FILE_PATH, contents=clone_b_contents)
+
+ source_isolated_readback = await read_file_contents(source_devbox)
+ clone_a_isolated_readback = await read_file_contents(clone_a)
+ clone_b_isolated_readback = await read_file_contents(clone_b)
+
+ await clone_b.shutdown()
+ clone_b_needs_cleanup = False
+
+ await clone_a.shutdown()
+ clone_a_needs_cleanup = False
+
+ await source_devbox.shutdown()
+ source_needs_cleanup = False
+
+ await snapshot.delete()
+ snapshot_needs_cleanup = False
+
+ return RecipeOutput(
+ resources_created=resources_created,
+ checks=[
+ ExampleCheck(
+ name="uploaded file is readable on the source devbox",
+ passed=uploaded_readback == uploaded_contents,
+ details=uploaded_readback,
+ ),
+ ExampleCheck(
+ name="suspend reaches the suspended state",
+ passed=suspended_info.status == "suspended",
+ details=f"status={suspended_info.status}",
+ ),
+ ExampleCheck(
+ name="resume preserves the baseline file contents",
+ passed=resumed_info.status == "running" and resumed_readback == baseline_contents,
+ details=f"status={resumed_info.status}, contents={resumed_readback}",
+ ),
+ ExampleCheck(
+ name="multiple devboxes can use the same snapshot baseline",
+ passed=(
+ clone_a_baseline_readback == baseline_contents and clone_b_baseline_readback == baseline_contents
+ ),
+ details=(f"clone_a={clone_a_baseline_readback}, clone_b={clone_b_baseline_readback}"),
+ ),
+ ExampleCheck(
+ name="devboxes diverge after isolated mutations",
+ passed=(
+ source_isolated_readback == source_contents
+ and clone_a_isolated_readback == clone_a_contents
+ and clone_b_isolated_readback == clone_b_contents
+ ),
+ details=(
+ "source="
+ f"{source_isolated_readback}, "
+ f"clone_a={clone_a_isolated_readback}, "
+ f"clone_b={clone_b_isolated_readback}"
+ ),
+ ),
+ ExampleCheck(
+ name="snapshot-backed devboxes stay isolated from one another",
+ passed=(
+ len(
+ {
+ source_isolated_readback,
+ clone_a_isolated_readback,
+ clone_b_isolated_readback,
+ }
+ )
+ == 3
+ ),
+ details=(f"values={[source_isolated_readback, clone_a_isolated_readback, clone_b_isolated_readback]}"),
+ ),
+ ExampleCheck(
+ name="snapshot can be deleted after the demo finishes",
+ passed=not snapshot_needs_cleanup,
+ details=f"deleted={not snapshot_needs_cleanup}",
+ ),
+ ],
+ )
+
+
+run_devbox_snapshots_example = wrap_recipe(recipe)
+
+
+if __name__ == "__main__":
+ run_as_cli(run_devbox_snapshots_example)
diff --git a/examples/registry.py b/examples/registry.py
index edc52f445..6f55774fa 100644
--- a/examples/registry.py
+++ b/examples/registry.py
@@ -9,6 +9,7 @@
from .devbox_tunnel import run_devbox_tunnel_example
from .example_types import ExampleResult
+from .devbox_snapshots import run_devbox_snapshots_example
from .mcp_github_tools import run_mcp_github_tools_example
from .secrets_with_devbox import run_secrets_with_devbox_example
from .devbox_snapshot_resume import run_devbox_snapshot_resume_example
@@ -39,6 +40,13 @@
"required_env": ["RUNLOOP_API_KEY"],
"run": run_devbox_snapshot_resume_example,
},
+ {
+ "slug": "devbox-snapshots",
+ "title": "Devbox Snapshots (Suspend, Resume, Restore, Delete)",
+ "file_name": "devbox_snapshots.py",
+ "required_env": ["RUNLOOP_API_KEY"],
+ "run": run_devbox_snapshots_example,
+ },
{
"slug": "devbox-tunnel",
"title": "Devbox Tunnel (HTTP Server Access)",
diff --git a/llms.txt b/llms.txt
index 3785def20..717c8d7fd 100644
--- a/llms.txt
+++ b/llms.txt
@@ -11,6 +11,7 @@
## Core Patterns
- [Devbox lifecycle example](examples/devbox_from_blueprint_lifecycle.py): Create blueprint, launch devbox, run commands, cleanup
+- [Devbox snapshots example](examples/devbox_snapshots.py): Suspend and resume a devbox, create a shared snapshot baseline, restore multiple devboxes, verify isolation, cleanup
- [Devbox snapshot and resume example](examples/devbox_snapshot_resume.py): Snapshot disk, resume from snapshot, verify state isolation
- [MCP GitHub example](examples/mcp_github_tools.py): MCP Hub integration with Claude Code
- [Secrets with Devbox example](examples/secrets_with_devbox.py): Create secret, inject into devbox, verify, cleanup