Cortex Sandboxes SDK reference

This page is the reference for the snowflake.sandbox Python API. To install the SDK and configure a Snowflake connection, see the Cortex Sandboxes quickstart.

Common operations

To do thisUse
Create a sandbox and run one commandSandbox.create(), then sb.exec()
Name the Snowflake connectionconnection= on Sandbox.create(), or with using(...)
Stream output as it is producedsb.exec_stream()
Start something and come back to it laterSandbox.create(command=...), then sb.wait()
Reattach to a sandbox you already createdSandbox.connect(name)
See what sandboxes you are holdingSandbox.list()
Terminate a sandboxsb.terminate(), or use Sandbox.create() as a with block
Read or write a file inside the sandboxsb.read_text(), sb.write_text(), sb.upload_file()
Read a Snowflake stage from inside the sandboxStageMount, passed as stage_mounts=
Give the sandbox a credentialSecret, passed as secrets=
Reach a host outside the default egress setEgress(external_access_integrations=...)
Read the container’s outputsb.logs()
Run an interactive shellsb.shell()
Keep a warm agent process aliveApp, agent_session()

Everything above is documented in full below. For the errors these calls raise, see the error reference.

Public surface

GroupNameWhat it is
Start hereSandboxA Cortex Sandbox (sync API).
AsyncSandboxA Cortex Sandbox (async API).
ImageA container image built from a catalog base.
ConnectionsusingRun a block against one Snowflake connection, without touching global state.
ConfigResolved SDK configuration.
ConnectionLikeRepresent a PEP 604 union type
current_configResolve the AMBIENT SDK configuration – the one a caller who named no connection gets (account, host, token).
resolve_configResolve a Config from a connection + overrides, WITHOUT storing it anywhere.
get_snowflake_connectionReturn a Snowflake connection for code running inside a Cortex Sandbox.
close_connectionsClose the connector sessions, clear the resolution cache, drop bound transports.
shutdownClose the process-wide async transport’s HTTP client(s) and drop it.
Results of running somethingExecResultResult of a non-streaming Sandbox.exec().
RunResultThe outcome of a detached run (spawn + wait).
StreamLineOne logical line yielded by Sandbox.exec_stream().
ExecStreamAsync iterator over the lines of a streaming exec.
SyncExecStreamSynchronous iterator over the lines of a streaming exec.
SandboxFailureThe server’s explanation for a failed sandbox.
Configure the containerEgressControls outbound network access from the sandbox.
SecretA Snowflake SECRET brokered to the sandbox, referenced by name.
StageMountA Snowflake stage or workspace FUSE-mounted into the container.
McpServerAn EXTERNAL MCP SERVER to expose to the sandbox, referenced by FQN.
BundleSource-tree slice bundled and uploaded to the sandbox.
broker_env_varThe environment variable a secret’s placeholder lands in without env_var.
Find and manage sandboxeslist_sandboxesList sandboxes the caller’s PAT has access to.
list_sandboxes_asyncList sandboxes the caller’s PAT has access to.
get_sandboxRehydrate a Sandbox by id (e.g. after a driver restart).
get_sandbox_asyncRehydrate an AsyncSandbox by id (e.g. after a driver restart).
get_sandbox_by_nameLook up a sandbox by its caller-assigned name= (Modal Sandbox.from_name).
get_sandbox_by_name_asyncLook up a sandbox by its caller-assigned name= (Modal Sandbox.from_name).
destroy_sandboxDestroy a sandbox by id. Idempotent for an already-absent container.
destroy_sandbox_asyncDestroy a sandbox by id. Idempotent for an already-absent container.
Detached runs and shellsJobA handle to a detached run (async API).
SyncJobA handle to a detached run (sync API) — the blocking counterpart of Job.
ShellOne interactive shell session on a sandbox.
SyncShellOne interactive shell session on a sandbox (blocking API).
ShellClosedRaised when input is sent to a shell whose process has exited.
Long-lived agent sessionsAppA named collection of deployable functions.
FunctionA deployable function with .remote() and .local() calling conventions.
SessionA handle to a warm session.
SessionAppReturned by @app.session — a deployable stateful Session class.
agent_sessionCreate-or-resume a keep-alive Session keyed by key.
agent_session_syncCreate-or-resume a keep-alive Session keyed by key — synchronous counterpart of agent_session.
session_loopRun the daemon-side message loop (the in-container half of a warm agent).
Deploy local codeDeployPlanWhat a deploy would do (used by –dry-run and surfaced on deploy).
DeployResultThe outcome of a deploy: what was planned, what happened, and what went wrong.
deploy_asyncSpawn a detached Job from a DeploySpec and return a handle.
deploy_async_syncSpawn a detached job synchronously — the blocking counterpart of deploy_async.
FilesFileInfoMetadata for one path, returned by Sandbox.list_files() / .stat().
FileWatchEventOne filesystem-change event yielded by Sandbox.watch().
FileWatchEventTypeCoarse change class for a Sandbox.watch() event.
MCP serverslist_mcp_serversList the account’s EXTERNAL MCP SERVERs with this user’s auth status.
mcp_server_statusRead the live connection status the platform publishes inside the sandbox.
validate_mcp_serversValidate mcp_servers= and convert it to wire entries.
mcp_token_env_varThe environment variable an MCP server’s dummy token lands in.
McpServerInfoOne EXTERNAL MCP SERVER in the account, with your authorization status.
McpConnectionStatusThe live, in-sandbox connection status of one EXTERNAL MCP SERVER.

