Machine API
Machine controls a microVM through either the embedded local engine or smol cloud. This page covers the public SDK class in the current smolmachines release. It is not the in-guest agent API or the standalone Cloud REST API.
TypeScript methods are asynchronous. Python’s default Machine is synchronous; use AsyncMachine when one event loop needs to drive many machines concurrently.
Create and connect
Machine.create
Creates, starts, and returns a machine.
const machine = await Machine.create(config?, connection?); machine = Machine.create(config=None, conn=None) Cloud creation requires MachineConfig.image and waits for readiness before returning. Set the target explicitly when the process may contain cloud credentials.
Machine.connect
Attaches to an existing machine without creating one.
const machine = await Machine.connect(id, connection?); machine = Machine.connect(machine_id, conn=None) For local, pass the name of a persisted machine, a paused one included. For cloud, pass the mach-... machine ID. On cloud connect() does not wait for readiness; call waitUntilReady() or wait_until_ready() before use. Locally it starts a stopped machine and returns once it is ready.
Machine.probe(connection?) (Machine.probe(conn) in Python) checks a cloud connection before any machine exists: it throws unless the key is accepted and the base URL answers with a machine list.
Identity, state, and readiness
| TypeScript | Python | Result |
|---|---|---|
machine.name | machine.name | Machine name or identifier |
await machine.state() | machine.state() | "created", "started", "running", "pausing", "paused", or "stopped" |
await machine.ready() | machine.ready() | Whether the machine can accept work |
await machine.readyAt() | machine.ready_at() | RFC 3339 ready timestamp, or null |
await machine.waitUntilReady(options?) | machine.wait_until_ready(timeout_s=120, interval_s=1) | Wait until ready or raise |
State and readiness are separate. On cloud, "started" means only that the VM process launched. ready: true means the guest agent is reachable and every published port is accepting connections. create() waits for readiness; connect() does not. Check the application’s own health endpoint separately when accepting a connection is not enough to establish application health.
TypeScript readiness options:
await machine.waitUntilReady({
timeoutMs: 120_000,
intervalMs: 1_000,
}); Execute commands
Commands are argv arrays, not shell strings.
exec
Executes a command directly in the machine. Supported on local and cloud.
const result = await machine.exec(["sh", "-lc", "echo $MODE"], {
env: { MODE: "test" },
workdir: "/workspace",
timeout: 60,
}); from smol import ExecOptions
result = machine.exec(
["sh", "-lc", "echo $MODE"],
ExecOptions(
env={"MODE": "test"},
workdir="/workspace",
timeout=60,
),
) background: true (ExecOptions(background=True)) starts the command detached, on either target, and returns at once with its pid, leaving it running after the call ends.
run
Pulls an OCI image if needed and runs a command in it. Local only.
const result = await machine.run(
"python:3.12-alpine",
["python", "-c", "print(40 + 2)"],
); result = machine.run(
"python:3.12-alpine",
["python", "-c", "print(40 + 2)"],
) For cloud, set MachineConfig.image during creation and use exec().
Stream output
execStream() / exec_stream() yields stdout, stderr, exit, and error events as they arrive. It is supported on the local target and on cloud machines. A client that is not using an SDK can read the Cloud REST API’s Server-Sent Events response directly.
for await (const event of machine.execStream(["sh", "-lc", "make test"])) {
if (event.kind === "stdout" || event.kind === "stderr") {
process.stdout.write(event.data);
}
} for event in machine.exec_stream(["sh", "-lc", "make test"]):
if event["kind"] in ("stdout", "stderr"):
print(event["data"], end="") Jobs
Cloud only. startJob(command, options?) (start_job in Python) runs a command detached and returns a Job at once; its output and exit code stay in the machine, so the caller can disconnect and come back. Unlike exec, a job survives a dropped connection, so prefer it for anything that runs longer than a few minutes. The machine does not auto-stop while a job runs.
const job = await machine.startJob(["sh", "-c", "make test"], { timeout: 3600 });
const done = await job.wait();
const stdout = await job.readAll("stdout"); job = machine.start_job(["sh", "-c", "make test"], timeout=3600)
done = job.wait()
stdout = job.read_all("stdout") The options are env, workdir, user and timeout in seconds, after which the job is stopped with SIGTERM. wait() returns the final state: exited with its exitCode, or lost when the machine stopped or restarted while the job ran. output() reads a slice of stdout or stderr from an offset, cancel() sends SIGTERM and records exit code 143, and machine.jobs() lists the machine’s jobs, newest first. A string command runs through sh -c, so the image needs sh. The routes behind these are in the API reference.
ExecResult
| TypeScript | Python | Meaning |
|---|---|---|
exitCode | exit_code | Process exit code |
stdout, stderr | stdout, stderr | UTF-8 text output |
stdoutBytes, stderrBytes | stdout_bytes, stderr_bytes | Byte output |
stdoutTruncated, stderrTruncated | stdout_truncated, stderr_truncated | Whether cloud text output hit its limit |
success | success | Exit code is zero |
output | output | Combined text output |
assertSuccess() | assert_success() | Raise ExecutionError on a nonzero exit |
Cloud responses can truncate captured text output. Check the truncation fields and use streaming execution or a file for large output.
Read and write files
Supported on local and cloud:
await machine.writeFile("/workspace/config.json", '{"enabled":true}');
const data = await machine.readFile("/workspace/config.json"); machine.write_file("/workspace/config.json", '{"enabled": true}')
data = machine.read_file("/workspace/config.json") writeFile() / write_file() accepts an optional numeric file mode.
Write to a path on the machine filesystem, such as /workspace, when the file must outlive the current run. /tmp holds its contents while the machine runs, and is empty again after a stop and start.
Images
Local only:
| TypeScript | Python | Result |
|---|---|---|
await machine.pullImage(image) | machine.pull_image(image) | Pulled ImageInfo |
await machine.listImages() | machine.list_images() | Cached ImageInfo objects |
ImageInfo contains reference, digest, size, architecture, and os.
Ports and ingress
url
url() returns the public ingress URL for the first published cloud port. It returns null / None for local machines, machines without a published port, or cloud machines whose host port is not allocated.
endpoint
Builds HTTP and WebSocket connection details for a published guest port without making a request. On a local machine it resolves to the current 127.0.0.1 host port with no headers; on cloud it uses the authenticated connect route and carries the auth header:
const { httpUrl, wsUrl, headers } = machine.endpoint(8080, "/healthz"); endpoint = machine.endpoint(8080, "/healthz")
print(endpoint.http_url, endpoint.ws_url, endpoint.headers) HTTP convenience methods
const response = await machine.fetch(8080, "/healthz"); body = machine.request(
8080,
"/healthz",
method="GET",
data=None,
timeout_s=30,
) The port must be declared in MachineConfig.ports, and the service must listen on the guest port.
Publish a port on a running machine
publish({ guest }) (publish(guest) in Python) publishes a guest port without a restart and returns the mapping in effect; without host, a free host port is picked. unpublish({ guest }) stops publishing it, and connections already open carry on. ports() lists what is published. A running local machine takes a TCP port at once; a UDP port, or a machine not on virtio-net networking, needs it stopped. On cloud the node picks the host port and ports are TCP.
Port links
Cloud only. portLinks() (port_links()) returns a login-free URL for each published port, keyed by guest port, for a client that cannot send an API key. Anyone holding a link reaches the port, so treat it as a credential. revokePortLinks() (revoke_port_links()) revokes them all, and the next call issues new ones.
Lifecycle
| TypeScript | Python | Behavior |
|---|---|---|
await machine.stop() | machine.stop() | Stops the machine without deleting storage |
await machine.start(options?) | machine.start() | Boots a stopped machine from its disk, not its previous RAM |
await machine.pause() | machine.pause() | Saves RAM and disk durably, then stops at that point |
await machine.resume(options?) | machine.resume() | Resumes the saved execution |
await machine.delete() | machine.delete() | Stops the machine and permanently deletes its storage |
Locally, stop() and delete() cut off commands and file transfers still running, which fail with SmolError code INTERRUPTED. Pass { drainSecs: 30 } (drain_secs=30) to let them finish for up to that long first; timeoutSecs (timeout_secs) sets how long the workload gets to exit after its stop signal.
A local machine belongs to the process that started it. Node and Go stop that process’s machines on SIGINT or SIGTERM and then re-raise the signal, and Python stops them when the interpreter exits. When your framework owns shutdown, set handleSignals: false in ConnectOptions (Go: NoSignalHandling) and stop or delete the machines yourself; create with detach to keep a machine running after the process ends.
start() and resume() wait for readiness; pass { waitUntilReady: false }, or wait_until_ready=False in Python, to return as soon as the request is accepted.
Three more are cloud only: info() returns the machine’s full cloud record, logs(tail) the last lines of its console log (100 by default), and exportArtifact() (export_artifact()) exports the stopped machine as a .smolmachine in your registry namespace. Machine.deleteCheckpoint(id) (Machine.delete_checkpoint(id)) deletes a durable cloud checkpoint, or a local checkpoint file by path; a cloud checkpoint a machine was restored from, or that holds a paused machine’s saved execution, is refused until that machine is gone.
Use try / finally in TypeScript:
const machine = await Machine.create(undefined, { target: "local" });
try {
await machine.exec(["echo", "work"]);
} finally {
await machine.delete();
} Python Machine is a context manager:
from smol import ConnectOptions
with Machine.create(conn=ConnectOptions(target="local")) as machine:
machine.exec(["echo", "work"]) Branching
The SDK exposes branch() on both targets. Create the source machine as branchable, branchable: true in TypeScript and branchable=True in Python, then clone its live state:
const clone = await source.branch("clone-1"); clone = source.branch("clone-1") Pass optional port mappings when a clone needs pinned host ports. Locally the clone gets the host port you name; on smol cloud the guest port is kept and the host port is chosen for you.
const clone = await source.branch("clone-1", [{ host: 18080, guest: 8080 }]); from smol import PortSpec
clone = source.branch(
"clone-1",
ports=[PortSpec(host=18080, guest=8080)],
) When ports are omitted, the control plane allocates fresh host ports so concurrent clones do not collide.
Cloud branches are node-local and require a branchable source machine. They do not provide portable checkpoints or live migration between local and cloud.
Local branching runs through the embedded engine, so it needs no CLI: a local source created branchable branches with the same call, and the child reads what the source wrote. resize() grows a machine in place (on smol cloud, cpus, memoryMb and storageGb, the machine’s one disk; memory and disk only grow), and branchBatch() / branch_batch() makes several children at
once. checkpoint() works on both targets; locally it takes the output path of the .checkpoint file to write.
Machine.list() returns the machines on a target, each with its name, state and labels. Labels are
set at create time and come back on the summary, which is how a caller finds its own machines
again:
const all = await Machine.list({ target: "local" }); A machine created with detach keeps running after the process that made it exits, and restoreCheckpoint() takes a keepIdentity flag with the same meaning as the CLI’s --keep-identity.
Configuration types
ConnectOptions
| TypeScript | Python | Description |
|---|---|---|
target | target | "local" or "cloud"; select explicitly when both are configured |
baseUrl | base_url | Cloud API base URL |
apiKey | api_key | Cloud API key |
fetch | TypeScript only: the fetch cloud requests go through, for a proxy, retries or tracing; the global fetch by default |
Cloud authentication also reads SMOL_CLOUD_TOKEN. The base URL override is SMOL_CLOUD_URL.
MachineConfig
| TypeScript | Python | Target | Description |
|---|---|---|---|
name | name | Both | Name; generated when omitted |
image | image | Both | Required for cloud; optional for local |
mounts | mounts | Local | Host-directory mounts |
ports | ports | Both | Host-to-guest or published port mappings |
resources | resources | Both | CPU, memory, disk, network, and local GPU settings |
persistent | persistent | Local | Keep the local machine record |
autoStopSeconds | auto_stop_seconds | Cloud | Stop after an idle period |
ttlSeconds | ttl_seconds | Cloud | Delete after a fixed period |
branchable | branchable | Both | Prepare as a live branch source |
env | env | Both | Workload environment at creation |
workdir | workdir | Both | Workload working directory at creation |
credentials | credentials | Both | Secrets the workload uses without seeing them |
Each credentials entry names a guest variable (envVar, env_var in Python) that holds a placeholder and the exact hosts the real value may be sent to; the value is substituted only in requests to those hosts. Locally the value stays in this process’s memory, or is read from this process’s own variable of that name at each start when omitted. On smol cloud it is stored sealed under the entry’s name, and an entry without a value uses the one already stored. methods limits a local binding to the listed HTTP methods.
ResourceSpec
| TypeScript | Python | Target | Description |
|---|---|---|---|
cpus | cpus | Both | vCPU count; omitted values use the target’s default |
memoryMb | memory_mb | Both | Memory in MiB; omitted values use the target’s default |
network | network | Both | Unrestricted guest outbound networking. Local default false. On cloud it only turns access on: omitting it and setting false both leave the control-plane default, which is open. To allow only specific destinations, set allowHosts / allowCidrs instead of this flag |
storageGb | storage_gb | Both | Storage disk size in GiB; local SDK default 20 |
overlayGb | overlay_gb | Local | Overlay disk size in GiB; local SDK default 10 |
diskDurability | disk_durability | Both | "full" (default): a guest fsync waits for the host’s disk. "deferred": writes reach it on the host’s schedule, faster for fsync-heavy work, and a host crash can lose recent writes. Go: DiskDurability; Rust: .disk_durability() |
allowHosts | allow_hosts | Both | Enable networking restricted to these hostnames and their subdomains. The gateway intercepts the machine’s DNS and allows the addresses it answers with, so a host served from another domain (a CDN) needs its own entry |
allowCidrs | allow_cidrs | Both | Enable networking restricted to these IP ranges |
gpu | gpu | Local Vulkan | Enable virtio-gpu/Venus; default false |
gpuVramMib | gpu_vram_mib | Local Vulkan | VRAM allocation; omitted values use the engine default |
cuda | cuda | Local CUDA remoting | Enable CUDA API remoting; default false |
MountSpec
Local host-directory mount:
{
source: "/absolute/host/path",
target: "/workspace",
readOnly: false,
} MountSpec(
source="/absolute/host/path",
target="/workspace",
read_only=False,
) Host mounts are local only
MountSpec binds a path on your own machine, so it applies to the local and
self-hosted targets. Smol cloud has no host filesystem to bind from, so a cloud
machine keeps its data on its own filesystem. See Cloud Lifecycle, Storage, and Networking.
Set readOnly / read_only explicitly for every mount. Use read-only mounts for source code and writable mounts only for directories the guest must change.
PortSpec
{ host: 8080, guest: 8080 } PortSpec(host=8080, guest=8080) Python AsyncMachine
Use AsyncMachine when synchronous calls would block an event loop:
import asyncio
from smol import AsyncMachine, ConnectOptions, MachineConfig
async def main():
async with await AsyncMachine.create(
MachineConfig(image="alpine:3.20"),
ConnectOptions(target="cloud"),
) as machine:
result = await machine.exec(["echo", "hello"])
print(result.stdout)
asyncio.run(main()) It mirrors the synchronous API with awaitable I/O methods. endpoint() remains synchronous because it only builds connection data.
Errors
SDK errors derive from SmolError and include a machine-readable code. For handling patterns and exit-code semantics, see the Error handling guide.
ExecutionError: a command assertion failedNotSupportedError: the selected target does not support the operationInvalidConfigError: the configuration is invalid