CrowNest
Quickstart

Python SDK quickstart

Create a sandbox, run a command, work with files, export an artifact, and expose a preview with the crownest package for Python.

Run the full sandbox lifecycle with the Python SDK: create a sandbox, run a command, write and read a file, export an artifact, expose a preview, and clean up. The end-to-end script is at the bottom of this page.

For archive-based repo/test work, use client.workspace_runs instead of creating a sandbox directly. Workspace Runs upload an existing .tar.gz or .tgz, run a command, stream execution events, and persist durable evidence:

import hashlib
from pathlib import Path

from crownest import CrowNest

archive = Path("repo.tgz").read_bytes()

with CrowNest() as client:
    run = client.workspace_runs.run_archive(
        archive,
        sha256=hashlib.sha256(archive).hexdigest(),
        size_bytes=len(archive),
        template="python-node",
        command="pytest -q",
    )

See Workspace Runs for the full SDK lifecycle.

Prerequisites

  • Python 3.11 or later. The SDK depends on httpx.
  • A CrowNest API key, created in the dashboard at https://crownest.dev. See API keys for scopes and presets.

Warning

The raw API key is shown once, at creation time. Copy it immediately and store it somewhere safe — you can't view it again later.

Set up

  1. Install the package from PyPI.

    Terminal
    pip install crownest

    With uv: uv add crownest.

  2. Export your bearer credential. The client reads CROWNEST_BEARER_TOKEN from the environment, with CROWNEST_API_KEY still supported for developer API keys.

    Terminal
    export CROWNEST_BEARER_TOKEN="cn_live_..."
  3. Create a client. The sync client supports use as a context manager, which closes the underlying connection for you; outside a with block, call .close() when you're done.

    main.py
    from crownest import CrowNest
    
    client = CrowNest(
        credential="cn_live_...",   # or env CROWNEST_BEARER_TOKEN
        base_url="https://api.crownest.dev",
        timeout=660,                 # seconds
    )

    All constructor arguments are optional; CrowNest() works on its own when CROWNEST_BEARER_TOKEN is set.

Walk through the lifecycle

Each step below builds on the previous one inside the same with block.

  1. Create a sandbox. create returns a SandboxHandle — the sandbox resource with .id, dict-style and attribute access, .to_dict(), a .kill() method, and bound sub-clients (.commands, .files, .artifacts, .previews, .code), so you don't repeat the sandbox ID on every call.

    with CrowNest() as client:
        sandbox = client.sandboxes.create(template="python-node")

    python-node is the default broad repository runner. Use node for a lean JavaScript and TypeScript environment. You can also pass ttl_ms to request a lifetime and metadata to attach small string labels — see Sandboxes. Idempotency keys are generated for you automatically.

  2. Run a command. run waits for the process to exit and returns the full command record, including exitCode, stdout, and stderr.

    result = sandbox.commands.run("python3 -c 'print(40 + 2)'")
    print(result["exitCode"], result["stdout"])

    Important

    A non-zero exit code does not raise. The command completed; your code inside it failed. Always check result["exitCode"] when the outcome matters. CrowNestApiError is reserved for API-level failures.

    Stream output as it happens. Use callbacks when you want live stdout and stderr while still getting the final Command record from run.

    import sys
    
    sandbox.commands.run(
        "python3 -c \"import time; [print('step', i, flush=True) or time.sleep(0.2) for i in range(3)]\"",
        on_stdout=lambda chunk: print(chunk, end=""),
        on_stderr=lambda chunk: print(chunk, end="", file=sys.stderr),
    )

    Run code, not just commands. Use Code Runs for notebook-style snippets and rich interpreter outputs. The Python SDK exposes the same Sandbox-bound Code helpers with snake_case option names.

    code_result = sandbox.code.run("print(40 + 2)")
    print(code_result["stdout"])
  3. Write and read a file. All file paths are inside /workspace.

    sandbox.files.write("notes.txt", "hello from crownest")
    content = sandbox.files.read("notes.txt")
    print(content)

    Note

    File APIs are confined to /workspace. Paths that resolve outside it through .. segments are rejected with the path_outside_workspace error code.

  4. Export an artifact. The workspace disappears with the sandbox, so copy anything you want to keep to durable storage with an explicit export.

    artifact = sandbox.artifacts.create(path="notes.txt")
    print(artifact["id"])

    You can download it later — even after the sandbox is gone — with client.artifacts.download(artifact["id"]), which returns bytes.

  5. Expose a preview. When your sandbox hosts an HTTP service, create a preview to get an authenticated URL like https://p-a1b2c3.crownest.dev.

    sandbox.commands.start("python3 -m http.server 8000")
    preview_create = sandbox.previews.create(port=8000)
    preview = preview_create["preview"]
    print(preview["url"])

    start launches the server without waiting for it to exit. Previews require authentication in v1. Use auth_mode="token" when you need a browser-openable link; the one-time previewToken is returned only from the create response. See Previews.

  6. Kill the sandbox. This stops billing and releases the environment. Sandboxes also expire automatically at their TTL.

    sandbox.kill()

Handle errors

API failures raise crownest.CrowNestApiError, which carries the HTTP status, a stable machine-readable code, and optional details. str(e) gives a readable summary.

from crownest import CrowNest, CrowNestApiError

with CrowNest() as client:
    try:
        client.sandboxes.get("sbx_does_not_exist")
    except CrowNestApiError as e:
        print(e.status, e.code, e.details)
        print(str(e))

See the error reference for the full list of codes.

Complete script

The end-to-end version of the steps above.

main.py
from crownest import CrowNest

with CrowNest() as client:
    sandbox = client.sandboxes.create(template="python-node")

    result = sandbox.commands.run("python3 -c 'print(40 + 2)'")
    print(result["exitCode"], result["stdout"])

    sandbox.files.write("notes.txt", "hello from crownest")
    content = sandbox.files.read("notes.txt")
    print(content)

    artifact = sandbox.artifacts.create(path="notes.txt")
    print("artifact:", artifact["id"])

    sandbox.commands.start("python3 -m http.server 8000")
    preview_create = sandbox.previews.create(port=8000)
    preview = preview_create["preview"]
    print("preview:", preview["url"])

    sandbox.kill()

Use the async client

AsyncCrowNest exposes the same surface with await on every call, and works as an async context manager.

main_async.py
import asyncio

from crownest import AsyncCrowNest


async def main() -> None:
    async with AsyncCrowNest() as client:
        sandbox = await client.sandboxes.create(template="python-node")
        result = await sandbox.commands.run("python3 -c 'print(40 + 2)'")
        print(result["exitCode"], result["stdout"])
        await sandbox.kill()


asyncio.run(main())

Next steps

On this page