Copy-paste form:

from snowflake.sandbox import (
    Sandbox, ExecResult, RunResult, StreamLine, ExecStream,
    App, Function, Image, Secret, Egress, Bundle, StageMount, McpServer,
    broker_env_var, session_loop, Config, using, get_snowflake_connection, current_config, close_connections,
    AsyncSandbox, SandboxAuthError, SandboxBuildError, SandboxConflictError, SandboxContractWarning, SandboxCreateError, SandboxError, SandboxExecError, SandboxExecTimeoutError, SandboxFailure, SandboxFileTooLargeError, SandboxNotFoundError, SandboxNotImplementedError, SandboxNotReadyError, SandboxRateLimitError, SandboxTransportError, SandboxValidationError,
    ConnectionLike, DeployPlan, DeployResult, FileInfo, FileWatchEvent, FileWatchEventType, Job, McpConnectionStatus, McpServerInfo, Session, SessionApp, Shell, ShellClosed, SyncExecStream, SyncJob, SyncShell, agent_session, agent_session_sync, deploy_async, deploy_async_sync, destroy_sandbox, destroy_sandbox_async, get_sandbox, get_sandbox_async, get_sandbox_by_name, get_sandbox_by_name_async, list_mcp_servers, list_sandboxes, list_sandboxes_async, mcp_server_status, mcp_token_env_var, resolve_config, shutdown, validate_mcp_servers,
)

RunResult is the outcome type of a detached run (create(command=...) + wait).

The Sandbox object

Sandbox represents one container. Construction is lazy—no network call happens until you call create(), or enter the object as a with context manager.

Create

Sandbox.create is the one-call factory. It creates the sandbox and returns a live handle.

sb = Sandbox.create(
    memory="4g",
    egress=Egress(),
    name="my-agent",
)

Key parameters of Sandbox.create() (all keyword-only):

ParameterTypeDefaultDescription
imagestr | Image""Image name. Empty string uses the deployment’s default image, which is the recommended form (the fleet default moves).
codestr | Path | NoneNoneBundle + upload a local project directory .
commandSequence[str] | NoneNoneManaged main process (for wait / a warm daemon / logs).
memoryMemoryTier"4g"Memory tier: "1g" | "4g" | "8g" | "16g" | "32g" | "64g".
cpufloat | NoneNoneCPU in cores, overriding the tier default (1g:1, 4g:2, 8g:3, 16g:4, 32g:6, 64g:8). Fractional allowed (0.5 = half a core). Sets only CPU; memory stays tier-selected.
envMapping[str, str] | NoneNoneEnvironment variables for in-container processes.
secretsSequence[Secret | Mapping[str, str]] | NoneNoneSnowflake SECRETs to broker in — the sandbox gets a dummy .
egressEgress | Mapping[str, object] | NoneNoneOutbound scope. Omitted applies the baseline (Snowflake, its stages, and the package-manager hosts — GitHub is not among them); e.g. Egress(external_access_integrations=["MY_EAI"]) to grant specific hosts, or Egress.only() to close egress. A caller-supplied host list is retired and refused .
code_stagestr | NoneNoneSnowflake stage the bundle is uploaded to / pulled from (needed when code= is set and command runs a bundled file).
namestr | NoneNoneOptional stable name for the container.
stage_mountsSequence[StageMount] | NoneNoneMount Snowflake stages into the container filesystem — a sequence of StageMount (e.g. StageMount.from_stage("@DB.SCHEMA.DATA", mount_path="/mnt/data")). The supported path for bulk/stage-backed data.
mcp_serversSequence[str | McpServer | Mapping[str, object]] | NoneNoneEXTERNAL MCP SERVERs to expose to the container — FQN strings (["<your_database>.<your_schema>.GITHUB_MCP_SERVER"]) or McpServer.external(fqn, name=...). Snowflake resolves the endpoint, API integration, and your OAuth secret server-side; the container sees a dummy in SANDBOX_MCP_TOKEN_<NAME> and the egress proxy swaps in the real token. Servers you have not authorized are omitted — list_mcp_servers() shows which those are.
tagsMapping[str, str] | NoneNoneUser-defined labels for the sandbox — a dict[str, str] (e.g. {"team": "ml", "env": "dev"}). Retrieve later with sb.get_tags().
rolestr | NoneNoneSnowflake role to assign to this sandbox’s session. Naming a connection is sufficient — role-scoped sessions are minted on demand. When omitted, the sandbox runs with the default session’s primary role and all of the user’s secondary roles. Pass role to limit the sandbox to that role’s grants. The role is returned by the server and stored on sb.role.
mount_snowflake_configboolTrueHave the platform render the sandbox’s Snowflake config so snow / cortex / the connector authenticate inside the container with no setup. Set False to manage your own: the platform then writes no connections.toml, sets no SNOWFLAKE_HOME, and omits the SNOWFLAKE_* connection env — so an opting-out caller must supply a full connection itself (a bare snow sql / connector.connect() otherwise fails with “Connection default is not configured”).
omit_snowflake_tokenboolFalseIf True, code in the sandbox can’t connect to Snowflake as you. Use it for workloads that shouldn’t have Snowflake access.
idle_suspenddt.timedelta | str | NoneNoneIdle-suspend timeout — how long the sandbox may sit idle (no inbound traffic) before the platform auto-suspends it, overriding the platform default of 10 minutes. A datetime.timedelta or a duration string ("30m", "2h", "1h30m"); a bare number is rejected because its unit would be ambiguous. Resolution is minutes, so one minute is the smallest value you can set: a value that is not a whole number of minutes (e.g. "90s") raises rather than being silently rounded. Zero and negatives are rejected too: 0 is the create field’s “no opinion” sentinel, for which the server substitutes its own default exactly as it does for an absent field, so it is not a threshold the platform can apply — omit idle_suspend (or pass None) to take the 10 minute default. There is no caller-facing way to say “never idle-suspend”: the server caps this value at 24 hours and there is no separate maximum sandbox lifetime to set it above. Idleness is measured from inbound traffic only, so CPU and disk work do not hold a sandbox open — a busy compute loop with nothing arriving is suspended on schedule — while merely polling a sandbox’s status is inbound traffic and keeps it alive and consuming compute. A suspend preserves only the filesystem. Memory is not preserved: the container is frozen, its filesystem snapshotted, and then stopped and deleted, and the resume builds a fresh container from the base image plus that snapshot. The platform also sends SIGUSR1 (default action: terminate) shortly before the checkpoint, which is why the SDK’s daemon and detached wrappers ignore it. So tmpfs (/tmp, /dev, /run, /snowflake/stages) and every running process are lost and only persistent rootfs writes (/var/tmp, /var/log, /home, /root) come back. Nothing re-executes the container’s command on resume, so a detached run or a warm daemon does not restart by itself.
connectionConnectionLike | NoneNonePick the Snowflake connection for this sandbox — a name in ~/.snowflake/connections.toml, a live snowflake.connector connection you already opened, a Snowpark Session, or a Config assembled from raw credentials (mirroring the connector’s connect(connection_name=...)). The handle carries it onward. The transport is pooled per credential identity, so exiting terminates the sandbox and leaves the HTTP client for the next call on that connection (shutdown() releases it, or lifecycle.shutdown_sync() from a sync-only program); two sandboxes on one connection share a client. Resolution is memoised, so naming it call after call costs one login. role= composes with a connection name only — a live connection or Snowpark Session is already logged in and raw credentials carry their own role, so any of those is refused rather than silently ignored. Mutually exclusive with transport=. Omit to use an enclosing using() block, else the default connection.
waitboolTrueWhen True (the default), create() blocks until the container is live. When False, returns immediately with the container in pending state; call sb.wait_until_ready() to block later (useful for parallel creates).

Use create() in a with block to destroy the container on exit:

with Sandbox.create() as sb:
    result = sb.exec(["python", "-c", "print(40 + 2)"])

The earlier sandbox.toml manifest format is retired. Use Sandbox.create() (with code=) or the App interface instead.

Properties

PropertyDescription
sb.cpuThe effective CPU allocation in cores.
sb.created_atThe server-reported container creation timestamp, if available.
sb.exit_codeThe managed process’s exit code, or None if it has not exited.
sb.failureWhy this sandbox failed, or None if it has not.
sb.generationA best-effort restart/resume counter, or None if the server reports none.
sb.idThe server-assigned container ID.
sb.imageThe image name the sandbox was constructed with.
sb.is_runningTrue if the sandbox has not yet terminated (cached status is not terminal).
sb.is_terminatedTrue if the sandbox has terminated (cached status is terminal).
sb.memoryThe effective memory tier ('1g', '4g', '8g', '16g', '32g', or '64g').
sb.nameThe sandbox’s name, once the server echoes it.
sb.roleThe Snowflake role this sandbox was created under, or None if not set.
sb.server_statusThe server’s raw status string, or "" if none has been reported.
sb.statusThe last status the server reported for this container.
sb.stderrThe managed command process’s stderr. See stdout.
sb.stdoutThe managed command process’s stdout, as an async-iterable of lines.
sb.stopped_atThe server-reported stop timestamp, if available.
sb.tagsThe tags (server labels) attached to this sandbox, as last known here.

Sandbox methods

Create and connect

MethodDescription
Sandbox.connect()Reconnect to an existing sandbox by its NAME (the identity list shows), for example after your driver restarts. The name is unique per owner.
Sandbox.list()Return the sandboxes the caller can see, optionally filtered by status.

Sandbox.connect()

Note

A name reaches only the sandboxes the default list view returns, so it cannot reattach to one the controller reports failed/unknown, nor one stopped more than 24h ago – those raise SandboxNotFoundError here. Keep the id as well if you need to reach a sandbox to diagnose why it died; the by-id read has no such filter.

sb = Sandbox.connect("my-agent")
result = sb.exec(["python", "-c", "print('still running')"])

Sandbox.list()

sandboxes = Sandbox.list()
for sb in sandboxes:
    print(sb.id, sb.status)

# What MY_ROLE can see. Resolved from the ambient connection at that role;
# under a bare env token, which carries its own role, role= only applies the
# server-side ?role= filter.
mine = Sandbox.list(role="MY_ROLE")

Run commands

MethodDescription
sb.exec()Run a command to completion and return an ExecResult.
sb.exec_stream()Stream a command’s output line by line. Returns a SyncExecStream iterator.

sb.exec()

Note

There is no pty= option: exec is pipe-based with separate stdout/stderr. For pseudo-terminal semantics use shell().

stdin, env, working_dir, and text=False are present in the signature but not supported by this method — passing any of them raises SandboxError. Use exec_stream(cmd, env=..., working_dir=...) for per-call environment or working directory, or set env once at create time with Sandbox.create(env=...).

result = sb.exec(["python", "-c", "print(40 + 2)"])
print(result.stdout)   # "42\n"

sb.exec_stream()

Note

There is no pty= option: exec_stream uses pipes with separate stdout/stderr. For TTY semantics use shell().

env REPLACES the environment for this call – it does not merge into the sandbox’s own. Passing env={"FOO": "1"} runs the command with FOO and nothing else: HOME, anything set at create time via Sandbox.create(env=...), and the proxy and CA variables the sandbox needs to reach the network (HTTP_PROXY, HTTPS_PROXY, NO_PROXY, CNG_CA_CERT, REQUESTS_CA_BUNDLE, NODE_EXTRA_CA_CERTS) are all gone. Measured on a live sandbox: about 30 variables without env=, 3 with it – and two of those three, PWD and SHLVL, are set by the shell itself.

The consequence is easy to misread, because nothing points back at env=: a command that needs the network – pip install, curl, anything using requests – starts failing with a proxy or TLS error purely because a variable was passed. Lost PATH is less visible, since a shell substitutes a default, so bare-name commands still run.

To add a variable while keeping the rest, set it inside the command instead of passing env= – for example sb.exec_stream(["/bin/sh", "-c", "FOO=1 my-command"]), which keeps the rest of the environment – or set it once for the whole sandbox with Sandbox.create(env=...), which IS visible to later calls.

for line in sb.exec_stream(["python", "generate_report.py"]):
    print(line.stream, line.data)

Wait and send

MethodDescription
sb.wait_until_ready()Block until the container is live (for sandboxes created with wait=False).
sb.wait()Wait for a detached run to finish and return its result.
sb.send()Send a message to a warm daemon in the sandbox and return its reply.

sb.wait_until_ready()

# Fire all creates immediately
sandboxes = [Sandbox.create(wait=False)
             for _ in range(10)]
# Wait for all in parallel
with ThreadPoolExecutor() as pool:
    pool.map(lambda sb: sb.wait_until_ready(), sandboxes)

sb.wait()

sb = Sandbox.create(
    command=["python", "main.py"],
)
result = sb.wait()
print(result.status, result.exit_code)

sb.send()

sb = Sandbox.create(
    command=["python", "daemon.py"],
)
reply = sb.send("what changed in prod yesterday?")
print(reply)

Manage the container

MethodDescription
sb.refresh()Re-read this container’s state from the server and return its status.
sb.refresh_live_status()Read the LIVE status of the managed process, updating status / server_status.
sb.logs()Return the container’s captured stdout and stderr as a string.
sb.shell()Open an interactive shell session inside the sandbox.
sb.suspend()Suspend the sandbox: snapshot its filesystem, then tear the container down.
sb.resume()Wake a suspended sandbox without running anything in it.
sb.destroy()Deprecated: use terminate() instead.
sb.terminate()Terminate the sandbox (idempotent).
sb.poll()Return the exit code if the run has finished, or None if still running.
sb.get_tags()Return the key-value tags attached to this sandbox.
sb.set_tags()Set key-value tags for this sandbox (must be called before creation).

sb.refresh()

status = sb.refresh()
print(status)  # "ready", "pending", "dead", "failed", or "unknown"

sb.refresh_live_status()

status = sb.refresh_live_status()
print(status)  # the LIVE process status, not the create-time snapshot

sb.logs()

output = sb.logs(tail=100)
print(output)

# What the commands you ran with `exec()` printed.
print(sb.logs(source="exec"))

sb.shell()

Note

shell is a terminal verb, not the SSH protocol. There is no port 22, no key exchange, and no scp. The shell is a PTY driven over the Snowflake REST path.

with sb.shell() as sh:
    sh.send("ls -la\n")
    for chunk in sh.output():
        sys.stdout.buffer.write(chunk)

sb.suspend()

sb.exec(["sh", "-c", "echo work > /var/tmp/state"])
sb.suspend()                     # state is banked
print(sb.exec(["cat", "/var/tmp/state"]).stdout)   # resumes; prints "work"

sb.resume()

sb.resume()    # returns before the sandbox is serving; the next exec waits

sb.poll()

Note

Completion is read from the run’s own result report, not from the container’s status: a sandbox created with command= routinely still reports ready after its command has exited. So a code comes back as soon as the run finishes, without waiting for the container to go away.

while (code := sb.poll()) is None:
    time.sleep(1)
print("exit code:", code)

sb.get_tags()

tags = sb.get_tags()
print(tags.get("team"))

sb.set_tags()

Note

Tags can only be set before create() is called – pass tags= to Sandbox.create(). There is no relabel endpoint, so calling this on an already-created sandbox raises SandboxError.

with Sandbox.create(tags={"team": "sandbox", "env": "dev"}) as sb:
    sb.exec(["python", "main.py"])

File operations

MethodDescription
sb.upload_file()Upload a local file into the running sandbox.
sb.upload_dir()Upload a local directory into the running sandbox and report what it sent.
sb.download_file()Download a file from the running sandbox to a local path.
sb.read_text()Read a text file from the sandbox and return its contents.
sb.read_bytes()Read a binary file from the sandbox and return its contents.
sb.write_text()Write a text string to a file inside the sandbox.
sb.write_bytes()Write bytes to a file inside the sandbox.
sb.list_files()List files and directories at path inside the sandbox.
sb.make_directory()Create a directory inside the sandbox.
sb.stat()Return metadata for a file or directory inside the sandbox.
sb.remove()Remove a file or directory inside the sandbox.
sb.stage_put()Not implemented – the stage/put route returns 501 on every backend.
sb.stage_get()Not implemented – the stage/get route returns 501 on every backend.
sb.watch()Stream filesystem-change events under path as an iterator.

sb.upload_file()

sb.upload_file("./data.csv", "/app/data.csv")

sb.upload_dir()

Note

There is no bulk route, so this is one request per file and the per-file MAX_FILE_BYTES ceiling still applies – an oversize file is refused before the first byte is sent, so a tree never half-uploads. dry_run=True sends nothing and so raises nothing: it returns the plan with any offenders listed in plan.oversized. Mount a stage (StageMount) for bulk data. Symlinks are never followed, in either direction, and appear in plan.skipped as symlink-dir / symlink-file rather than vanishing silently. An empty directory is not uploaded – there is no file to create it under – and is reported as empty-dir.

plan = sb.upload_dir("./project", "/app", exclude=["*.csv"])
print(f"sent {plan.file_count} files, skipped {plan.skipped_by_reason()}")

sb.download_file()

Note

Written to a temp file beside the destination and renamed on success, so a download that fails part-way leaves neither a truncated file where a whole one is expected nor a directory tree the caller did not have before.

sb.download_file("/app/output.csv", "./output.csv")

sb.read_text()

content = sb.read_text("/app/output.txt")

sb.read_bytes()

data = sb.read_bytes("/app/output.bin")

sb.write_text()

sb.write_text("/app/config.json", '{"key": "value"}')

sb.write_bytes()

sb.write_bytes("/app/model.pkl", model_bytes)

sb.list_files()

entries = sb.list_files("/app")
for entry in entries:
    print(entry.path, entry.size_bytes, entry.is_dir)

sb.make_directory()

sb.make_directory("/app/output", create_parents=True)

sb.stat()

info = sb.stat("/app/output.txt")
print(info.size_bytes)

sb.remove()

sb.remove("/app/temp", recursive=True)

sb.stage_put()

Note

Not implemented on any backend: the stage/put route answers 501, so this always raises SandboxNotImplementedError. Mount the stage with StageMount and write through the mount instead.

# This raises SandboxNotImplementedError. Mount the stage and write
# through the mount instead:
data = StageMount.from_stage("MY_STAGE", mount_path="/data")
with Sandbox.create(stage_mounts=[data]) as sb:
    sb.write_bytes("/data/results.parquet", payload)

sb.stage_get()

Note

Not implemented on any backend: the stage/get route answers 501, so this always raises SandboxNotImplementedError. Mount the stage with StageMount and read through the mount instead.

# This raises SandboxNotImplementedError. Mount the stage and read
# through the mount instead:
data = StageMount.from_stage("MY_STAGE", mount_path="/data", readonly=True)
with Sandbox.create(stage_mounts=[data]) as sb:
    model = sb.read_bytes("/data/model.pkl")

sb.watch()

for event in sb.watch("/app"):
    print(event.type, event.paths)

Result and stream types

Sandbox.exec() returns an ExecResult, a named tuple with the following fields:

FieldDescription
stdoutDecoded standard output.
stderrDecoded standard error.
exit_codeThe in-container process exit code.
elapsed_msWall-clock time on the server side, not including network round-trip.

Sandbox.exec_stream() yields StreamLine named tuples:

FieldDescription
streamThe channel the line came from: "stdout" or "stderr".
dataThe line text, with its trailing newline stripped.

The SyncExecStream returned by exec_stream() exposes exit_code, elapsed_ms, and error after the iterator drains. It does not raise on a non-zero exit; check stream.exit_code. It raises SandboxExecError on an error frame (meaning the exec never produced a result).

Sandbox.wait() returns a RunResult:

FieldDescription
statusOne of "succeeded", "failed", or "timed_out".
exit_codeThe entry command’s exit code, or None if unknown.
resultThe parsed result artifact, or None if no result was produced.
logs_refThe container ID that the logs are read from.

File and watch types

FileInfo – Metadata for one path, returned by Sandbox.list_files() / .stat(). Fields: path, is_dir, size_bytes, modified_at.

Mirrors Modal’s Sandbox.filesystem.stat shape. Both producers are implemented (files.py runs an in-container python3 helper over exec); modified_at is a Unix epoch (seconds, UTC) or None when the server did not report it.

FileWatchEvent – One filesystem-change event yielded by Sandbox.watch(). Fields: paths, type.

Mirrors Modal’s FileWatchEvent: paths are the absolute in-sandbox path(s) the change concerns (usually one), and type is the coarse FileWatchEventType.

FileWatchEventType – Coarse change class for a Sandbox.watch() event. Members: Unknown, Access, Create, Modify, Delete.

Mirrors Modal’s FileWatchEventType. The sandbox observes raw Linux inotify(7) masks and maps them onto this small enum — several inotify bits collapse to one class (e.g. IN_MODIFY and IN_CLOSE_WRITE both read as Modify; IN_CREATE and IN_MOVED_TO both as Create). A mask that maps to nothing meaningful surfaces as Unknown rather than being dropped, so a contract drift is visible instead of silent. str-valued so it compares equal to its wire string and JSON-serializes cleanly.

Images, secrets, egress, and bundles

The declarative specs (Image, Secret, Egress, Bundle, StageMount, DeploySpec) describe a deployable app. @app.function(...) takes them directly, and they compile to a DeploySpec. There is no manifest file.

Image

An Image is a reference to a catalog base image. Use Image.from_catalog() to select one:

image = Image.from_catalog("sandbox-base")

Pass an Image (or a plain catalog-name string) directly to Sandbox.create():

with Sandbox.create(image=Image.from_catalog("sandbox-base")) as sb:
    result = sb.exec(["python", "-c", "print(40 + 2)"])

SDK-side image dependency baking (the layered-image builders pip_install, run_commands, copy_local_*, env, workdir, and build) is temporarily unavailable and will return in a later release. Until then, either use a base image that already includes the dependencies you need, or install them into the running sandbox with sb.exec(["pip", "install", ...]). Public PyPI is in the always-on egress baseline, so a runtime install needs no egress configuration and no external access integration.

Secret

Secret is how a sandbox uses an external credential. It references a Snowflake SECRET object. The secret’s value is not placed in the container; the platform supplies it on outbound requests to the declared host. See Cortex Sandboxes concepts and security.

secret = Secret.from_name("<your_database>.<your_schema>.GH_TOKEN", env_var="GH_TOKEN", host="github.com")
  • Secret.from_name(fqn, *, env_var, host, hosts) references a SECRET by fully qualified name. host and hosts are mutually exclusive.
  • The env var that receives the value is env_var if set, otherwise derived from the fully qualified name via broker_env_var.

Egress

Egress controls outbound network access. There are exactly three levels, and the middle one is the default:

LevelHowReaches
baselinethe default, or allow_public_package_registries=TrueSnowflake, the cloud-storage stages, and roughly 43 package-manager hosts
closedallow_public_package_registries=False or Egress.only()Snowflake and its stages only
grantedexternal_access_integrations=(...)the above, plus whatever the integration’s network rules resolve to

The baseline is not the internet. It is a curated host set that the platform maintains, and anything outside it is refused. A host you need that is not on that list requires an external access integration at every level, including the baseline.

egress = Egress()                                # baseline
egress = Egress.only()                           # closed: nothing arbitrary
egress = Egress(allow_public_package_registries=False, external_access_integrations=("MY_EAI",))

Key fields:

FieldDescription
allow_public_package_registriesWhether the sandbox reaches the public package registries described above. None uses the platform default. Setting it to False does not stop egress: Snowflake and its stages remain reachable, and an external access integration still grants.
allow_githubDeprecated and inert. The platform is removing the GitHub/dbt flag, so those hosts need an external access integration (or a secret naming one of them).
allow_pypiDeprecated and inert. Public PyPI is in the always-on package-manager group; no flag was ever needed.
external_access_integrationsNamed external access integrations — the only per-host grant. Their network rules carry ports. Required for OAUTH2 secrets.

broker_env_var(fqn, *, key=None) returns the environment variable name that the secret broker uses for a fully qualified SECRET name. Pass key="password" or "username" for a PASSWORD-type secret.

Bundle

A Bundle is a source-tree slice uploaded to the sandbox.

bundle = Bundle.from_dir(".", include=["tools/jira-triage/**"])

Bundle.from_dir(root, *, include, exclude) constructs a bundle from a root directory, where include and exclude are glob patterns.

StageMount

A StageMount mounts a Snowflake stage or workspace into the container.

mount = StageMount.from_stage("MY_STAGE", mount_path="/data")
ws_mount = StageMount.from_workspace("my_workspace", mount_path="/ws", readonly=True)
  • StageMount.from_stage(name, *, mount_path, readonly) mounts a regular Snowflake stage.
  • StageMount.from_workspace(name, *, mount_path, readonly) mounts a Snowflake workspace.

McpServer

An McpServer declares an EXTERNAL MCP SERVER the sandbox may call. Addressing is by fully-qualified name, the same form Cortex Agents take; Snowflake resolves the endpoint, the API integration, and the calling user’s OAuth secret server-side. A bare FQN string works wherever an McpServer does.

with Sandbox.create(mcp_servers=["<your_database>.<your_schema>.GITHUB_MCP_SERVER"]) as sb:
    ...

gh = McpServer.external("<your_database>.<your_schema>.GITHUB_MCP_SERVER", name="github")
  • McpServer.external(fqn, *, name, profile) declares a server. name keys mcp.json and is uppercased into the dummy-token env var (SANDBOX_MCP_TOKEN_GITHUB); it defaults to the FQN’s last part.
  • list_mcp_servers() lists the account’s external MCP servers with your per-user authorization status (CONNECTED, NOT_CONNECTED, NEEDS_REAUTH, INTEGRATION_NOT_FOUND, UNKNOWN). Only CONNECTED servers reach a sandbox.
  • The OAuth token is not placed in the container. The platform supplies it on requests to the server’s host, the same way it handles a Secret.

DeploySpec

DeploySpec is the internal representation that the decorator API and the CLI compile to. It records the image, entry command, memory, CPU, environment, egress, secrets, bundle, and stage mounts for a deployment. You typically do not construct one directly.

The App interface

The App decorator layer lets you call a typed Python function in a sandbox with .remote(), or run it in-process with .local().

from snowflake.sandbox import App, Image, Secret, Egress

app = App("my-agent")

@app.function(
    image=Image.from_catalog("sandbox-base"),
    entry=["python3", "agent.py"],
    secrets=[Secret.from_name("<your_database>.<your_schema>.GH_TOKEN", env_var="GH_TOKEN", host="github.com")],
    egress=Egress(),
)
def agent() -> None: ...

A registered Function supports:

MethodPurpose
fn.remote(**kwargs)Deploy and run in a sandbox; returns a DeployResult.
fn.spawn(**kwargs)Start the run detached and return a live Sandbox handle; pull the result later with sb.wait().
fn.local(*args)Run the function in-process, with no sandbox or deployment.
fn.deploy_spec(**env_overrides)Return the DeploySpec this function would deploy, for review or CI.

A function can run in one of two modes:

  • Process mode: entry= is specified. fn.remote(**kwargs) passes keyword arguments as environment variable overrides, and the function body runs only when you call fn() or fn.local() directly.
  • Function mode: no entry=. The function body runs in the container; fn.remote(**kwargs) JSON-encodes the keyword arguments and runs the body inside the sandbox.

You can also register a stateful, keep-alive class with @app.session(...). The class defines on_message(self, msg) for each turn and may use the @enter decorator to mark a cold-start hook. The resulting SessionApp.session(key=...) deploys the daemon and returns a Session you drive with s.send(msg).

session_loop(handle) is the in-container half of a warm agent. handle(msg, state) is called for each incoming message; state is a plain dict that persists across calls in process memory — for as long as the process lives. It runs forever. Note that an idle auto-suspend deletes the container rather than checkpointing its memory, so state does not survive one and the loop is not restarted; keep anything you cannot recompute on the persistent rootfs (under /var/tmp), not in state and not in /tmp.

Configuration and connections

Name the connection where you construct the thing. connection= is on every entry point that does not already have a handle to carry it — Sandbox.create(), Sandbox.connect(), Sandbox.list(), list_sandboxes(), get_sandbox_by_name(), destroy_sandbox(), Job.from_id(), deploy_async(), snowflake.sandbox.deploy.deploy_spec(), Session.from_id(), agent_session() — and on both the sync and async twins:

from snowflake.sandbox import Sandbox, destroy_sandbox, list_sandboxes

sb = Sandbox.create(image="python-3.11", connection="prod_account")
sb.exec(["echo", "hi"])                                  # the handle carries it
for info in list_sandboxes(connection="prod_account"):  # no handle yet, so name it
    print(info.name)
destroy_sandbox(sb.id, connection="prod_account")

It takes one of four things (typed as ConnectionLike), mirroring the connector’s connect(connection_name=...): a connection name from ~/.snowflake/connections.toml, a live snowflake.connector connection you already opened, a Snowpark Session — what you hold inside Streamlit-in-Snowflake and stored procedures — or a Config of raw credentials for a process that has no connections file at all:

from snowflake.sandbox import Config, Sandbox

sb = Sandbox.create(connection=Config(account="my_account", pat=my_token))

# Reuse a session you already hold rather than authenticating twice:
from snowflake.snowpark.context import get_active_session
sb = Sandbox.create(connection=get_active_session())

A connection or Session you opened stays yours to close — the SDK reuses it and never closes what it did not open.

Resolution is memoised per connection and role, so passing it on every call costs one login, not one per call. Every handle returned carries the connection onward, so you name it once per handle rather than once per operation.

role= overrides the role the session uses when creating sandboxes, and is applied when the session is minted — so it composes with a connection name only. A live connection or Snowpark Session is already logged in, and raw credentials carry their own role, so passing role= with any of those is refused rather than silently ignored.

A role that cannot be applied is an error on every path — create(), and the read paths (list_sandboxes(), get_sandbox_by_name(), Sandbox.connect(), destroy_sandbox()) which used to tolerate it because role= there also carried the server-side ?role= filter, valid under any credential. role= means one thing now: the credential. The filter is still applied, as an internal narrowing consistent with that credential rather than a second meaning you can reach independently. If omitted, the role comes from the connection. A role the user does not hold is refused at login.

role is not an isolation boundary — user + account is. Listing with no role= returns every sandbox you own across all your roles, so a sandbox created under one role is still yours to see, attach to and terminate from a session at another. Passing role= narrows the server-side filter; it does not partition ownership.

using() is the scoped form, for when a sequence of calls shares one connection and repeating the argument would be noise. It binds a connection to a block:

from snowflake.sandbox import Sandbox, list_sandboxes, using

with using("prod_account"):
    for info in list_sandboxes():      # reads prod_account
        print(info.name)
    sb = Sandbox.connect("my-agent")   # attaches over prod_account

It takes the same arguments as connection= plus the client knobs, and yields the resolved config. Everything inside the block uses that connection. Nothing outside it is affected: the binding lives in a contextvars.ContextVar, so it cannot outlive the block, and two threads or asyncio tasks can hold two different connections at the same time. A sandbox handle built outside a block keeps its own connection when used inside one — using() decides where a newly acquired transport comes from, and never re-points an existing one. A per-call role= composes, minted from the block’s connection; repeating the same role is a no-op, while a different one raises SandboxError rather than silently preferring either.

Name nothing and the SDK resolves an ambient connection, the same way the snow CLI does: a credential in the environment (SNOWFLAKE_PAT / SNOWFLAKE_TOKEN, which is how a process running inside a sandbox authenticates with no config at all), else SNOWFLAKE_DEFAULT_CONNECTION_NAME or default_connection_name in ~/.snowflake/config.toml.

Those two are the process-wide default, and the SDK deliberately offers no setter of its own for it:

import os
os.environ["SNOWFLAKE_DEFAULT_CONNECTION_NAME"] = "prod_account"
# ~/.snowflake/config.toml
default_connection_name = "prod_account"

One source of truth, honoured by every Snowflake library in the process — the connector, the snow CLI, Snowpark, Streamlit — so your sandboxes and your cursors cannot drift onto different accounts. A sandbox-only global would carry the same value with a different blast radius, and a caller using the SDK alongside snowflake.connector could end up running SQL against prod and sandboxes against dev with nothing warning them.

It is read when a connection is resolved, not at import, so setting it works mid-process — including from a notebook’s first cell, which a using() block cannot span.

current_config() returns the resolved config — useful for checking which account, host, and role the SDK picked. It is a reader: it never changes what a later call resolves, and it does not report an enclosing using() block (use the value the block yields for that).

close_connections() releases everything the process resolved — the connector sessions, the resolution memo, and every connection-bound transport. It is a teardown, not an un-configure: the next call resolves again and works.

get_snowflake_connection(**kwargs) returns a Snowflake connection for code running inside a Cortex Sandbox. Prefer it for in-sandbox code, production included. It resolves the credential the way the platform provides it, in order: SNOWFLAKE_PAT or SNOWFLAKE_TOKEN in the environment; a token file named by SNOWFLAKE_TOKEN_FILE_PATH, else /snowflake/session/token, together with SNOWFLAKE_ACCOUNT; then a [default] entry in the platform-rendered $SNOWFLAKE_HOME/connections.toml. Deployments differ in which of those they set, so code that reads only one of them fails where a credential is in fact present.

It also supplies three things hand-rolled connector code usually gets wrong: SNOWFLAKE_ACCOUNT_URL as a bare host, since the connector wants no scheme; role from SNOWFLAKE_ROLE and warehouse from SNOWFLAKE_WAREHOUSE, which are otherwise not applied, leaving the connector on the user’s default role; and a default login_timeout, so a closed-egress sandbox fails fast instead of hanging. You don’t need to set up TLS trust: the sandbox already trusts the platform’s egress proxy.

The helper is for in-sandbox use only, because the credential it reads exists only inside a sandbox. It refuses keyword overrides that would redirect the credential (account, host, token), raising ValueError; non-credential options such as warehouse, database, schema, and role pass through to connect().

When you need to build the connection yourself, see the in-sandbox example in Connect to Snowflake from the SDK, which covers the host handling the helper does for you.

Exceptions

Most errors raised by the SDK derive from SandboxError. Two categories fall outside that hierarchy: configuration problems raise RuntimeError (unconfigured client, unresolvable connection), and some transport/timeout conditions surface as RuntimeError (event loop closed) or asyncio.TimeoutError. Guard those separately.

ExceptionMeaning
SandboxAuthErrorHTTP 401/403 from the Cortex Sandboxes service.
SandboxBuildErrorImage build failed.
SandboxConflictErrorHTTP 409 — the resource already exists or is in a conflicting state.
SandboxCreateErrorA create reached a terminal failed state on the server. NOT retryable.
SandboxErrorBase class for every error raised by snowflake.sandbox.
SandboxExecErrorNon-zero exit from Sandbox.exec. Carries stdout/stderr/exit_code.
SandboxExecTimeoutErrorThe server enforced its exec deadline, killing the command.
SandboxFileTooLargeErrorA file transfer exceeded the size the platform will carry.
SandboxNotFoundErrorHTTP 404 — container or resource missing.
SandboxNotImplementedErrorA surfaced SDK method has no backend and will not until one lands.
SandboxNotReadyErrorContainer not ready: HTTP 409 cold-start exhausted, or async create timed out.
SandboxRateLimitErrorHTTP 429 after the SDK retry budget.
SandboxTransportError5xx, DNS, connection, or read-timeout failures after retries.
SandboxValidationErrorHTTP 400 — the request parameters are invalid.
ShellClosedRaised when input is sent to a shell whose process has exited.

| SandboxContractWarning | The server sent something the SDK does not recognize. Emitted as a warning rather than silently guessing. |

For error handling guidance including what to do for each error, see Cortex Sandboxes error reference.