# Daytona Documentation (/docs/quickstart) Daytona is secure and elastic cloud infrastructure built for autonomous agents. Daytona provides **composable computers for agents** — [sandboxes](/docs/sandboxes) — virtual machines with hardware isolation, a dedicated kernel, filesystem, network, and allocated vCPU, RAM, and disk. Sandboxes are the core component of the Daytona platform, spinning up in under 90ms from code to execution and running any code in Python, TypeScript, and JavaScript. Built on OCI/Docker compatibility and massive parallelization, sandboxes deliver consistent, predictable environments for AI, autonomous agents, and reinforcement learning. ## Get started [#get-started] 1. Create an account → [app.daytona.com](https://app.daytona.com) 2. Get an API key → [app.daytona.com/dashboard/keys](https://app.daytona.com/dashboard/keys) 3. Save the API key → `DAYTONA_API_KEY` environment variable or in a `.env` file. ```bash DAYTONA_API_KEY=YOUR_API_KEY ``` ```bash export DAYTONA_API_KEY=YOUR_API_KEY ``` ```powershell $env:DAYTONA_API_KEY="YOUR_API_KEY" ``` 4. Install the Python SDK ```bash pip install daytona==2.0.0a11 ``` 5. Create a sandbox and run code ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() response = sandbox.run_code('print("Hello World")') print(response.logs.stdout) ``` 4. Install the TypeScript SDK ```bash npm install @daytona/sdk@2.0.0-alpha.11 ``` 5. Create a sandbox and run code ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const response = await sandbox.runCode('print("Hello World")') console.log(response.logs.stdout) ``` 4. Install the CLI ```bash brew install daytonaio/cli/daytona-alpha ``` ```bash # Installs the newest 2.x release for this host after verifying its checksum curl -fsSL https://github.com/daytona/clients/releases/download/v2.0.0-alpha.11/install.sh | sh ``` ```powershell irm https://github.com/daytona/clients/releases/download/v2.0.0-alpha.11/install.ps1 | iex ``` 5. Log in ```bash daytona login --token "$DAYTONA_API_KEY" --api-url https://api.daytona.com ``` ```powershell daytona login --token "$env:DAYTONA_API_KEY" --api-url https://api.daytona.com ``` 6. Create a sandbox and run code ```bash daytona sandbox create daytona sandbox exec {sandboxId} -- python -c 'print("Hello World")' ``` 4. Create a sandbox and run code ```bash # Create sandbox curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header "Authorization: Bearer $DAYTONA_API_KEY" \ --data '{}' # Wait until running curl 'https://api.daytona.com/sandboxes/{sandboxId}?wait=running&timeout_ms=60000' \ --header "Authorization: Bearer $DAYTONA_API_KEY" # Run code; {toolboxProxyUrl} is toolbox_proxy_url from the sandbox response curl '{toolboxProxyUrl}/{sandboxId}/processes?waitMs=30000&logs=text' \ --request POST \ --header 'Content-Type: application/json' \ --header "Authorization: Bearer $DAYTONA_API_KEY" \ --data '{"kind": "code", "language": "python", "code": "print(\"Hello World\")"}' ``` ## Reference [#reference] Daytona provides a comprehensive set of resources for developers and agents to get started and build on the platform. You can find the following resources in the documentation: - **Daytona Sandboxes**: [Linux](/docs/sandboxes#linux-sandboxes), [Windows](/docs/sandboxes#windows-sandboxes), [GPU](/docs/sandboxes#gpu-sandboxes), [macOS](/docs/sandboxes#macos-sandboxes) - **Daytona SDKs**: [TypeScript](/docs/typescript-sdk), [Python](/docs/python-sdk) - **Daytona APIs**: [Platform](/docs/api/daytona) ([OpenAPI](/docs/openapi.json)), [Toolbox](/docs/api/toolbox) ([OpenAPI](/docs/toolbox-openapi.json)) - **Daytona CLI**: [Mac/Linux/Windows](/docs/cli) - **Daytona LLMs**: [llms.txt](https://www.daytona.com/llms.txt), [llms-full.txt](https://www.daytona.com/llms-full.txt) - **Daytona Markdown**: suffix pages with `.md` (e.g [sandboxes.md](/docs/sandboxes.md)) # Authentication (/docs/authentication) Daytona authenticates SDK, CLI, and API requests with a project [API key](/docs/api-keys). The key identifies the [project](/docs/projects) that owns the sandboxes you create and carries the [permissions](/docs/api-keys#scopes) a request may use. ## Get an API key [#get-an-api-key] 1. Go to [Daytona Dashboard ↗](https://app.daytona.com/dashboard/keys) 2. Select the project in the header switcher 3. Click 4. Enter 5. Select permission 6. Click 7. Copy and save the API key as an environment variable ## Environment variables [#environment-variables] Daytona resolves configuration in order of precedence: 1. Configuration in code 2. Environment variables | **Variable** | **Description** | | ------------------------- | ---------------------------------------------------------------------------------------------- | | **`DAYTONA_API_KEY`** | Your Daytona API key.
Required unless `DAYTONA_JWT_TOKEN` is set | | **`DAYTONA_JWT_TOKEN`** | A JWT instead of an API key.
Optional; needs `DAYTONA_PROJECT_ID` | | **`DAYTONA_PROJECT_ID`** | The project a JWT is scoped to.
Required with `DAYTONA_JWT_TOKEN` | ### .env file [#env-file] ```bash DAYTONA_API_KEY=YOUR_API_KEY ``` ### Shell [#shell] ```bash export DAYTONA_API_KEY=YOUR_API_KEY ``` ```powershell $env:DAYTONA_API_KEY="YOUR_API_KEY" ``` ## Authenticate the SDK [#authenticate-the-sdk] Construct the client without arguments to read the environment variables, or pass the configuration in code. Every call made through the client sends the API key or, with a JWT, a short-lived project token the client exchanges the JWT for. ```python from daytona import Daytona, DaytonaConfig # Using environment variables daytona = Daytona() # Using explicit configuration config = DaytonaConfig(api_key="YOUR_API_KEY") daytona = Daytona(config) # Pick the region per create sandbox = daytona.v2.create(region="earth") ``` | **Parameter** | **Type** | **Description** | | ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `api_key` | str | Falls back to `DAYTONA_API_KEY`. When neither this nor `jwt_token` is set, the first access to `daytona.v2` raises `UnauthorizedError`. | | `jwt_token` | str | A JWT instead of an API key. Falls back to `DAYTONA_JWT_TOKEN`. Needs `project_id`; setting both `api_key` and `jwt_token` raises `BadRequestError`. | | `project_id` | str | The project a JWT is scoped to. Falls back to `DAYTONA_PROJECT_ID`. | | `request_timeout` | float | Per-request HTTP timeout in seconds. `0` disables the timeout. | ```typescript import { Daytona } from '@daytona/sdk' // Using environment variables const daytona = new Daytona() // Using explicit configuration const daytonaWithConfig = new Daytona({ apiKey: 'YOUR_API_KEY' }) // Pick the region per create const sandbox = await daytonaWithConfig.v2.create({ region: 'earth' }) ``` | **Parameter** | **Type** | **Description** | | ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `apiKey` | string | Falls back to `DAYTONA_API_KEY`. When neither this nor `jwtToken` is set, the first access to `daytona.v2` throws `DaytonaAuthenticationError`. | | `jwtToken` | string | A JWT instead of an API key. Falls back to `DAYTONA_JWT_TOKEN`. Needs `projectId`; setting both `apiKey` and `jwtToken` throws `DaytonaValidationError`. | | `projectId` | string | The project a JWT is scoped to. Falls back to `DAYTONA_PROJECT_ID`. | | `requestTimeout` | number | Per-request HTTP timeout in milliseconds. `0` disables the timeout. | ### Close the client [#close-the-client] Close the client when your program no longer needs it. The client holds network resources between calls, such as the connection that reports sandbox state changes, and closing it releases them. Closing does not stop or delete any sandbox. Closing an already closed client does nothing. ```python from daytona import Daytona # Close the client when the block exits with Daytona() as daytona: sandbox = daytona.v2.create() # Close the client explicitly daytona = Daytona() sandbox = daytona.v2.create() daytona.close() ``` - Use `AsyncDaytona` with `async with` or `await daytona.close()` in asynchronous code. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() // Close the client explicitly await daytona.close() ``` - Declare the client with `await using` to close it when the scope exits. ## Authenticate the CLI [#authenticate-the-cli] Sign in once and the CLI saves the session in its active profile, so following commands need no key or flags. After a browser or device login, the CLI selects your personal organization and its default project the first time and remembers your last selection afterwards. Sandbox commands use a short-lived platform token where the deployment issues one. Toolbox access, and sandbox commands on other deployments, use a project API key the CLI mints on your behalf with a 24-hour expiry; logging out attempts to revoke these keys. ```bash # Browser login daytona login --api-url https://api.daytona.com # Device-code login on a host without a browser daytona login --device --api-url https://api.daytona.com # Store an API key in the active profile daytona login --token YOUR_API_KEY --api-url https://api.daytona.com # Confirm who you are and what is selected daytona whoami # Create a sandbox with the saved session daytona sandbox create # Remove the saved session from this machine daytona logout ``` ```bash # Using environment variables (DAYTONA_API_KEY, DAYTONA_API_URL) daytona sandbox create # Using explicit flags daytona sandbox create --api-key YOUR_API_KEY --api-url https://api.daytona.com ``` | **Flag** | **Type** | **Description** | | ----------- | -------- | ------------------------------------------------------------------------------------------------ | | `--api-key` | string | Takes precedence over `DAYTONA_API_KEY`. Either one replaces the saved login for that command. | | `--api-url` | string | Takes precedence over `DAYTONA_API_URL`, and either one overrides the URL stored in the profile. | | `--profile` | string | Selects a profile other than the active one. `DAYTONA_PROFILE` does the same. | ### Profiles [#profiles] A profile is a named connection: an API URL, a credential, and the selected organization and project. One machine can keep several profiles, for example one per organization or deployment, and switch between them without signing in again. A command uses the profile it names, then the profile in the environment, then the active profile, then the profile named `default`. Signing in to a new profile creates it, and makes it active only when no profile is active yet. Deleting the active profile makes `default` active when it exists, otherwise no profile is active. ```bash # Sign in to a second profile daytona login --profile work --api-url https://api.daytona.com # List profiles daytona profile list # Run one command with another profile daytona sandbox list --profile work # Make a profile active for following commands daytona profile use work # Delete a profile and its saved credentials daytona profile delete work ``` | **Argument** | **Type** | **Description** | | ------------ | -------- | --------------- | | `name` | string | Sets the profile name: from `1` to `64` ASCII letters, digits, `.`, `_`, or `-`, excluding `.` and `..`. This argument is required. | The output of the list has the following columns: | **Column** | **Description** | | ------------ | ----------------------------------------------------------------------- | | (unnamed) | `*` for the active profile. | | `NAME` | Profile name. | | `API URL` | The API URL the profile connects to. | | `CREDENTIAL` | How the profile authenticates: `oidc` for a browser or device login, `token` for a static credential stored with `--token`, such as a project or organization API key. | | `ORG` | ID of the selected organization. | | `PROJECT` | ID of the selected project. | - A profile named by `--profile` or `DAYTONA_PROFILE` must exist, except in `daytona login`, which creates it. - Deleting a profile attempts to revoke its CLI-minted project keys. The profile is deleted from the machine even when the revocation fails, and the keys expire after 24 hours. ### Confirmation prompts [#confirmation-prompts] At a terminal, the CLI asks for confirmation before destructive and billable commands. The setting applies to every profile on the machine. Turning it off removes the prompt only: when the CLI is not attached to a terminal, prints JSON or YAML, or runs with the `CI` environment variable set, those commands still require an explicit confirmation. ```bash # Turn off confirmation prompts daytona config set confirm false # Print the current value daytona config get confirm # Print the directory that holds profiles, credentials, and settings daytona config path ``` | **Setting** | **Type** | **Description** | | ----------- | -------- | --------------- | | `confirm` | bool | Sets whether destructive and billable commands ask for confirmation at a terminal: `true` or `false`. The default is `true`. | - Pass `--yes` or set `DAYTONA_YES=1` to confirm a command without a prompt, for example in scripts and CI. - The config directory is `DAYTONA_CONFIG_DIR` when set, else `$XDG_CONFIG_HOME/daytona`, else `~/.config/daytona`. ## Authenticate API requests [#authenticate-api-requests] Send the API key as a bearer token in the authorization header of every request. ```bash curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{}' ``` # Sandboxes (/docs/sandboxes) A sandbox is a hardware-isolated virtual machine with its own kernel, filesystem, network stack, and allocated vCPU, RAM, and disk, with isolation enforced by the hypervisor. Sandboxes run as [Linux](#linux-sandboxes) (default), [Windows](#windows-sandboxes), [macOS](#macos-sandboxes), or [GPU](#gpu-sandboxes)-accelerated virtual machines. Every sandbox moves through a lifecycle and can have several different states. While the sandbox is running, it exposes a toolbox for code, command, and process execution, sessions, filesystem and git operations, computer use, language server protocol support, and log streaming. Use sandboxes to run code written by an agent, build and test software, train or serve a model on a dedicated GPU, drive desktop through screenshots and input events, or spin up thousands of identical environments for autonomous, evaluation, and reinforcement learning workflows. | **Concept** | **Description** | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Sandbox** | An isolated, dedicated virtual machine. | | **Image** | The OCI image, Dockerfile build, or snapshot the sandbox boots from. | | **Resources** | The vCPU, memory, and disk allocated to the sandbox. | | **Lifecycle** | The states a sandbox can be in and the transitions between them. | | **Persistence** | Deleting a sandbox discards its filesystem. Persistent macOS sandboxes can stop/start, pause/resume, and fork. | | **Toolbox** | A set of features for code, command, and process execution, sessions, filesystem and git operations, computer use, and language server protocol support. | ## Create sandboxes [#create-sandboxes] Daytona provides methods to create sandboxes from the dashboard or programmatically. 1. Go to [Daytona Sandboxes ↗](https://app.daytona.com/dashboard/sandboxes) 2. Click 3. Select , , , or 4. Click ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() ``` ```bash daytona sandbox create ``` ```bash curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{}' ``` ### Linux sandboxes [#linux-sandboxes] [Linux sandboxes](/docs/linux-sandboxes) are Firecracker microVMs that run their own Linux kernel, and they are the default sandbox type. A Linux sandbox boots from an image that you import from a container registry, build from a Dockerfile, or capture from another sandbox. It can also start from a snapshot of another Linux sandbox. Each sandbox runs on an amd64 or arm64 CPU. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() ``` ```bash daytona sandbox create ``` ```bash curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{}' ``` ### Windows sandboxes [#windows-sandboxes] [Windows sandboxes](/docs/windows-sandboxes) are virtual machines running Windows Server 2022. A Windows sandbox starts from a prepared Windows base image, or from an image captured from another Windows sandbox. Commands run in PowerShell, and the desktop is reached over the remote desktop protocol. ### macOS sandboxes [#macos-sandboxes] [macOS sandboxes](/docs/macos-sandboxes) are virtual machines running macOS on Apple silicon Mac computers. A macOS sandbox starts from a macOS image. A create can set the sandbox's cores, memory and disk, up to the whole Mac. By default it gets half of the Mac's cores, memory and sandbox disk, so two sandboxes can share one Mac. ### GPU sandboxes [#gpu-sandboxes] [GPU sandboxes](/docs/gpu-sandboxes) have one or more dedicated NVIDIA or AMD GPUs attached for workloads that require GPU acceleration, model inference, training, fine-tuning, and reinforcement learning. A GPU sandbox is ephemeral and can be saved as a checkpoint that includes its processes, memory, and GPU memory, and resumed in a new GPU sandbox. - **NVIDIA H100** - **NVIDIA H200** - **NVIDIA B200** - **NVIDIA B300** - **NVIDIA RTX Pro 6000** - **NVIDIA RTX 4090** - **NVIDIA RTX 5090** - **NVIDIA L40S** - **AMD Instinct MI355X** ## Resources [#resources] Linux sandboxes have **0.5 cores**, **1024MiB memory**, and **512MiB disk** by default. A Linux sandbox may guarantee at most **16 cores**, **32GiB memory**, and **256GiB disk**. A macOS sandbox takes its [size](/docs/macos-sandboxes#sizes) from its Mac reservation. CPU is measured and billed in cores, from fractions of a core up to many cores. One core is **2 vCPUs** where simultaneous multithreading (SMT, also called hyperthreading) is available and enabled, and **1 vCPU** otherwise. Each resource has a guarantee and a limit. A sandbox can always use its guarantee and takes more as its workload needs it, up to the limit, with no manual resize. The **guarantee** is the CPU, memory, and disk that Daytona reserves for the sandbox on its host. It is billed as a minimum, whether the sandbox uses it or not. The CPU guarantee is at least **1 core** for every **8GiB** of guaranteed memory. When a create guarantees more memory than its CPU guarantee covers, Daytona raises the CPU guarantee to that floor, so a sandbox that guarantees **16GiB** memory reserves and is billed for at least **2 cores**. | **Resource** | **Unit** | **Default guarantee** | **Minimum guarantee** | **Maximum guarantee** | | ------------ | -------- | --------------------- | --------------------------------- | --------------------- | | CPU | Cores | `0.5` | **`0.125`** (when set explicitly) | **`16`** | | Memory | MiB | `1024` | **`512`** (when set explicitly) | **`32768`** | | Disk | MiB | `512` | **`10240`** (when set explicitly) | **`262144`** | The **limit** is the most the sandbox can use. You set it when you create the sandbox. When a create sets no limit, the limit is the platform maximum or the cap of the organization's [plan](/docs/limits#sandbox-limits), whichever is lower. A limit equal to its guarantee gives the sandbox a fixed size that does not grow. | **Resource** | **Unit** | **Default limit** | **Minimum limit** | **Maximum limit** | | ------------ | -------- | ---------------------------- | ------------------------------- | ----------------- | | CPU | Cores | Platform maximum or plan cap | **`0.5`** (a multiple of `0.5`) | **`16`** | | Memory | MiB | Platform maximum or plan cap | **`256`** | **`32768`** | | Disk | MiB | Platform maximum | **`512`** | **`262144`** | ```python from daytona import Daytona from daytona.v2 import Resources daytona = Daytona() sandbox = daytona.v2.create( # memory and disk are GiB: 2 maps to memory_mib=2048, 20 to disk_mib=20480 resources=Resources(cpu=1, memory=2, disk=20, cpu_max=2, memory_max=4, disk_max=40), ) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ------------------------------------------------------------------- | | `cpu` | float | Sets the guaranteed CPU in cores. | | `memory` | int | Sets the guaranteed memory in GiB. | | `disk` | int | Sets the guaranteed disk in GiB. | | `cpu_max` | float | Sets the CPU limit in cores, a multiple of `0.5`. | | `memory_max` | float | Sets the memory limit in GiB. | | `disk_max` | float | Sets the disk limit in GiB. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create({ // memory and disk are GiB: 2 maps to memory_mib=2048, 20 to disk_mib=20480 resources: { cpu: 1, memory: 2, disk: 20, cpuMax: 2, memoryMax: 4, diskMax: 40 }, }) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ------------------------------------------------------------------- | | `cpu` | number | Sets the guaranteed CPU in cores. | | `memory` | number | Sets the guaranteed memory in GiB. | | `disk` | number | Sets the guaranteed disk in GiB. | | `cpuMax` | number | Sets the CPU limit in cores, a multiple of `0.5`. | | `memoryMax` | number | Sets the memory limit in GiB. | | `diskMax` | number | Sets the disk limit in GiB. | ```bash # --memory, --disk, --memory-max, and --disk-max are MiB daytona sandbox create --cpu 1 --memory 2048 --disk 20480 \ --cpu-max 2 --memory-max 4096 --disk-max 40960 ``` | **Flag** | **Type** | **Description** | | -------------- | -------- | ------------------------------------------------------------------- | | `--cpu` | float | Sets the guaranteed CPU in cores. | | `--memory` | int | Sets the guaranteed memory in MiB. | | `--disk` | int | Sets the guaranteed disk in MiB. | | `--cpu-max` | float | Sets the CPU limit in cores, a multiple of `0.5`. | | `--memory-max` | int | Sets the memory limit in MiB. | | `--disk-max` | int | Sets the disk limit in MiB. | ```bash curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "cpu": 1, "memory_mib": 2048, "disk_mib": 20480, "cores_max": 2, "memory_max_mib": 4096, "disk_max_mib": 40960 }' ``` | **Field** | **Type** | **Description** | | ---------------- | -------- | ---------------------------------------------------------------- | | `cpu` | number | Sets the guaranteed CPU in cores. | | `memory_mib` | integer | Sets the guaranteed memory in MiB. | | `disk_mib` | integer | Sets the guaranteed disk in MiB. | | `cores_max` | number | Sets the CPU limit in cores, a multiple of `0.5`. | | `memory_max_mib` | integer | Sets the memory limit in MiB. | | `disk_max_mib` | integer | Sets the disk limit in MiB. | ## Regions [#regions] Every sandbox runs in a [region](/docs/regions), a geographic or logical group of compute infrastructure. A create request can name a region, and Daytona places the sandbox on available capacity inside it. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create(region="earth") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create({ region: 'earth' }) ``` ```bash daytona sandbox create --region earth ``` ```bash curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "region_id": "earth" }' ``` ## Hardware virtualization [#hardware-virtualization] Server processors include instructions for running virtual machines directly on the hardware. A virtual machine monitor uses these instructions to create a guest with its own virtual processors, memory, and devices. The processor rejects any access from a guest to host memory or to another guest's memory. Linux, Windows, and macOS sandboxes run as virtual machines, each on the virtual machine monitor that its operating system needs. | **Sandbox** | **Host** | **Runtime** | | ----------------------------------------- | ---------------------- | ---------------------------------------- | | [**Linux**](/docs/linux-sandboxes) | Linux with KVM | Firecracker virtual machine | | [**Windows**](/docs/windows-sandboxes) | Linux with KVM | Cloud Hypervisor virtual machine | | [**macOS**](/docs/macos-sandboxes) | Mac with Apple silicon | Virtualization framework virtual machine | **Linux** exposes the processor's virtualization through the Kernel-based Virtual Machine (KVM). Linux sandboxes run on Firecracker, a virtual machine monitor built to run many small, short-lived virtual machines on one host. Firecracker provides only the devices a server needs: virtual disks, a network card, and a serial console. Each Linux sandbox runs as one Firecracker process on a host, in its own network namespace, with its own virtual processors and memory. **Windows** sandboxes also use KVM, but Firecracker does not support Windows guests. Windows sandboxes run on **Cloud Hypervisor**, a virtual machine monitor that also keeps its device model small but can start an operating system from firmware. Each Windows sandbox runs as one Cloud Hypervisor process on a host, in its own network namespace, with its own virtual processors and memory. Cloud Hypervisor provides each Windows sandbox with the following: - UEFI firmware, which loads the Windows boot manager from the sandbox's disk - Hyper-V enlightenments: interfaces Windows uses to run efficiently as a guest - Virtual disks and a virtual network card, for which the base image carries Windows drivers - A serial console for management, and no graphics card **macOS sandboxes** run on Macs, because Apple's license allows macOS virtual machines only on Apple hardware. Apple silicon includes hardware support for virtual machines, which macOS exposes through Apple's Virtualization framework. Each macOS sandbox runs as its own virtual machine on its Mac, with its own virtual processors, memory, disk, and network card. ## Sandbox lifecycle [#sandbox-lifecycle] Every sandbox moves through a lifecycle. A sandbox can have several different states. Each state reflects the status of your sandbox. A sandbox transitions between states in response to user actions, or automatically when one of its timeouts elapses. See the lifecycle of [Linux](/docs/linux-sandboxes#lifecycle), [Windows](/docs/windows-sandboxes), [GPU](/docs/gpu-sandboxes), and [macOS](/docs/macos-sandboxes#lifecycle) sandboxes. | **Feature** | **Linux** | **Windows** | **macOS** | **GPU** | | -------------------------- | --------------- | ---------------- | ---------------- | ------------ | | **Stop and start** | ✗ | ✓ persistent | ✓ persistent | ✗ | | **Pause and resume** | ✗ | ✗ | ✓ persistent | ✗ | | **Fork** | ✗ | ✗ | ✓ persistent | ✗ | | **Delete** | ✓ | ✓ | ✓ | ✓ | | **Filesystem persistence** | ✗ | ✓ | ✓ | ✗ | | **Memory persistence** | ✗ | ✗ | ✓ persistent | ✗ | | **Memory snapshot** | ✓ warm template | ✗ | ✗ | ✓ checkpoint | | **Filesystem snapshot** | ✓ | ✓ block snapshot | ✓ persistent | ✗ | | **Spot (Preemption)** | ✓ | ✓ | ✗ | ✗ | | **Network policy** | ✓ | ✗ | ✓ | ✗ | | **Desktop** | ✓ | ✓ RDP | ✓ Screen Sharing | ✗ | The following tables list the states of a Linux sandbox and the transitions between them. The SDKs report the states in the **State** column. The CLI and API report the more detailed status in the **API status** column. | **State** | **API status** | **Description** | | ---------------- | ------------------------------ | ------------------------------------------------------------------------------- | | **`queued`** | **`queued`** | The create request is recorded and waits to be published for placement. | | **`creating`** | **`pending`**, **`assigned`** | The create request is accepted, and the sandbox is queued for and placed on a host. | | **`starting`** | **`booting`** | The microVM is booting and will be ready to use. | | **`started`** | **`running`** | The sandbox is running and ready to accept requests. | | **`destroying`** | **`deleting`** | The sandbox is being deleted. | | **`destroyed`** | **`deleted`** | The sandbox and its storage are removed. | | **`error`** | **`failed`**, **`lost`** | The sandbox could not boot, hit an unrecoverable error, or its host was lost. | | **`unknown`** | | The SDK does not recognize the status the API reports. | A sandbox can transition between states in response to various actions. The following table lists the initial state, target state, and trigger for the transition. | **Initial state** | **Target state** | **Trigger** | | ----------------- | ---------------- | ---------------------------------------------------------------------------------- | | **`queued`** | **`creating`** | The create request is published for placement. | | **`creating`** | **`starting`** | The sandbox is placed on a host, the host prepares its image, and the microVM boots. | | **`starting`** | **`started`** | The sandbox finishes booting and accepts requests. | | any state | **`destroying`** | A delete is requested, a timeout elapses, or the platform reclaims a spot sandbox. | | **`destroying`** | **`destroyed`** | The sandbox is removed and its resources are released. | | any active state | **`error`** | The sandbox fails to boot, hits an unrecoverable error, or its host is lost. | ## Sandbox operations [#sandbox-operations] Daytona provides methods to manage existing sandboxes. Sandbox operations find sandboxes, read their current state and configuration, and update their metadata. They do not change the state of a sandbox or touch anything running inside it. To stop, pause, fork, or delete a sandbox, see [lifecycle](#sandbox-lifecycle). To run commands or work with files inside a sandbox, use the toolbox. ### List sandboxes [#list-sandboxes] List sandboxes in the project, page by page. Unfiltered lists read the region-local database, so new sandboxes appear at once. To filter by name, state, labels, or resources, use [search](#search-sandboxes). ```python from daytona import Daytona daytona = Daytona() page = daytona.v2.list() for sandbox in page.items: print(sandbox.id, sandbox.name, sandbox.state) ``` - The request requires the `read:sandboxes` permission. - `page.next_cursor` is `None` on the last page. - Pass `next_cursor` back as `cursor` for the next page. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const page = await daytona.v2.list() for (const sandbox of page.items) { console.log(sandbox.id, sandbox.name, sandbox.state) } ``` - The request requires the `read:sandboxes` permission. - `page.nextCursor` is unset on the last page. - Pass `nextCursor` back as `cursor` for the next page. ```bash daytona sandbox list ``` | **Flag** | **Type** | **Description** | | ---------- | -------- | ----------------------------------- | | `--limit` | int | Sets the page size. The default is `100`. | | `--cursor` | string | Continues from a previous page. | ```bash curl 'https://api.daytona.com/sandboxes' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Type** | **Description** | | --------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `limit` | integer | Sets the page size, from `1` to `200`. The default is `100`. | - The request requires the `read:sandboxes` permission. - Pass `next_cursor` from the response as `cursor` to read the next page. ##### Iterate over all sandboxes [#iterate-over-all-sandboxes] Iterate over every sandbox in the project without handling page cursors. The iterator fetches the next page when the current one is exhausted and stops when the server returns no cursor. ```python from daytona import Daytona daytona = Daytona() for sandbox in daytona.v2.iter(): print(sandbox.id, sandbox.name, sandbox.state) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() for await (const sandbox of daytona.v2.iter()) { console.log(sandbox.id, sandbox.name, sandbox.state) } ``` ```bash # Follow every page until the server returns no cursor daytona sandbox list --all ``` ### Search sandboxes [#search-sandboxes] Search sandboxes across every region. Search reads a central index that is eventually consistent: a create, update, or delete becomes searchable about 2 to 3 seconds after it is committed, so a sandbox created a moment ago may be missing. To read your own writes, use [list](#list-sandboxes) without filters or [get](#get-a-sandbox). Filters combine with AND, and deleted sandboxes are left out of the results. ```python from daytona import Daytona daytona = Daytona() # Free text over name and image, or an exact sandbox id page = daytona.v2.search(q="my-sandbox") for sandbox in page.items: print(sandbox.id, sandbox.name, sandbox.state) ``` | **Parameter** | **Type** | **Description** | | ------------- | --------------- | --------------------------------------------------------------------------------------- | | `status` | Sequence\[str\] | Filters by state. Pass API state names such as `running`, not the SDK names returned in `sandbox.state`. | The response has the following fields: | **Field** | **Description** | | ----------------- | ---------------------------------------------------------------------------------------- | | `next_cursor` | The cursor is tied to the filters and sort of its call. | | `partial_regions` | Lists the regions that are backfilling. Results from these regions can be incomplete. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() // Free text over name and image, or an exact sandbox id const page = await daytona.v2.search({ q: 'my-sandbox' }) for (const sandbox of page.items) { console.log(sandbox.id, sandbox.name, sandbox.state) } ``` | **Parameter** | **Type** | **Description** | | ------------- | ------------------------------ | --------------------------------------------------------------------------------- | | `status` | string \| readonly string\[\] | Filters by state. Pass API state names such as `running`, not the SDK names returned in `sandbox.state`. | The response has the following fields: | **Field** | **Description** | | ---------------- | ---------------------------------------------------------------------------------------- | | `nextCursor` | The cursor is tied to the filters and sort of its call. | | `partialRegions` | Lists the regions that are backfilling. Results from these regions can be incomplete. | ```bash # Free text over name and image, or an exact sandbox id daytona sandbox search my-sandbox ``` | **Flag** | **Type** | **Description** | | ---------- | -------- | ---------------------------------------------------------- | | `--status` | string | Repeatable or comma-separated. Matches any listed value. | | `--class` | string | Repeatable or comma-separated. Matches any listed value. | - The request requires the `read:sandboxes` permission. - The query is optional. - Filter flags such as `--status`, `--class`, `--name-prefix`, and `--labels` select sandboxes on their own or narrow the query. ```bash # Free text over name and image, or an exact sandbox id curl 'https://api.daytona.com/sandboxes/search?q=my-sandbox' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Type** | **Description** | | --------- | --------- | ------------------------------------- | | `limit` | integer | Sets the page size, from `1` to `200`. The default is `100`. | | `sort` | string | Sets the sort field. The default is `created_at`. | | `order` | string | Sets the sort order. The default is `desc`. | - The request requires the `read:sandboxes` permission. - A `cursor` is tied to the filters and sort of its request. ### Get a sandbox [#get-a-sandbox] Get a sandbox by its ID. The response includes the current state, resources, labels, and lifecycle settings. Get reads the control plane directly, so it reflects changes immediately, unlike [search](#search-sandboxes). ```python from daytona import Daytona daytona = Daytona() # Fetch the sandbox from the control plane sandbox = daytona.v2.get("{sandboxId}") print(sandbox.state, sandbox.cpu, sandbox.memory, sandbox.disk) # Store the sandbox data as JSON; it carries no URL or credential saved = sandbox.to_json() # {"sandboxId": "...", "name": "...", "state": "...", ...} # Later, get the sandbox again by its ID sandbox = daytona.v2.get(saved["sandboxId"]) ``` The response has the following fields: | **Field** | **Description** | | --------- | ------------------------------------------------------------------------------------------- | | `memory` | Gives the memory in GiB, rounded up to a whole GiB. The API reports memory in MiB as `memory_mib`. | | `disk` | Gives the disk in GiB, rounded up to a whole GiB. The API reports disk in MiB as `disk_mib`. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() // Fetch the sandbox from the control plane const sandbox = await daytona.v2.get('{sandboxId}') console.log(sandbox.state, sandbox.cpu, sandbox.memory, sandbox.disk) // Store the sandbox data as JSON; it carries no URL or credential const saved = sandbox.toJSON() // { sandboxId: '...', name: '...', state: '...', ... } // Later, get the sandbox again by its ID const again = await daytona.v2.get(saved.sandboxId) ``` The response has the following fields: | **Field** | **Description** | | --------- | ------------------------------------------------------------------------------------------- | | `memory` | Gives the memory in GiB, rounded up to a whole GiB. The API reports memory in MiB as `memory_mib`. | | `disk` | Gives the disk in GiB, rounded up to a whole GiB. The API reports disk in MiB as `disk_mib`. | ```bash daytona sandbox get {sandboxId} ``` ```bash curl 'https://api.daytona.com/sandboxes/{sandboxId}' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ##### Refresh a sandbox [#refresh-a-sandbox] Refresh a sandbox object with its current data. A sandbox object keeps the fields from the request that returned it, so its state, resources, and labels can be out of date after a change made by another client. Refresh reads the sandbox from the control plane and updates the object in place. Lifecycle operations such as start and stop refresh the object when they finish. ```python # Re-read the sandbox from the control plane sandbox.refresh_data() print(sandbox.state, sandbox.labels) ``` | **Parameter** | **Type** | **Description** | | ------------- | --------- | ---------------------------------------------------------------------------------- | | `timeout` | float | Sets the request timeout in seconds. By default, the client request timeout applies. | ```typescript // Re-read the sandbox from the control plane await sandbox.refreshData() console.log(sandbox.state, sandbox.labels) ``` ### Find sandboxes by name [#find-sandboxes-by-name] Find sandboxes by the name they were created with. Names are not unique, so the result is a list, newest first, and it is empty when no sandbox matches. The lookup reads the [search](#search-sandboxes) index, so a sandbox created a moment ago can be missing. The name is stored as the name label. Replacing a sandbox's labels without it removes the sandbox from name lookups. ```python from daytona import Daytona daytona = Daytona() # Reuse a named sandbox, or create it when none exists found = daytona.v2.find_by_name("my-sandbox") sandbox = found[0] if found else daytona.v2.create(name="my-sandbox") ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------------------------------------------------------------------------------------------- | | `name` | str | Matches the sandbox name exactly. This parameter is required. A blank name raises `BadRequestError` before the request is sent. | | `region` | str | Sets the region to search. `"local"` or omitting it means the region of the API host. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() // Reuse a named sandbox, or create it when none exists const found = await daytona.v2.findByName('my-sandbox') const sandbox = found[0] ?? (await daytona.v2.create({ name: 'my-sandbox' })) ``` | **Parameter** | **Type** | **Description** | | ---------------- | -------- | --------------------------------------------------------------------------------------------------- | | `name` | string | Required. Exact match. A blank name throws `DaytonaError` before the request is sent. | | `options.region` | string | Sets the region to search. `'local'` or omitting it means the region of the API host. | ```bash # Search by the name label: labels={"name":"my-sandbox"} curl 'https://api.daytona.com/sandboxes/search?labels=%7B%22name%22%3A%22my-sandbox%22%7D' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ### Create an access token [#create-an-access-token] Create a short-lived access token for one sandbox. The token reaches the sandbox toolbox, SSH, desktop, and private preview ports, and can stop, pause, or delete the sandbox, limited to the operations and ports you grant. Give it to a collaborator, a CI job, or a browser instead of a project API key. The token lasts one hour by default and at most 24 hours. It cannot be refreshed, revoked, or used to create other credentials. Create a new token when it expires. ```python # Grant file system and SSH access for 15 minutes token = sandbox.create_access_token(operations=["toolbox:fs", "ssh"], ttl_seconds=900) print(token.access_token, token.expires_at) ``` | **Parameter** | **Type** | **Description** | | ------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `operations` | Sequence\[str\] | Lists the operations the token grants. Each operation is one of `toolbox`, `toolbox:exec`, `toolbox:fs`, `toolbox:git`, `toolbox:lsp`, `desktop`, `ssh`, `sandbox:stop`, `sandbox:pause`, or `sandbox:delete`. `[]` grants preview ports only. The default is every operation the API key can grant. Granting `sandbox:delete` requires the `delete:sandboxes` permission. | | `ports` | Sequence\[int\] | Sets the private preview ports that the token can reach. `[]` grants none. The default is all ports. | | `ttl_seconds` | int | Sets the token lifetime in seconds, at most `86400`. The default is `3600`. | ```typescript // Grant file system and SSH access for 15 minutes const token = await sandbox.createAccessToken({ operations: ['toolbox:fs', 'ssh'], ttlSeconds: 900 }) console.log(token.accessToken, token.expiresAt) ``` | **Parameter** | **Type** | **Description** | | ------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `operations` | string\[\] | Lists the operations the token grants. Each operation is one of `toolbox`, `toolbox:exec`, `toolbox:fs`, `toolbox:git`, `toolbox:lsp`, `desktop`, `ssh`, `sandbox:stop`, `sandbox:pause`, or `sandbox:delete`. `[]` grants preview ports only. The default is every operation the API key can grant. Granting `sandbox:delete` requires the `delete:sandboxes` permission. | | `ports` | number\[\] | Sets the private preview ports that the token can reach. `[]` grants none. The default is all ports. | | `ttlSeconds` | number | Sets the token lifetime in seconds, at most `86400`. The default is `3600`. | ```bash # Grant file system and SSH access for 15 minutes curl 'https://api.daytona.com/sandboxes/{sandboxId}/access-token' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"operations": ["toolbox:fs", "ssh"], "ttl_seconds": 900}' ``` | **Field** | **Type** | **Description** | | ------------ | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `operations` | string\[\] | Sets the operations that the token grants: any of `toolbox`, `toolbox:exec`, `toolbox:fs`, `toolbox:git`, `toolbox:lsp`, `desktop`, `ssh`, `sandbox:stop`, `sandbox:pause`, and `sandbox:delete`. `[]` grants preview ports only. The default is every operation that the API key can grant. | | `ports` | integer\[\] | Sets the private preview ports that the token can reach, from `1` to `65535`, at most 128. `[]` grants none. The default is all ports. | | `ttl_seconds` | integer | Sets the token lifetime in seconds, at most `86400`. The default is `3600`. | The response has the following fields: | **Field** | **Description** | | -------------- | ------------------------------------------------------------------------------------------------ | | `access_token` | The access token. | | `token_type` | The token type is `Bearer`. | | `expires_at` | The time when the token expires. | | `operations` | The operations that the token grants. | | `ports` | Lists the private preview ports the token can reach. The value is `null` when every port is granted. | ### Label sandboxes [#label-sandboxes] Update a sandbox's labels. Labels are key-value strings for organizing sandboxes, and you can filter by them when you [list](#list-sandboxes) or [search](#search-sandboxes). An update replaces the full label set; omitted labels are removed. A sandbox holds at most 100 labels, each key and value at most 512 bytes. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() # Replace the full label set labels = sandbox.set_labels({"team": "research"}) print(labels) ``` | **Parameter** | **Type** | **Description** | | ------------- | ---------------- | ------------------------------------------------------- | | `labels` | Mapping\[str, str\] | Replaces the full label set. `{}` removes all labels. This parameter is required and positional only. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() // Replace the full label set const labels = await sandbox.setLabels({ team: 'research' }) console.log(labels) ``` | **Parameter** | **Type** | **Description** | | ------------- | ------------------------ | ------------------------------------ | | `labels` | Record\ | Replaces the full label set. `{}` removes all labels. This parameter is required. | ```bash # Replace the full label set daytona sandbox update {sandboxId} --label team=research ``` | **Flag** | **Type** | **Description** | | ---------------- | -------- | -------------------------------------------- | | `--label` | string | Repeatable, one `key=value` pair per flag. | | `--clear-labels` | bool | Removes all labels. | ```bash # Replace the full label set curl 'https://api.daytona.com/sandboxes/{sandboxId}/labels' \ --request PUT \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "labels": { "team": "research" } }' ``` | **Field** | **Type** | **Description** | | --------- | -------- | --------------------------- | | `labels` | object | Replaces the full label set. `{}` removes all labels. A sandbox holds at most 100 labels. Each key and value is at most 512 bytes. | ### Metrics [#metrics] Read the current CPU, memory, and disk usage of a running sandbox. The toolbox measures each value inside the sandbox and returns one snapshot per request. Memory in use excludes memory the operating system can reclaim on demand, such as the file cache. Disk usage is measured on the filesystem that holds the sandbox working directory. CPU utilization is the share of total CPU capacity in use across all CPUs since the previous metrics request to the sandbox, from any client. The first request after the toolbox starts measures over about 100 milliseconds. ```python metrics = sandbox.metrics() print(metrics.cpu_utilization, metrics.memory_usage_bytes, metrics.disk_usage_bytes) ``` The response has the following fields: | **Field** | **Description** | | -------------------- | ------------------------------------------------------------------------ | | `timestamp` | The time of the snapshot, as an RFC 3339 UTC timestamp. | | `cpu_count` | The number of logical CPUs visible inside the sandbox. | | `cpu_utilization` | The fraction of total CPU capacity in use, from `0` to `1`. | | `memory_usage_bytes` | The memory in use, in bytes. | | `memory_total_bytes` | The total memory, in bytes. | | `disk_usage_bytes` | The used space on the working directory filesystem, in bytes. | | `disk_total_bytes` | The total space on the working directory filesystem, in bytes. | ```typescript const metrics = await sandbox.metrics() console.log(metrics.cpuUtilization, metrics.memoryUsageBytes, metrics.diskUsageBytes) ``` The response has the following fields: | **Field** | **Description** | | ------------------ | ------------------------------------------------------------------------ | | `timestamp` | The time of the snapshot, as an RFC 3339 UTC timestamp. | | `cpuCount` | The number of logical CPUs visible inside the sandbox. | | `cpuUtilization` | The fraction of total CPU capacity in use, from `0` to `1`. | | `memoryUsageBytes` | The memory in use, in bytes. | | `memoryTotalBytes` | The total memory, in bytes. | | `diskUsageBytes` | The used space on the working directory filesystem, in bytes. | | `diskTotalBytes` | The total space on the working directory filesystem, in bytes. | ```bash curl '{toolboxProxyUrl}/{sandboxId}/system/metrics' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` The response has the following fields: | **Field** | **Description** | | ------------------ | ------------------------------------------------------------------------ | | `timestamp` | The time of the snapshot, as an RFC 3339 UTC timestamp. | | `cpuCount` | The number of logical CPUs visible inside the sandbox. | | `cpuUtilization` | The fraction of total CPU capacity in use, from `0` to `1`. | | `memoryUsageBytes` | The memory in use, in bytes. | | `memoryTotalBytes` | The total memory, in bytes. | | `diskUsageBytes` | The used space on the working directory filesystem, in bytes. | | `diskTotalBytes` | The total space on the working directory filesystem, in bytes. | - The request requires the `write:sandboxes` permission. - `{toolboxProxyUrl}` is the `toolbox_proxy_url` field of the [sandbox](/docs/sandboxes#get-a-sandbox). ### Logs [#logs] Read the serial console log of a sandbox. For a running sandbox the log is read live from its runner; for a sandbox in any other state the persisted log is returned. ```bash # Serial console log of the microVM daytona sandbox logs {sandboxId} ``` | **Flag** | **Type** | **Description** | | -------------- | -------- | ------------------------------------------------------- | | `--follow` | bool | Polls for new log output until interrupted. | | `--entrypoint` | bool | Reads the entrypoint logs instead of the serial console. | ```bash # Serial console log of the microVM curl 'https://api.daytona.com/sandboxes/{sandboxId}/console-logs' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ##### Entrypoint logs [#entrypoint-logs] Read the log of the sandbox entrypoint process as text. For a running sandbox the log is read live; for a sandbox in any other state the persisted log is returned. To page through the log or follow it, read the [entrypoint process](/docs/process-execution#entrypoint-process) through the toolbox. ```python print(sandbox.entrypoint_logs()) ``` ```typescript console.log(await sandbox.entrypointLogs()) ``` ```bash daytona sandbox logs {sandboxId} --entrypoint ``` ```bash curl 'https://api.daytona.com/sandboxes/{sandboxId}/logs' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ##### Build logs [#build-logs] Read the output of the image build behind a sandbox. A sandbox has a build log when its image is built from a Dockerfile, including a build that failed before the sandbox was placed; a sandbox created from a registry image has none. When the sandbox reuses an image built earlier, the log is the output of that original build. Following the log streams output while the build runs and ends when the build finishes. Stopping the reader does not stop the build. To read the log of an image without a sandbox, see [image build logs](/docs/images#build-logs). ```python from daytona import Daytona from daytona.v2 import ImageBuilder daytona = Daytona() sandbox = daytona.v2.create_from_build(ImageBuilder.from_dockerfile("FROM python:3.12-slim")) # Output retained so far, as text print(sandbox.build_logs()) # Stream output until the build finishes result = sandbox.follow_build_logs(on_logs=lambda chunk: print(chunk, end="")) print(result.status, result.reason) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ------------------------------------------------------------------- | | `on_logs` | Callable\[\[str\], None\] | Receives each chunk of decoded output. This parameter is required. | | `cursor` | str | Resumes after the output a previous follow returned in `cursor`. | | `timeout` | float | Sets the follow limit in seconds. By default, there is no limit. | The response has the following fields: | **Field** | **Description** | | ----------------- | --------------------------------------------------------------------------------------- | | `status` | The status of the build. | | `reason` | The value is `terminal`, `deadline`, `not_applicable`, or `resource_deleted`. | | `output_complete` | Whether the build output is complete. | | `cursor` | Pass this value as `cursor` to resume after this output. | ```typescript import { Daytona } from '@daytona/sdk' import { ImageBuilder } from '@daytona/sdk/v2' const daytona = new Daytona() const sandbox = await daytona.v2.createFromBuild(ImageBuilder.fromDockerfile('FROM python:3.12-slim')) // Output retained so far, as text console.log(await sandbox.buildLogs()) // Stream output until the build finishes const result = await sandbox.followBuildLogs({ onLogs: (chunk) => { process.stdout.write(chunk) } }) console.log(result.status, result.reason) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ------------------------------------------------------------------------------------------------ | | `onLogs` | (chunk: string) => unknown | Receives each chunk of decoded output. This parameter is required. | | `cursor` | string | Resumes after the output a previous follow returned in `cursor`. | | `timeoutSeconds` | number | Sets the follow limit in seconds. By default, there is no limit. | The response has the following fields: | **Field** | **Description** | | ---------------- | --------------------------------------------------------------------------------------- | | `status` | The status of the build. | | `reason` | The value is `terminal`, `deadline`, `not_applicable`, or `resource_deleted`. | | `outputComplete` | Whether the build output is complete. | | `cursor` | Pass this value as `cursor` to resume after this output. | ```bash BUILD_LOGS_URL=$(curl 'https://api.daytona.com/sandboxes/{sandboxId}/build-logs-url' \ --header 'Authorization: Bearer YOUR_API_KEY' | jq -r '.url') # Output retained so far, as text curl "$BUILD_LOGS_URL?follow=false&format=text" \ --header 'Authorization: Bearer YOUR_API_KEY' # Stream output until the build finishes curl --no-buffer "$BUILD_LOGS_URL?follow=true&format=sse" \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Accept: text/event-stream' ``` ## Permissions [#permissions] Sandboxes are scoped to a project. An API key reaches only the sandboxes of its own project. Access is governed by three permissions, which you can grant to an [organization role](/docs/organizations#roles) or an [API key](/docs/api-keys). Setting, changing, or clearing a delete action in a lifecycle policy also requires the permission to delete sandboxes, and so does creating an access token that can delete the sandbox. Attaching secrets when creating a sandbox requires the permission to read secrets. The [audit logs](/docs/audit-logs) record sandbox creates, lifecycle changes, deletes, and access token creation. | **Permission** | **Grants** | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | **`read:sandboxes`** | List, search, and get sandboxes, and read their logs, usage, and exposed ports | | **`write:sandboxes`** | Create, start, stop, pause, resume, fork, and label sandboxes, change their lifecycle policy, create access tokens, and use the toolbox | | **`delete:sandboxes`** | Delete sandboxes | # Linux Sandboxes (/docs/linux-sandboxes) Linux sandboxes are **Linux virtual machines (VMs)**. Each sandbox is a virtual machine, a microVM, that runs its own Linux kernel on shared physical hardware and has its own operating system, files, network connection, and fixed amount of processor, memory, and disk. The hardware enforces the boundary and isolation between sandboxes. | **Term** | **Description** | | -------------- | -------------------------------------------------------------------------------------------------------------- | | **Sandbox** | An isolated Linux machine created on demand to run code. | | **Host** | The physical server that runs many sandboxes at once. Daytona calls these servers runners. | | **Guest** | The operating system running inside a sandbox, as opposed to the operating system of the host. | | **Kernel** | The core of an operating system. It manages memory, processes, files, and devices. | | **Hypervisor** | Software on the host that creates virtual machines and keeps them separated from each other and from the host. | | **Image** | A packaged filesystem that defines what software a sandbox starts with. | | **Snapshot** | A saved copy of a sandbox's memory and disk that new sandboxes resume from. | | **Spot** | A sandbox that runs on discounted capacity that Daytona can reclaim at any time. | ## Use cases [#use-cases] Sandboxes are used wherever code must run in a real environment, but you cannot trust the code or you cannot afford for it to affect anything else. - **Code execution for AI agents.** An agent writes code, runs it in a sandbox, reads the output, and decides what to do next. The sandbox gives the agent a real shell and filesystem while keeping its actions away from your infrastructure. Once the agent is done, a [delete](#delete) discards the sandbox's filesystem and memory. - **Coding agents.** An agent clones a repository, installs dependencies, edits files, runs the test suite, and opens a pull request, all inside one sandbox. The sandbox keeps its filesystem on its host until it is deleted. A [filesystem snapshot](#filesystem-snapshot) saves that filesystem as an image, and new sandboxes boot from it with the captured files. - **Reinforcement learning (RL).** Training runs create very large numbers of identical environments, run one episode in each, score the result, and discard the environment. Fast creation from a shared snapshot keeps the cost of each environment low. A restored sandbox resumes the captured processes and memory instead of booting. - **Untrusted user code.** Online coding platforms, grading systems, and plugin hosts run code submitted by users without exposing other users or the service. Each sandbox is a microVM with its own Linux kernel. The hardware enforces the boundary between sandboxes. - **Development environments.** A developer gets a preconfigured machine from an image, connects over SSH or from an editor, and previews web applications through a URL. Requests through SSH, VNC, and preview URLs count as activity for the idle timeout. A [reset of the idle timer](#reset-the-idle-timer) keeps the sandbox alive during work that Daytona does not count as activity. ## Create a Linux sandbox [#create-a-linux-sandbox] Create a Linux sandbox from the dashboard or programmatically. 1. Go to [Daytona Sandboxes ↗](https://app.daytona.com/dashboard/sandboxes) 2. Click 3. Select 4. Click ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() ``` The following example sets the remaining parameters: ```python from daytona import Daytona from daytona.v2 import PortMapping, Resources daytona = Daytona() sandbox = daytona.v2.create( name="my-sandbox", labels={"team": "research", "run": "42"}, resources=Resources(cpu=2, memory=4, disk=20), ports=[PortMapping(port=8080), PortMapping(port=5353, proto="udp")], network_policy={"block_all": True, "allow_domains": ["pypi.org", "*.github.com"]}, env={"LOG_LEVEL": "debug"}, secrets=["GITHUB_TOKEN"], spot=False, cpu_arch="amd64", cpu_type="zen5", lifecycle_policy={ "idle_expiry_seconds": 3600, "on_idle_timeout": "delete", "timeout_seconds": 604800, "on_timeout": "delete", }, idempotency_key="run-42-create", wait_seconds=120, ) ``` | **Parameter** | **Type** | **Description** | | ----------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `image` | str | Sets the registered image that the sandbox starts from, by name or `img_` ID. This is the first positional argument. If omitted, the sandbox boots the platform default image, the platform image whose `default` field is `true` in the [image list](/docs/images#list-images). When no default image is set, the request returns a 400 error. | | `name` | str | Sets the display name. If omitted, a name is generated. Within one [workload cell](/docs/regions), a create with the same name and different arguments is refused until the first sandbox is deleted. The name is also stored as the `name` label. | | `labels` | Dict\[str, str\] | Attaches key-value metadata to the sandbox. | | `user` | str | Sets the default OS user inside the sandbox. The default is the image's `USER`, or `root` if the image sets none, sets root, or sets a numeric user ID. | | `resources` | Resources | Sets the sandbox size. `cpu` sets the guaranteed CPU cores as a float, raised to at least 0.125; the value is raised to at least one core per 8 GiB of guaranteed memory, and if omitted it is one core per 8 GiB of guaranteed memory, and at least 0.5: 0.5 with the default 1 GiB. `memory` sets the guaranteed memory in GiB, raised to at least 0.5 GiB, default 1 GiB. `disk` sets the guaranteed writable disk in GiB, default 512 MiB. When `cpu`, `memory`, or `disk` is set, the disk is raised to at least 10 GiB. `cpu_max` sets the CPU limit in cores as a multiple of 0.5, which defaults to the system or plan cap. `memory_max` sets the memory limit in GiB, from 0.25 to 32, which defaults to 32. If `memory` is omitted, the guaranteed memory is lowered to the limit. If `memory` is set, a guaranteed memory above the limit, after the raise to 0.5 GiB, is refused. `disk_max` sets the writable disk limit in GiB. Memory and disk must convert to a whole number of MiB. | | `ports` | Sequence\[PortMapping\] | Forwards inbound ports to the sandbox. Each mapping has a `port` and a `proto`, which defaults to `tcp`. | | `volumes` | Sequence\[VolumeMount\] | Attaches volumes to the sandbox. See [attach volumes](/docs/volumes#attach-volumes). | | `env` | Dict\[str, str\] | Sets environment variables inside the sandbox. | | `secrets` | Sequence\[str\] | Attaches project secrets by name. The sandbox sees each secret as an opaque placeholder. A name that is not a secret of the project is refused. | | `network_policy` | NetworkPolicyInput | Sets the outbound network policy with `block_all`, `allow_domains`, and `allow_cidrs`. `allow_domains` implies `block_all`. `allow_cidrs` restricts egress only together with `block_all` or `allow_domains`. A `*.` wildcard domain matches subdomains only, so list the parent domain separately. A create with a policy is refused when no runner in the region can enforce it for the sandbox's CPU architecture and type. You can [change it](/docs/network-limits#update-network-policy) after create. | | `spot` | bool | Runs the sandbox on discounted capacity that can be reclaimed. The default is `False`. | | `region` | str | Sets the region, any listed region including `earth`. The default is the project's default region, then the organization's, then `earth`. A create in a region with no online Linux runner is refused. | | `cpu_arch` | str | Sets the CPU architecture, `amd64` or `arm64`. The default is the region's default architecture, or `amd64` if the region sets none. A create with an architecture that no runner in the region provides is refused, or waits until its create deadline. | | `cpu_type` | str | Sets the CPU hardware tier slug: `zen3`, `zen4`, `zen5`, `zen5c`, `arrowlake1`, `arrowlake2`, `graviton`, `graviton3`, and `graviton4`. The slug also sets the architecture. A create with a type that no runner in the region serves, itself or as a fallback, is refused, or waits until its create deadline. See [CPU types](#cpu-types). | | `lifecycle_policy` | LifecyclePolicyOptions | Sets the lifecycle policy with `timeout_seconds` and `on_timeout`, and `idle_expiry_seconds` and `on_idle_timeout`. The action is `delete`. By default, there is no idle timeout unless the project sets one, and the wall-clock timeout uses the project or platform default, if one is enabled. | | `idempotency_key` | str | Makes a retried create in the same workload cell return the same sandbox. A create in another cell makes a separate sandbox. To keep creates in one region, set `region`. If `name` is set, the key is derived from it. Otherwise, each call generates a new key. | | `create_timeout_seconds` | int | Sets how many seconds the sandbox has to reach running before it is deleted. `-1` sets the maximum, 86400. The default is the server default, 3600. | | `wait_seconds` | int | Sets how many seconds create waits for the sandbox to start. The server holds the request for up to 3 seconds, and the SDK waits for the rest. `0` returns immediately. The default is 60. If the sandbox has not started in time, or fails, `create()` returns it without raising. Check `sandbox.state` before using it. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() ``` The following example sets the remaining parameters: ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create({ name: 'my-sandbox', labels: { team: 'research', run: '42' }, resources: { cpu: 2, memory: 4, disk: 20 }, ports: [{ port: 8080 }, { port: 5353, proto: 'udp' }], networkPolicy: { blockAll: true, allowDomains: ['pypi.org', '*.github.com'] }, env: { LOG_LEVEL: 'debug' }, secrets: ['GITHUB_TOKEN'], spot: false, cpuArch: 'amd64', cpuType: 'zen5', lifecyclePolicy: { idleExpirySeconds: 3600, onIdleTimeout: 'delete', timeoutSeconds: 604800, onTimeout: 'delete', }, idempotencyKey: 'run-42-create', waitSeconds: 120, }) ``` | **Parameter** | **Type** | **Description** | | ---------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `image` | string | Sets the registered image that the sandbox starts from, by name or `img_` ID. This is the first positional argument. If omitted, the sandbox boots the platform default image, the platform image whose `default` field is `true` in the [image list](/docs/images#list-images). When no default image is set, the request returns a 400 error. | | `name` | string | Sets the display name. If omitted, a name is generated. Within one [workload cell](/docs/regions), a create with the same name and different arguments is refused until the first sandbox is deleted. The name is also stored as the `name` label. | | `labels` | Record\ | Attaches key-value metadata to the sandbox. | | `user` | string | Sets the default OS user inside the sandbox. The default is the image's `USER`, or `root` if the image sets none, sets root, or sets a numeric user ID. | | `resources` | Resources | Sets the sandbox size. `cpu` sets the guaranteed CPU cores as a float, raised to at least 0.125; the value is raised to at least one core per 8 GiB of guaranteed memory, and if omitted it is one core per 8 GiB of guaranteed memory, and at least 0.5: 0.5 with the default 1 GiB. `memory` sets the guaranteed memory in GiB, raised to at least 0.5 GiB, default 1 GiB. `disk` sets the guaranteed writable disk in GiB, default 512 MiB. When `cpu`, `memory`, or `disk` is set, the disk is raised to at least 10 GiB. `cpuMax` sets the CPU limit in cores as a multiple of 0.5, which defaults to the system or plan cap. `memoryMax` sets the memory limit in GiB, from 0.25 to 32, which defaults to 32. If `memory` is omitted, the guaranteed memory is lowered to the limit. If `memory` is set, a guaranteed memory above the limit, after the raise to 0.5 GiB, is refused. `diskMax` sets the writable disk limit in GiB. Memory and disk must convert to a whole number of MiB. | | `ports` | PortMapping\[\] | Forwards inbound ports to the sandbox. Each mapping has a `port` and a `proto`, which defaults to `tcp`. | | `volumes` | VolumeMount\[\] | Attaches volumes to the sandbox. See [attach volumes](/docs/volumes#attach-volumes). | | `env` | Record\ | Sets environment variables inside the sandbox. | | `secrets` | string\[\] | Attaches project secrets by name. The sandbox sees each secret as an opaque placeholder. A name that is not a secret of the project is refused. | | `networkPolicy` | NetworkPolicyInput | Sets the outbound network policy with `blockAll`, `allowDomains`, and `allowCidrs`. `allowDomains` implies `blockAll`. `allowCidrs` restricts egress only together with `blockAll` or `allowDomains`. A `*.` wildcard domain matches subdomains only, so list the parent domain separately. A create with a policy is refused when no runner in the region can enforce it for the sandbox's CPU architecture and type. You can [change it](/docs/network-limits#update-network-policy) after create. | | `spot` | boolean | Runs the sandbox on discounted capacity that can be reclaimed. The default is `false`. | | `region` | string | Sets the region, any listed region including `earth`. The default is the project's default region, then the organization's, then `earth`. A create in a region with no online Linux runner is refused. | | `cpuArch` | CpuArch | Sets the CPU architecture, `amd64` or `arm64`. The default is the region's default architecture, or `amd64` if the region sets none. A create with an architecture that no runner in the region provides is refused, or waits until its create deadline. | | `cpuType` | string | Sets the CPU hardware tier slug: `zen3`, `zen4`, `zen5`, `zen5c`, `arrowlake1`, `arrowlake2`, `graviton`, `graviton3`, and `graviton4`. The slug also sets the architecture. A create with a type that no runner in the region serves, itself or as a fallback, is refused, or waits until its create deadline. See [CPU types](#cpu-types). | | `lifecyclePolicy` | LifecyclePolicyOptions | Sets the lifecycle policy with `timeoutSeconds` and `onTimeout`, and `idleExpirySeconds` and `onIdleTimeout`. The action is `delete`. By default, there is no idle timeout unless the project sets one, and the wall-clock timeout uses the project or platform default, if one is enabled. | | `idempotencyKey` | string | Makes a retried create in the same workload cell return the same sandbox. A create in another cell makes a separate sandbox. To keep creates in one region, set `region`. If `name` is set, the key is derived from it. Otherwise, each call generates a new key. | | `createTimeoutSeconds` | number | Sets how many seconds the sandbox has to reach running before it is deleted. `-1` sets the maximum, 86400. The default is the server default, 3600. | | `waitSeconds` | number | Sets how many seconds create waits for the sandbox to start. The server holds the request for up to 3 seconds, and the SDK waits for the rest. `0` returns immediately. The default is 60. If the sandbox has not started in time, or fails, `create()` resolves with it without throwing. Check `sandbox.state` before using it. | ```bash daytona sandbox create ``` The following example sets the remaining flags: ```bash daytona sandbox create \ --name my-sandbox \ --label team=research --label run=42 \ --user root \ --cpu 2 --memory 4096 --disk 20480 \ --cpu-max 4 --memory-max 16384 \ --port 8080 --port 5353/udp \ --block-all --allow-domain pypi.org --allow-domain '*.github.com' \ --env LOG_LEVEL=debug \ --secret GITHUB_TOKEN \ --cpu-arch amd64 --cpu-type zen5 \ --idle-timeout 1h --on-idle-timeout delete \ --lifecycle-timeout 168h --on-lifecycle-timeout delete \ --idempotency-key run-42-create ``` | **Flag** | **Type** | **Description** | | ------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `--image` | string | Sets the registered image that the sandbox starts from, by name or `img_` ID. If omitted, the sandbox boots the platform default image, the platform image whose `default` field is `true` in the [image list](/docs/images#list-images). When no default image is set, the request returns a 400 error. | | `--name` | string | Sets the display name. If omitted, a name is generated. | | `--label` | KEY=VALUE | Attaches a `KEY=VALUE` metadata label. Repeat the flag for more labels. | | `--user` | string | Sets the default OS user inside the sandbox. The default is the image's `USER`, or `root` if the image sets none, sets root, or sets a numeric user ID. | | `--cpu` | float | Sets the guaranteed CPU cores as a float, raised to at least 0.125. The value is raised to at least one core per 8 GiB of guaranteed memory. If omitted, the value is one core per 8 GiB of guaranteed memory, and at least 0.5: 0.5 with the default 1024 MiB. | | `--memory` | int32 | Sets the guaranteed memory in MiB, raised to at least 512. The default is 1024. | | `--disk` | int32 | Sets the guaranteed writable disk in MiB. The default is 512. When `--cpu`, `--memory`, or `--disk` is set, the disk is raised to at least 10240. | | `--cpu-max` | float | Sets the CPU limit of the sandbox in cores, a multiple of 0.5. One core is 2 vCPUs. The sandbox boots at this size. The default is the system or plan cap. | | `--memory-max` | int32 | Sets the memory limit of the sandbox in MiB, from 256 to 32768. If `--memory` is omitted, the guaranteed memory is lowered to the limit. If `--memory` is set, a guaranteed memory above the limit, after the raise to 512, is refused. The default is 32768. | | `--disk-max` | int32 | Sets the writable disk limit of the sandbox in MiB, from 512 to 262144. | | `--port` | PORT\[/PROTO\] | Forwards an inbound port, with an optional protocol. Repeat the flag for more ports. | | `--volume` | VOLUME:MOUNT_PATH\[:SUBPATH\]\[:ro\] | Attaches a volume. Repeat the flag for more volumes. See [attach volumes](/docs/volumes#attach-volumes). | | `--env` | KEY=VALUE | Sets an environment variable inside the sandbox. Repeat the flag for more variables. | | `--secret` | string | Attaches a project secret by name. The sandbox sees it as an opaque placeholder. A name that is not a secret of the project is refused. Repeat the flag for more secrets. | | `--block-all` | bool | Denies all egress that is not explicitly allowed. A create with a network policy is refused when no runner in the region can enforce it for the sandbox's CPU architecture and type. You can [change the policy](/docs/network-limits#update-network-policy) after create through the SDKs or the API. The default is `false`. | | `--allow-domain` | string | Allows egress to a hostname or a `*.` wildcard domain. A wildcard matches subdomains only, so list the parent domain separately. It implies `--block-all`. Repeat the flag for more domains. | | `--allow-cidr` | string | Allows egress to an IPv4 address or CIDR block, on any port. It needs `--block-all` or `--allow-domain`. Repeat the flag for more addresses. | | `--spot` | bool | Runs the sandbox on discounted capacity that can be reclaimed. The default is `false`. | | `--region` | string | Sets the region, any listed region including `earth`. The default is the project's default region, then the organization's, then `earth`. A create in a region with no online Linux runner is refused. | | `--cpu-arch` | string | Sets the CPU architecture, `amd64` or `arm64`. The default is the region's default architecture, or `amd64` if the region sets none. A create with an architecture that no runner in the region provides is refused, or waits until its create deadline. | | `--cpu-type` | string | Sets the CPU hardware tier slug: `zen3`, `zen4`, `zen5`, `zen5c`, `arrowlake1`, `arrowlake2`, `graviton`, `graviton3`, and `graviton4`. The slug also sets the architecture. A create with a type that no runner in the region serves, itself or as a fallback, is refused, or waits until its create deadline. See [CPU types](#cpu-types). | | `--lifecycle-timeout` | duration | Sets the wall-clock lifetime as a Go duration such as `2h`, until `--on-lifecycle-timeout` fires. The default is the project or platform default, if one is enabled. | | `--on-lifecycle-timeout` | string | Sets the action when `--lifecycle-timeout` elapses, `delete`. The default is `delete`. | | `--idle-timeout` | duration | Sets the idle time as a Go duration, at least `1m`, until `--on-idle-timeout` fires. By default, there is no idle timeout unless the project sets one. | | `--on-idle-timeout` | string | Sets the action when `--idle-timeout` elapses, `delete`. It is required with `--idle-timeout`. | | `--idempotency-key` | string | Makes a retried create return the same sandbox. | | `--server-wait` | duration | Sets how long the server holds the create response for the sandbox to start, as a Go duration. `0s` answers at once. The server default is `3s`. The command then waits until the sandbox is running and prints it, unless `--no-wait` is set. | | `--timeout` | duration | Sets how long the command waits for the sandbox to start, as a Go duration. The default is `10m`. The sandbox keeps starting after the command stops waiting. | | `--no-wait` | bool | Prints the create response immediately instead of waiting for the sandbox to start. | | `--create-ttl` | duration | Sets how long the sandbox has to reach running before the server deletes it, as a Go duration. `max` sets the server maximum, 24 hours. The default is `1h`. | | `-f`, `--file` | string | Reads the JSON request body from a file, or from standard input with `-`. Flags set on the command line override its fields. | ```bash curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{}' ``` The following example sets the remaining fields: ```bash curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "name": "my-sandbox", "labels": { "team": "research", "run": "42" }, "user": "root", "cpu": 2, "memory_mib": 4096, "disk_mib": 20480, "cores_max": 4, "memory_max_mib": 16384, "ports": [{ "port": 8080 }, { "port": 5353, "proto": "udp" }], "network_policy": { "block_all": true, "allow_domains": ["pypi.org", "*.github.com"] }, "env": { "LOG_LEVEL": "debug" }, "secrets": ["GITHUB_TOKEN"], "spot": false, "cpu_arch": "amd64", "cpu_type": "zen5", "lifecycle_policy": { "idle_expiry_seconds": 3600, "on_idle_timeout": "delete", "timeout_seconds": 604800, "on_timeout": "delete" }, "idempotency_key": "run-42-create" }' ``` | **Field** | **Type** | **Description** | | ----------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `image` | string | Sets the registered image that the sandbox starts from, by name or `img_` ID. If omitted, the sandbox boots the platform default image, the platform image whose `default` field is `true` in the [image list](/docs/images#list-images). When no default image is set, the request returns a 400 error. | | `name` | string | Sets the display name. If omitted, a name is generated. Names need not be unique. To make a retried create return the same sandbox, send `idempotency_key`. | | `labels` | object | Attaches key-value metadata to the sandbox. | | `user` | string | Sets the default OS user inside the sandbox. The default is the image's `USER`, or `root` if the image sets none, sets root, or sets a numeric user ID. | | `cpu` | number | Sets the guaranteed CPU cores as a float, raised to at least 0.125. The value is raised to at least one core per 8 GiB of guaranteed memory. If omitted, the value is one core per 8 GiB of guaranteed memory, and at least 0.5: 0.5 with the default 1024 MiB. | | `memory_mib` | integer | Sets the guaranteed memory in MiB, raised to at least 512. The default is 1024. | | `disk_mib` | integer | Sets the guaranteed writable disk in MiB. The default is 512. When `cpu`, `memory_mib`, or `disk_mib` is set, the disk is raised to at least 10240. | | `cores_max` | number | Sets the CPU limit of the sandbox in cores, a positive multiple of 0.5. One core is 2 vCPUs. The sandbox boots at this size. The default is the system or plan cap. | | `memory_max_mib` | integer | Sets the memory limit of the sandbox in MiB, from 256 to 32768. If `memory_mib` is omitted, the guaranteed memory is lowered to the limit. If `memory_mib` is set, a guaranteed memory above the limit, after the raise to 512, is refused. The default is 32768. | | `disk_max_mib` | integer | Sets the writable disk limit of the sandbox in MiB, from 512 to 262144. | | `ports` | array | Forwards inbound ports to the sandbox. Each entry has a `port` and a `proto`, which defaults to `tcp`. | | `volumes` | array | Attaches volumes to the sandbox. See [attach volumes](/docs/volumes#attach-volumes). | | `env` | object | Sets environment variables inside the sandbox. | | `secrets` | array | Attaches project secrets by name. The sandbox sees each secret as an opaque placeholder. A name that is not a secret of the project is refused. | | `network_policy` | object | Sets the outbound network policy with `block_all`, `allow_domains`, and `allow_cidrs`. `allow_domains` implies `block_all`. `allow_cidrs` restricts egress only together with `block_all` or `allow_domains`. A `*.` wildcard domain matches subdomains only, so list the parent domain separately. A create with a policy is refused when no runner in the region can enforce it for the sandbox's CPU architecture and type. You can [change it](/docs/network-limits#update-network-policy) after create. | | `spot` | boolean | Runs the sandbox on discounted capacity that can be reclaimed. The default is `false`. | | `region_id` | string | Sets the region, any listed region including `earth`. The default is the project's default region, then the organization's, then `earth`. A create in a region with no online Linux runner is refused. | | `cpu_arch` | string | Sets the CPU architecture, `amd64` or `arm64`. The default is the region's default architecture, or `amd64` if the region sets none. A create with an architecture that no runner in the region provides is refused, or waits until its create deadline. | | `cpu_type` | string | Sets the CPU hardware tier slug: `zen3`, `zen4`, `zen5`, `zen5c`, `arrowlake1`, `arrowlake2`, `graviton`, `graviton3`, and `graviton4`. The slug also sets the architecture. A create with a type that no runner in the region serves, itself or as a fallback, is refused, or waits until its create deadline. See [CPU types](#cpu-types). | | `lifecycle_policy` | object | Sets the lifecycle policy with `timeout_seconds` and `on_timeout`, and `idle_expiry_seconds` and `on_idle_timeout`. The action is `delete`. By default, there is no idle timeout unless the project sets one, and the wall-clock timeout uses the project or platform default, if one is enabled. | | `idempotency_key` | string | Makes a retried create return the same sandbox. You can also send it as the `Idempotency-Key` header. It is required when `region_id` is `earth` or another region that covers other regions. | | `create_timeout_seconds` | integer | Sets how many seconds the sandbox has to reach running before it is deleted. `-1` sets the maximum, 86400. The default is 3600. | | `wait_seconds` | integer | Sets how many seconds the server holds the request for the sandbox to start. `0` returns immediately. The default and the maximum are 3, and a larger value is lowered to 3. | The response has the following fields: | **Field** | **Description** | | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `status` | The status of the sandbox when the response is sent. The server holds the request for up to `wait_seconds`, so the status is `running`, `failed` when the sandbox failed to start, or an earlier status such as `pending` or `booting` when the sandbox has not started in time. Poll `GET /sandboxes/{sandboxId}?wait=running` while the status is not `running` or `failed`. A `failed` sandbox does not start; the `error` field of the sandbox names the cause. | | `access_token` | The access token is returned only once. An idempotent replay returns `null`. | ### Spot [#spot] Linux sandboxes run on reserved capacity by default. A spot Linux sandbox runs on discounted capacity that Daytona can reclaim at any time to free resources for full-price sandboxes. Spot multiplies the CPU, memory, and disk price by 0.7. Apart from price and reclaim, a spot Linux sandbox supports the same images, resources, volumes, and lifecycle operations as any other Linux sandbox. A reclaim is immediate, with no advance notice and no grace period. An ephemeral sandbox is deleted, and only data on a [volume](/docs/volumes) or in other storage outside the sandbox remains. [Preemption](/docs/preemption) describes the reclaim policy. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create(spot=True) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `spot` | bool | Places the sandbox on discounted, reclaimable capacity. The default is `False`. Set it when you create the sandbox. It cannot be changed on an existing sandbox. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create({ spot: true }) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `spot` | boolean | Places the sandbox on discounted, reclaimable capacity. The default is `false`. Set it when you create the sandbox. It cannot be changed on an existing sandbox. | ```bash daytona sandbox create --spot ``` | **Flag** | **Type** | **Description** | | -------- | -------- | --------------- | | `--spot` | bool | Places the sandbox on discounted, reclaimable capacity. The default is `false`. Set it when you create the sandbox. It cannot be changed on an existing sandbox. | ```bash curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"spot": true}' ``` | **Field** | **Type** | **Description** | | --------- | -------- | --------------- | | `spot` | boolean | Places the sandbox on discounted, reclaimable capacity. The default is `false`. Set it when you create the sandbox. It cannot be changed on an existing sandbox. | ### CPU types [#cpu-types] A CPU type is a processor model that a Linux sandbox can request when it is created. Each type belongs to one architecture, amd64 or arm64, and has a tier: its rank within that architecture, starting at 1 for the lowest. Requesting a CPU type also sets the sandbox's architecture. The CPU type list contains every type that a create request accepts, whether or not a runner provides it. A create with a type that no runner in the region serves, itself or as a fallback, is refused, or waits until its create deadline. To see which types a region can serve, add the region. A type is available in a region when a running runner there provides it. When no runner provides the type itself, a runner with a higher tier of the same architecture can serve it as a fallback. Availability reflects the runners that are live at request time. [List regions](/docs/regions#list-regions) gives the region IDs. ```bash # List CPU types daytona cpu-type list # Check CPU type availability in a region daytona cpu-type list --region us-east-ash-1 ``` | **Flag** | **Type** | **Description** | | ---------- | -------- | ------------------------------------------------------------------------------------- | | `--region` | string | Sets one region ID. Without it, the command lists CPU types without availability. With it, the output adds the `AVAILABLE` column. | In a terminal, the output is a table with the following columns. Otherwise, the output is JSON unless `-o table` is set. | **Column** | **Description** | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `ID` | The CPU type slug that a create request accepts. | | `NAME` | The display name of the CPU type. | | `ARCH` | The CPU architecture, `amd64` or `arm64`. | | `TIER` | The rank within the architecture, starting at 1 for the lowest. | | `AVAILABLE` | Shown with `--region`. `exact` if a runner in the region provides the type, `fallback` if a higher tier of the same architecture serves it, and `-` otherwise. | ```bash # List CPU types curl 'https://api.daytona.com/cpu-catalog' \ --header 'Authorization: Bearer YOUR_API_KEY' # Check CPU type availability in a region curl 'https://api.daytona.com/cpu-catalog?region_ids=us-east-ash-1' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Type** | **Description** | | ---------- | -------- | ---------------------------------------------------------------------------------------------------- | | `region_ids` | string | Sets a comma-separated list of region IDs, at most 64. The default is every region that the answering workload cell hosts. | The response has the following fields: | **Field** | **Description** | | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `cpu_types` | Lists every CPU type with `id`, `name`, `cpu_arch`, and `tier`, and whether the requested regions can serve it: `available`, `exact_region_ids`, and `fallback_region_ids`. | | `arches` | Lists each CPU architecture and the requested regions where it is available. | | `regions` | Lists the covered regions. In each region, `default_arch` and `default_type` are the values that a create request that sets neither uses. `default_type` is `null` when the region sets none. | ## Filesystem snapshot [#filesystem-snapshot] A filesystem snapshot saves the filesystem of a running Linux sandbox as an [image](/docs/images). The sandbox keeps running, and new sandboxes boot from the image with the captured files. Unlike a [memory snapshot](#memory-snapshot), a filesystem snapshot holds no processes or memory. The capture runs in the background: the image starts as pending, moves to building, and becomes ready when the capture finishes. A named image is permanent until you delete it. An unnamed image is anonymous. Daytona deletes an anonymous image after its idle window passes without a sandbox create from it. The idle window is the server maximum of 7 days by default. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.process.run("pip install requests") # Capture the filesystem; the sandbox keeps running image = sandbox.capture_image(name="my-env") # Wait until the capture is ready, then boot new sandboxes from it while image.status not in ("ready", "failed"): image = daytona.v2.images.get(image.id, wait="ready") if image.status == "failed": raise RuntimeError(image.error) clone = daytona.v2.create("my-env") ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `name` | str | Sets the image name. The default is `None`, which makes the image anonymous. The name must be unique in the project. | | `idle_expiry_seconds` | int | Sets how many seconds an anonymous image can go without a sandbox create before deletion, from `1` to the server maximum. `-1` sets the maximum. The default is the server maximum. It applies only to anonymous images. A capture that also sets `name` is refused. | - The requests require the `write:images` and `write:sandboxes` permissions. - The sandbox must be a running Linux sandbox. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.process.run('pip install requests') // Capture the filesystem; the sandbox keeps running let image = await sandbox.captureImage({ name: 'my-env' }) // Wait until the capture is ready, then boot new sandboxes from it while (image.status !== 'ready' && image.status !== 'failed') { image = await daytona.v2.images.get(image.id, { wait: 'ready' }) } if (image.status === 'failed') throw new Error(image.error ?? 'image capture failed') const clone = await daytona.v2.create('my-env') ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `name` | string | Sets the image name. The default is `undefined`, which makes the image anonymous. The name must be unique in the project. | | `idleExpirySeconds` | number | Sets how many seconds an anonymous image can go without a sandbox create before deletion, from `1` to the server maximum. `-1` sets the maximum. The default is the server maximum. It applies only to anonymous images. A capture that also sets `name` is refused. | - The requests require the `write:images` and `write:sandboxes` permissions. - The sandbox must be a running Linux sandbox. ```bash # Capture the filesystem and wait until the image is ready daytona image capture {sandboxId} --name my-env # Boot a new sandbox from the captured filesystem daytona sandbox create --image my-env ``` | **Flag** | **Type** | **Description** | | -------- | -------- | --------------- | | `--name` | string | Sets the image name. Without a name, the image is anonymous. The name must be unique in the project. | | `--timeout` | string | Sets how long the command waits for the image to be ready. The default is `10m`. | | `--no-wait` | boolean | Returns once the capture is queued, without waiting for the image to be ready. | - The requests require the `write:images` and `write:sandboxes` permissions. - The sandbox must be a running Linux sandbox. ```bash # Capture the filesystem; the sandbox keeps running curl 'https://api.daytona.com/sandboxes/{sandboxId}/images' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"name": "my-env"}' # Wait for the capture to be ready curl 'https://api.daytona.com/images/{imageId}?wait=ready' \ --header 'Authorization: Bearer YOUR_API_KEY' # Boot a new sandbox from the captured filesystem curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"image": "my-env"}' ``` | **Field** | **Type** | **Description** | | --------- | -------- | --------------- | | `name` | string | Sets the image name. Without a name, the image is anonymous. The name must be unique in the project. | | `idle_expiry_seconds` | integer | Sets how many seconds an anonymous image can go without a sandbox create before deletion, from `1` to the server maximum. `-1` sets the maximum. The default is the server maximum. It applies only to anonymous images. A capture that also sets `name` is refused. | - The requests require the `write:images` and `write:sandboxes` permissions. - The sandbox must be a running Linux sandbox. - The wait request returns at `ready` or `failed`, or after 30 seconds. Repeat it while the image status is `pending` or `building`. A `failed` image does not become `ready`. ## Memory snapshot [#memory-snapshot] A memory snapshot saves the memory and disk of a running Linux sandbox under a name. A sandbox restored from the snapshot resumes the captured processes and memory instead of booting. The restored sandbox gets the CPU, memory, and disk of the source sandbox. Deleting the source sandbox does not delete its snapshots. The capture runs in the background: the snapshot starts as pending, is building while the runner captures the sandbox, and becomes ready when the capture finishes. A snapshot restores on the runner that captured it, or on another runner of the region with the same CPU model and hypervisor version. It restores in another region only where Daytona has enabled cross-region restore and the source sandbox was not persistent. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() # Capture the memory and disk of the running sandbox snapshot = sandbox.create_snapshot("my-snapshot") # Wait until the capture is ready while snapshot.status not in ("ready", "failed"): snapshot = daytona.v2.snapshots.get(snapshot.id, wait="ready") if snapshot.status == "failed": raise RuntimeError(snapshot.error) # Resume the captured processes and memory in a new sandbox restored = daytona.v2.create_from_snapshot("my-snapshot", labels={"team": "research"}) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `name` | str | Sets the snapshot name, 1 to 128 characters of letters, digits, `.`, `_`, and `-`. The name cannot start with `.` or with a reserved prefix: `sbx_`, `vol_`, `fn_`, `img_`, `snap_`, `snap-`, `build-`, `refs`, `by-digest`, `prj-`, or `glb-`. This parameter is required. Snapshots and images in the project share one name space. A name in use fails with `409`. | | `snapshot` | str | Sets the snapshot that `create_from_snapshot()` restores from, by name or by ID, `snap_...`. This parameter is required. | | `labels` | Dict\[str, str\] | Attaches key-value metadata to the restored sandbox. The restored sandbox does not inherit the labels of the source sandbox. | - The requests require the `write:snapshots` and `write:sandboxes` permissions. - The source sandbox must be running. - The source sandbox must be persistent or served from a warm template. A snapshot of any other sandbox, such as one created from a filesystem snapshot or with volumes attached, fails with `409`. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() // Capture the memory and disk of the running sandbox let snapshot = await sandbox.createSnapshot('my-snapshot') // Wait until the capture is ready while (snapshot.status !== 'ready' && snapshot.status !== 'failed') { snapshot = await daytona.v2.snapshots.get(snapshot.id, { wait: 'ready' }) } if (snapshot.status === 'failed') throw new Error(snapshot.error ?? 'snapshot capture failed') // Resume the captured processes and memory in a new sandbox const restored = await daytona.v2.createFromSnapshot('my-snapshot', { labels: { team: 'research' } }) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `name` | string | Sets the snapshot name, 1 to 128 characters of letters, digits, `.`, `_`, and `-`. The name cannot start with `.` or with a reserved prefix: `sbx_`, `vol_`, `fn_`, `img_`, `snap_`, `snap-`, `build-`, `refs`, `by-digest`, `prj-`, or `glb-`. This parameter is required. Snapshots and images in the project share one name space. A name in use fails with `409`. | | `snapshot` | string | Sets the snapshot that `createFromSnapshot()` restores from, by name or by ID, `snap_...`. This parameter is required. | | `labels` | Record\ | Attaches key-value metadata to the restored sandbox. The restored sandbox does not inherit the labels of the source sandbox. | - The requests require the `write:snapshots` and `write:sandboxes` permissions. - The source sandbox must be running. - The source sandbox must be persistent or served from a warm template. A snapshot of any other sandbox, such as one created from a filesystem snapshot or with volumes attached, fails with `409`. ```bash # Capture the memory and disk of the running sandbox and wait until it is ready daytona snapshot create {sandboxId} --name my-snapshot # Resume the captured processes and memory in a new sandbox daytona sandbox create-from-snapshot my-snapshot --label team=research ``` | **Flag** | **Type** | **Description** | | -------- | -------- | --------------- | | `--name` | string | Sets the snapshot name. This flag is required. | | `--label` | KEY=VALUE | Attaches a `KEY=VALUE` metadata label to the restored sandbox. The restored sandbox does not inherit the labels of the source sandbox. Repeat the flag for more labels. | | `--timeout` | string | Sets how long the command waits for the snapshot to be `ready`. The default is `10m`. | | `--no-wait` | boolean | Returns once the capture is queued, without waiting for `ready`. | - The requests require the `write:snapshots` and `write:sandboxes` permissions. - The source sandbox must be running. - The source sandbox must be persistent or served from a warm template. A snapshot of any other sandbox, such as one created from a filesystem snapshot or with volumes attached, fails with `409`. ```bash # Capture the memory and disk of the running sandbox curl 'https://api.daytona.com/sandboxes/{sandboxId}/snapshots' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"name": "my-snapshot"}' # Wait for the capture to be ready curl 'https://api.daytona.com/snapshots/{snapshotId}?wait=ready&timeout_ms=60000' \ --header 'Authorization: Bearer YOUR_API_KEY' # Resume the captured processes and memory in a new sandbox curl 'https://api.daytona.com/sandboxes/from-snapshot' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"snapshot": "my-snapshot", "labels": {"team": "research"}}' ``` | **Field** | **Type** | **Description** | | --------- | -------- | --------------- | | `name` | string | Sets the snapshot name, 1 to 128 characters of letters, digits, `.`, `_`, and `-`. The name cannot start with `.` or with a reserved prefix: `sbx_`, `vol_`, `fn_`, `img_`, `snap_`, `snap-`, `build-`, `refs`, `by-digest`, `prj-`, or `glb-`. This field is required. Snapshots and images in the project share one name space. A name in use fails with `409`. | | `snapshot` | string | Sets the snapshot that `POST /sandboxes/from-snapshot` restores from, by name or by ID, `snap_...`. This field is required. | | `labels` | object | Attaches key-value metadata to the restored sandbox. The restored sandbox does not inherit the labels of the source sandbox. | - The requests require the `write:snapshots` and `write:sandboxes` permissions. - The source sandbox must be running. - The source sandbox must be persistent or served from a warm template. A snapshot of any other sandbox, such as one created from a filesystem snapshot or with volumes attached, fails with `409`. - The wait request returns at `ready` or `failed`, or after 60 seconds. Repeat it while the snapshot status is `pending` or `building`. A `failed` snapshot does not become `ready`. - A restore from a snapshot that is not ready is refused. ## Filesystem persistence [#filesystem-persistence] Filesystem persistence keeps the contents of a sandbox's disk: files, installed packages, cloned repositories, and build artifacts. A sandbox with a preserved filesystem starts with its environment intact, with nothing to reinstall or rebuild. A Linux sandbox keeps its filesystem on its host until it is deleted. Deleting the sandbox or losing its host discards the filesystem. [Filesystem snapshots](#filesystem-snapshot) keep the filesystem beyond the sandbox, and new sandboxes boot from them. ## Memory persistence [#memory-persistence] Memory persistence keeps the runtime state of a sandbox: running processes, open connections, and everything loaded in RAM. A sandbox with preserved memory continues from the point it was frozen, with processes still running, instead of starting from a clean boot. A Linux sandbox keeps its memory only while it runs. [Memory snapshots](#memory-snapshot) keep memory and processes beyond the sandbox. ## Lifecycle [#lifecycle] Every Linux sandbox moves through a lifecycle. A Linux sandbox can have several different states. Each state reflects the status of your sandbox. A Linux sandbox transitions between states in response to user actions, or automatically when one of its [timeouts](#timeouts) elapses. Every Linux sandbox supports wait for state, delete, timeouts, and lifecycle policy updates. A Linux sandbox keeps its filesystem and memory until it is deleted, or until its host fails: the sandbox then moves to error (lost in the CLI and API), and its local filesystem and memory are gone. The following tables list the states of a Linux sandbox and the transitions between them. The SDKs report the states in the **State** column. The CLI and API report the more detailed status in the **API status** column. | **State** | **API status** | **Description** | | ---------------- | ------------------------------ | ------------------------------------------------------------------------------- | | **`queued`** | **`queued`** | The create request is recorded and waits to be published for placement. | | **`creating`** | **`pending`**, **`assigned`** | The create request is accepted, and the sandbox is queued for and placed on a host. | | **`starting`** | **`booting`** | The microVM is booting and will be ready to use. | | **`started`** | **`running`** | The sandbox is running and ready to accept requests. | | **`destroying`** | **`deleting`** | The sandbox is being deleted. | | **`destroyed`** | **`deleted`** | The sandbox and its storage are removed. | | **`error`** | **`failed`**, **`lost`** | The sandbox could not boot, hit an unrecoverable error, or its host was lost. | | **`unknown`** | | The SDK does not recognize the status the API reports. | A sandbox can transition between states in response to various actions. The following table lists the initial state, target state, and trigger for the transition. | **Initial state** | **Target state** | **Trigger** | | ----------------- | ---------------- | ---------------------------------------------------------------------------------- | | **`queued`** | **`creating`** | The create request is published for placement. | | **`creating`** | **`starting`** | The sandbox is placed on a host, the host prepares its image, and the microVM boots. | | **`starting`** | **`started`** | The sandbox finishes booting and accepts requests. | | any state | **`destroying`** | A delete is requested, a timeout elapses, or the platform reclaims a spot sandbox. | | **`destroying`** | **`destroyed`** | The sandbox is removed and its resources are released. | | any active state | **`error`** | The sandbox fails to boot, hits an unrecoverable error, or its host is lost. | ### Wait for state [#wait-for-state] Wait until a Linux sandbox reaches a target state. The wait stops early when the sandbox reaches a state from which the target is no longer reachable, such as error or destroyed. The SDKs use the states in the [sandbox states](#lifecycle) table, so the SDK started state corresponds to the running status in the CLI and API. ```python # Block until the sandbox is started; returns the sandbox sandbox.wait(until="started", timeout=60) # Block until the sandbox is in one of the listed states sandbox.wait(until=["starting", "started"], timeout=60) ``` | **Parameter** | **Type** | **Description** | | ------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------- | | `until` | str \| Sequence\[str\] | Sets one state or a list of states. `wait()` returns on any of them. The default is `"started"`. | | `timeout` | float | Sets the wait limit in seconds. When the timeout elapses, `wait()` raises `RequestTimeoutError`. When the sandbox reaches `destroying`, `destroyed`, or `error` first, it raises `SandboxTerminalStateError`. `0` waits without a limit. The default is `60`. | ```typescript // Block until the sandbox is started; returns the sandbox await sandbox.wait({ until: 'started', timeoutSeconds: 60 }) // Block until the sandbox is in one of the listed states await sandbox.wait({ until: ['starting', 'started'], timeoutSeconds: 60 }) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- | | `until` | SandboxWaitState \| SandboxWaitState\[\] | Sets one state or a list of states. `wait()` returns on any of them. The default is `'started'`. | | `timeoutSeconds` | number | Sets the wait limit in seconds. When the timeout elapses, `wait()` throws `RequestTimeoutError`. When the sandbox reaches `destroying`, `destroyed`, or `error` first, it throws `SandboxTerminalStateError`. `0` waits without a limit. The default is `60`. | ```bash # Block until the sandbox is running, or the timeout elapses daytona sandbox wait {sandboxId} --for running --timeout 60s ``` | **Flag** | **Type** | **Description** | | -------- | -------- | --------------- | | `--for` | string | Sets the state to wait for: `running`, `stopped`, `paused`, or `deleted`. The SDK names `started` and `destroyed` are also accepted. The default is `running`. | | `--timeout` | string | Sets how long the command waits. The default is `10m`. | ```bash # Long-poll until the sandbox is running, a terminal state, or the timeout elapses curl 'https://api.daytona.com/sandboxes/{sandboxId}?wait=running&timeout_ms=60000' \ --header 'Authorization: Bearer YOUR_API_KEY' # Long-poll until the sandbox is exactly deleted; wait_exact skips the transient deleting state curl 'https://api.daytona.com/sandboxes/{sandboxId}?wait_exact=deleted&timeout_ms=60000' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Type** | **Description** | | ------------ | --------- | ----------------------------------------------------------------------------------------------------------- | | `wait` | string | Blocks the response until the sandbox reaches this state or a terminal state. The wait also ends on `stopping`, `stopped`, `deleting`, `deleted`, `failed`, or `lost`. There is no default. | | `wait_exact` | string | Blocks the response until the sandbox reaches exactly this state. Only `failed` and `lost` also end the wait. There is no default. | | `timeout_ms` | integer | Sets the wait limit in milliseconds, at most `60000`. A larger value is lowered to `60000`. When the limit elapses, the response is `200` with the current sandbox. Check `status` in the response. The default is `30000`. | ### Delete [#delete] Delete a Linux sandbox to tear down its virtual machine. Deletion works in any state. The API returns once teardown starts, and the SDKs and the CLI wait until the sandbox is deleted. The sandbox moves from deleting to deleted when the teardown finishes. Deleting a sandbox discards its filesystem and memory. Images and snapshots captured from the sandbox remain. To keep files from a sandbox, take a [filesystem snapshot](#filesystem-snapshot) or write the files to a [volume](/docs/volumes) or other storage outside the sandbox. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.delete() # Delete by sandbox ID, without fetching the sandbox first daytona.v2.delete("{sandboxId}") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.delete() // Delete by sandbox ID, without fetching the sandbox first await daytona.v2.delete('{sandboxId}') ``` ```bash daytona sandbox delete {sandboxId} ``` ```bash curl 'https://api.daytona.com/sandboxes/{sandboxId}' \ --request DELETE \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ### Timeouts [#timeouts] Daytona deletes a Linux sandbox automatically when one of its two timers elapses. Each timer pairs an interval in seconds with the action it fires. Set both timers at create in the lifecycle policy, and change them later through [update lifecycle policy](#update-lifecycle-policy). | **Timer** | **Purpose** | **Fires after** | **Default** | | --------------------------------- | --------------------------------------------------- | ------------------------------------------------------------ | --------------------------- | | [**Timeout**](#timeout) | Cap how long a sandbox lives | The interval has passed since the sandbox last started running, active or not | Project or platform default | | [**Idle timeout**](#idle-timeout) | Delete a sandbox that is not in use | The sandbox has had no activity for the interval | Project default, or off | The action for both timers is delete. Auto-delete is not a separate setting. It is the idle timeout with the delete action. #### Timeout [#timeout] The timeout is a wall-clock budget. It starts when the sandbox starts running. Activity does not extend it. A delete timeout fires in any state, except while the sandbox is still being created: a sandbox that does not reach running is deleted at its create deadline. Once set, the timeout cannot be cleared. When the timeout is omitted at create, Daytona applies the project's [lifecycle defaults](/docs/projects#lifecycle-defaults). Without a project default, Daytona applies the platform default timeout of 300 seconds, if the platform default is enabled. An explicit timeout must not exceed the organization maximum, 86,400 seconds (24 hours) unless the organization sets a different maximum. When the action is omitted, it defaults to delete. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create( # Delete the sandbox 2 hours after it is created lifecycle_policy={"timeout_seconds": 7200, "on_timeout": "delete"}, ) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create({ // Delete the sandbox 2 hours after it is created lifecyclePolicy: { timeoutSeconds: 7200, onTimeout: 'delete' }, }) ``` ```bash # Delete the sandbox 2 hours after it is created daytona sandbox create --lifecycle-timeout 2h --on-lifecycle-timeout delete ``` ```bash # Delete the sandbox 2 hours after it is created curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "lifecycle_policy": { "timeout_seconds": 7200, "on_timeout": "delete" } }' ``` #### Idle timeout [#idle-timeout] The idle timeout fires after the sandbox has had no activity for the interval. Activity is any request through the toolbox API, preview URLs, VNC, and SSH, a toolbox command that is still running, the sandbox being created or starting to run, and a reset of the idle timer. A change to the lifecycle policy does not count as activity. The idle timeout is off by default unless the project sets one, has a minimum of 60 seconds, and requires an action. The API raises a smaller value to 60 seconds, and the CLI refuses it. The action fires within about 2 minutes after the interval elapses. ##### Auto-delete [#auto-delete] Delete a Linux sandbox after a period of inactivity. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create( lifecycle_policy={"idle_expiry_seconds": 3600, "on_idle_timeout": "delete"}, ) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create({ lifecyclePolicy: { idleExpirySeconds: 3600, onIdleTimeout: 'delete' }, }) ``` ```bash daytona sandbox create --idle-timeout 1h --on-idle-timeout delete ``` ```bash curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "lifecycle_policy": { "idle_expiry_seconds": 3600, "on_idle_timeout": "delete" } }' ``` ##### Reset the idle timer [#reset-the-idle-timer] Reset the idle timer of a Linux sandbox to keep it alive while it does work that Daytona does not count as activity. The reset counts as activity, so the idle countdown restarts from now. The timeout is not affected. The reset succeeds whether or not an idle timeout is set, in any state except deleting and deleted. It never moves the timer backwards: when newer activity arrives at the same time, the newer activity wins. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create( lifecycle_policy={"idle_expiry_seconds": 3600, "on_idle_timeout": "delete"}, ) lifecycle = sandbox.reset_idle_timer() print(lifecycle.idle_timeout_at) ``` - The request requires the `write:sandboxes` permission. - When the idle action is delete, the request also requires the `delete:sandboxes` permission. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create({ lifecyclePolicy: { idleExpirySeconds: 3600, onIdleTimeout: 'delete' }, }) const lifecycle = await sandbox.resetIdleTimer() console.log(lifecycle.idleTimeoutAt) ``` - The request requires the `write:sandboxes` permission. - When the idle action is delete, the request also requires the `delete:sandboxes` permission. ```bash curl 'https://api.daytona.com/sandboxes/{sandboxId}/reset-idle-timer' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' ``` The response has the sandbox ID and its lifecycle policy with the following fields: | **Field** | **Description** | | ----------------- | ---------------------------------------- | | `idle_timeout_at` | The reset moves this value forward. | | `timeout_at` | The reset leaves this value unchanged. | - The request requires the `write:sandboxes` permission. - When the idle action is delete, the request also requires the `delete:sandboxes` permission. ### Update lifecycle policy [#update-lifecycle-policy] Update the timeouts of an existing Linux sandbox. Omitted fields keep their current values. The new actions must be valid for the sandbox, as listed in [timeouts](#timeouts). A deleting or deleted sandbox cannot be updated. ```python from daytona import Daytona from daytona.v2 import DISABLED daytona = Daytona() sandbox = daytona.v2.create() # Delete the sandbox after 30 minutes without activity; omitted fields keep their current value lifecycle = sandbox.update_lifecycle_policy(idle_expiry_seconds=1800, on_idle_timeout="delete") print(lifecycle.idle_expiry_seconds, lifecycle.on_idle_timeout) # Re-anchor the timeout to 1 hour from now lifecycle = sandbox.update_lifecycle_policy(timeout_seconds=3600) print(lifecycle.timeout_at) # Clear the idle timeout lifecycle = sandbox.update_lifecycle_policy(idle_expiry_seconds=DISABLED) ``` - The request requires the `write:sandboxes` permission. - Setting, changing, or clearing a delete action also requires the `delete:sandboxes` permission. ```typescript import { Daytona } from '@daytona/sdk' import { DISABLED } from '@daytona/sdk/v2' const daytona = new Daytona() const sandbox = await daytona.v2.create() // Delete the sandbox after 30 minutes without activity; omitted fields keep their current value let lifecycle = await sandbox.updateLifecyclePolicy({ idleExpirySeconds: 1800, onIdleTimeout: 'delete' }) console.log(lifecycle.idleExpirySeconds, lifecycle.onIdleTimeout) // Re-anchor the timeout to 1 hour from now lifecycle = await sandbox.updateLifecyclePolicy({ timeoutSeconds: 3600 }) console.log(lifecycle.timeoutAt) // Clear the idle timeout lifecycle = await sandbox.updateLifecyclePolicy({ idleExpirySeconds: DISABLED }) ``` - The request requires the `write:sandboxes` permission. - Setting, changing, or clearing a delete action also requires the `delete:sandboxes` permission. ```bash # Delete the sandbox after 30 minutes without activity daytona sandbox update {sandboxId} --idle-timeout 30m --on-idle-timeout delete # Re-anchor the timeout to 1 hour from now daytona sandbox update {sandboxId} --lifecycle-timeout 1h # Clear the idle timeout daytona sandbox update {sandboxId} --idle-timeout off ``` - The request requires the `write:sandboxes` permission. - Setting, changing, or clearing a delete action also requires the `delete:sandboxes` permission. ```bash # Delete the sandbox after 30 minutes without activity curl 'https://api.daytona.com/sandboxes/{sandboxId}/lifecycle-policy' \ --request PATCH \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "idle_expiry_seconds": 1800, "on_idle_timeout": "delete" }' # Re-anchor the timeout to 1 hour from now curl 'https://api.daytona.com/sandboxes/{sandboxId}/lifecycle-policy' \ --request PATCH \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "timeout_seconds": 3600 }' # Clear the idle timeout curl 'https://api.daytona.com/sandboxes/{sandboxId}/lifecycle-policy' \ --request PATCH \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "idle_expiry_seconds": -1 }' ``` | **Field** | **Type** | **Description** | | ------------------- | --------- | ----------------------------------------------------------------------------------------------------------------- | | `timeout_seconds` | integer | Sets the seconds until the timeout, counted from now. If omitted, the value is unchanged. | | `idle_expiry_seconds` | integer | Sets the seconds without activity before the idle action. `-1` clears the idle timeout and its action. If omitted, the value is unchanged. | | `on_timeout` | string | Sets the timeout action, `delete`. If omitted, the value is unchanged. | | `on_idle_timeout` | string | Sets the idle action, `delete`. It is required when you set an idle timeout on a sandbox that has none. If omitted, the value is unchanged. | - The request requires the `write:sandboxes` permission. - Setting, changing, or clearing a delete action also requires the `delete:sandboxes` permission. ## Permissions [#permissions] Linux sandboxes use the sandbox permissions of their project, and snapshots use the image and snapshot permissions. You can grant each to an [organization role](/docs/organizations#roles) or an [API key](/docs/api-keys). Setting, changing, or clearing a delete action in a lifecycle policy, and resetting the idle timer of a sandbox whose idle action is delete, also require the permission to delete sandboxes. The [audit logs](/docs/audit-logs) record sandbox creates, lifecycle changes, deletes, and snapshots. | **Permission** | **Grants** | | ---------------------- | --------------------------------------------------------------------------- | | **`read:sandboxes`** | Get sandboxes and wait for their state | | **`write:sandboxes`** | Create sandboxes, change their lifecycle policy, and reset the idle timer | | **`delete:sandboxes`** | Delete sandboxes | | **`write:images`** | Capture filesystem snapshots as images | | **`write:snapshots`** | Create memory snapshots | # Windows Sandboxes (/docs/windows-sandboxes) Windows sandboxes are **Windows virtual machines (VMs)**. Each sandbox runs its own Windows operating system on shared physical hardware and has its own files, network connection, and fixed amount of processor, memory, and disk. The hypervisor enforces the boundary and isolation between sandboxes and the host. A Windows sandbox starts from a prepared Windows base image or from an image captured from another Windows sandbox. The base image stays read-only, and each sandbox writes its changes to its own disk. A persistent Windows sandbox keeps its disk across stop and start. While the sandbox is running, it exposes a toolbox for command and process execution, sessions, filesystem and git operations, and computer use. Commands run in PowerShell, and the desktop is reached over the remote desktop protocol. The features and limits of Windows sandboxes can differ from those of Linux sandboxes and may change over time. | **Term** | **Description** | | ------------------ | -------------------------------------------------------------------------------------------------------------- | | **Sandbox** | An isolated Windows machine created on demand to run code. | | **Host** | The physical server that runs many sandboxes at once. Daytona calls these servers runners. | | **Guest** | The Windows operating system running inside a sandbox, as opposed to the operating system of the host. | | **Hypervisor** | Software on the host that creates virtual machines and keeps them separated from each other and from the host. | | **Base image** | A prepared Windows disk that a sandbox starts from. Sandboxes do not write to it. | | **Captured image** | A saved copy of a Windows sandbox's disk that new sandboxes start from. | | **Persistent** | A sandbox that keeps its disk across stop and start. | | **PowerShell** | The Windows shell that runs commands and sessions inside the sandbox. | | **Remote desktop** | Access to the Windows desktop of the sandbox over the remote desktop protocol (RDP). | Windows sandboxes for Daytona v2 are coming soon. # macOS Sandboxes (/docs/macos-sandboxes) macOS sandboxes are **macOS virtual machines (VMs)** on dedicated Apple silicon hardware. Each sandbox runs on a Mac Mini that your organization reserves. You can run commands in a macOS sandbox, connect with SSH, see and control its desktop, and keep disk between sessions. A Mac is reserved for a 24-hour period. You choose the cores, memory and disk of each sandbox when you create it: one sandbox can use the whole Mac, or two sandboxes share it. macOS sandboxes run only on reservations. When your organization holds no reservation, Daytona can buy one for you when you create a sandbox. | **Term** | **Description** | | ---------------- | ------------------------------------------------------------------------------------------------------------ | | **Reservation** | One dedicated Mac for your organization for a 24-hour period. Nobody else uses that Mac while you hold it. | | **Size** | The cores, memory and disk of one sandbox. You choose it when you create the sandbox. | | **Model** | The type of Mac. | | **Auto-reserve** | An organization setting that buys a Mac for you when you create a macOS sandbox and hold no reservation. | | **Image** | A macOS disk that a sandbox starts from: a base image or a snapshot of your sandbox. | | **Persistent** | A sandbox that keeps its disk across stop and start. Stop, start, pause, resume and snapshots need it. | ## Use cases [#use-cases] macOS sandboxes are used when the software, the build, or the task requires macOS or Apple tooling. - **Build and test Apple apps.** An agent or a CI job builds and tests iOS and macOS apps with Xcode and the iPhone simulator on a clean Mac. The base image ships with Xcode, its iPhone simulator, Homebrew, and Python. The image runs macOS 26.6.2, and its Homebrew includes CocoaPods, fastlane, and Node.js 24. - **Computer-use agents on macOS.** An agent operates macOS applications through screenshots and mouse and keyboard input. Computer use also reads the accessibility tree, acts on controls, and records the screen. The desktop is 1280 by 800, and your own [VNC client](#vnc-client) can connect to it over a WebSocket. - **Long-lived build machines.** A persistent sandbox keeps its disk across stop and start, so installed tools and build caches stay between sessions. To keep the disk after the reservation ends, take a [filesystem snapshot](#filesystem-snapshot). Automatic renewal stops when every sandbox on the Mac is stopped or paused, and the reservation then ends with its period. - **Remote access over SSH.** A developer or a script connects to the Mac over SSH to run commands and inspect builds. The CLI opens the connection itself, and other SSH clients sign in with a short-lived sandbox token in the user name. The token is limited to one sandbox and to SSH, so no project API key goes into the SSH command. - **Fleets of Macs.** One request reserves up to 100 Macs for parallel builds and tests. Each Mac is a separate reservation with its own ID, period, charge, and renewal. A minimum count accepts fewer Macs when not enough are available. ## Requirements [#requirements] Your organization must have macOS sandboxes turned on. Without it, reserving a Mac or creating a macOS sandbox fails with a [feature error](#errors). Sign in to the dashboard at [app.daytona.com ↗](https://app.daytona.com) and create a project API key. The examples read the key from an environment variable. ### Endpoints [#endpoints] macOS sandboxes have their own API address. Send all macOS requests to the macOS API. | **What** | **Address** | | ----------------------------------------- | ----------------------------------------------- | | Dashboard | **`https://app.daytona.com`** | | Daytona API (organizations, API keys) | **`https://api.daytona.com`** | | macOS API (reservations, macOS sandboxes) | **`https://api.us-macos.daytona.com`** | | Sandbox SSH | **`ssh.us-macos.daytona.com`**, port **`2222`** | | Region of the Macs | **`macos-1`** | The SDKs and the CLI read the API address and your key from environment variables. Give the region of the Macs, listed in the table above, on each reservation and each macOS create. ```bash export DAYTONA_API_KEY=dtn_... export DAYTONA_API_URL=https://api.us-macos.daytona.com export DAYTONA_REGION=macos-1 # the CLI's default region ``` ### Install [#install] Pin these exact versions. Pre-release and latest installs pull other versions that do not have the macOS features. Python 3.10 or later: ```bash pip install daytona==2.0.0a11 ``` Do not use `pip install --pre daytona`. It installs another version that does not have the macOS features. Node.js 22.4 or later: ```bash npm install @daytona/sdk@2.0.0-alpha.11 ``` Do not use `npm install @daytona/sdk@latest`. It installs another version that does not have the macOS features. On macOS or Linux: ```bash curl -fsSL https://github.com/daytona/clients/releases/download/v2.0.0-alpha.11/install.sh | sh -s -- --version 2.0.0-alpha.11 ``` With the API key set in the environment, the CLI needs no login. ### Connect [#connect] The SDK clients read your key and the macOS API address from the environment. ```python from daytona import Daytona daytona = Daytona() # reads DAYTONA_API_KEY and DAYTONA_API_URL # AsyncDaytona has the same methods with await ``` - `memory` and `disk` are in GiB. - Timeouts are in seconds. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() // reads DAYTONA_API_KEY and DAYTONA_API_URL ``` - `resources.memory` and `resources.disk` are in GiB. - Every timeout is in seconds: `timeoutSeconds`, `waitSeconds` and `createTimeoutSeconds`. ## Create a macOS sandbox [#create-a-macos-sandbox] Create a macOS sandbox from a macOS image. Each sandbox runs on one of your [Mac reservations](#mac-reservations). When your organization holds no reservation in the region and [auto-reserve](#auto-reserve) is on, which it is by default, the create buys one for you, so you do not need to reserve a Mac first. With auto-reserve off, reserve a Mac before you create a sandbox. The base image has macOS, Xcode and common build tools. The following table lists its contents. | **Property** | **Value** | | ---------------- | ----------------------------------------------------------------------------------------------------------- | | Image | `base-macos` | | Operating system | macOS 26.6.2 | | Tools | Xcode 26.6 and its iPhone simulator, Homebrew (with git, Node.js 24, Ruby, CocoaPods, fastlane, gh and jq) and Python 3.14 | | User | `lume`, with home `/Users/lume` | | Password | `lume`. See [user and password](#user-and-password) | You choose the [size](#sizes) of each sandbox when you create it. Two sandboxes share one Mac when their sizes fit in the Mac together. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create_macos("base-macos", region="macos-1") ``` The following example creates two sandboxes of different sizes on one reservation: ```python from daytona.v2 import MacosResources r = daytona.v2.reservations.create(region="macos-1", model="m4pro-12c-24g", idempotency_key="shared-mac-1") a = daytona.v2.create_macos("base-macos", region="macos-1", reservation=r.id, resources=MacosResources(cpu=4, memory=8, disk=100)) b = daytona.v2.create_macos("base-macos", region="macos-1", reservation=r.id, resources=MacosResources(cpu=8, memory=16, disk=120)) ``` The following example creates a persistent sandbox that uses the whole Mac: ```python from daytona.v2 import MacosResources r = daytona.v2.reservations.create(region="macos-1", model="m4pro-12c-24g", idempotency_key="ios-builder-mac-1") sandbox = daytona.v2.create_macos( "base-macos", region="macos-1", reservation=r.id, resources=MacosResources(cpu=12, memory=24, disk=220), persistent=True, name="ios-builder", wait_seconds=120, ) print(sandbox.id, sandbox.reservation_id, sandbox.cpu, sandbox.memory, sandbox.disk) ``` | **Parameter** | **Type** | **Description** | | ---------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `image` | str | Sets the macOS image that the sandbox starts from, for example `base-macos` or a snapshot. This is the first positional argument. | | `region` | str | Sets the region, `macos-1`. | | `reservation` | str | Sets the reservation that the sandbox runs on: a reservation ID, or `"new"`. See [which reservation](#which-reservation). | | `auto_renew` | bool | Sets [automatic renewal](#automatic-renewal) of the reservation that the create buys. Only with `reservation="new"`. | | `resources` | MacosResources | Sets the size: `cpu`, the number of CPU cores; `memory`, the memory in GiB; and `disk`, the whole guest disk in GiB, macOS included. See [sizes](#sizes). | | `persistent` | bool | Keeps the disk across stop and start. See [filesystem persistence](#filesystem-persistence). | | `name` | str | Sets the display name. | | `wait_seconds` | int | Sets how many seconds the call waits for the sandbox to start. | | `create_timeout_seconds` | int | Sets how many seconds the server tries before it deletes the waiting sandbox. | - The request requires the `write:sandboxes` permission. - The other create options, `env`, `labels`, `secrets`, `ports`, and `network_policy`, work as on [Linux sandboxes](/docs/linux-sandboxes#create-a-linux-sandbox). ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.createMacos('base-macos', { region: 'macos-1' }) ``` The following example creates two sandboxes of different sizes on one reservation: ```typescript const r = await daytona.v2.reservations.create({ region: 'macos-1', model: 'm4pro-12c-24g', idempotencyKey: 'shared-mac-1', }) const a = await daytona.v2.createMacos('base-macos', { region: 'macos-1', reservation: r.id, resources: { cpu: 4, memory: 8, disk: 100 }, }) const b = await daytona.v2.createMacos('base-macos', { region: 'macos-1', reservation: r.id, resources: { cpu: 8, memory: 16, disk: 120 }, }) ``` The following example creates a persistent sandbox that uses the whole Mac: ```typescript const r = await daytona.v2.reservations.create({ region: 'macos-1', model: 'm4pro-12c-24g', idempotencyKey: 'ios-builder-mac-1', }) const sandbox = await daytona.v2.createMacos('base-macos', { region: 'macos-1', reservation: r.id, resources: { cpu: 12, memory: 24, disk: 220 }, persistent: true, name: 'ios-builder', waitSeconds: 120, }) console.log(sandbox.id, sandbox.reservationId) ``` | **Parameter** | **Type** | **Description** | | -------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `image` | string | Sets the macOS image that the sandbox starts from, for example `base-macos` or a snapshot. This is the first positional argument. | | `region` | string | Sets the region, `macos-1`. | | `reservation` | string | Sets the reservation that the sandbox runs on: a reservation ID, or `'new'`. See [which reservation](#which-reservation). | | `autoRenew` | boolean | Sets [automatic renewal](#automatic-renewal) of the reservation that the create buys. Only with `reservation: 'new'`. | | `resources` | MacosResources | Sets the size: `cpu`, the number of CPU cores; `memory`, the memory in GiB; and `disk`, the whole guest disk in GiB, macOS included. See [sizes](#sizes). | | `persistent` | boolean | Keeps the disk across stop and start. See [filesystem persistence](#filesystem-persistence). | | `name` | string | Sets the display name. | | `waitSeconds` | number | Sets how many seconds the call waits for the sandbox to start. | | `createTimeoutSeconds` | number | Sets how many seconds the server tries before it deletes the waiting sandbox. | - The request requires the `write:sandboxes` permission. - The other create options, `env`, `labels`, `secrets`, `ports`, and `networkPolicy`, work as on [Linux sandboxes](/docs/linux-sandboxes#create-a-linux-sandbox). ```bash daytona sandbox create-macos --region macos-1 --image base-macos ``` The following example creates two sandboxes of different sizes on one reservation: ```bash daytona sandbox create-macos --region macos-1 --image base-macos --reservation RESERVATION_ID \ --cores 4 --memory 8192 --disk 102400 daytona sandbox create-macos --region macos-1 --image base-macos --reservation RESERVATION_ID \ --cores 8 --memory 16384 --disk 122880 ``` The following example creates a persistent sandbox that uses the whole Mac: ```bash daytona sandbox create-macos --region macos-1 --image base-macos --reservation RESERVATION_ID \ --cores 12 --memory 24576 --disk 225280 --persistent --name ios-builder ``` | **Flag** | **Type** | **Description** | | ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- | | `--image` | string | Sets the macOS image that the sandbox starts from, for example `base-macos` or a snapshot. | | `--region` | string | Sets the region, `macos-1`. | | `--reservation` | string | Sets the reservation that the sandbox runs on: a reservation ID, or `new`. See [which reservation](#which-reservation). | | `--auto-renew` | bool | Sets [automatic renewal](#automatic-renewal) of the reservation that the create buys. Only with `--reservation new`. | | `--cores` | int32 | Sets the number of CPU cores. See [sizes](#sizes). | | `--memory` | int32 | Sets the memory in MiB. See [sizes](#sizes). | | `--disk` | int32 | Sets the whole guest disk in MiB, macOS included. See [sizes](#sizes). | | `--persistent` | bool | Keeps the disk across stop and start. See [filesystem persistence](#filesystem-persistence). | | `--name` | string | Sets the display name. | | `--server-wait` | duration | Sets how long the server holds the create response for the sandbox to start, as a Go duration. `0s` answers at once. | | `--create-ttl` | duration | Sets how long the server tries before it deletes the waiting sandbox, as a Go duration. | - The request requires the `write:sandboxes` permission. - Other create options work as on [Linux sandboxes](/docs/linux-sandboxes#create-a-linux-sandbox), including the network flags `--block-all`, `--allow-domain`, and `--allow-cidr`. ```bash curl 'https://api.us-macos.daytona.com/sandboxes/macos' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "image": "base-macos", "region_id": "macos-1" }' ``` The following example creates two sandboxes of different sizes on one reservation: ```bash curl 'https://api.us-macos.daytona.com/sandboxes/macos' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "image": "base-macos", "region_id": "macos-1", "reservation": "RESERVATION_ID", "cores": 4, "memory_mib": 8192, "disk_mib": 102400 }' curl 'https://api.us-macos.daytona.com/sandboxes/macos' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "image": "base-macos", "region_id": "macos-1", "reservation": "RESERVATION_ID", "cores": 8, "memory_mib": 16384, "disk_mib": 122880 }' ``` The following example creates a persistent sandbox that uses the whole Mac: ```bash curl 'https://api.us-macos.daytona.com/sandboxes/macos' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Idempotency-Key: ios-builder-create-1' \ --data '{ "image": "base-macos", "region_id": "macos-1", "reservation": "RESERVATION_ID", "cores": 12, "memory_mib": 24576, "disk_mib": 225280, "persistent": true, "name": "ios-builder", "wait_seconds": 120 }' ``` | **Field** | **Type** | **Description** | | ------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------- | | `image` | string | Sets the macOS image that the sandbox starts from, by name or `img_` ID, for example `base-macos` or a snapshot. This field is required. | | `region_id` | string | Sets the region, `macos-1`. | | `reservation` | string | Sets the reservation that the sandbox runs on: a reservation ID, or `new`. See [which reservation](#which-reservation). | | `auto_renew` | boolean | Sets [automatic renewal](#automatic-renewal) of the reservation that the create buys. Only with `"reservation": "new"`. | | `cores` | integer | Sets the number of CPU cores. See [sizes](#sizes). | | `memory_mib` | integer | Sets the memory in MiB. See [sizes](#sizes). | | `disk_mib` | integer | Sets the whole guest disk in MiB, macOS included. See [sizes](#sizes). | | `persistent` | boolean | Keeps the disk across stop and start. See [filesystem persistence](#filesystem-persistence). | | `name` | string | Sets the display name. | | `wait_seconds` | integer | Sets how many seconds the server holds the request for the sandbox to start. `0` returns immediately. The default is 3. | | `create_timeout_seconds` | integer | Sets how many seconds the server tries before it deletes the waiting sandbox. | | `idempotency_key` | string | Makes a retried create return the same sandbox and never buy a second reservation. You can also send it as the `Idempotency-Key` header. | - The request requires the `write:sandboxes` permission. - The other create fields, `env`, `labels`, `secrets`, `ports`, and `network_policy`, work as on [Linux sandboxes](/docs/linux-sandboxes#create-a-linux-sandbox). ### Sizes [#sizes] A macOS sandbox takes its cores, memory and disk from the Mac of its reservation. The disk is the whole guest disk, macOS included. On the M4 Pro [model](#models-and-prices), the sandboxes of one Mac share 12 cores, 24 GiB memory and 220 GiB disk. Up to two sandboxes run on one Mac at the same time, and their sizes must fit in the Mac together. For example, one sandbox of 12 cores, 24 GiB memory and 220 GiB disk uses the whole Mac, and a sandbox of 4 cores, 8 GiB and 100 GiB fits beside a sandbox of 8 cores, 16 GiB and 120 GiB. A Mac fixes the size when it accepts the sandbox, and the sandbox reports that size from then on. | **Setting** | **SDK (GiB)** | **CLI (MiB)** | **API (MiB)** | **Minimum** | **Maximum on `m4pro-12c-24g`** | **If you omit it, on `m4pro-12c-24g`** | | ----------- | ----------------- | -------------- | ------------------ | ------------------ | ------------------------------ | -------------------------------------- | | Cores | **`resources.cpu`** (cores) | **`--cores`** | **`cores`** | 4 | 12 | 6 | | Memory | **`resources.memory`** | **`--memory`** | **`memory_mib`** | 4 GiB (4096 MiB) | 24 GiB (24576 MiB) | 12 GiB (12288 MiB) | | Disk | **`resources.disk`** | **`--disk`** | **`disk_mib`** | 80 GiB (81920 MiB) | 220 GiB (225280 MiB) | 110 GiB (112640 MiB) | ### Which reservation [#which-reservation] A create places the sandbox on a reservation. The reservation option decides which one, and whether the create buys a new one. | **Reservation option** | **Where the sandbox goes** | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | | Omitted | Any of your reservations in the region that has room. If you have none, [auto-reserve](#auto-reserve) buys one (if it is on). | | `reservation=""` | That reservation. | | `reservation="new"` | A new reservation that the create always buys for this sandbox. Only here can you also set `auto_renew`. | The create can wait for room on a reservation. Two options set how long: in seconds in the SDKs, and as a Go duration in the CLI. If the time runs out, a reservation that the create bought stays bought. | **Option** | **Description** | | -------------------------------------------------------------- | -------------------------------------------------------------------- | | `wait_seconds` (`waitSeconds`, `--server-wait`) | How long the call waits for the sandbox to start. | | `create_timeout_seconds` (`createTimeoutSeconds`, `--create-ttl`) | How long the server tries before it deletes the waiting sandbox. | ### Get a sandbox [#get-a-sandbox] Get a macOS sandbox by its ID, or list the sandboxes in the macOS region. ```python sandbox = daytona.v2.get("SANDBOX_ID") ``` ```typescript const sandbox = await daytona.v2.get('SANDBOX_ID') ``` ```bash daytona sandbox get SANDBOX_ID daytona sandbox list --region macos-1 ``` ## Mac reservations [#mac-reservations] A reservation gives your organization one dedicated Mac for a 24-hour period. Nobody else uses that Mac while you hold it. You pay for each period when it starts. The first period starts when a Mac accepts the reservation, not when you send the request, and time spent waiting for a Mac is not charged. You cannot end a reservation early. It ends at the end of its period unless it renews. macOS sandboxes run only on your reservations: a create picks a reservation with room left, or you name one. Up to two sandboxes run on the Mac at the same time, and you choose the [size](#sizes) of each sandbox when you create it. | **Status** | **Description** | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `pending_charge` | We are charging the period. | | `pending_mac` | The period is paid. The reservation waits for an available Mac. If no Mac accepts it within 5 minutes, it becomes `failed` and we refund the charge. | | `active` | A Mac holds the reservation. `period_end` tells you when the period ends. | | `ending` | The period ended. We stop the sandboxes, save persistent disks and erase the Mac. See [when a reservation ends](#when-a-reservation-ends). | | `released` | The reservation ended. `end_reason` tells you why, for example `expired`. | | `failed` | The charge failed, or no Mac accepted the reservation (refunded). | ### Models and prices [#models-and-prices] The production Macs are **Apple M4 Pro, 12 cores, 24 GB memory**. Always give this model when you reserve. Its model ID is in the table below. The price of one reservation of this model is **$2.5032 per Mac-hour, $60.08 per 24-hour period**. | **Model** | **Cores** | **Memory** | **Sandbox disk** | **Sandboxes at a time** | | --------------------- | --------- | ---------- | ---------------- | ----------------------- | | **`m4pro-12c-24g`** | 12 | 24 GiB | 220 GiB | Up to 2 | The sizes of the sandboxes on one Mac must fit in its cores, memory and sandbox disk together. See [sizes](#sizes). Read the current price and the number of available Macs from the Mac catalog. An available Mac is one that nobody has reserved. The number is a guide only: another purchase can reserve an available Mac before yours. ```python catalog = daytona.v2.mac_models.catalog(region="macos-1") for region in catalog.regions: for entry in region.models: price = entry.price print( entry.model.id, "available:", entry.available, "per period: $%.2f" % (price.period_cents / 100) if price else "no price", ) # Model specs only; the catalog adds prices and available Macs per region for model in daytona.v2.mac_models.list(): print(model.id, model.cores, model.memory_mib, model.max_sandboxes) ``` ```typescript const catalog = await daytona.v2.macModels.catalog({ region: 'macos-1' }) for (const region of catalog.regions) { for (const entry of region.models) { const perPeriod = entry.price ? `$${(entry.price.periodCents / 100).toFixed(2)}` : 'no price' console.log(entry.model.id, 'available:', entry.available, 'per period:', perPeriod) } } // Model specs only; the catalog adds prices and available Macs per region for (const model of await daytona.v2.macModels.list()) { console.log(model.id, model.cores, model.memoryMib, model.maxSandboxes) } ``` ```bash daytona mac-model list --region macos-1 ``` ```bash # Catalog: available Macs and prices per model; region_ids is a comma-separated list, all regions by default curl 'https://api.us-macos.daytona.com/mac-catalog?region_ids=macos-1' \ --header 'Authorization: Bearer YOUR_API_KEY' # Model specs only; the catalog adds prices and available Macs per region curl 'https://api.us-macos.daytona.com/mac-models' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Description** | | ---------------------- | --------------------------------------------------------------------------------------------------- | | `default_model` | The model a reservation in the region gets when it names none. | | `available` | The number of Macs of the model that nobody has reserved. A guide only; another purchase can reserve one first. | | `price` | `period_cents`, `cents_per_hour`, `period_seconds` and `currency`. `null` where billing is not enabled. | | `max_sandboxes` | The most sandboxes a Mac of the model runs at once. | | `sandbox_disk_mib` | The disk room for all sandboxes on one Mac, stopped sandboxes included. | ### Reserve a Mac [#reserve-a-mac] Reserve one Mac of a model in a region. Each purchase has an idempotency key. If you retry with the same key, you get the same reservation and you do not pay again. If you do not give a key, the SDK and the CLI make a new key for each call, so a retry buys a second Mac. Give your own key when your code can retry. ```python reservation = daytona.v2.reservations.create( region="macos-1", model="m4pro-12c-24g", idempotency_key="build-mac-2026-10-24", ) print(reservation.id, reservation.status) ``` | **Parameter** | **Type** | **Description** | | ----------------- | -------- | ---------------------------------------------------------------------------------------------------------------- | | `region` | str | Sets the region, `macos-1`. This parameter is required. | | `model` | str | Sets the Mac [model](#models-and-prices), `m4pro-12c-24g`. | | `idempotency_key` | str | Sets the idempotency key. Defaults to a new key for each call. | | `auto_renew` | bool | Sets [automatic renewal](#automatic-renewal). If you omit it, it follows your organization's auto-reserve setting, which is on by default. | ```typescript const reservation = await daytona.v2.reservations.create({ region: 'macos-1', model: 'm4pro-12c-24g', idempotencyKey: 'build-mac-2026-10-24', }) console.log(reservation.id, reservation.status) ``` | **Parameter** | **Type** | **Description** | | ---------------- | -------- | ---------------------------------------------------------------------------------------------------------------- | | `region` | string | Sets the region, `macos-1`. This parameter is required. | | `model` | string | Sets the Mac [model](#models-and-prices), `m4pro-12c-24g`. | | `idempotencyKey` | string | Sets the idempotency key. Defaults to a new key for each call. | | `autoRenew` | boolean | Sets [automatic renewal](#automatic-renewal). If you omit it, it follows your organization's auto-reserve setting, which is on by default. | ```bash daytona reservation create --region macos-1 --model m4pro-12c-24g \ --idempotency-key build-mac-2026-10-24 ``` | **Flag** | **Type** | **Description** | | ------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- | | `--region` | string | Sets the region, `macos-1`. | | `--model` | string | Sets the Mac [model](#models-and-prices), `m4pro-12c-24g`. | | `--idempotency-key` | string | Sets the idempotency key. Defaults to a new key for each invocation. | | `--auto-renew` | bool | Sets [automatic renewal](#automatic-renewal). If you omit it, it follows your organization's auto-reserve setting, which is on by default. | ```bash curl 'https://api.us-macos.daytona.com/reservations' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Idempotency-Key: build-mac-2026-10-24' \ --data '{ "region_id": "macos-1", "model": "m4pro-12c-24g" }' ``` | **Field** | **Type** | **Description** | | ----------------- | --------- | ---------------------------------------------------------------------------------------------------------------- | | `Idempotency-Key` | header | Sets the idempotency key. This header is required; the API does not generate a key. | | `region_id` | string | Sets the region, `macos-1`. This field is required. | | `model` | string | Sets the Mac [model](#models-and-prices), `m4pro-12c-24g`. | | `auto_renew` | boolean | Sets [automatic renewal](#automatic-renewal). If you omit it, it follows your organization's auto-reserve setting, which is on by default. | ### Reserve many Macs [#reserve-many-macs] Reserve 1 to 100 Macs of one model in one request. Each Mac is a separate reservation with its own ID, period, charge and renewal. By default, the request is all or nothing: if fewer Macs are available than you ask for, it reserves none and fails because no Mac is available. Set a minimum count to accept fewer: the request then reserves as many as are available, if that is at least the minimum. ```python batch = daytona.v2.reservations.create_bulk( region="macos-1", model="m4pro-12c-24g", count=20, min_count=10, idempotency_key="ci-fleet-2026-10-24", ) print(f"reserved {batch.reserved} of {batch.requested} Macs") ids = [r.id for r in batch.reservations] ``` | **Parameter** | **Type** | **Description** | | ----------------- | -------- | ----------------------------------------------------------------------- | | `count` | int | Sets how many Macs to reserve, 1 to 100. | | `min_count` | int | Sets the fewest Macs to accept when fewer than the count are available. Defaults to the count, all or nothing. | | `idempotency_key` | str | Sets the idempotency key. Defaults to a new key for each call. | ```typescript const batch = await daytona.v2.reservations.createBulk({ region: 'macos-1', model: 'm4pro-12c-24g', count: 20, minCount: 10, idempotencyKey: 'ci-fleet-2026-10-24', }) console.log(`reserved ${batch.reserved} of ${batch.requested} Macs`) ``` | **Parameter** | **Type** | **Description** | | ---------------- | -------- | ----------------------------------------------------------------------- | | `count` | number | Sets how many Macs to reserve, 1 to 100. | | `minCount` | number | Sets the fewest Macs to accept when fewer than the count are available. Defaults to the count, all or nothing. | | `idempotencyKey` | string | Sets the idempotency key. Defaults to a new key for each call. | A dry run shows the price and the available Macs and reserves nothing. ```bash daytona reservation create --region macos-1 --model m4pro-12c-24g --count 20 --dry-run daytona reservation create --region macos-1 --model m4pro-12c-24g --count 20 --min-count 10 \ --idempotency-key ci-fleet-2026-10-24 ``` | **Flag** | **Type** | **Description** | | ------------------- | -------- | ----------------------------------------------------------------------- | | `--count` | int | Sets how many Macs to reserve, 1 to 100. | | `--min-count` | int | Sets the fewest Macs to accept when fewer than the count are available. Defaults to the count, all or nothing. | | `--idempotency-key` | string | Sets the idempotency key. Defaults to a new key for each invocation. | | `--dry-run` | bool | Prints the price and the available Macs and reserves nothing. | ```bash curl 'https://api.us-macos.daytona.com/reservations/bulk' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Idempotency-Key: ci-fleet-2026-10-24' \ --data '{ "region_id": "macos-1", "model": "m4pro-12c-24g", "count": 20, "min_count": 10 }' ``` | **Field** | **Type** | **Description** | | ----------------- | --------- | ----------------------------------------------------------------------- | | `Idempotency-Key` | header | Sets the idempotency key. This header is required; the API does not generate a key. | | `count` | integer | Sets how many Macs to reserve, 1 to 100. | | `min_count` | integer | Sets the fewest Macs to accept when fewer than the count are available. Defaults to the count, all or nothing. | ### List reservations [#list-reservations] List the reservations of your organization, one page at a time. The list returns the open reservations and the reservations that ended in the last 30 days. ```python cursor = None while True: page = daytona.v2.reservations.list(limit=100, cursor=cursor) for r in page.items: print(r.id, r.status, r.model, r.period_end, r.auto_renew) cursor = page.next_cursor # None on the last page if cursor is None: break ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ---------------------------------------------------------------------------------- | | `limit` | int | Sets the page size, from `1` to `200`. The default is `100`. | | `cursor` | str | Resumes the list after the previous page. Pass the `next_cursor` of that page. | ```typescript let cursor: string | undefined do { const page = await daytona.v2.reservations.list({ limit: 100, cursor }) for (const r of page.items) { console.log(r.id, r.status, r.model, r.periodEnd, r.autoRenew) } cursor = page.nextCursor // undefined on the last page } while (cursor) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ---------------------------------------------------------------------------------- | | `limit` | number | Sets the page size, from `1` to `200`. The default is `100`. | | `cursor` | string | Resumes the list after the previous page. Pass the `nextCursor` of that page. | ```bash daytona reservation list ``` ```bash # Paginated; follow next_cursor for more rows curl 'https://api.us-macos.daytona.com/reservations?limit=100' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Type** | **Description** | | ------------ | ------------- | -------------------------------------------------------------------------------------- | | `limit` | integer | Sets the maximum number of items, from `1` to `200`. The default is `100`. | | `cursor` | string | Pass the `next_cursor` value of the previous response to read the next page. | #### Iterate over all reservations [#iterate-over-all-reservations] Iterate over every reservation of your organization without handling page cursors. The iterator fetches the next page when the current one is exhausted and stops when the server returns no cursor. ```python for r in daytona.v2.reservations.iter(): print(r.id, r.status, r.model, r.period_end, r.auto_renew) ``` ```typescript for await (const r of daytona.v2.reservations.iter()) { console.log(r.id, r.status, r.model, r.periodEnd, r.autoRenew) } ``` ### Get a reservation [#get-a-reservation] Get one reservation of your organization by its ID. The response has the same fields as an item of the list, including its current status and the end of its period. ```python r = daytona.v2.reservations.get("RESERVATION_ID") print(r.id, r.status, r.period_end, r.auto_renew) ``` ```typescript const r = await daytona.v2.reservations.get('RESERVATION_ID') console.log(r.id, r.status, r.periodEnd, r.autoRenew) ``` ```bash daytona reservation get RESERVATION_ID ``` ```bash curl 'https://api.us-macos.daytona.com/reservations/RESERVATION_ID' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ### Renew a reservation [#renew-a-reservation] Renew a reservation to pay for the next 24-hour period on the same Mac. You keep the Mac, its sandboxes and their disks. Use a new key for each period you want to buy, and the same key when you retry. ```python daytona.v2.reservations.renew(reservation.id, idempotency_key=f"renew-{reservation.id}-2") ``` ```typescript await daytona.v2.reservations.renew(reservation.id, { idempotencyKey: `renew-${reservation.id}-2` }) ``` ```bash daytona reservation renew RESERVATION_ID --idempotency-key renew-RESERVATION_ID-2 ``` ```bash curl 'https://api.us-macos.daytona.com/reservations/RESERVATION_ID/renew' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Idempotency-Key: renew-RESERVATION_ID-2' ``` - The request requires the `write:sandboxes` permission. - The `Idempotency-Key` header is required. - The API does not generate an idempotency key. ### Automatic renewal [#automatic-renewal] When automatic renewal is on, we renew the reservation in the last hour of its period, **but only while a sandbox on that Mac is active** (being created, starting or running). If all its sandboxes are stopped or paused, or it has none, the reservation does not renew and ends at the end of its period. If a renewal fails, the reservation records the reason and ends at the end of its period. Turn automatic renewal on or off for one reservation. ```python daytona.v2.reservations.update(reservation.id, auto_renew=False) ``` ```typescript await daytona.v2.reservations.update(reservation.id, { autoRenew: false }) ``` ```bash daytona reservation update RESERVATION_ID --auto-renew=false ``` ```bash curl 'https://api.us-macos.daytona.com/reservations/RESERVATION_ID' \ --request PATCH \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "auto_renew": false }' ``` ### Auto-reserve [#auto-reserve] Auto-reserve buys a Mac for you when you create a macOS sandbox and your organization holds no live reservation in the region. It buys the cheapest model that fits the sandbox, with automatic renewal on. If you want to control every purchase, turn auto-reserve off and reserve Macs yourself. - It is **on by default**. It holds at most **2** reservations at a time. Above that, a create fails because the auto-reserve limit is reached. - It does not buy a Mac when you have a reservation that is only full: no free slot, or too little disk left. - An organization admin can turn it off, or change the limit, in the dashboard: Settings, **Auto-reserve Macs**, or through the [macOS settings](#macos-settings). ### macOS settings [#macos-settings] The macOS settings control [auto-reserve](#auto-reserve) for macOS sandboxes. With auto-reserve on, a macOS create in a region where the organization holds no live Mac reservation buys a 24-hour reservation that renews while a sandbox runs on it. Auto-reserve is on by default, and the organization caps how many auto-reserved reservations it holds at once. #### Get the settings [#get-the-settings] Get the organization's macOS settings. ```bash curl 'https://api.daytona.com/orgs/ORGANIZATION_ID/macos-settings' \ --header 'Authorization: Bearer YOUR_OIDC_TOKEN' ``` ```json { "macos_auto_reserve": true, "macos_auto_reserve_max_reservations": 2, "macos_auto_reserve_disabled_at": null, "macos_auto_reserve_disabled_by": null } ``` The response has the following fields: | **Field** | **Description** | | -------------------------------------- | --------------------------------------------------------------------------------------------------- | | `macos_auto_reserve` | Shows whether auto-reserve is on. | | `macos_auto_reserve_max_reservations` | The most auto-reserved reservations the organization holds at once. | | `macos_auto_reserve_disabled_at` | Records when auto-reserve was last turned off. The value is `null` while auto-reserve is on. | | `macos_auto_reserve_disabled_by` | Records who last turned auto-reserve off. The value is `null` while auto-reserve is on. | #### Set the settings [#set-the-settings] Turn auto-reserve on or off, or change the cap on auto-reserved reservations. Fields left out of the request keep their stored values. ```bash curl 'https://api.daytona.com/orgs/ORGANIZATION_ID/macos-settings' \ --request PATCH \ --header 'Authorization: Bearer YOUR_OIDC_TOKEN' \ --header 'Content-Type: application/json' \ --data '{ "macos_auto_reserve": true, "macos_auto_reserve_max_reservations": 4 }' ``` | **Field** | **Type** | **Description** | | -------------------------------------- | -------- | ------------------------------------------------------------------------------------- | | `macos_auto_reserve` | boolean | Turns auto-reserve on or off. If omitted, the stored value stays unchanged. Turning auto-reserve off while it is already off keeps the original `macos_auto_reserve_disabled_at` and `macos_auto_reserve_disabled_by`. Turning auto-reserve on clears both fields. | | `macos_auto_reserve_max_reservations` | integer | Sets the most auto-reserved reservations the organization holds at once. If omitted, the stored value stays unchanged. The value must be at least `1`. A cap below `1` returns `400`. | - The request requires the `write:organization` permission. - Set at least one field in the request. ### When a reservation ends [#when-a-reservation-ends] A reservation ends at the end of its period when it does not renew. There is no grace period, and we do not send a notice. Watch the end of the period, the automatic renewal setting and any renewal error on your reservations, or [renew by hand](#renew-a-reservation). At the end, in this order: 1. The reservation enters the ending status. You cannot start, resume or create sandboxes on it. 2. **Persistent sandboxes** are shut down (paused ones are resumed first, then shut down). Each one that ran is **exported as an image**: an image without a name that records the sandbox's ID. 3. **Sandboxes that are not persistent** are deleted. Their data is lost. 4. When every export is saved or has failed, the Mac is **erased**: the sandbox disks, the console logs, the macOS user and its files. A failed export holds the erase for up to 24 hours. The disk of that sandbox is lost. 5. The Mac is checked and goes back to the pool for the next customer. 6. The reservation enters the released status. Every sandbox on it is deleted. #### What you keep [#what-you-keep] | **Data** | **After the end** | | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | Disk of a persistent sandbox | Kept as an image without a name, unless its export fails. It is deleted when no sandbox is created from it for 30 days. Give it a name to keep it. | | Snapshots you made | Kept. Named ones until you delete them. Unnamed ones while a sandbox is created from them at least once every 7 days. | | Usage records and bills | Kept. | | Sandboxes that are not persistent | Lost. | | Memory of paused sandboxes | Lost. The disk is exported. | | Console logs | Lost. | | The Mac | Erased. | #### Continue after the end [#continue-after-the-end] Find the exported image, then create a sandbox from it on a reservation. You can also give the image a name to keep it. The new sandbox needs a reservation with room for the disk: an existing one, a new one you buy, or one that auto-reserve buys. It gets a new machine identity. ```python exports = daytona.v2.images.list(anonymous=True, sandbox_id="OLD_SANDBOX_ID") image = exports.items[0] daytona.v2.images.promote(image.id, "ios-builder-day-2") # optional: keep it sandbox = daytona.v2.create_macos(image.id, region="macos-1", persistent=True, wait_seconds=120) ``` ```typescript const exports = await daytona.v2.images.list({ anonymous: true, sandboxId: 'OLD_SANDBOX_ID' }) const image = exports.items[0] await daytona.v2.images.promote(image.id, 'ios-builder-day-2') // optional: keep it const sandbox = await daytona.v2.createMacos(image.id, { region: 'macos-1', persistent: true, waitSeconds: 120 }) ``` ```bash daytona image list --anonymous true --sandbox OLD_SANDBOX_ID daytona image promote IMAGE_ID ios-builder-day-2 daytona sandbox create-macos --region macos-1 --image IMAGE_ID --persistent ``` ## Filesystem snapshot [#filesystem-snapshot] A filesystem snapshot saves the disk of a stopped persistent macOS sandbox as an image. You can create new sandboxes from it on any Mac of the region. Snapshots save the disk only, not the memory. A new sandbox from an image gets a new machine identity. Apps that are bound to one machine (for example an iCloud sign-in or some licences) need you to sign in again. ```python sandbox.stop() image = sandbox.capture_image(name="ios-builder-ready") image = daytona.v2.images.get(image.id, wait="ready", timeout=3600) print(image.id, image.status) copy = daytona.v2.create_macos("ios-builder-ready", region="macos-1", persistent=True) ``` - The requests require the `write:images` and `write:sandboxes` permissions. - The sandbox must be persistent and stopped. - The sandbox must have an active reservation. - The sandbox cannot start until the snapshot is `ready`. ```typescript await sandbox.stop() const image = await sandbox.captureImage({ name: 'ios-builder-ready' }) const ready = await daytona.v2.images.get(image.id, { wait: 'ready', timeoutSeconds: 3600 }) const copy = await daytona.v2.createMacos('ios-builder-ready', { region: 'macos-1', persistent: true }) ``` - The requests require the `write:images` and `write:sandboxes` permissions. - The sandbox must be persistent and stopped. - The sandbox must have an active reservation. - The sandbox cannot start until the snapshot is `ready`. ```bash daytona sandbox stop SANDBOX_ID daytona image capture SANDBOX_ID --name ios-builder-ready daytona image list --name ios-builder-ready daytona sandbox create-macos --region macos-1 --image ios-builder-ready --persistent ``` - The requests require the `write:images` and `write:sandboxes` permissions. - The sandbox must be persistent and stopped. - The sandbox must have an active reservation. - The sandbox cannot start until the snapshot is `ready`. ## Filesystem persistence [#filesystem-persistence] A persistent macOS sandbox keeps its disk on its Mac across stop and start for as long as the reservation lives. To keep the disk after the reservation, make a [filesystem snapshot](#filesystem-snapshot), or let the end of the reservation save it (see [when a reservation ends](#when-a-reservation-ends)). ## Lifecycle [#lifecycle] Every macOS sandbox moves through a lifecycle. Stop, start, pause and resume need a **persistent** sandbox. A sandbox that is not persistent can only run or be deleted. If a Mac restarts, a running persistent sandbox comes back stopped with its disk. A running sandbox that is not persistent is lost. | **Action** | **What happens** | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Stop | macOS shuts down. The disk stays on the same Mac and keeps its disk room on the reservation. The cores, memory and slot become available to other sandboxes. | | Start | macOS boots from the same disk on the same Mac. The reservation needs an open slot and enough cores and memory for the sandbox, or start fails with `NO_FREE_SLOT`. Start never buys a reservation. | | Pause | The Mac saves the memory of the sandbox. It keeps its place on the reservation. | | Resume | The sandbox continues from the saved memory. If the memory cannot be restored, macOS boots from the disk and `mac_boot_mode` is `cold_after_restore_failure`. | | Delete | The sandbox and its disk are erased. You cannot undo this. | A stopped or paused sandbox **does not keep automatic renewal going**. If all sandboxes on a reservation are stopped or paused at the end of the period, the reservation ends. See [when a reservation ends](#when-a-reservation-ends). ### Stop / start [#stop--start] Stop a persistent macOS sandbox to shut down macOS and keep its disk on the same Mac. Start boots macOS from the same disk on the same Mac. ```python sandbox.stop() sandbox.wait(until="stopped") sandbox.start(timeout=120) # Stop a sandbox by ID, without fetching it first daytona.v2.stop("SANDBOX_ID") # Start a stopped sandbox by ID daytona.v2.start("SANDBOX_ID", timeout=120) ``` ```typescript await sandbox.stop() await sandbox.wait({ until: 'stopped' }) await sandbox.start({ timeoutSeconds: 120 }) // Stop a sandbox by ID, without fetching it first await daytona.v2.stop('SANDBOX_ID') // Start a stopped sandbox by ID await daytona.v2.start('SANDBOX_ID', { timeoutSeconds: 120 }) ``` ```bash daytona sandbox stop SANDBOX_ID daytona sandbox start SANDBOX_ID ``` ```bash curl 'https://api.us-macos.daytona.com/sandboxes/{sandboxId}/stop' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' # Long-poll until the sandbox is stopped curl 'https://api.us-macos.daytona.com/sandboxes/{sandboxId}?wait=stopped&timeout_ms=60000' \ --header 'Authorization: Bearer YOUR_API_KEY' curl 'https://api.us-macos.daytona.com/sandboxes/{sandboxId}/start' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ### Pause / resume [#pause--resume] Pause a persistent macOS sandbox to save its memory on the Mac. Resume continues the sandbox from the saved memory. ```python sandbox.pause() sandbox.resume() # Pause a sandbox by ID, without fetching it first daytona.v2.pause("SANDBOX_ID") # Resume a paused sandbox by ID daytona.v2.resume("SANDBOX_ID") ``` ```typescript await sandbox.pause() await sandbox.resume() // Pause a sandbox by ID, without fetching it first await daytona.v2.pause('SANDBOX_ID') // Resume a paused sandbox by ID await daytona.v2.resume('SANDBOX_ID') ``` ```bash daytona sandbox pause SANDBOX_ID daytona sandbox resume SANDBOX_ID ``` ```bash curl 'https://api.us-macos.daytona.com/sandboxes/{sandboxId}/pause' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' curl 'https://api.us-macos.daytona.com/sandboxes/{sandboxId}/resume' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ### Fork [#fork] Fork a persistent macOS sandbox to copy its disk into a new sandbox. A fork does not copy memory, and a fork that asks for memory fails. ```python twin = sandbox.fork(name="ios-builder-2") ``` ```typescript const twin = await sandbox.fork({ name: 'ios-builder-2' }) ``` ```bash daytona sandbox fork SANDBOX_ID --name ios-builder-2 ``` ```bash curl 'https://api.us-macos.daytona.com/sandboxes/{sandboxId}/fork' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "name": "ios-builder-2" }' ``` | **Field** | **Type** | **Description** | | --------- | -------- | ---------------------------------------------------------------- | | `name` | string | Sets the display name of the fork. The default is `-fork`. | ### Delete [#delete] Delete a macOS sandbox to erase the sandbox and its disk. You cannot undo this. ```python sandbox.delete() ``` ```typescript await sandbox.delete() ``` ```bash daytona sandbox delete SANDBOX_ID ``` ```bash curl 'https://api.us-macos.daytona.com/sandboxes/{sandboxId}' \ --request DELETE \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Run commands [#run-commands] Run commands in a macOS sandbox, in the foreground or in the background. Files, git and PTYs work as on Linux sandboxes. ```python result = sandbox.exec("sw_vers -productVersion && xcodebuild -version") print(result.exit_code, result.output) handle = sandbox.process.start("xcodebuild -scheme App build") # runs in the background print(handle.wait().exit_code) ``` ```typescript const result = await sandbox.exec('sw_vers -productVersion && xcodebuild -version') console.log(result.exitCode, result.output) ``` ```bash daytona sandbox exec SANDBOX_ID -- sw_vers -productVersion daytona sandbox exec SANDBOX_ID --tty -- zsh # an interactive shell daytona sandbox cp ./App.zip SANDBOX_ID:/Users/lume/App.zip ``` ## SSH [#ssh] Connect to a macOS sandbox over SSH. The CLI opens SSH for you. With your own SSH client, make a short-lived sandbox token that allows SSH, and put the sandbox ID and the token in the user name, joined by a hash sign (#). The gateway checks the token, not a password or an SSH key. A token works until it expires. Make a new one when you need it. ```python token = sandbox.create_access_token(operations=["ssh"], ttl_seconds=3600) print(f'ssh -p 2222 "{sandbox.id}#{token.access_token}@ssh.us-macos.daytona.com"') ``` Do not put a project API key in an SSH command. Use a sandbox token: it is limited to this sandbox and to SSH. ```typescript const token = await sandbox.createAccessToken({ operations: ['ssh'], ttlSeconds: 3600 }) console.log(`ssh -p 2222 "${sandbox.id}#${token.accessToken}@ssh.us-macos.daytona.com"`) ``` Do not put a project API key in an SSH command. Use a sandbox token: it is limited to this sandbox and to SSH. ```bash daytona sandbox ssh SANDBOX_ID daytona sandbox ssh SANDBOX_ID -- ls /Users/lume ``` ## Desktop [#desktop] macOS sandboxes have a desktop of 1280 × 800. Open the sandbox in the dashboard and choose the desktop view. The desktop takes mouse and keyboard input as soon as it connects. ### User and password [#user-and-password] A macOS sandbox signs in to its macOS user when it starts, so the desktop opens without a login screen. macOS asks for the password of this user when you change a protected setting, for example when you allow an app under **Privacy & Security**, and when you unlock the screen. SSH and the desktop view do not use this password. Daytona checks your sandbox token instead. The following table lists the user settings of the base image. A sandbox from a [filesystem snapshot](#filesystem-snapshot) keeps the user settings of the sandbox that the snapshot was taken from, including a changed password. | **Property** | **Value** | | ---------------- | ----------------------------------------------------------------------------------------------- | | User | `lume` | | Password | `lume` | | Administrator | Yes | | `sudo` | Does not ask for a password | | Sign-in at start | Automatic | | Screen lock | Only when you lock the screen or the display sleeps. The sandbox does not sleep on its own. | ### Computer use [#computer-use] Take screenshots and send mouse and keyboard input inside the sandbox. Computer use can also read the accessibility tree, act on controls, and record the screen. ```python import base64 cu = sandbox.computer_use cu.start() print(cu.status()) shot = cu.screen.screenshot() with open("screen.png", "wb") as f: f.write(base64.b64decode(shot.screenshot)) cu.mouse.click(x=640, y=400) cu.keyboard.type(text="hello from Daytona") cu.keyboard.hotkey(keys="cmd+space") # modifiers: cmd, ctrl, alt (option), shift print(cu.display.windows()) ``` - The request requires the `write:sandboxes` permission. - `cu.a11y` reads the accessibility tree and acts on controls, and `cu.recording` records the screen. ```typescript const cu = sandbox.computerUse await cu.start() const shot = await cu.screen.screenshot() // shot.screenshot is a base64 PNG await cu.mouse.click({ x: 640, y: 400 }) await cu.keyboard.type({ text: 'hello from Daytona' }) await cu.keyboard.hotkey({ keys: 'cmd+space' }) ``` - The request requires the `write:sandboxes` permission. - `cu.a11y` reads the accessibility tree and acts on controls, and `cu.recording` records the screen. ### VNC client [#vnc-client] Connect your own VNC client to the desktop. The desktop is also a VNC connection over a WebSocket. Make a token with the desktop operation and connect a noVNC client to the address below. The proxy does the VNC password step for you. Your client must support the DesktopSize encoding (noVNC does). ```text wss://proxy.us-macos.daytona.app/vnc/SANDBOX_ID?daytona_token=TOKEN ``` ```python token = sandbox.create_access_token(operations=["desktop"], ttl_seconds=3600) ``` ## Logs [#logs] A macOS sandbox has a console log, and your own processes can keep their own logs. The console log is erased with the Mac when the reservation ends. Copy what you need before then. | **Log** | **Description** | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Console log** | The macOS sandbox's console output and our lifecycle lines. The API route is `GET /sandboxes/SANDBOX_ID/console-logs`. | | **Your own process logs** | Run a command with `process.start(...)` and read `handle.logs()` or `handle.stream_logs()` (TypeScript: `logs()`, `streamLogs()`), or write logs to a file and copy it out. | | **Usage** | `daytona sandbox usage SANDBOX_ID`. | ```bash daytona sandbox logs SANDBOX_ID daytona sandbox logs SANDBOX_ID --follow ``` ## Errors [#errors] The SDKs raise these errors with the codes in the following table. If something does not work as this page says, send us the sandbox ID or the reservation ID and the time. We read the server logs from those. | **Code** | **Meaning** | **What to do** | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | | **`NO_ACTIVE_RESERVATION`** | You have no live reservation in the region, and auto-reserve is off. Resume also returns it when the reservation of the paused sandbox is `ending`. | Reserve a Mac, or use `reservation="new"`. | | **`NO_FREE_SLOT`** | Your reservations have no room for this size. | Use a smaller size, delete a sandbox, or reserve another Mac. | | **`MAC_DISK_FULL`** | The Macs of the reservations with room for this size have too little disk space left. A smaller `disk` does not help. | Delete a macOS sandbox on one of them, or reserve another Mac. | | **`NO_MAC_AVAILABLE`** | No Mac of the model is available to reserve. | Try again later, or reserve fewer (`min_count`). | | **`AUTO_RESERVE_LIMIT_REACHED`** | Auto-reserve holds its maximum number of reservations. | Reserve a Mac yourself, or ask an admin to raise the limit. | | **`IDEMPOTENCY_KEY_REUSED`** | The key was used for a different purchase, or for a sandbox create with a different size. | Use a new key. | | **`INSUFFICIENT_BALANCE`** | Your balance does not cover the period of each Mac you reserve (402). | Add balance, or reserve fewer Macs. | | **`CHARGE_FAILED`** | The charge for the period failed (402). | Contact us. | | **`SNAPSHOT_IN_PROGRESS`** | A snapshot of this sandbox is in progress. | Wait until the image is `ready`. | | **`NOT_SUPPORTED_FOR_CLASS`** | The action does not work on macOS, for example a fork with memory. | See the section for that action. | | **`FEATURE_NOT_ENABLED`** | macOS sandboxes are not on for your organization. | Contact us. | ## Permissions [#permissions] macOS sandboxes and Mac reservations use the sandbox permissions of their project, which you can grant to an [organization role](/docs/organizations#roles) or an [API key](/docs/api-keys). The macOS settings of the organization use organization permissions. The [audit logs](/docs/audit-logs) record sandbox creates and deletes, and Mac reservations and renewals. | **Permission** | **Grants** | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | | **`read:sandboxes`** | Get sandboxes, and list and get Mac reservations | | **`write:sandboxes`** | Create, stop, start, pause, resume, and fork sandboxes, use the toolbox and SSH, and reserve, renew, and update Mac reservations | | **`delete:sandboxes`** | Delete sandboxes | | **`write:images`** | Capture filesystem snapshots as images | | **`read:organization`** | Get the macOS settings | | **`write:organization`** | Change the macOS settings | # GPU Sandboxes (/docs/gpu-sandboxes) GPU sandboxes are **Linux sandboxes with one or more dedicated GPUs** attached. Each sandbox has its own files, network connection, and fixed amount of processor, memory, and disk. The GPUs assigned to a sandbox are not shared with other sandboxes while it runs, and Daytona isolates each GPU sandbox from other sandboxes and from the host. GPU sandboxes run workloads that require GPU acceleration, such as model inference, training, fine-tuning, and reinforcement learning. A create request asks for a number of GPUs, and Daytona places the sandbox on a host that has enough free GPUs. A GPU sandbox is ephemeral: deleting it discards its filesystem. Its state can be saved as a checkpoint that includes its processes, memory, and GPU memory, and resumed in a new GPU sandbox. The available GPU models and the features and limits of GPU sandboxes may change over time. | **Term** | **Description** | | -------------- | ------------------------------------------------------------------------------------------------ | | **Sandbox** | An isolated Linux machine with dedicated GPUs, created on demand to run code. | | **GPU** | A graphics processing unit assigned to one sandbox at a time. | | **GPU count** | The number of GPUs a sandbox requests when it is created. | | **Host** | The physical server with GPUs that runs sandboxes. Daytona calls these servers runners. | | **Image** | A packaged filesystem that defines what software a sandbox starts with. | | **GPU memory** | The memory on a GPU that holds the data the GPU works on, such as model weights. | | **Checkpoint** | A saved copy of a sandbox's processes, memory, and GPU memory that a new GPU sandbox resumes from. | | **Ephemeral** | A sandbox whose filesystem is discarded when it is deleted. | GPU sandboxes for Daytona v2 are coming soon. # Isolation (/docs/isolation) Daytona sandboxes are isolated by default. Each sandbox runs as a hardware-isolated virtual machine with its own kernel: code running in a sandbox cannot read another sandbox's filesystem or memory, is not on a shared network with other sandboxes, and its credentials and API access are scoped to its own project. Isolation operates at three boundaries: | **Boundary** | **What is separated** | **Mechanisms** | | ------------ | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Runtime** | Kernel, processes, filesystem, and memory of each sandbox | • [**Sandboxes**](/docs/sandboxes)
• [**Resources**](/docs/sandboxes#resources) | | **Network** | Traffic entering (ingress) and leaving (egress) each sandbox | • [**Network policy**](/docs/network-limits)
• [**Preview**](/docs/preview)
• [**Ports**](/docs/linux-sandboxes#create-a-linux-sandbox) | | **Project** | Access to sandboxes, data, and credentials | • [**Projects**](/docs/projects)
• [**API keys**](/docs/api-keys#scopes)
• [**Secrets**](/docs/secrets) | ## Runtime isolation [#runtime-isolation] Runtime isolation separates what runs inside one sandbox from the host it executes on and from every other sandbox. Each sandbox runs as a virtual machine with its own kernel, filesystem, and network stack: Linux sandboxes on Firecracker, Windows and GPU sandboxes on Cloud Hypervisor, and macOS sandboxes on Apple's Virtualization framework ([hardware virtualization](/docs/sandboxes#hardware-virtualization)). The boundary is enforced by the hypervisor, not by the guest: code inside a sandbox cannot observe or interfere with other sandboxes or the host. Resources are part of the runtime boundary. Each sandbox has its own [**vCPU**, **memory**, and **disk**](/docs/sandboxes#resources), and the guest kernel sees only the resources of its own virtual machine. The guest boots with the burst ceiling of the sandbox, so the CPU count and the total memory it reports are that ceiling, not the guarantee. Tools that size themselves from the CPU count, such as parallel test runners and build tools, start one worker per visible CPU. ```bash nproc # vCPUs of the sandbox's virtual machine grep MemTotal /proc/meminfo # memory of the sandbox's virtual machine ``` ## Network isolation [#network-isolation] Network isolation controls traffic in each direction separately. Egress is open by default and locked down per sandbox with a network policy; ingress is closed by default and opened per endpoint; sandboxes never share a network. | **Direction** | **Default** | **Controls** | | ----------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Sandbox to internet** | Open egress | [**Network policy**](/docs/network-limits): default-deny with an allow list of IPs, CIDRs, and domains | | **Internet to sandbox** | Closed; no port is reachable until exposed | [**Preview URLs**](/docs/preview) (token-authenticated unless an endpoint is made public), [**declared ports**](/docs/linux-sandboxes#create-a-linux-sandbox), and [**SSH**](/docs/ssh-access) | | **Sandbox to sandbox** | No shared network | None; sandboxes reach each other only through exposed endpoints | **Outbound** traffic is open by default. A [network policy](/docs/network-limits) turns on blocking and admits only the domains and IPv4 ranges on its allow lists. With blocking on and empty allow lists, the sandbox reaches no external address and DNS resolves no names. ```python from daytona import Daytona daytona = Daytona() # Block all outbound traffic sandbox = daytona.v2.create(network_policy={"block_all": True}) # Or allow specific domains only sandbox = daytona.v2.create( network_policy={"allow_domains": ["example.com", "*.daytona.io"]}, ) # Or allow specific IPv4 ranges only sandbox = daytona.v2.create( network_policy={"block_all": True, "allow_cidrs": ["208.80.154.232/32", "192.168.1.0/24"]}, ) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() // Block all outbound traffic const blocked = await daytona.v2.create({ networkPolicy: { blockAll: true }, }) // Or allow specific domains only const domainRestricted = await daytona.v2.create({ networkPolicy: { allowDomains: ['example.com', '*.daytona.io'] }, }) // Or allow specific IPv4 ranges only const cidrRestricted = await daytona.v2.create({ networkPolicy: { blockAll: true, allowCidrs: ['208.80.154.232/32', '192.168.1.0/24'] }, }) ``` ```bash # Block all outbound traffic daytona sandbox create --block-all # Or allow specific domains only daytona sandbox create \ --allow-domain example.com \ --allow-domain '*.daytona.io' # Or allow specific IPv4 ranges only daytona sandbox create \ --block-all \ --allow-cidr 208.80.154.232/32 \ --allow-cidr 192.168.1.0/24 ``` ```bash # Block all outbound traffic curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "network_policy": { "block_all": true } }' # Or allow specific domains only curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "network_policy": { "allow_domains": ["example.com", "*.daytona.io"] } }' # Or allow specific IPv4 ranges only curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "network_policy": { "block_all": true, "allow_cidrs": ["208.80.154.232/32", "192.168.1.0/24"] } }' ``` **Inbound** traffic reaches a sandbox only through a port exposed as a [preview](/docs/preview) endpoint or through [SSH access](/docs/ssh-access). A port is exposed as a private endpoint when you first request its preview URL, and a private endpoint requires a preview token or an API key. A port made public accepts requests without a credential. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() # Requesting a preview link exposes the port as a private endpoint preview = sandbox.ports.preview_link(3000) print(preview.url) # https://3000-{sandboxId}.{proxy-domain} print(preview.token) # sent in the x-daytona-preview-token header # A public endpoint accepts requests without a credential sandbox.ports.expose(8080, public=True) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() // Requesting a preview link exposes the port as a private endpoint const preview = await sandbox.ports.previewLink(3000) console.log(preview.url) // https://3000-{sandboxId}.{proxy-domain} console.log(preview.token) // sent in the x-daytona-preview-token header // A public endpoint accepts requests without a credential await sandbox.ports.expose(8080, { public: true }) ``` ```bash # Requesting a preview URL exposes the port as a private endpoint daytona sandbox port preview-url {sandboxId} 3000 ``` ```bash # Requesting a preview URL exposes the port as a private endpoint curl 'https://api.daytona.com/sandboxes/{sandboxId}/ports/3000/preview-url' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' # Authenticate to the private endpoint with the returned token curl 'https://3000-{sandboxId}.{proxy-domain}' \ --header 'x-daytona-preview-token: PREVIEW_TOKEN' # A public endpoint accepts requests without a credential curl 'https://api.daytona.com/sandboxes/{sandboxId}/ports/8080' \ --request PUT \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "public": true }' ``` ## Project isolation [#project-isolation] Project isolation separates tenants. [Organizations](/docs/organizations) are the unit of membership and billing; [projects](/docs/projects) are the unit of isolation. Every sandbox, image, snapshot, volume, secret, and project API key belongs to exactly one project, and access control is enforced at that boundary: a project API key cannot see or operate on another project's resources. Organization API keys are scoped to the organization. Within a project, access narrows further to the following mechanisms: | **Mechanism** | **What it scopes** | **How it works** | | ------------------------------------------------------------ | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [**Scoped API keys**](/docs/api-keys#scopes) | What a key can do | Each key carries an explicit `verb:resource` scope list chosen at creation, and every request is confined to those scopes; `write:` and `delete:` imply `read:` on the same resource. Keys can carry an expiry. Revocation takes effect immediately on the API; toolbox and SSH access through the proxy can continue for up to 30 seconds after revocation. | | [**Secrets**](/docs/secrets) | Credentials used in a sandbox | A sandbox holds an opaque placeholder; an outbound proxy substitutes the real value only for requests to the secret's allowed domains. Responses are not rewritten, so a destination that echoes the credential back returns the real value into the sandbox. | **[API key permissions](/docs/api-keys#scopes)** scope what a key can do. A key created with only the sandbox write and delete permissions can manage sandboxes but cannot read secrets or delete images. A key can hold only permissions its creator holds. ```bash # A key limited to creating and deleting sandboxes in one project daytona key mint --project PROJECT_ID \ --name tenant-a-key \ --permission write:sandboxes,delete:sandboxes \ --expires 720h ``` ```bash # A key limited to creating and deleting sandboxes in one project curl 'https://api.daytona.com/projects/PROJECT_ID/api-keys' \ --request POST \ --header 'Authorization: Bearer YOUR_OIDC_TOKEN' \ --header 'Content-Type: application/json' \ --data '{ "name": "tenant-a-key", "project_permissions": ["write:sandboxes", "delete:sandboxes"], "expires_at": "2027-01-01T00:00:00Z" }' ``` **[Secrets](/docs/secrets)** keep credentials out of sandboxes. A sandbox holds an opaque placeholder; an outbound proxy substitutes the real value only for HTTPS requests to the secret's allowed domains. Code in the sandbox can use the credential but cannot read it directly or send it to another host. Responses are not rewritten, so an allowed destination that echoes the credential back exposes the real value to the sandbox. ```python from daytona import Daytona daytona = Daytona() daytona.v2.secrets.set( "MY_API_KEY", "secret-value", allowed_domains=["api.example.com"], ) # The sandbox receives a placeholder, never the plaintext value sandbox = daytona.v2.create(secrets=["MY_API_KEY"]) # Code uses the credential without being able to read it sandbox.process.run( 'curl -H "Authorization: Bearer $MY_API_KEY" https://api.example.com/v1/data' ) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() await daytona.v2.secrets.set('MY_API_KEY', 'secret-value', { allowedDomains: ['api.example.com'], }) // The sandbox receives a placeholder, never the plaintext value const sandbox = await daytona.v2.create({ secrets: ['MY_API_KEY'], }) // Code uses the credential without being able to read it await sandbox.process.run( 'curl -H "Authorization: Bearer $MY_API_KEY" https://api.example.com/v1/data' ) ``` ```bash printf %s 'secret-value' | daytona secret set MY_API_KEY --stdin --allowed-domain api.example.com # The sandbox receives a placeholder, never the plaintext value daytona sandbox create --secret MY_API_KEY # Code uses the credential without being able to read it daytona sandbox exec {sandboxId} -- sh -c \ 'curl -H "Authorization: Bearer $MY_API_KEY" https://api.example.com/v1/data' ``` ```bash curl 'https://api.daytona.com/secrets/MY_API_KEY' \ --request PUT \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "value": "secret-value", "allowed_domains": ["api.example.com"] }' # The sandbox receives a placeholder, never the plaintext value curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "secrets": ["MY_API_KEY"] }' ``` # Persistence (/docs/persistence) The writable filesystem of a Daytona sandbox exists only while the sandbox does. Deleting a sandbox discards its filesystem, memory, and processes. To keep state beyond the sandbox, capture it as an image or a snapshot, or write it to storage outside the sandbox. Snapshots on this page are Linux snapshots: they save memory and disk and restore on the runner that captured them, or on another runner of the region with the same CPU model and hypervisor version. A persistent macOS sandbox keeps its disk across stop and start, and a macOS snapshot saves only the disk of a stopped persistent sandbox, as an image that restores on any Mac of the region. See [macOS filesystem persistence](/docs/macos-sandboxes#filesystem-persistence). | **Layer** | **What is preserved** | **Mechanisms** | | -------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------- | | **Filesystem** | Files, packages, repositories, build artifacts | • [**Image capture**](/docs/images#capture-images) | | **Memory** | Processes, connections, states | • [**Snapshots**](/docs/snapshots#create-snapshots) | | **External storage** | Data that outlives any single sandbox | • [**Volumes**](/docs/volumes) | ## Use cases [#use-cases] Persistence is used when the state of a sandbox must survive past the sandbox itself. - **Prepared environments.** An image capture keeps installed packages, repositories, and configuration. New sandboxes start from the image with nothing to reinstall or rebuild. Capture works on a running Linux or Windows sandbox, which keeps running during the capture. - **Runs from a known state.** A snapshot copies a configured sandbox, so several runs or every agent in a fleet start from the same state. A restored sandbox continues with its processes still running. A snapshot restores in its home region, or, where Daytona has enabled cross-region restore, in another region if the source sandbox was not persistent. - **Servers that resume without a restart.** A snapshot taken after a server starts keeps the server in memory. A restored sandbox continues with the server already running, instead of starting it again after a clean boot. The restored sandbox gets a fresh IP and port mappings. - **Results that outlive the sandbox.** A sandbox writes its output to storage outside the sandbox, such as your own object storage, and the data stays after the sandbox is deleted. Data on a [volume](/docs/volumes) also stays, and a later sandbox can attach the same volume and read it. A disk volume mounts as a local filesystem, while a bucket volume writes straight to an S3 bucket. - **Work that survives timeouts and reclaims.** A [timeout](#retention-and-lifecycle) or a spot reclaim can delete a sandbox without a request from you, and a host failure loses an ephemeral sandbox, along with its state. Results on a volume or in a captured image stay when that happens. A persistent macOS sandbox stops on a timeout instead of being deleted, and keeps its disk. ## Filesystem persistence [#filesystem-persistence] Filesystem persistence keeps the contents of a sandbox's disk: files, installed packages, cloned repositories, and build artifacts. Delete discards a sandbox's filesystem, and a host failure loses an ephemeral sandbox. Capture an [**image**](/docs/images#capture-images) or write results to storage outside the sandbox to keep them. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() # Prepare the environment to capture sandbox.process.run("pip install requests") # An image capture keeps the filesystem beyond the sandbox image = sandbox.capture_image(name="my-env") # Capture runs asynchronously; wait until the image is ready while image.status not in ("ready", "failed"): image = daytona.v2.images.get(image.id, wait="ready") if image.status == "failed": raise RuntimeError(image.error) # Delete discards the sandbox; the image remains sandbox.delete() # Any number of new sandboxes can start from the same image clone = daytona.v2.create("my-env") response = clone.process.run("pip show requests") print(response.stdout) # requests is already installed ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() // Prepare the environment to capture await sandbox.process.run('pip install requests') // An image capture keeps the filesystem beyond the sandbox let image = await sandbox.captureImage({ name: 'my-env' }) // Capture runs asynchronously; wait until the image is ready while (image.status !== 'ready' && image.status !== 'failed') { image = await daytona.v2.images.get(image.id, { wait: 'ready' }) } if (image.status === 'failed') throw new Error(image.error ?? 'image capture failed') // Delete discards the sandbox; the image remains await sandbox.delete() // Any number of new sandboxes can start from the same image const clone = await daytona.v2.create('my-env') const response = await clone.process.run('pip show requests') console.log(response.stdout) // requests is already installed ``` ```bash daytona sandbox create # Prepare the environment to capture daytona sandbox exec {sandboxId} -- pip install requests # An image capture keeps the filesystem beyond the sandbox; the command waits until the image is ready daytona image capture {sandboxId} --name my-env # Delete discards the sandbox; the image remains daytona sandbox delete {sandboxId} --yes # Any number of new sandboxes can start from the same image daytona sandbox create --image my-env daytona sandbox exec {cloneId} -- pip show requests ``` ```bash curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{}' # Prepare the environment to capture curl '{toolboxProxyUrl}/{sandboxId}/processes?waitMs=60000&logs=text' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"kind": "exec", "shellCommand": "pip install requests"}' # An image capture keeps the filesystem beyond the sandbox curl 'https://api.daytona.com/sandboxes/{sandboxId}/images' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"name": "my-env"}' # Capture runs asynchronously; block until the image is ready curl 'https://api.daytona.com/images/{imageId}?wait=ready' \ --header 'Authorization: Bearer YOUR_API_KEY' # Delete discards the sandbox; the image remains curl 'https://api.daytona.com/sandboxes/{sandboxId}' \ --request DELETE \ --header 'Authorization: Bearer YOUR_API_KEY' # Any number of new sandboxes can start from the same image curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"image": "my-env"}' ``` - The requests require the `write:sandboxes`, `write:images`, and `delete:sandboxes` permissions. - `{toolboxProxyUrl}` is the `toolbox_proxy_url` field of the [sandbox](/docs/sandboxes#get-a-sandbox). - The wait request returns at `ready` or `failed`, or after 30 seconds. - Repeat the wait request while the image status is `pending` or `building`. A `failed` image does not become `ready`. ## Memory persistence [#memory-persistence] Memory persistence keeps the runtime state of a sandbox: running processes, open connections, and everything loaded in RAM. A [**snapshot**](/docs/snapshots#create-snapshots) captures the memory and disk of a running persistent Linux sandbox or one served from a warm template, and a sandbox restored from it continues from the point it was captured, with processes still running, instead of starting from a clean boot. macOS snapshots do not save memory. ```python from daytona import Daytona daytona = Daytona() # Snapshot capture needs a persistent sandbox or one served from a warm template sandbox = daytona.v2.create() # Start a server that runs in the background sandbox.process.start("python3 -m http.server 8000") # A snapshot keeps memory and running processes beyond the sandbox snapshot = sandbox.create_snapshot("my-snapshot") # Capture runs asynchronously; wait until the snapshot is ready while snapshot.status not in ("ready", "failed"): snapshot = daytona.v2.snapshots.get(snapshot.id, wait="ready") if snapshot.status == "failed": raise RuntimeError(snapshot.error) # Delete discards the sandbox; the snapshot remains sandbox.delete() # The restored sandbox resumes the server without starting it again restored = daytona.v2.create_from_snapshot("my-snapshot") response = restored.process.run(argv=[ "python3", "-c", "import urllib.request; print(urllib.request.urlopen('http://localhost:8000').status)", ]) print(response.stdout) # 200 ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() // Snapshot capture needs a persistent sandbox or one served from a warm template const sandbox = await daytona.v2.create() // Start a server that runs in the background await sandbox.process.start('python3 -m http.server 8000') // A snapshot keeps memory and running processes beyond the sandbox let snapshot = await sandbox.createSnapshot('my-snapshot') // Capture runs asynchronously; wait until the snapshot is ready while (snapshot.status !== 'ready' && snapshot.status !== 'failed') { snapshot = await daytona.v2.snapshots.get(snapshot.id, { wait: 'ready' }) } if (snapshot.status === 'failed') throw new Error(snapshot.error ?? 'snapshot capture failed') // Delete discards the sandbox; the snapshot remains await sandbox.delete() // The restored sandbox resumes the server without starting it again const restored = await daytona.v2.createFromSnapshot('my-snapshot') const response = await restored.process.run({ argv: [ 'python3', '-c', "import urllib.request; print(urllib.request.urlopen('http://localhost:8000').status)", ], }) console.log(response.stdout) // 200 ``` ```bash # Snapshot capture needs a persistent sandbox or one served from a warm template daytona sandbox create # Start a server that runs in the background daytona sandbox exec {sandboxId} -- bash -c 'nohup python3 -m http.server 8000 > /tmp/server.log 2>&1 &' # A snapshot keeps memory and running processes beyond the sandbox; the command waits until the snapshot is ready daytona snapshot create {sandboxId} --name my-snapshot # Delete discards the sandbox; the snapshot remains daytona sandbox delete {sandboxId} --yes # The restored sandbox resumes the server without starting it again daytona sandbox create-from-snapshot my-snapshot daytona sandbox exec {restoredId} -- python3 -c "import urllib.request; print(urllib.request.urlopen('http://localhost:8000').status)" ``` ```bash # Snapshot capture needs a persistent sandbox or one served from a warm template curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{}' # Start a server that runs in the background curl '{toolboxProxyUrl}/{sandboxId}/processes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"kind": "exec", "shellCommand": "python3 -m http.server 8000"}' # A snapshot keeps memory and running processes beyond the sandbox curl 'https://api.daytona.com/sandboxes/{sandboxId}/snapshots' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"name": "my-snapshot"}' # Capture runs asynchronously; block until the snapshot is ready curl 'https://api.daytona.com/snapshots/{snapshotId}?wait=ready&timeout_ms=60000' \ --header 'Authorization: Bearer YOUR_API_KEY' # Delete discards the sandbox; the snapshot remains curl 'https://api.daytona.com/sandboxes/{sandboxId}' \ --request DELETE \ --header 'Authorization: Bearer YOUR_API_KEY' # The restored sandbox resumes the server without starting it again curl 'https://api.daytona.com/sandboxes/from-snapshot' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"snapshot": "my-snapshot"}' curl '{toolboxProxyUrl}/{restoredId}/processes?waitMs=5000&logs=text' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"kind": "exec", "argv": ["python3", "-c", "import urllib.request; print(urllib.request.urlopen(\"http://localhost:8000\").status)"]}' ``` - The requests require the `write:sandboxes`, `write:snapshots`, and `delete:sandboxes` permissions. - The wait request returns at `ready` or `failed`, or after `timeout_ms`. - Repeat the wait request while the snapshot status is `pending` or `building`. A `failed` snapshot does not become `ready`. ## Persistence beyond a sandbox [#persistence-beyond-a-sandbox] Deleting a sandbox deletes its state. To persist state beyond the sandbox itself, capture it as an **image** or a **snapshot**, or write it to storage outside the sandbox. ### Images and snapshots [#images-and-snapshots] An [image capture](/docs/images#capture-images) saves the filesystem of a running Linux or Windows sandbox under a name, and new sandboxes boot from it. A [snapshot](/docs/snapshots) saves the memory and disk of a running persistent Linux sandbox or one served from a warm template, and a restored sandbox resumes the captured processes. A snapshot restores on the runner that captured it, or on another runner of the region with the same CPU model and hypervisor version. A macOS snapshot is an image of the disk of a stopped persistent sandbox and restores on any Mac of the region ([macOS snapshots](/docs/macos-sandboxes#filesystem-snapshot)). A snapshot covers the sandbox's own state. A restored sandbox gets a fresh IP and port mappings. Deleting the source sandbox does not delete its snapshots. ### Volumes [#volumes] A [volume](/docs/volumes) is project-scoped storage that a sandbox attaches when it is created. Data on a volume stays when the sandbox is deleted, and a later sandbox can attach the same volume and read it. A disk volume is mounted as a local filesystem and commits each sandbox's changes in the background; a bucket volume mounts an S3 bucket, managed by Daytona or your own, and writes go straight to the bucket. ## Retention and lifecycle [#retention-and-lifecycle] When one of a sandbox's [timeouts](/docs/linux-sandboxes#timeouts) elapses, Daytona runs the action set for that timeout: delete, or stop for a [persistent](/docs/macos-sandboxes#lifecycle) macOS sandbox. The timeouts and actions come from the sandbox's lifecycle policy, the project's lifecycle defaults, or the platform default timeout. Two platform events also act on sandboxes without a request from you: - **Spot reclaim**: when the platform reclaims a [spot](/docs/preemption#spot-sandboxes) sandbox, an ephemeral sandbox is deleted, and a Windows sandbox is stopped with its disk kept. Reclaimed sandboxes record the time of the reclaim. See [preemption](/docs/preemption). - **Host failure**: an ephemeral sandbox on a failed host is lost. ```python from daytona import Daytona daytona = Daytona() # Create, use, delete sandbox = daytona.v2.create() # Run the workload and read the results before deleting response = sandbox.process.run("python3 -c 'print(1 + 1)'") print(response.stdout) # Delete discards the sandbox and all of its state sandbox.delete() ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() // Create, use, delete const sandbox = await daytona.v2.create() // Run the workload and read the results before deleting const response = await sandbox.process.run("python3 -c 'print(1 + 1)'") console.log(response.stdout) // Delete discards the sandbox and all of its state await sandbox.delete() ``` ```bash # Create, use, delete daytona sandbox create # Run the workload and read the results before deleting daytona sandbox exec {sandboxId} -- python3 -c 'print(1 + 1)' # Delete discards the sandbox and all of its state daytona sandbox delete {sandboxId} --yes ``` ```bash # Create, use, delete curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{}' # Run the workload and read the results before deleting curl '{toolboxProxyUrl}/{sandboxId}/processes?waitMs=5000&logs=text' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"kind": "exec", "shellCommand": "python3 -c '\''print(1 + 1)'\''"}' # Delete discards the sandbox and all of its state curl 'https://api.daytona.com/sandboxes/{sandboxId}' \ --request DELETE \ --header 'Authorization: Bearer YOUR_API_KEY' ``` # Scale (/docs/scale) Daytona sandboxes are designed to be created and operated at high volume. A single sandbox scales up by bursting beyond its guaranteed resources, a fleet scales out by running many sandboxes at once, and each sandbox runs multiple processes concurrently. Scale operates along three dimensions: | **Dimension** | **What scales** | **Mechanisms** | | ---------------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Sandbox**
(vertical) | vCPU, RAM, and disk of a single sandbox | • [**Custom resources**](/docs/sandboxes#resources)
• [**Resource limits**](/docs/limits#sandbox-limits) | | **Fleet**
(horizontal) | Number of sandboxes running simultaneously | • [**Snapshot fan-out**](/docs/snapshots) | | **Workload**
(concurrency) | Concurrent processes in a sandbox | • [**Sessions**](/docs/process-execution#session-operations)
• [**Background commands**](/docs/process-execution#start-a-background-process)
• [**PTY sessions**](/docs/pty) | ## Sandbox scaling [#sandbox-scaling] Sandbox scaling sets the resources of a single sandbox: **vCPU**, **memory**, and **disk**. The resource envelope is fixed when the sandbox is created; there is no resize operation. The requested resources are a guaranteed floor, reserved at placement; beyond it the sandbox uses idle host capacity up to its [resource limits](/docs/limits#sandbox-limits), which default to the [plan caps](/docs/limits#sandbox-limits) of your organization. Size the guarantee for the workload's average, and let bursting absorb the peaks. For a fixed shape with no bursting, set every limit equal to its request. Guarantee CPU, memory, and disk when creating a sandbox. ```python from daytona import Daytona from daytona.v2 import Resources daytona = Daytona() # Guarantee 2 cores, 4 GiB of RAM, and 16 GiB of disk for the sandbox sandbox = daytona.v2.create( resources=Resources(cpu=2, memory=4, disk=16), ) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ---------------------------------------- | | `cpu` | float | Sets the CPU guarantee in cores. The API raises the CPU guarantee to at least 1 core per 8 GiB of memory. The value can be fractional. | | `memory` | int | Sets the memory guarantee in GiB. | | `disk` | int | Sets the disk guarantee in GiB. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() // Guarantee 2 cores, 4 GiB of RAM, and 16 GiB of disk for the sandbox const sandbox = await daytona.v2.create({ resources: { cpu: 2, memory: 4, disk: 16 }, }) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ---------------------------------------- | | `cpu` | number | Sets the CPU guarantee in cores. The API raises the CPU guarantee to at least 1 core per 8 GiB of memory. The value can be fractional. | | `memory` | number | Sets the memory guarantee in GiB. | | `disk` | number | Sets the disk guarantee in GiB. | ```bash # Guarantee 2 cores, 4 GiB of RAM, and 16 GiB of disk; --memory and --disk are MiB daytona sandbox create --cpu 2 --memory 4096 --disk 16384 ``` | **Flag** | **Type** | **Description** | | -------- | -------- | ---------------------------- | | `--cpu` | float | Sets the CPU guarantee in cores. The API raises the CPU guarantee to at least 1 core per 8192 MiB of memory. The value can be fractional. | ```bash curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "cpu": 2, "memory_mib": 4096, "disk_mib": 16384 }' ``` | **Field** | **Type** | **Description** | | ------------- | --------- | ---------------------------- | | `cpu` | number | Sets the CPU guarantee in cores. The value can be fractional. The API raises `cpu` to at least 1 core per 8192 MiB of `memory_mib`. | | `memory_mib` | integer | In MiB. | | `disk_mib` | integer | In MiB. | A sandbox bursts beyond its guarantee up to the [plan caps](/docs/limits#sandbox-limits) of your organization: **4** cores (8 vCPUs) and **16** GiB of memory on Starter, **8** cores (16 vCPUs) and **32** GiB on Team, and the platform caps of **16** cores (32 vCPUs) and **32** GiB on Enterprise. The ranges below are the platform caps; a value above the plan cap is rejected. The per-sandbox core and memory limits lower those ceilings and cannot raise them. CPU guarantees can be fractional, as small as 0.125 cores. Usage above the guarantee is billed as measured; see [billing](/docs/billing). ```python from daytona import Daytona from daytona.v2 import Resources daytona = Daytona() # Guarantee half a core and 2 GiB, and burst up to 2 cores (4 vCPUs) and 8 GiB sandbox = daytona.v2.create( resources=Resources(cpu=0.5, cpu_max=2, memory=2, memory_max=8), ) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------------------------------- | | `cpu_max` | float | Sets the CPU limit in cores, a multiple of `0.5`, up to the plan cap. One core is 2 vCPUs. | | `memory_max` | float | Sets the memory limit in GiB, from 256 MiB to 32 GiB. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() // Guarantee half a core and 2 GiB, and burst up to 2 cores (4 vCPUs) and 8 GiB const sandbox = await daytona.v2.create({ resources: { cpu: 0.5, cpuMax: 2, memory: 2, memoryMax: 8 }, }) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ------------------------------- | | `cpuMax` | number | Sets the CPU limit in cores, a multiple of `0.5`, up to the plan cap. One core is 2 vCPUs. | | `memoryMax` | number | Sets the memory limit in GiB, from 256 MiB to 32 GiB. | ```bash # Guarantee half a core and 2 GiB, and burst up to 2 cores (4 vCPUs) and 8 GiB daytona sandbox create --cpu 0.5 --cpu-max 2 --memory 2048 --memory-max 8192 ``` | **Flag** | **Type** | **Description** | | -------------- | -------- | ----------------------------------- | | `--cpu-max` | float | Cores, a multiple of 0.5, up to the plan cap. One core is 2 vCPUs. | | `--memory-max` | int32 | In MiB, from `256` to `32768`. | ```bash curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "cpu": 0.5, "cores_max": 2, "memory_mib": 2048, "memory_max_mib": 8192 }' ``` | **Field** | **Type** | **Description** | | ------------- | --------- | ------------------------------ | | `cores_max` | number | Cores, a multiple of 0.5, up to the plan cap. One core is 2 vCPUs. | | `memory_max_mib` | integer | In MiB, from `256` to `32768`. | ## Fleet scaling [#fleet-scaling] Fleet scaling raises how many sandboxes run at once. The unit of scale is the sandbox: instead of packing unrelated workloads into one environment, create one per user, task, or agent. Each sandbox is a hardware-isolated microVM, so a fleet scales linearly: each sandbox adds capacity without contention, and a failure in one does not affect others. Creation is fast and asynchronous, so scale on demand: create when work arrives, delete when done. [Snapshot fan-out](/docs/snapshots) starts each unit of work from the memory and disk of one prepared snapshot, so any number of identical environments run from the same state. Snapshot fan-out works with Linux sandboxes. macOS sandboxes start from a disk image instead: see [macOS snapshots](/docs/macos-sandboxes#filesystem-snapshot). ```python from concurrent.futures import ThreadPoolExecutor from daytona import Daytona daytona = Daytona() def run_task(shard: int) -> str: # Each task gets its own isolated sandbox from the same snapshot sandbox = daytona.v2.create_from_snapshot("my-snapshot") response = sandbox.process.run(f"python3 run.py --shard {shard}") # Delete the sandbox when the task is done sandbox.delete() return response.stdout # Create and run 20 sandboxes concurrently from the same snapshot with ThreadPoolExecutor(max_workers=20) as pool: results = list(pool.map(run_task, range(20))) print(results) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ----------------------------------------------------------------------------------- | | `wait` | int | Sets how many seconds `create_from_snapshot` waits for the sandbox to start. When `wait` runs out, `create_from_snapshot` returns the sandbox without raising. The returned sandbox may not have started. The default is `60`. | ```python import asyncio from daytona import AsyncDaytona async def run_task(daytona: AsyncDaytona, shard: int) -> str: # Each task gets its own isolated sandbox from the same snapshot sandbox = await daytona.v2.create_from_snapshot("my-snapshot") response = await sandbox.process.run(f"python3 run.py --shard {shard}") # Delete the sandbox when the task is done await sandbox.delete() return response.stdout async def main() -> None: async with AsyncDaytona() as daytona: # Create and run 20 sandboxes concurrently from the same snapshot results = await asyncio.gather(*(run_task(daytona, i) for i in range(20))) print(results) asyncio.run(main()) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ----------------------------------------------------------------------------------- | | `wait` | int | Sets how many seconds `create_from_snapshot` waits for the sandbox to start. When `wait` runs out, `create_from_snapshot` returns the sandbox without raising. The returned sandbox may not have started. The default is `60`. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() async function runTask(shard: number): Promise { // Each task gets its own isolated sandbox from the same snapshot const sandbox = await daytona.v2.createFromSnapshot('my-snapshot') const response = await sandbox.process.run(`python3 run.py --shard ${shard}`) // Delete the sandbox when the task is done await sandbox.delete() return response.stdout } // Create and run 20 sandboxes concurrently from the same snapshot const results = await Promise.all( Array.from({ length: 20 }, (_, i) => runTask(i)) ) console.log(results) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------------------------------------------------------------------------- | | `wait` | number | Sets how many seconds `createFromSnapshot` waits for the sandbox to start. When `wait` runs out, `createFromSnapshot` returns the sandbox without throwing. The returned sandbox may not have started. The default is `60`. | ```bash # Create each sandbox from the same snapshot (run commands concurrently as needed) daytona sandbox create-from-snapshot my-snapshot ``` ```bash # Create each sandbox from the same snapshot (issue requests concurrently as needed) curl 'https://api.daytona.com/sandboxes/from-snapshot' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "snapshot": "my-snapshot" }' ``` ## Workload concurrency [#workload-concurrency] Workload concurrency runs multiple processes inside a single sandbox. A single sandbox can run an API server, a database, and background workers side by side: all processes share the sandbox's filesystem and network, and exposed ports are reachable through [previews](/docs/preview). Every command runs as a process: run starts a process and waits for it, while start returns a handle immediately and the process continues in the background. [Sessions](/docs/process-execution#session-operations) are shared shells: commands in a session run sequentially and share state, so parallelism comes from separate sessions or background processes. [Pseudo terminal (PTY) sessions](/docs/pty) add interactive terminals. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() # Start the server as a background process; the handle returns immediately sandbox.process.start("python3 -m http.server 8000") # Other commands run concurrently in the same sandbox response = sandbox.process.run(argv=[ "python3", "-c", "import time, urllib.request as u; time.sleep(1); print(u.urlopen('http://localhost:8000').status)", ]) print(response.stdout) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() // Start the server as a background process; the handle returns immediately await sandbox.process.start('python3 -m http.server 8000') // Other commands run concurrently in the same sandbox const response = await sandbox.process.run({ argv: ['python3', '-c', "import time, urllib.request as u; time.sleep(1); print(u.urlopen('http://localhost:8000').status)"], }) console.log(response.stdout) ``` ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() # start returns a handle immediately while the command runs in the background handle = sandbox.process.start("pip install numpy") # Wait for completion and read the exit code later result = handle.wait() print(result.exit_code) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() // start returns a handle immediately while the command runs in the background const handle = await sandbox.process.start('pip install numpy') // Wait for completion and read the exit code later const result = await handle.wait() console.log(result.exitCode) ``` ## Capacity and throughput [#capacity-and-throughput] Fleet size operates within per-sandbox platform and plan caps and your organization's credit balance. | **Control** | **What it limits** | **Scope** | | --------------------------------------------- | ----------------------------------------------------------------------------- | ------------ | | [**Resources**](/docs/sandboxes#resources) | Maximum vCPU, memory, and disk of a single sandbox | Sandbox | | [**Credits**](/docs/billing) | Spending across sandboxes; creates are rejected when the balance is exhausted | Organization | ### Reclaiming capacity [#reclaiming-capacity] Delete sandboxes when their work is done to release compute and storage: - **Sandboxes** release compute and storage immediately on delete. - **[Spot sandboxes](/docs/preemption#spot-sandboxes)** are reclaimed automatically when the platform needs capacity. See [preemption](/docs/preemption). ### Operating at scale [#operating-at-scale] At high creation rates, two practices keep an application efficient: - Make creates safe to retry with an [idempotency key](/docs/linux-sandboxes#create-a-linux-sandbox): a retried create with the same key returns the same sandbox instead of minting a new one. - Wait with long-polls, not tight polling. The SDKs block on create until the sandbox runs. On the API, `wait` long-polls one sandbox, and the batch wait endpoint waits on up to `200` sandboxes over one connection, returning once one reaches the target state or `timeout_ms` elapses. ```bash # Wait for one sandbox to reach running without tight polling curl 'https://api.daytona.com/sandboxes/{sandboxId}?wait=running' \ --header 'Authorization: Bearer YOUR_API_KEY' # Wait on a whole batch over one connection (up to 200 IDs) curl 'https://api.daytona.com/sandboxes/batch-wait?ids={sandboxId1},{sandboxId2},{sandboxId3}&wait=running&timeout_ms=60000' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` # Preemption (/docs/preemption) Daytona sandboxes run on reserved capacity by default: once placed, a sandbox keeps its resources until you delete it. A [spot](#spot-sandboxes) sandbox is preemptible: it is billed at a discount, and Daytona can reclaim it at any time to free capacity for full-price sandboxes. The tier is set when the sandbox is created and cannot be changed afterwards. A reclaimed ephemeral Linux spot sandbox is deleted, with its filesystem. To continue the work of an ephemeral sandbox, create a new sandbox and keep results in storage outside the sandbox, such as your own object storage. A reclaimed [Windows](/docs/windows-sandboxes) spot sandbox is stopped, with the disk kept, and a start boots Windows from the preserved disk. ## Use cases [#use-cases] Spot sandboxes are used for work that tolerates an interruption in exchange for a lower price. - **Batch jobs.** Data processing, builds, and test runs start again from the beginning when a sandbox is reclaimed. Spot multiplies the CPU, memory, and disk price by 0.7. Each job opts in to spot when its sandbox is created, and the tier cannot change afterwards. - **Evaluation and reinforcement learning (RL).** Many short test cases or episodes run on spot sandboxes. A reclaimed test case or episode runs again in a new sandbox. Newer spot sandboxes are reclaimed before older ones, so a reclaim interrupts the least accumulated work first. - **Stateless workers.** Workers that keep no state in the sandbox lose nothing on a reclaim. A new spot sandbox replaces the reclaimed one. Daytona does not create the replacement on its own, so the application creates it. - **Results outside the sandbox.** A spot sandbox writes its output to storage outside the sandbox, such as your own object storage, so a reclaim loses only the work in progress. A replacement sandbox continues from the last checkpoint in that storage. The [reclaim time](#reclaim-detection) on the old sandbox tells a reclaim apart from a delete you requested. - **Development and experiments.** Sandboxes whose loss costs only a restart run at the spot price. A reclaim comes with no warning, and an ephemeral sandbox loses its filesystem. GPU sandboxes cannot be spot, so GPU experiments run at full price. ## Spot sandboxes [#spot-sandboxes] A spot sandbox differs from an on-demand sandbox only in price and reclaim. In every other respect it is an ordinary sandbox: it supports the same images, resources, and lifecycle operations as an on-demand sandbox of its type. Spot multiplies the CPU, memory, and disk price by 0.7; see [pricing dimensions](/docs/billing#pricing-dimensions). Linux and Windows sandboxes can be spot. GPU sandboxes cannot. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create(spot=True) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------------------------------------------------------------- | | `spot` | bool | Places the sandbox on discounted, reclaimable capacity. The default is `False`. The value is fixed at create. `create_from_snapshot()` has no `spot` parameter. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create({ spot: true }) ``` | **Parameter** | **Type** | **Description** | | ------------- | --------- | --------------------------------------------------------------------- | | `spot` | boolean | Places the sandbox on discounted, reclaimable capacity. The default is `false`. The value is fixed at create. `createFromSnapshot()` has no `spot` option. | ```bash daytona sandbox create --spot ``` | **Flag** | **Type** | **Description** | | -------- | -------- | --------------- | | `--spot` | bool | Places the sandbox on discounted, reclaimable capacity. The default is `false`. The value is fixed at create. `sandbox create-from-snapshot` has no `--spot` flag. | ```bash curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"spot": true}' ``` | **Field** | **Type** | **Description** | | --------- | --------- | --------------------------------------------------------------------- | | `spot` | boolean | Places the sandbox on discounted, reclaimable capacity. The default is `false`. The value is fixed at create. `POST /sandboxes/windows` also accepts `spot`. `POST /sandboxes/gpu` and `POST /sandboxes/from-snapshot` do not accept `spot`. | ## Reclaim policy [#reclaim-policy] Daytona tracks the CPU, memory, and disk reserved on each host. When the reservation on a host crosses a threshold on any of the three, Daytona reclaims running spot sandboxes on that host until the projected reservation drops below it. Newer spot sandboxes are reclaimed before older ones, interrupting the least accumulated work first. On-demand sandboxes are never reclaimed. A reclaim is immediate. Daytona does not notify the sandbox or your application in advance, and there is no grace period. Daytona does not replace a reclaimed sandbox on its own when capacity frees up: creating a new one is up to you. A reclaimed sandbox is [billed](/docs/billing) like a sandbox you delete, pause, or stop. ## Reclaim outcomes [#reclaim-outcomes] Daytona records the reclaim time, then tears an ephemeral sandbox down like a user request: the sandbox is deleted, and only data in storage outside the sandbox remains. A Windows sandbox is stopped, since Windows cannot pause, and keeps its disk. For the full state model, see [lifecycle](/docs/sandboxes#sandbox-lifecycle). ## Reclaim detection [#reclaim-detection] Reclaim detection tells a reclaim apart from a delete you requested. Every reclaimed sandbox carries the time of the reclaim until it is started or resumed. A sandbox that was never reclaimed carries no reclaim time. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.get("{sandboxId}") # None unless the platform reclaimed this spot sandbox if sandbox.spot_evicted_at: print(f"reclaimed at {sandbox.spot_evicted_at}, state: {sandbox.state}") # Find spot sandboxes page = daytona.v2.search(spot=True) for sandbox in page.items: print(sandbox.id, sandbox.state, sandbox.spot_evicted_at) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ------------------------------------------------------------------------------------- | | `spot` | bool | Filters by capacity type. `True` returns only spot sandboxes, and `False` returns only on-demand sandboxes. The default is `None`, which returns both. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.get('{sandboxId}') // undefined unless the platform reclaimed this spot sandbox if (sandbox.spotEvictedAt) { console.log(`reclaimed at ${sandbox.spotEvictedAt}, state: ${sandbox.state}`) } // Find spot sandboxes const page = await daytona.v2.search({ spot: true }) for (const item of page.items) { console.log(item.id, item.state, item.spotEvictedAt) } ``` | **Parameter** | **Type** | **Description** | | ------------- | --------- | ------------------------------------------------------------------------------------------ | | `spot` | boolean | Filters by capacity type. `true` returns only spot sandboxes, and `false` returns only on-demand sandboxes. By default, the search returns both. | ```bash # spot_evicted_at is present only if the platform reclaimed the sandbox daytona sandbox get {sandboxId} --output json # Find spot sandboxes daytona sandbox search --spot ``` | **Flag** | **Type** | **Description** | | -------- | -------- | --------------- | | `--spot` | bool | Filters by capacity type. `--spot` returns only spot sandboxes, and `--spot=false` returns only on-demand sandboxes. By default, the search returns both. | ```bash # spot_evicted_at is an RFC 3339 timestamp, or null if the sandbox was never reclaimed curl 'https://api.daytona.com/sandboxes/{sandboxId}' \ --header 'Authorization: Bearer YOUR_API_KEY' # Find spot sandboxes curl 'https://api.daytona.com/sandboxes/search?spot=true' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Type** | **Description** | | --------- | --------- | ------------------------------------------------------------------------------------------ | | `spot` | boolean | Filters by capacity type. `true` returns only spot sandboxes, and `false` returns only on-demand sandboxes. By default, the search returns both. | ## Reclaim recovery [#reclaim-recovery] A reclaimed ephemeral sandbox is gone; continue in a new sandbox from data stored outside the sandbox. ```python from daytona import Daytona daytona = Daytona() previous = daytona.v2.get("{sandboxId}") # A reclaimed sandbox is deleted; only data outside it remains if previous.spot_evicted_at: sandbox = daytona.v2.create(spot=True) # Continue from the last checkpoint the previous sandbox wrote to your own storage sandbox.process.run("python3 train.py --resume-from s3://my-bucket/checkpoint") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const previous = await daytona.v2.get('{sandboxId}') // A reclaimed sandbox is deleted; only data outside it remains if (previous.spotEvictedAt) { const sandbox = await daytona.v2.create({ spot: true }) // Continue from the last checkpoint the previous sandbox wrote to your own storage await sandbox.process.run('python3 train.py --resume-from s3://my-bucket/checkpoint') } ``` ## Opting in to preemption [#opting-in-to-preemption] Spot suits work that tolerates interruption and can be restarted or continued: batch jobs, evaluation and reinforcement learning rollouts, CI runs, and stateless workers. Keep spot off sandboxes that serve live traffic or hold state that exists nowhere else. - **Checkpoint to storage outside the sandbox** from spot sandboxes, so a replacement sandbox can continue where the reclaimed one stopped. - **Check after interruptions**: when a sandbox stops responding or changes state unexpectedly, read its [reclaim time](#reclaim-detection) to tell a reclaim apart from a failure. # Snapshots (/docs/snapshots) A snapshot is a capture of the memory and disk of a running sandbox, saved under a name. A sandbox restored from a snapshot resumes the captured processes and memory instead of booting. Deleting the source sandbox does not affect its snapshots. A Linux sandbox produces a full snapshot, and only a persistent sandbox or one served from a warm template can be captured. A GPU sandbox produces a [checkpoint](/docs/gpu-sandboxes) that also holds GPU memory. A full snapshot restores in its home region, on the runner that captured it or on another runner of the region with the same CPU model and hypervisor version. Where Daytona has enabled cross-region restore, a full snapshot of a sandbox that is not persistent can also restore in another region, which receives a copy of it first. Without it, a restore that names another region returns `400`. A checkpoint restores on any GPU runner in the region that meets the [restore requirements](/docs/gpu-sandboxes). Windows sandboxes do not support snapshots. macOS sandboxes do not produce these snapshots: a macOS filesystem snapshot saves the disk of a stopped persistent sandbox as an image, and it restores on any Mac of the region. A snapshot holds a running state. To save only the filesystem of a sandbox, [capture an image](/docs/images#capture-images) instead. To register a registry image or a Dockerfile build under a name, [pull](/docs/images#pull-images) or [build](/docs/images#build-images) an image. | **Concept** | **Description** | | ----------------- | ---------------------------------------------------------------------------------------------------------------- | | **Full snapshot** | The memory and disk of a running persistent Linux sandbox or one served from a warm template. | | **Checkpoint** | The processes, memory, and GPU memory of a running GPU sandbox. | | **Restore** | A new sandbox that resumes the captured processes and memory, with the CPU, memory, and disk of the source. | | **Home region** | The region of the source sandbox. A snapshot restores there unless the restore names another region. | | **Name** | Unique in the project. Snapshots and images share one name space. | ## Use cases [#use-cases] Snapshots are used when a new sandbox must continue from a running state instead of booting and repeating its setup. - **Checkpoints of running work.** A full snapshot saves the memory and disk of a running sandbox. A restore resumes the captured processes instead of starting them again. The capture runs in the background, and the source sandbox keeps running if the capture fails. - **Fast starts for slow-booting programs.** A full snapshot taken after a server, a kernel, or a model has loaded restores with that program already running. New sandboxes skip the load time. The program must run in a persistent Linux sandbox or one served from a warm template, because only those sandboxes can be captured. - **Evaluation and reinforcement learning (RL).** Each test case or episode resumes from the same snapshot. Results do not depend on another run. Every restored sandbox keeps the CPU, memory, and disk of the source, so each run gets the same resources. - **Long GPU jobs.** A checkpoint saves a GPU job together with its GPU memory. A new GPU sandbox resumes the job from the point it was saved. The checkpoint restores on any GPU runner in the region that meets the [restore requirements](/docs/gpu-sandboxes). - **Parallel attempts from one state.** Several sandboxes restore from the same snapshot, and each continues on its own. An agent tries several approaches from one point without repeating the setup. The snapshot cannot be deleted while a sandbox restored from it exists. ## Create snapshots [#create-snapshots] Create a snapshot of a running sandbox. The capture runs in the background: the snapshot starts as pending, is building while the runner captures the sandbox, and becomes ready when the capture finishes. Deleting the source sandbox during the capture fails the snapshot. ```python from daytona import Daytona daytona = Daytona() # Snapshot capture needs a persistent sandbox or one served from a warm template sandbox = daytona.v2.create() snapshot = sandbox.create_snapshot("my-snapshot") print(snapshot.id, snapshot.kind, snapshot.status) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `name` | str | Sets the snapshot name, 1 to 128 characters of letters, digits, `.`, `_`, and `-`. It cannot start with `.` or with the reserved prefixes `sbx_`, `vol_`, `fn_`, `snap-`, `build-`, `refs`, `by-digest`, `prj-`, and `glb-`. Names are case-sensitive. This parameter is required. Snapshots and images in the project share one name space. A name in use fails with `409`. | The response has the following fields: | **Field** | **Description** | | --------- | --------------- | | `status` | The status is `pending` or `building`, because the call returns before the capture finishes. To wait, call `daytona.v2.snapshots.get(snapshot.id, wait="ready")`. It returns at `ready` or `failed`, or after the server wait of 30 seconds. `timeout` sets a total wait in seconds: the SDK repeats the server wait until the snapshot is `ready` or `failed`, and raises `RequestTimeoutError` if the time runs out first. | - The request requires the `write:snapshots` permission. - The sandbox must be running. - On Linux, the sandbox must be persistent or served from a warm template. - This call does not support Windows sandboxes. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() // Snapshot capture needs a persistent sandbox or one served from a warm template const sandbox = await daytona.v2.create() const snapshot = await sandbox.createSnapshot('my-snapshot') console.log(snapshot.id, snapshot.kind, snapshot.status) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `name` | string | Sets the snapshot name, 1 to 128 characters of letters, digits, `.`, `_`, and `-`. It cannot start with `.` or with the reserved prefixes `sbx_`, `vol_`, `fn_`, `snap-`, `build-`, `refs`, `by-digest`, `prj-`, and `glb-`. Names are case-sensitive. This parameter is required. Snapshots and images in the project share one name space. A name in use fails with `409`. | The response has the following fields: | **Field** | **Description** | | --------- | --------------- | | `status` | The status is `pending` or `building`, because the call returns before the capture finishes. To wait, call `daytona.v2.snapshots.get(snapshot.id, { wait: 'ready' })`. It returns at `ready` or `failed`, or after the server wait of 30 seconds. `timeoutSeconds` sets a total wait in seconds: the SDK repeats the server wait until the snapshot is `ready` or `failed`, and throws `RequestTimeoutError` if the time runs out first. | - The request requires the `write:snapshots` permission. - The sandbox must be running. - On Linux, the sandbox must be persistent or served from a warm template. - This call does not support Windows sandboxes. ```bash daytona snapshot create {sandboxId} --name my-snapshot ``` | **Flag** | **Type** | **Description** | | -------- | -------- | --------------- | | `--name` | string | Sets the snapshot name. This flag is required. | | `--timeout` | duration | Sets how long the command waits for the snapshot to be `ready`, as a duration such as `30s` or `10m`. The capture continues after the command stops waiting. The default is `10m`. | | `--no-wait` | boolean | Returns once the capture is queued, without waiting for `ready`. | | `--file`, `-f` | string | Reads the request body from a JSON file, or from standard input with `-`. Flags override its fields. | ```bash curl 'https://api.daytona.com/sandboxes/{sandboxId}/snapshots' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "name": "my-snapshot" }' ``` The response has the following fields: | **Field** | **Description** | | --------- | --------------- | | `id` | Identifies the new snapshot. | | `status` | The status is `pending`. To wait, call `GET /snapshots/{snapshotId}?wait=ready&timeout_ms=60000`. It answers `200` at `ready` or `failed`, or when the wait expires. `timeout_ms` defaults to `30000` and is at most `60000`. | - The request requires the `write:snapshots` permission. - The sandbox must be running. - On Linux, the sandbox must be persistent or served from a warm template. | **Kind** | **Captures** | **Restore behavior** | | ---------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`full`** | Memory and disk of a Linux sandbox | Resumes the captured processes and memory. Requires a persistent sandbox or one served from a warm template. Restores in its home region. A snapshot of a sandbox that is not persistent also restores in a region whose runners have the same CPU model and hypervisor version. | | **`checkpoint`** | Processes, memory, and GPU memory of a GPU sandbox | Resumes in a new GPU sandbox. See [GPU sandboxes](/docs/gpu-sandboxes). | | **Status** | **Meaning** | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pending` | Accepted; the capture job is queued on the sandbox's runner. | | `building` | The runner is pausing, copying, and packing the sandbox state. | | `ready` | Restorable. | | `failed` | Terminal. `error` carries the reason. The source sandbox keeps running. | ## Restore snapshots [#restore-snapshots] Restore a sandbox from a snapshot. It resumes the captured processes and memory and keeps the source sandbox's CPU, memory, disk, and persistence. The restore runs on the runner that captured the snapshot when that runner can take it, and otherwise on another runner of the region with the same CPU model and hypervisor version. Where cross-region restore is enabled, a restore in another region waits while the snapshot is copied there. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create_from_snapshot("my-snapshot") ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `snapshot` | str | Sets the snapshot to restore from, by name or by ID, `snap_...`. This parameter is required. | | `name` | str | Sets the display name. If omitted, a name is generated. | | `labels` | Dict\[str, str\] | Attaches key-value metadata to the sandbox. | | `lifecycle_policy` | LifecyclePolicyOptions | Sets the lifecycle policy. It takes the same fields as `lifecycle_policy` in [create a Linux sandbox](/docs/linux-sandboxes#create-a-linux-sandbox). | | `ports` | Sequence\[PortMapping\] | Forwards inbound ports to the sandbox. Import `PortMapping` from `daytona.v2`, for example `PortMapping(port=8080)`. | | `region` | str | Sets the region. The default is the snapshot's home region. Another region requires cross-region restore to be enabled, a source sandbox that was not persistent, and runners in that region with the CPU model and hypervisor version of the snapshot. | | `idempotency_key` | str | Makes a retried create return the same sandbox. If `name` is set, the key is derived from it. Otherwise, each call generates a new key. | | `wait_seconds` | int | Sets how many seconds create waits for the sandbox to start. The server holds the request for up to 3 seconds, and the SDK waits for the rest. `0` returns immediately. The default is `60`. | | `create_timeout_seconds` | int | Sets how many seconds the sandbox has to reach running before it is deleted, at most `86400`. `-1` sets the maximum. The default is `3600`. | - The request requires the `write:sandboxes` permission. - The snapshot must have the `ready` status. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.createFromSnapshot('my-snapshot') ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `snapshot` | string | Sets the snapshot to restore from, by name or by ID, `snap_...`. This parameter is required. | | `name` | string | Sets the display name. If omitted, a name is generated. | | `labels` | Record\ | Attaches key-value metadata to the sandbox. | | `lifecyclePolicy` | LifecyclePolicyOptions | Sets the lifecycle policy. It takes the same fields as `lifecyclePolicy` in [create a Linux sandbox](/docs/linux-sandboxes#create-a-linux-sandbox). | | `ports` | PortMapping\[\] | Forwards inbound ports to the sandbox, for example `[{ port: 8080 }]`. | | `region` | string | Sets the region. The default is the snapshot's home region. Another region requires cross-region restore to be enabled, a source sandbox that was not persistent, and runners in that region with the CPU model and hypervisor version of the snapshot. | | `idempotencyKey` | string | Makes a retried create return the same sandbox. If `name` is set, the key is derived from it. Otherwise, each call generates a new key. | | `waitSeconds` | number | Sets how many seconds create waits for the sandbox to start. The server holds the request for up to 3 seconds, and the SDK waits for the rest. `0` returns immediately. The default is `60`. | | `createTimeoutSeconds` | number | Sets how many seconds the sandbox has to reach running before it is deleted, at most `86400`. `-1` sets the maximum. The default is `3600`. | - The request requires the `write:sandboxes` permission. - The snapshot must have the `ready` status. ```bash daytona sandbox create-from-snapshot my-snapshot --name resumed ``` | **Flag** | **Type** | **Description** | | -------- | -------- | --------------- | | `--name` | string | Sets the display name. If omitted, a name is generated. | | `--label` | stringArray | Attaches a label as `KEY=VALUE`. Repeat the flag for each label. | | `--port` | stringArray | Forwards an inbound port as `PORT` or `PORT/PROTO`. Repeat the flag for each port. | | `--region` | string | Sets the region. The default is the snapshot's home region. Another region requires cross-region restore to be enabled, a source sandbox that was not persistent, and runners in that region with the CPU model and hypervisor version of the snapshot. | | `--idempotency-key` | string | Makes a retried create return the same sandbox. The default is a key derived from `--name`, or a random key when no name is set. | | `--lifecycle-timeout` | duration | Sets the wall-clock lifetime as a Go duration such as `2h`, until `--on-lifecycle-timeout` fires. The default is the project or platform default, if one is enabled. | | `--on-lifecycle-timeout` | string | Sets the action when `--lifecycle-timeout` elapses. The CLI accepts only `delete`. The default is `pause` when the source sandbox of the snapshot was persistent, and `delete` otherwise. | | `--idle-timeout` | duration | Sets the idle time as a Go duration, at least `1m`, until `--on-idle-timeout` fires. By default, there is no idle timeout unless the project sets one. | | `--on-idle-timeout` | string | Sets the action when `--idle-timeout` elapses, `delete`. It is required with `--idle-timeout`. | | `--create-ttl` | duration | Sets how long the sandbox has to reach running before the server deletes it, as a Go duration. `max` sets the server maximum, 24 hours. The default is `1h`. | | `--timeout` | duration | Sets how long the command waits for the sandbox to run, as a duration such as `30s` or `10m`. The default is `10m`. | | `--no-wait` | boolean | Prints the create response once the server answers, without waiting for the sandbox to run. The server holds the response for up to 3 seconds. | | `--file`, `-f` | string | Reads the request body from a JSON file, or from standard input with `-`. Flags override its fields. | - The request requires the `write:sandboxes` permission. - The snapshot must have the `ready` status. - Reference the snapshot by name or by ID, `snap_...`. ```bash curl 'https://api.daytona.com/sandboxes/from-snapshot' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "snapshot": "my-snapshot" }' ``` | **Field** | **Type** | **Description** | | --------- | -------- | --------------- | | `snapshot` | string | Sets the snapshot to restore from, by name or by ID, `snap_...`. This field is required. | | `name` | string | Sets the display name. If omitted, a name is generated. | | `labels` | object | Attaches key-value metadata to the sandbox. | | `lifecycle_policy` | object | Sets the lifecycle policy. It takes the same fields as `lifecycle_policy` in [create a Linux sandbox](/docs/linux-sandboxes#create-a-linux-sandbox). | | `ports` | array | Forwards inbound ports to the sandbox. | | `region_id` | string | Sets the region. The default is the snapshot's home region. Another region requires cross-region restore to be enabled, a source sandbox that was not persistent, and runners in that region with the CPU model and hypervisor version of the snapshot. A region that covers other regions, such as `us-east`, requires `idempotency_key`. | | `idempotency_key` | string | Makes a retried create return the same sandbox. You can also send it as the `Idempotency-Key` header. | | `create_timeout_seconds` | integer | Sets how many seconds the sandbox has to reach running before it is deleted, at most `86400`. `-1` sets the maximum. The default is `3600`. | | `wait_seconds` | integer | Sets how many seconds the server holds the request for the sandbox to start, at most `3`. `0` returns immediately. The default is `3`. | - The request requires the `write:sandboxes` permission. - The snapshot must have the `ready` status. ## Operations [#operations] ### List snapshots [#list-snapshots] ```python from daytona import Daytona daytona = Daytona() page = daytona.v2.snapshots.list() for snapshot in page.items: print(snapshot.name, snapshot.kind, snapshot.status) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `name` | str | Filters the list by exact snapshot name. | | `sandbox_id` | str | Filters the list to snapshots captured from this sandbox. | | `limit` | int | Sets the page size, from `1` to `200`. The default is `100`. | | `cursor` | str | Resumes the list after the previous page. Pass the `next_cursor` of that page. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const page = await daytona.v2.snapshots.list() for (const snapshot of page.items) { console.log(snapshot.name, snapshot.kind, snapshot.status) } ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `name` | string | Filters the list by exact snapshot name. | | `sandboxId` | string | Filters the list to snapshots captured from this sandbox. | | `limit` | number | Sets the page size, from `1` to `200`. The default is `100`. | | `cursor` | string | Resumes the list after the previous page. Pass the `nextCursor` of that page. | ```bash daytona snapshot list ``` | **Flag** | **Type** | **Description** | | -------- | -------- | --------------- | | `--limit` | int | Sets the page size, from `1` to `200`. The default is `100`. | | `--cursor` | string | Resumes the list after the previous page. | ```bash curl 'https://api.daytona.com/snapshots' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Type** | **Description** | | --------- | -------- | --------------- | | `name` | string | Filters the list by exact snapshot name. | | `sandbox_id` | string | Filters the list to snapshots captured from this sandbox. | | `limit` | integer | Sets the page size, from `1` to `200`. The default is `100`. | | `cursor` | string | Resumes the list after the previous page. Pass the `next_cursor` of that page. | The API returns the following fields. The SDKs use the same names, except where the description says otherwise. | **Field** | **Description** | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | `id` | Snapshot ID. | | `name` | Unique name within the project. Used for restore. | | `class` | `linux` or `gpu`. The SDKs name it `sandbox_class` in Python and `sandboxClass` in TypeScript. | | `kind` | `full` for Linux, or `checkpoint` for GPU. | | `status` | `pending`, `building`, `ready`, or `failed`. | | `sandbox_id` | The ID of the source sandbox. It stays after the source sandbox is deleted, and becomes `null` once Daytona purges the record of the deleted sandbox. | | `region_id` | Home region. The SDKs name it `region`. | | `region_ids` | The home region, followed by every other region that holds a copy of the snapshot. The SDKs name it `available_in` in Python and `availableIn` in TypeScript. | | `bytes` | Size of the captured disk in bytes. `null` for `full` snapshots. | | `memory_bytes` | Size of the captured memory, `full` only. | | `error` | Failure reason when `status` is `failed`. | #### Iterate over all snapshots [#iterate-over-all-snapshots] ```python from daytona import Daytona daytona = Daytona() for snapshot in daytona.v2.snapshots.iter(): print(snapshot.name, snapshot.kind, snapshot.status) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() for await (const snapshot of daytona.v2.snapshots.iter()) { console.log(snapshot.name, snapshot.kind, snapshot.status) } ``` ```bash # Follow every page until the server returns no cursor daytona snapshot list --all ``` ```bash curl 'https://api.daytona.com/snapshots?cursor={next_cursor}' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ### Get a snapshot [#get-a-snapshot] ```python from daytona import Daytona daytona = Daytona() snapshot = daytona.v2.snapshots.get("{snapshotId}") print(snapshot.kind, snapshot.status) ``` - The request requires the `read:snapshots` permission. - `get` takes the snapshot ID, `snap_...`, or the name. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const snapshot = await daytona.v2.snapshots.get('{snapshotId}') console.log(snapshot.kind, snapshot.status) ``` - The request requires the `read:snapshots` permission. - `get` takes the snapshot ID, `snap_...`, or the name. ```bash daytona snapshot get {snapshotId} ``` - The request requires the `read:snapshots` permission. - The command takes the snapshot ID, `snap_...`, or the name. ```bash curl 'https://api.daytona.com/snapshots/{snapshotId}' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Type** | **Description** | | --------- | -------- | --------------- | | `id_only` | boolean | Resolves the path value only as an ID, without falling back to a name. The default is `false`. | - The request requires the `read:snapshots` permission. - The path takes the snapshot ID, `snap_...`, or the name. ### Delete snapshots [#delete-snapshots] Delete a snapshot. It cannot be deleted while a sandbox restored from it exists, including a restored sandbox whose deletion has not finished. 1. Go to [Daytona Dashboard ↗](https://app.daytona.com/dashboard/snapshots) 2. Open the actions menu of a snapshot and click 3. Click to confirm ```python from daytona import Daytona daytona = Daytona() daytona.v2.snapshots.delete("{snapshotId}") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() await daytona.v2.snapshots.delete('{snapshotId}') ``` ```bash daytona snapshot delete {snapshotId} ``` - The request requires the `delete:snapshots` permission. - The command asks for confirmation. - A non-interactive shell requires `--yes`. ```bash curl 'https://api.daytona.com/snapshots/{snapshotId}' \ --request DELETE \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Type** | **Description** | | --------- | -------- | --------------- | | `id_only` | boolean | Resolves the path value only as an ID, without falling back to a name. The default is `false`. | ## Errors [#errors] The SDKs raise these errors with the codes in the following table, and the API returns them in the `code` field. | **Code** | **Meaning** | **What to do** | | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | **`SNAPSHOT_NOT_FOUND`** | The restore names a snapshot that does not exist in the project. | Check the name or ID with [list snapshots](#list-snapshots). | | **`SNAPSHOT_NOT_READY`** | The restore names a snapshot that is `pending`, `building`, or `failed`. | Wait until the snapshot is `ready`. A `failed` snapshot does not become `ready`: create a new one. | | **`NOT_SUPPORTED_FOR_CLASS`** | The snapshot create names a Windows or macOS sandbox. | To save the disk, [capture an image](/docs/images#capture-images). | | **`IDEMPOTENCY_KEY_REQUIRED`** | An API restore names a region that covers other regions, such as `us-east`, without an `Idempotency-Key` header. The SDKs and the CLI always send a key. | Pass an idempotency key, or name a single region. | | **`CONFLICT`** | The name is in use by a snapshot or an image, the sandbox is not running, the Linux sandbox is neither persistent nor served from a warm template, the target region has no runner with the CPU model and hypervisor version of the snapshot, or a delete names a snapshot that a sandbox still references. | Use another name, start the sandbox, restore in the home region, or delete the restored sandboxes first. | | **`BAD_REQUEST`** | The snapshot name is invalid, the restore names another region that the snapshot cannot be copied to (for example, cross-region restore is not enabled, the source sandbox was persistent, or the snapshot is too large), the restore names a `checkpoint`, or the path value matches both a snapshot ID and the name of another snapshot. | Fix the name, restore in the home region, restore a `checkpoint` as a [GPU sandbox](/docs/gpu-sandboxes), or pass `id_only`. | ## Permissions [#permissions] Snapshots are scoped to a project. Access is governed by three permissions, which you can grant to an [organization role](/docs/organizations#roles) or an [API key](/docs/api-keys). Restoring a snapshot creates a sandbox, so it requires the permission to create sandboxes. The [audit logs](/docs/audit-logs) record snapshot creates and deletes. | **Permission** | **Grants** | | ---------------------- | ---------------------- | | **`read:snapshots`** | List and get snapshots | | **`write:snapshots`** | Create snapshots | | **`delete:snapshots`** | Delete snapshots | # Images (/docs/images) An image is the filesystem a sandbox boots from, together with the environment variables, working directory, entrypoint, and command the sandbox starts with. Daytona accepts standard OCI images, the same ones you build and push with Docker, and converts each one into a boot-optimized format when you register it. Images belong to a project, and a sandbox references its image by name or ID. A sandbox create does not pull from a registry, so a sandbox boots only from an image already registered in the project. Every sandbox created from an image starts with the same filesystem and configuration. An image holds no running processes or memory. A [snapshot](/docs/snapshots) saves the memory and disk of a running sandbox, and a restored sandbox resumes its processes. | **Concept** | **Description** | | ---------------------- | --------------------------------------------------------------------------------------------------- | | **Registry image** | An image pulled from a public or private registry and registered under a name. | | **Dockerfile build** | An image Daytona builds from Dockerfile text, with or without a build context. | | **Capture** | An image made from the filesystem of a running Linux or Windows sandbox. | | **Windows base image** | A base image Daytona provides for Windows sandboxes. It boots in every region. | | **Anonymous image** | An image without a name. Daytona deletes it after it goes unused for its idle window. | | **Status** | Pending, building, ready, or failed. Sandboxes boot only from a ready image. | | **Home region** | The region where Daytona builds, pulls, or captures the image. | ## Use cases [#use-cases] Images are used when many sandboxes need the same software, files, or configuration. - **Preinstalled dependencies.** Packages, language runtimes, and tools are installed once in a Dockerfile build, and every sandbox from the image starts with them. A repeat build of the same Dockerfile and build context reuses the earlier build instead of running a new one. A Dockerfile that copies local files needs a [build context](#build-context) of at most 1 GiB. - **Configured sandboxes as templates.** A sandbox set up by hand or by an agent is captured, and new sandboxes start from its filesystem. The source sandbox keeps running during the capture. A named capture stays until you delete it. - **Private application images.** Images that a CI pipeline pushes to a private registry are imported with the project's [registry credentials](/docs/registries) and boot in sandboxes. Pulling again under the same name re-imports the image, and creates from it fail until the new pull is ready. The API call returns once the pull is queued, and the SDKs and the CLI wait by default until the image is ready. - **Environments written by agents.** An agent writes a Dockerfile, builds it, and boots sandboxes from the result, without access to your build infrastructure. The SDKs build the image and create the sandbox in one call. A failed build creates no sandbox. - **Evaluation and reinforcement learning (RL).** Each test case or episode starts in a new sandbox from the same image, so results do not depend on another run. An image holds no running processes or memory, so each run boots from the same files. Runs that need processes already running start from a [snapshot](/docs/snapshots) instead, which saves memory and disk. ## Create sandboxes from images [#create-sandboxes-from-images] Create a sandbox from an image registered in the project, by its name or ID. A sandbox create does not pull from a registry, so the image is [pulled](#pull-images), [built](#build-images), or [captured](#capture-images) first. Registration runs in the background, and a create from an image that is not ready yet fails. ```python from daytona import Daytona daytona = Daytona() # Register python:3.12-slim under the name python-3.12 image = daytona.v2.images.pull("python:3.12-slim", name="python-3.12") # Each wait returns after 30 seconds at most; a create from an image that is not ready fails while image.status not in ("ready", "failed"): image = daytona.v2.images.get(image.id, wait="ready") if image.status == "failed": raise RuntimeError(image.error) sandbox = daytona.v2.create("python-3.12") ``` - The requests require the `write:images` and `write:sandboxes` permissions. - Set `timeout` in seconds to wait in one call until the image is ready or failed. When the time runs out first, the call raises `RequestTimeoutError`. - A create rejects a value that contains `/` or `:` as a registry reference, so pass the name or ID of a registered image. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() // Register python:3.12-slim under the name python-3.12 let image = await daytona.v2.images.pull('python:3.12-slim', { name: 'python-3.12' }) // Each wait returns after 30 seconds at most; a create from an image that is not ready fails while (image.status !== 'ready' && image.status !== 'failed') { image = await daytona.v2.images.get(image.id, { wait: 'ready' }) } if (image.status === 'failed') throw new Error(image.error ?? 'image build failed') const sandbox = await daytona.v2.create('python-3.12') ``` - The requests require the `write:images` and `write:sandboxes` permissions. - Set `timeoutSeconds` to wait in one call until the image is ready or failed. When the time runs out first, the call throws `RequestTimeoutError`. - A create rejects a value that contains `/` or `:` as a registry reference, so pass the name or ID of a registered image. ```bash # Register python:3.12-slim under the name python-3.12; waits until the image is ready daytona image create python-3.12 --ref python:3.12-slim daytona sandbox create --image python-3.12 ``` - The requests require the `write:images` and `write:sandboxes` permissions. - A create rejects a value that contains `/` or `:` as a registry reference, so pass the name or ID of a registered image. ```bash # Register python:3.12-slim under the name python-3.12; the response carries its id curl 'https://api.daytona.com/images' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "name": "python-3.12", "source": { "type": "registry", "ref": "python:3.12-slim" } }' # Returns on ready or failed, or after timeout_ms; repeat while status is pending or building curl 'https://api.daytona.com/images/{imageId}?wait=ready&timeout_ms=60000' \ --header 'Authorization: Bearer YOUR_API_KEY' curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "image": "python-3.12" }' ``` - The requests require the `write:images` and `write:sandboxes` permissions. - The default `timeout_ms` is `30000`, and the maximum is `60000`. Larger values are treated as `60000`. - A create rejects a value that contains `/` or `:` as a registry reference, so pass the name or ID of a registered image. ### Inline Dockerfile [#inline-dockerfile] Build an image from Dockerfile text and create a sandbox from it. The SDKs do both in one call. The image is anonymous: it has no name, and the default image list does not show it. If the same Dockerfile was built before under a name, the sandbox starts from that named image instead. Daytona deletes an anonymous image once it goes unused for its idle window, the server maximum of 7 days by default; every sandbox create from the image restarts the window. The window starts when the image is created, so a window shorter than the build deletes the image before it is ready. The Dockerfile must be self-contained. There is no build context, so instructions that copy local files have nothing to copy. Use a [build context](#build-context) for those. ```python from daytona import Daytona from daytona.v2 import ImageBuilder daytona = Daytona() # Builds an anonymous image from the Dockerfile, then creates the sandbox from it sandbox = daytona.v2.create_from_build( ImageBuilder.from_dockerfile("FROM python:3.12-slim\nRUN pip install requests\n") ) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `build_timeout` | float | Sets how many seconds the build can take. A build that runs longer fails with `408` `REQUEST_TIMEOUT` and creates no sandbox. The build keeps running, and its anonymous image is deleted after its idle window. By default, or with `0`, there is no limit. | | `wait_seconds` | int | Sets how many seconds the create waits for the sandbox to start, as in `create`. It does not limit the build. The default is `60`. | | `idle_expiry_seconds` | int | Sets the idle window of the image in seconds. It applies only to the image. It is ignored when the build reuses the build of a named image. | | `region` | str | Selects the region for both the build and the sandbox. | - The requests require the `write:images` and `write:sandboxes` permissions. - `cpu_arch` also applies to both the build and the sandbox. - Every other keyword argument goes to `create`. ```typescript import { Daytona } from '@daytona/sdk' import { ImageBuilder } from '@daytona/sdk/v2' const daytona = new Daytona() // Builds an anonymous image from the Dockerfile, then creates the sandbox from it const sandbox = await daytona.v2.createFromBuild( ImageBuilder.fromDockerfile('FROM python:3.12-slim\nRUN pip install requests\n'), ) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `buildTimeoutSeconds` | number | Sets how many seconds the build can take. A build that runs longer fails with `408` `REQUEST_TIMEOUT` and creates no sandbox. The build keeps running, and its anonymous image is deleted after its idle window. By default, or with `0`, there is no limit. | | `waitSeconds` | number | Sets how many seconds the create waits for the sandbox to start, as in `create`. It does not limit the build. The default is `60`. | | `idleExpirySeconds` | number | Sets the idle window of the image in seconds. It applies only to the image. It is ignored when the build reuses the build of a named image. | | `region` | string | Selects the region for both the build and the sandbox. | - The requests require the `write:images` and `write:sandboxes` permissions. - `cpuArch` also applies to both the build and the sandbox. ```bash # Build an anonymous image from the inline Dockerfile; the response carries its id curl 'https://api.daytona.com/images' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "dockerfile": "FROM python:3.12-slim\nRUN pip install requests\n" }' # Returns on ready or failed, or after timeout_ms; repeat while status is pending or building curl 'https://api.daytona.com/images/{imageId}?wait=ready&timeout_ms=60000' \ --header 'Authorization: Bearer YOUR_API_KEY' # Create the sandbox from the image id curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "image": "{imageId}" }' ``` | **Field** | **Type** | **Description** | | --------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `dockerfile` | string | Sets the Dockerfile text. The Dockerfile must not copy local files. This field is required. This field cannot be sent together with `source`. | | `name` | string | Sets the image name. Without a name, the image is anonymous, unless the build reuses the build of a named image; the call then returns that named image. | | `idle_expiry_seconds` | integer | Sets the idle window of the image in seconds, from `1` up to the server maximum. `-1` sets the maximum. Other values return `400` `IDLE_OUT_OF_RANGE`. The default is the server maximum. It is ignored when the build reuses the build of a named image. When the build reuses an anonymous image, that image keeps the longer of the two windows. | ### Windows images [#windows-images] Create a [Windows sandbox](/docs/windows-sandboxes) from a Windows image. Windows sandboxes boot only from Windows images: a platform base image, which boots in every region, or a Windows image captured in the project, which boots only in the region it lives in. {/* snippet-check: skip POST /sandboxes/windows is hidden from the public API contract until relaunch. */} ```bash curl 'https://api.daytona.com/sandboxes/windows' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "image": "win2022" }' ``` | **Field** | **Type** | **Description** | | --------- | -------- | ----------------------------------------------------------------------------------------------------------- | | `image` | string | Sets the Windows image by name or ID. The image is a platform base image or a Windows image captured in the project. This field is required. | - The request requires the `write:sandboxes` permission. - The organization must have the `windows_sandboxes` feature flag. - Windows sandboxes are available on request. ## Pull images [#pull-images] Pull an image from a registry and register it under a name. The pull runs in the background. The API call returns once the pull is queued, and the SDKs and the CLI wait until the image is ready by default. Pulling again under the name of an existing registry or Dockerfile image re-imports the image: it keeps its ID, returns to pending, and sandbox creates from it fail until the new pull is ready. A failed pull keeps its name, so pull again under the name to retry. A private registry authenticates with the [registry credential](/docs/registries) stored for the image host. ```python from daytona import Daytona daytona = Daytona() image = daytona.v2.images.pull("node:22-slim", name="my-awesome-image") ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ------------------------------------- | | `name` | str | Sets the image name. The name is 1 to 128 characters of letters, digits, `.`, `_`, and `-`. It must not start with `.`, `sbx_`, `vol_`, `fn_`, `snap-`, `build-`, `refs`, `by-digest`, `prj-`, or `glb-`. It must not have the format of an image ID, `img_...`. Without a name, the image is anonymous. An anonymous pull reuses an existing pending, building, or ready anonymous image of the same reference in the same region. Images and snapshots share one name space per project. A name that belongs to a captured image, a Windows image, or a snapshot returns `409`. | - The request requires the `write:images` permission. - `images.pull` waits until the image is ready unless you pass `wait=False`. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const image = await daytona.v2.images.pull('node:22-slim', { name: 'my-awesome-image' }) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ------------------------------------- | | `name` | string | Sets the image name. The name is 1 to 128 characters of letters, digits, `.`, `_`, and `-`. It must not start with `.`, `sbx_`, `vol_`, `fn_`, `snap-`, `build-`, `refs`, `by-digest`, `prj-`, or `glb-`. It must not have the format of an image ID, `img_...`. Without a name, the image is anonymous. An anonymous pull reuses an existing pending, building, or ready anonymous image of the same reference in the same region. Images and snapshots share one name space per project. A name that belongs to a captured image, a Windows image, or a snapshot returns `409`. | - The request requires the `write:images` permission. - `images.pull` waits until the image is ready unless you pass `wait: false`. ```bash daytona image create my-awesome-image --ref node:22-slim ``` | **Flag** | **Type** | **Description** | | -------- | -------- | --------------- | | `--ref` | string | Sets the registry reference to pull. This flag is required. | | `--timeout` | string | Sets how long the command waits for the pull to finish. The default is `10m`. | | `--no-wait` | boolean | Returns once the pull is queued, without waiting for it to finish. | - The request requires the `write:images` permission. - The name is 1 to 128 characters of letters, digits, `.`, `_`, and `-`. - The name must not start with `.`, `sbx_`, `vol_`, `fn_`, `snap-`, `build-`, `refs`, `by-digest`, `prj-`, or `glb-`. - The name must not have the format of an image ID, `img_...`. - Images and snapshots share one name space per project, so the name must not belong to a captured image, a Windows image, or a snapshot. ```bash curl 'https://api.daytona.com/images' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "name": "my-awesome-image", "source": { "type": "registry", "ref": "node:22-slim" } }' ``` - The request requires the `write:images` permission. - Wait with `GET /images/{imageId}?wait=ready`, using the `id` from the response, before creating sandboxes. - The name is 1 to 128 characters of letters, digits, `.`, `_`, and `-`. - The name must not start with `.`, `sbx_`, `vol_`, `fn_`, `snap-`, `build-`, `refs`, `by-digest`, `prj-`, or `glb-`. - The name must not have the format of an image ID, `img_...`. - Images and snapshots share one name space per project, so the name must not belong to a captured image, a Windows image, or a snapshot. ## Build images [#build-images] Build an image from a Dockerfile and register it under a name. The build runs in the background: the call returns once it is queued, and the image can be used once it is ready. Builds are deduplicated within a project and region. Submitting the same Dockerfile and build context for the same CPU architecture reuses the earlier build instead of running a new one; if that build already finished, the new image is ready at once. A build without a name that reuses the build of a named image returns that named image, which stays permanent. ```python from daytona import Daytona from daytona.v2 import ImageBuilder daytona = Daytona() image = daytona.v2.images.build( ImageBuilder.from_dockerfile("FROM python:3.12-slim\nRUN pip install requests"), name="my-awesome-image", ) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ------------------------------------- | | `name` | str | Sets the image name. Without a name, the image is anonymous, unless the build reuses the build of a named image; the call then returns that named image. | | `idle_expiry_seconds` | int | Sets how many seconds an anonymous image can go without a sandbox create before deletion. `-1` sets the maximum. The default is the server maximum. It is allowed only without `name`. It is ignored when the build reuses the build of a named image. When the build reuses an anonymous image, that image keeps the longer of the two windows. | - The request requires the `write:images` permission. - `images.build` takes an `ImageBuilder`, not a raw Dockerfile string. - Wait with `images.get(image.id, wait="ready")` before you use the image. ```typescript import { Daytona } from '@daytona/sdk' import { ImageBuilder } from '@daytona/sdk/v2' const daytona = new Daytona() const image = await daytona.v2.images.build( ImageBuilder.fromDockerfile('FROM python:3.12-slim\nRUN pip install requests'), { name: 'my-awesome-image' }, ) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ------------------------------------- | | `name` | string | Sets the image name. Without a name, the image is anonymous, unless the build reuses the build of a named image; the call then returns that named image. | | `idleExpirySeconds` | number | Sets how many seconds an anonymous image can go without a sandbox create before deletion. `-1` sets the maximum. The default is the server maximum. It is allowed only without `name`. It is ignored when the build reuses the build of a named image. When the build reuses an anonymous image, that image keeps the longer of the two windows. | - The request requires the `write:images` permission. - Wait with `images.get(image.id, { wait: 'ready' })` before you use the image. ```bash # The CLI uploads a build context; write the Dockerfile into it first mkdir -p build && printf 'FROM python:3.12-slim\nRUN pip install requests\n' > build/Dockerfile # Waits until the image is ready daytona image build my-awesome-image --context build ``` - The request requires the `write:images` permission. - The command waits up to `--timeout`, which defaults to `10m`. ```bash curl 'https://api.daytona.com/images' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "name": "my-awesome-image", "dockerfile": "FROM python:3.12-slim\nRUN pip install requests" }' ``` | **Field** | **Type** | **Description** | | ------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `dockerfile` | string | Sets the Dockerfile text. The Dockerfile must not copy local files. To send a build context, use the multipart form in [build context](#build-context). | | `name` | string | Sets the image name. Without a name, the image is anonymous, unless the build reuses the build of a named image; the call then returns that named image. | The response has the following fields: | **Field** | **Description** | | --------- | --------------- | | `id` | Identifies the new image. | | `status` | The status is `pending` for a new build. It is `building` when the request joined a build in progress, and `ready` when the request reused a finished build. | ### Build context [#build-context] Build an image from a Dockerfile and a build context. Daytona uploads the context to the image's home region and runs the build there. A build context is needed when the Dockerfile copies local files. Without one, the Dockerfile is sent as text, as in [build images](#build-images). The SDKs and the CLI upload a context of up to 1 GiB. ```python from daytona import Daytona daytona = Daytona() # The context holds the Dockerfile at its root image = daytona.v2.images.build_from_context("context.tar", name="my-awesome-image") ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `context` | bytes or str | Sets the build context: the bytes of a tar archive, or the path of a `.tar` file. The archive is at most 1 GiB. This parameter is required. | | `dockerfile` | str | Sets the path of the Dockerfile inside the context. The default is `Dockerfile`. | | `name` | str | Sets the image name. Without a name, the image is anonymous, unless the build reuses the build of a named image; the call then returns that named image. | | `idle_expiry_seconds` | int | Sets how many seconds an anonymous image can go without a sandbox create before deletion. The default is the server maximum. It is allowed only without `name`. It is ignored when the build reuses the build of a named image. When the build reuses an anonymous image, that image keeps the longer of the two windows. | - The request requires the `write:images` permission. - `region` and `cpu_arch` work as in [build options](#build-options). - Wait with `images.get(image.id, wait="ready")` before you use the image. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() // The context holds the Dockerfile at its root const image = await daytona.v2.images.buildFromContext('context.tar', { name: 'my-awesome-image' }) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `context` | Uint8Array, Blob, or string | Sets the build context: the bytes of a tar archive, or in Node.js the path of a `.tar` file. The archive is at most 1 GiB. This parameter is required. | | `dockerfile` | string | Sets the path of the Dockerfile inside the context. The default is `Dockerfile`. | | `name` | string | Sets the image name. Without a name, the image is anonymous, unless the build reuses the build of a named image; the call then returns that named image. | | `idleExpirySeconds` | number | Sets how many seconds an anonymous image can go without a sandbox create before deletion. The default is the server maximum. It is allowed only without `name`. It is ignored when the build reuses the build of a named image. When the build reuses an anonymous image, that image keeps the longer of the two windows. | - The request requires the `write:images` permission. - `region` and `cpuArch` work as in [build options](#build-options). - Wait with `images.get(image.id, { wait: 'ready' })` before you use the image. ```bash # The context holds the Dockerfile at its root daytona image build my-awesome-image --context context.tar ``` | **Flag** | **Type** | **Description** | | -------------- | -------- | ------------------------------------------------------------------ | | `--context` | string | Sets the build context, a tar archive or a directory. A directory is uploaded without the files its `.dockerignore` excludes. The uploaded context is at most 1 GiB. This flag is required. | | `--dockerfile` | string | Sets the path of the Dockerfile inside the context. The default is `Dockerfile`. | | `--timeout` | string | Sets how long the command waits for the build to finish. The default is `10m`. | | `--no-wait` | boolean | Returns once the build is queued, without waiting for it to finish. | ```bash curl 'https://api.daytona.com/images' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --form 'name=my-awesome-image' \ --form 'context=@context.tar' ``` | **Field** | **Type** | **Description** | | ------------ | -------- | ------------------------------------------------------------------------------------------------------------ | | `context` | file | Sets the build context as a tar archive, at most 64 MiB. This field is required. | | `name` | string | Sets the image name. Without a name, the image is anonymous, unless the build reuses the build of a named image; the call then returns that named image. | | `dockerfile` | string | Sets the path of the Dockerfile inside the context. The default is `Dockerfile`. | | `region_id` | string | Sets the home region of the build. The default is the resolved default region. | | `cpu_arch` | string | Sets the CPU architecture, `amd64` or `arm64`. The default is the region's architecture. | - The request requires the `write:images` permission. - The multipart form takes a context of at most 64 MiB. Upload a larger context, up to 1 GiB, as in [large build contexts](#large-build-contexts). - Wait with `GET /images/{imageId}?wait=ready` before creating sandboxes. ### Large build contexts [#large-build-contexts] Upload a build context of up to 1 GiB and build an image from it. The upload declares the archive by its size and SHA-256, sends the archive in parts straight to storage, and is then completed. Completing the upload verifies the stored archive: its size and SHA-256 must match the declared ones, it must be a well-formed tar archive, and its Dockerfile must be accepted for a build. An archive that fails verification is deleted. An upload belongs to one region, and only a build in that region can use it. When the project already holds a verified upload of the same archive in the region, Daytona returns that upload and the archive is not sent again. The SDKs and the CLI run these steps when they upload a [build context](#build-context). ```bash # Size in bytes and SHA-256 of the archive (on macOS use: shasum -a 256 context.tar) wc -c < context.tar sha256sum context.tar # Declare the upload: replace 10240 with the size from wc and {sha256} with the hash from sha256sum curl 'https://api.daytona.com/build-contexts' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "bytes": 10240, "sha256": "{sha256}" }' # Split the archive into parts of part_size bytes: part 1 is part-aa, part 2 is part-ab, and so on split -b {partSize} context.tar part- # Upload each part to its url, without an Authorization header curl '{partUrl}' --upload-file part-aa # Verify the uploaded archive curl 'https://api.daytona.com/build-contexts/{uploadId}/complete' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "region_id": "{regionId}" }' # Build from the verified upload curl 'https://api.daytona.com/images' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "name": "my-awesome-image", "context_upload_id": "{uploadId}" }' ``` | **Field** | **Type** | **Description** | | --------- | -------- | --------------- | | `bytes` | integer | Sets the size of the archive in bytes, from `1` to `1073741824` (1 GiB). This field is required. | | `sha256` | string | Sets the SHA-256 of the archive, as 64 lowercase hex characters. This field is required. | | `dockerfile` | string | Sets the path of the Dockerfile inside the context. The default is `Dockerfile`. | | `region_id` | string | In the declare request, sets the region of the upload. The default is the resolved default region. In the complete request, it is the `region_id` the declare request returned. This field is required in the complete request. | | `context_upload_id` | string | Sets the `ready` upload to build. The image must be created in the region of the upload. It cannot be combined with `dockerfile` or `source`. | | `name` | string | Sets the image name. Without a name, the image is anonymous, unless the build reuses the build of a named image; the call then returns that named image. | The declare and complete requests return the upload, with the following fields: | **Field** | **Description** | | --------- | --------------- | | `id` | Identifies the upload. | | `region_id` | The region the upload belongs to. | | `state` | The state is `uploading` until the upload is completed and verified, then `ready`. | | `part_size` | The size of every part but the last, in bytes. It is set while the upload is `uploading`. | | `parts` | The parts to upload, each with its `part_number`, its size in `bytes`, and its `url`. It is empty once the upload is `ready`. | | `urls_expire_at` | The time the part URLs stop accepting uploads. | | `upload_expires_at` | The time an upload that was not completed expires and is deleted. | | `expires_at` | The time a `ready` upload can no longer be used by a build. | - The requests require the `write:images` permission. - When the declare request returns the upload as `ready`, the archive is already stored; skip to the build. - Part `n` holds the archive bytes from `(n - 1) * part_size` on. Each part URL accepts one `PUT` of exactly the part's `bytes`. The URL is a credential; do not log or share it. - Upload every part before `urls_expire_at`, complete the upload before `upload_expires_at`, and build before `expires_at`. - Wait with `GET /images/{imageId}?wait=ready` before creating sandboxes. ### Build options [#build-options] [Build](#build-images) accepts an optional home region and CPU architecture. [Pull](#pull-images) accepts a home region and always uses the region's architecture. The same Dockerfile built for two architectures produces two separate images. ```python from daytona import Daytona from daytona.v2 import ImageBuilder daytona = Daytona() image = daytona.v2.images.build( ImageBuilder.from_dockerfile("FROM python:3.12-slim\nRUN pip install requests"), name="my-awesome-image", region="us-east", cpu_arch="amd64", ) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | -------------------------------------------------------------------------------------------------------------------- | | `region` | str | Sets the home region of the image. Earth and country-wide regions such as `us` return an error. The default is the resolved default region for the project, organization, or platform. | | `cpu_arch` | str | Sets the CPU architecture to build for, `amd64` or `arm64`. A build for an architecture that no runner in the region provides stays `pending`. `images.pull` also accepts `cpu_arch`, but a pull ignores the value and uses the region's architecture. A `from_registry` builder with no steps ignores the value the same way. The default is the region's architecture. | ```typescript import { Daytona } from '@daytona/sdk' import { ImageBuilder } from '@daytona/sdk/v2' const daytona = new Daytona() const image = await daytona.v2.images.build( ImageBuilder.fromDockerfile('FROM python:3.12-slim\nRUN pip install requests'), { name: 'my-awesome-image', region: 'us-east', cpuArch: 'amd64' }, ) ``` | **Parameter** | **Type** | **Description** | | ------------- | --------- | ---------------------------------------------------------------------------------------------------------- | | `region` | string | Sets the home region of the image. Earth and country-wide regions such as `us` return an error. The default is the resolved default region for the project, organization, or platform. | | `cpuArch` | CpuArch | Sets the CPU architecture to build for, `amd64` or `arm64`. A build for an architecture that no runner in the region provides stays `pending`. `images.pull` also accepts `cpuArch`, but a pull ignores the value and uses the region's architecture. A `fromRegistry` builder with no steps ignores the value the same way. The default is the region's architecture. | ```bash # build/ holds the Dockerfile from Build images; cpu_arch is read from the JSON request body on stdin echo '{ "cpu_arch": "amd64" }' | daytona image build my-awesome-image \ --context build \ --region us-east \ --file - ``` | **Flag** | **Type** | **Description** | | ---------- | -------- | ----------------------------------------------------------------------------------------------------------------------- | | `--region` | string | Sets the home region of the image. Earth and country-wide regions such as `us` return an error. The default is the resolved default region for the project, organization, or platform. | | `--file` | string | Reads the JSON request body from a file, or from stdin with `-`. The `cpu_arch` field of the body sets the CPU architecture, `amd64` or `arm64`. A build for an architecture that no runner in the region provides stays `pending`. Without `cpu_arch`, the image is built for the region's architecture. | ```bash curl 'https://api.daytona.com/images' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "name": "my-awesome-image", "dockerfile": "FROM python:3.12-slim\nRUN pip install requests", "region_id": "us-east", "cpu_arch": "amd64" }' ``` | **Field** | **Type** | **Description** | | ---------- | -------- | ---------------------------------------------------------------------------------------------------------- | | `region_id` | string | Sets the home region of the image. Earth and country-wide regions such as `us` return an error. The default is the resolved default region for the project, organization, or platform. | | `cpu_arch` | string | Sets the CPU architecture to build for, `amd64` or `arm64`. A build for an architecture that no runner in the region provides stays `pending`. The default is the region's architecture. | ### Declarative builds [#declarative-builds] Describe an image in code instead of writing a Dockerfile. Start from a registry image, chain the steps the image needs, and build it like any other image. Each step adds one Dockerfile instruction, and the SDK renders the Dockerfile when the build is submitted. Both SDKs render the same Dockerfile for the same steps, so a build submitted from Python and the same build submitted from TypeScript are [deduplicated](#build-images) into one image. A sandbox booted from the image gets its environment variables and runs its entrypoint and command in its working directory, or in the user's home directory when the image sets none or sets the root directory. A sandbox runs as the image's user, or as root when the image sets none, sets root, or sets a numeric user ID. A user set when the sandbox is created replaces the image's user. ```python from daytona import Daytona from daytona.v2 import ImageBuilder daytona = Daytona() builder = ( ImageBuilder.from_registry("python:3.12-slim") .pip_install("requests", "numpy>=1.26") .run_commands("mkdir -p /app/data") .env({"LOG_LEVEL": "debug"}) .workdir("/app") ) image = daytona.v2.images.build(builder, name="my-awesome-image") ``` | **Method** | **Type** | **Description** | | --------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `from_registry` | str | Sets the registry image reference to start from, rendered as `FROM`. A builder from `from_registry` with no steps is a registry import, not a build. `images.build` pulls that image as [pull images](#pull-images) does. `from_dockerfile` starts from Dockerfile text instead, and the steps are appended after that text. This method is required unless the builder starts from `from_dockerfile`. | | `pip_install` | \*str | Adds Python packages as one `RUN python -m pip install` step. Each package is shell-quoted, so a version specifier such as `numpy>=1.26` stays one argument. | | `run_commands` | \*str | Adds shell commands as one `RUN` step per command. | | `env` | str, str or Mapping\[str, str\] | Sets one variable as a name and a value, or several as a mapping. Each variable is rendered as one `ENV` step. | | `workdir` | str | Sets the working directory, rendered as `WORKDIR`. | - The request requires the `write:images` permission. - Each method returns a new builder and leaves the original unchanged, so one base builder can start several images. ```typescript import { Daytona } from '@daytona/sdk' import { ImageBuilder } from '@daytona/sdk/v2' const daytona = new Daytona() const builder = ImageBuilder.fromRegistry('python:3.12-slim') .pipInstall('requests', 'numpy>=1.26') .runCommands('useradd --create-home app') .env({ LOG_LEVEL: 'debug' }) .user('app') .workdir('/home/app') .entrypoint('python') .cmd('-m', 'http.server', '8080') const image = await daytona.v2.images.build(builder, { name: 'my-awesome-image' }) ``` | **Method** | **Type** | **Description** | | -------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `fromRegistry` | string | Sets the registry image reference to start from, rendered as `FROM`. A builder from `fromRegistry` with no steps is a registry import, not a build. `images.build` pulls that image as [pull images](#pull-images) does. `fromDockerfile` starts from Dockerfile text instead, and the steps are appended after that text. This method is required unless the builder starts from `fromDockerfile`. | | `pipInstall` | ...string\[\] | Adds Python packages as one `RUN python -m pip install` step. Each package is shell-quoted, so a version specifier such as `numpy>=1.26` stays one argument. | | `runCommands` | ...string\[\] | Adds shell commands as one `RUN` step per command. | | `env` | string, string or Record\ | Sets one variable as a name and a value, or several as a record. Each variable is rendered as one `ENV` step. | | `user` | string | Sets the user for the build steps that follow and for sandboxes from the image, rendered as `USER`. This method is available only in the TypeScript SDK. | | `workdir` | string | Sets the working directory, rendered as `WORKDIR`. | | `entrypoint` | ...string\[\] | Sets the image entrypoint, rendered as `ENTRYPOINT` in exec form. This method is available only in the TypeScript SDK. | | `cmd` | ...string\[\] | Sets the image command, rendered as `CMD` in exec form. With an entrypoint, the `cmd` values are the default arguments of the entrypoint. This method is available only in the TypeScript SDK. | - The request requires the `write:images` permission. - Each method returns a new builder and leaves the original unchanged, so one base builder can start several images. ### Preview the Dockerfile [#preview-the-dockerfile] Render the Dockerfile a builder describes, without submitting a build. Use it to review the generated instructions, or to send the build yourself through the API as the `dockerfile` field of `POST /images`. ```python from daytona.v2 import ImageBuilder builder = ImageBuilder.from_registry("python:3.12-slim").pip_install("requests") print(builder.to_dockerfile()) # FROM python:3.12-slim # RUN python -m pip install requests ``` ```typescript import { ImageBuilder } from '@daytona/sdk/v2' const builder = ImageBuilder.fromRegistry('python:3.12-slim').pipInstall('requests') console.log(builder.toDockerfile()) // FROM python:3.12-slim // RUN python -m pip install requests ``` ## Capture images [#capture-images] Capture the filesystem of a sandbox as an image, and boot new sandboxes from the image. The capture runs in the background, and the image becomes ready when the capture finishes. Linux and Windows sandboxes are captured while running, and they keep running. A macOS sandbox is captured when it is persistent and stopped, as a [filesystem snapshot](/docs/macos-sandboxes#filesystem-snapshot). A GPU sandbox saves its state as a [checkpoint](/docs/snapshots) instead. A named capture is permanent until you delete it. An unnamed capture is anonymous and is deleted once unused for its idle window, the server maximum of 7 days by default; every sandbox create from the image restarts the window. To keep an anonymous capture, [promote](#promote-images) it. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.process.run("pip install requests") image = sandbox.capture_image(name="my-env") # Wait until the capture is ready, then boot new sandboxes from it while image.status not in ("ready", "failed"): image = daytona.v2.images.get(image.id, wait="ready") if image.status == "failed": raise RuntimeError(image.error) clone = daytona.v2.create("my-env") ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------------------------------------------------------------------------------------------------------------------- | | `name` | str | Sets the image name. The default is `None`, which makes the image anonymous. The name must be unique in the project. | | `idle_expiry_seconds` | int | Sets how many seconds an anonymous image can go without a sandbox create before deletion. `-1` sets the maximum. The default is the server maximum. It is allowed only without `name`. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.process.run('pip install requests') let image = await sandbox.captureImage({ name: 'my-env' }) // Wait until the capture is ready, then boot new sandboxes from it while (image.status !== 'ready' && image.status !== 'failed') { image = await daytona.v2.images.get(image.id, { wait: 'ready' }) } if (image.status === 'failed') throw new Error(image.error ?? 'image build failed') const clone = await daytona.v2.create('my-env') ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------------------------------------------------------------------------------------------------------------------- | | `name` | string | Sets the image name. The default is `undefined`, which makes the image anonymous. The name must be unique in the project. | | `idleExpirySeconds` | number | Sets how many seconds an anonymous image can go without a sandbox create before deletion. `-1` sets the maximum. The default is the server maximum. It is allowed only without `name`. | ```bash # Named capture: permanent until deleted daytona image capture {sandboxId} --name my-env # Anonymous capture, deleted after the server maximum idle window without a sandbox create daytona image capture {sandboxId} ``` | **Flag** | **Type** | **Description** | | ---------- | -------- | --------------------------------------------------------------------------------------------------------------------------- | | `--name` | string | Sets the image name, which must be unique in the project. Without a name, the image is anonymous. | | `--timeout` | string | Sets how long the command waits for the image to be ready. The default is `10m`. | | `--no-wait` | boolean | Returns once the capture is queued, without waiting for the image to be ready. | ```bash curl 'https://api.daytona.com/sandboxes/{sandboxId}/images' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "name": "my-env" }' ``` | **Field** | **Type** | **Description** | | --------- | --------- | --------------------------------------------------------------------------------------------------------------------------- | | `name` | string | Sets the image name. Without a name, the image is anonymous. The name must be unique in the project. | | `idle_expiry_seconds` | integer | Sets how many seconds an anonymous image can go without a sandbox create before deletion. `-1` sets the maximum. The default is the server maximum. It is allowed only without `name`. | - The request requires the `write:images` and `write:sandboxes` permissions. - Wait with `GET /images/{imageId}?wait=ready` before using the image. ## Promote images [#promote-images] Promote an anonymous image by giving it a name. A promoted image is permanent: it is no longer deleted after its idle window, and the default image list shows it. Promotion works only on anonymous images; an image that already has a name cannot be renamed. ```python from daytona import Daytona daytona = Daytona() image = daytona.v2.images.promote("{imageId}", "my-env") print(image.name) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ------------------------------------------------------- | | `image_id` | str | Identifies the image by its ID, `img_...`. This parameter is required. | | `name` | str | Sets the image name. This parameter is required. The name must be unique in the project. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const image = await daytona.v2.images.promote('{imageId}', 'my-env') console.log(image.name) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ------------------------------------------------------- | | `imageId` | string | Identifies the image by its ID, `img_...`. This parameter is required. | | `name` | string | Sets the image name. This parameter is required. The name must be unique in the project. | ```bash daytona image promote {imageId} my-env ``` ```bash curl 'https://api.daytona.com/images/{imageId}' \ --request PATCH \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "name": "my-env" }' ``` | **Field** | **Type** | **Description** | | --------- | -------- | ---------------------------------------------------- | | `name` | string | Sets the image name. This field is required. The name must be unique in the project. | ## Convert images [#convert-images] Convert an image to the current image format before its first use. Images registered by the migration from Daytona v0 are stored in an older format until they are converted. Such an image converts on its first Linux sandbox create, which waits in the queued state while the conversion runs. GPU sandboxes, Windows sandboxes, and builds that name the image as their source refuse it until it is converted. Converting ahead of time removes the wait from the first create and makes the image usable everywhere. ```bash curl 'https://api.daytona.com/images/{imageId}/convert' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' # Returns on ready or failed, or after timeout_ms; repeat while status is pending or building curl 'https://api.daytona.com/images/{imageId}?wait=ready&timeout_ms=60000' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` The response has the following fields: | **Field** | **Description** | | --------- | --------------- | | `id` | Identifies the image. | | `converted` | The value is `true` when the call started a conversion or joined one in progress. The value is `false` when the image is already in the current format and nothing was queued. | | `from_format` | The `format_version` of the image before the call. | | `to_format` | The format the conversion targets, the current one. | | `status` | The image status after the call: `pending` or `building` while the image converts, or its own status when it is already in the current format. | - The requests require the `write:images` permission. - The call takes the image ID, `img_...`, or the image name. - An image that needs conversion has `format_outdated` set to `true`. ## Operations [#operations] ### List images [#list-images] List images in the project, newest first. The list returns named images by default. Anonymous images, such as the ones [inline Dockerfile](#inline-dockerfile) builds create, appear only when you ask for them. ```python from daytona import Daytona daytona = Daytona() page = daytona.v2.images.list() for image in page.items: print(image.name, image.status) ``` | **Parameter** | **Type** | **Description** | | ------------- | ------------------------- | --------------------------------------------------------------------------------- | | `anonymous` | bool | Selects which images to list. `True` returns anonymous images only, and `False` returns named images only. The default is `False`. | | `limit` | int | Sets the page size, from `1` to `200`. The default is `100`. | | `cursor` | str | Resumes the list after the previous page. Pass the `next_cursor` of that page, which is set when more images exist. | | `name` | str | Filters the list by exact image name. | | `sandbox_id` | str | Lists only the captures taken from this sandbox. | | `platform` | str | Selects platform images, which Daytona publishes for every project. `"true"` lists platform images only, `"false"` lists the project's images only, and `"any"` lists both. The default is `"false"`. | - The request requires the `read:images` permission. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const page = await daytona.v2.images.list() for (const image of page.items) { console.log(image.name, image.status) } ``` | **Parameter** | **Type** | **Description** | | ------------- | --------------------------------- | ------------------------------------------------------------------- | | `anonymous` | boolean | Selects which images to list. `true` returns anonymous images only, and `false` returns named images only. The default is `false`. | | `limit` | number | Sets the page size, from `1` to `200`. The default is `100`. | | `cursor` | string | Resumes the list after the previous page. Pass the `nextCursor` of that page, which is set when more images exist. | | `name` | string | Filters the list by exact image name. | | `sandboxId` | string | Lists only the captures taken from this sandbox. | | `platform` | string | Selects platform images, which Daytona publishes for every project. `'true'` lists platform images only, `'false'` lists the project's images only, and `'any'` lists both. The default is `'false'`. | - The request requires the `read:images` permission. ```bash daytona image list ``` | **Flag** | **Type** | **Description** | | ------------- | -------- | ------------------------------------------------------- | | `--anonymous` | string | Selects which images to list. `true` lists anonymous images only, and `false` lists named images only. The default is `false`. | | `--platform` | string | Selects platform images, which Daytona publishes for every project. `true` lists platform images only, `false` lists the project's images only, and `any` lists both. The default is `false`. | | `--limit` | int | Sets the page size, from `1` to `200`. The default is `100`. | | `--cursor` | string | Resumes the list after the previous page. Pass the `next_cursor` of that page. | | `--all` | bool | Follows every page until the list is exhausted. | | `--name` | string | Filters the list by exact image name. | | `--sandbox` | string | Lists only the captures taken from this sandbox. | | `--region` | string | Lists only the images available in this region or a region inside it. A page can then hold fewer than `--limit` images. By default, the list covers every region. | - The request requires the `read:images` permission. ```bash curl 'https://api.daytona.com/images' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Type** | **Description** | | ------------ | -------- | ------------------------------------------------------------------------------- | | `limit` | integer | Sets the page size, clamped to `1` through `200`. The default is `100`. | | `cursor` | string | Resumes the list after the previous page. Pass the `next_cursor` of that page. | | `name` | string | Filters the list by exact image name. | | `anonymous` | boolean | Selects which images to list. `false` lists named images, and `true` lists anonymous images. The default is `false`. | | `sandbox_id` | string | Lists only the captures taken from this sandbox. | | `platform` | string | Selects platform images, which Daytona publishes for every project. `true` lists platform images only, `false` lists the project's images only, and `any` lists both. The default is `false`. | - The request requires the `read:images` permission. Each image has the following fields. The SDKs return `region_id` as `region`, `region_ids` as `available_in` (`availableIn` in TypeScript), and `class` as `sandbox_class` (`sandboxClass` in TypeScript). | **Field** | **Description** | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | Image ID (`img_...`), used by the get, promote, convert, build-logs-url, and delete endpoints; returned as `id` when the image is created | | `name` | The name sandboxes reference through `image` | | `status` | `pending`, `building`, `ready`, or `failed` | | `error` | Build failure reason; `null` unless `status` is `failed` | | `source` | The build source as submitted: a registry reference, a named image, or a Dockerfile context; `null` for captured images | | `origin` | How the image was created, such as `registry`, `dockerfile`, or `capture` | | `class` | Sandbox class the image supports, such as `linux` or `macos` | | `anonymous` | `true` when the image has no name | | `retention` | `permanent` for a named image, `idle` for an anonymous image | | `expires_at` | When an anonymous image is deleted unless a sandbox create restarts its idle window; `null` for a permanent image | | `region_id` | Home region the image was built, pulled, or captured in | | `region_ids` | Regions that hold a copy of the image right now; the home region is listed only while its own copy is there. Empty until the build finishes and after every copy is evicted | | `platform` | `true` for a platform image that Daytona publishes for every project | | `default` | `true` for the image that a sandbox create without an image boots | | `deprecated` | `true` for a deprecated platform image, which is not listed and cannot be used by name; its ID still resolves | | `bytes` | Size of the converted image in bytes; `null` until the build completes | | `layer_count` | Number of layers in the converted image | | `disk_mib` | Disk size in MiB of a macOS sandbox created from the image without `disk_mib`, and the smallest disk such a create accepts; set for images captured from a macOS sandbox, `null` otherwise | | `format_version` | Storage format of the image: `1` for images registered by the migration from Daytona v0, `2` for the current format | | `format_outdated` | `true` when `format_version` is below the current format; [convert](#convert-images) the image before use outside Linux sandboxes | | `imported_from` | Source platform and resource ID of an image imported by the [migration from Daytona v0](/docs/migrate-from-v0), as `{"platform": "v0", "id": "..."}`; absent for an image created on Daytona v2 | | `created_at` | Creation timestamp | | `updated_at` | Timestamp of the last change | | `cursor` | Token to resume the list immediately after this row; returned by list only | #### Iterate over all images [#iterate-over-all-images] Iterate over every image in the project without handling page cursors. The iterator requests the next page when the current page is exhausted. ```python from daytona import Daytona daytona = Daytona() for image in daytona.v2.images.iter(): print(image.name, image.status) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() for await (const image of daytona.v2.images.iter()) { console.log(image.name, image.status) } ``` ```bash # Follow every page until the server returns no cursor daytona image list --all ``` ```bash curl 'https://api.daytona.com/images?cursor={next_cursor}' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ### Get an image [#get-an-image] Get an image by its ID or name. The response carries the image fields described under [list images](#list-images), including its status and failure reason. ```python from daytona import Daytona daytona = Daytona() image = daytona.v2.images.get("{imageId}") print(image.name, image.status) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const image = await daytona.v2.images.get('{imageId}') console.log(image.name, image.status) ``` ```bash daytona image get {imageId} ``` ```bash curl 'https://api.daytona.com/images/{imageId}' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ### Delete images [#delete-images] Delete an image. Deletion fails while a sandbox created from the image is not deleted. A captured image also cannot be deleted while another capture, sandbox, or snapshot still uses its files. ```python from daytona import Daytona daytona = Daytona() daytona.v2.images.delete("{imageId}") ``` - The request requires the `delete:images` permission. - `images.delete` takes the image ID, `img_...`, or the image name. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() await daytona.v2.images.delete('{imageId}') ``` - The request requires the `delete:images` permission. - `images.delete` takes the image ID, `img_...`, or the image name. ```bash daytona image delete {imageId} ``` - The request requires the `delete:images` permission. - The command takes the image ID or the name. - The command asks for confirmation, and a non-interactive shell requires `--yes`. ```bash curl 'https://api.daytona.com/images/{imageId}' \ --request DELETE \ --header 'Authorization: Bearer YOUR_API_KEY' ``` - The request requires the `delete:images` permission. - The call takes the image ID or the name. ### Build logs [#build-logs] Read the output of an image build. Following the log streams output while the build runs and ends when it finishes. Stopping the reader does not stop the build. ```python from daytona import Daytona from daytona.v2 import ImageBuilder daytona = Daytona() image = daytona.v2.images.build( ImageBuilder.from_dockerfile("FROM python:3.12-slim\nRUN pip install requests"), name="my-awesome-image", ) daytona.v2.images.follow_build_logs(image.id, on_logs=lambda chunk: print(chunk, end="")) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ------------------------------------------------------- | | `on_logs` | Callable | Receives the build output as text. Without `on_event`, it also receives one line per state change and per `gap`, `unavailable`, `source_changed`, or `error` event, prefixed with the event type, such as `[build logs: state]`. This parameter is required. | | `on_event` | Callable | Receives every stream event, including state changes. With `on_event`, these events no longer reach `on_logs`. | | `timeout` | float | Bounds the follow in seconds. When the timeout expires, the call raises `BuildLogInterruptedError` instead of returning. The `cursor` of the error resumes the follow. By default, there is no deadline. | The response has the following fields: | **Field** | **Description** | | --------- | --------------- | | `status` | The image status when the follow ended. | | `reason` | The reason is `terminal`, `deadline`, `not_applicable`, `resource_deleted`, or `unknown`. | | `output_complete` | The value is `true` when all build output was delivered. | | `cursor` | Resumes the follow after the last output. Pass this value in a later call. | ```typescript import { Daytona } from '@daytona/sdk' import { ImageBuilder } from '@daytona/sdk/v2' const daytona = new Daytona() const image = await daytona.v2.images.build( ImageBuilder.fromDockerfile('FROM python:3.12-slim\nRUN pip install requests'), { name: 'my-awesome-image' }, ) await daytona.v2.images.followBuildLogs(image.id, { onLogs: (chunk) => { process.stdout.write(chunk) } }) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ---------------------------------------------------------------------------------------------- | | `onLogs` | function | Receives the build output as text. Without `onEvent`, it also receives one line per state change and per `gap`, `unavailable`, `source_changed`, or `error` event, prefixed with the event type, such as `[build logs: state]`. This parameter is required. | | `onEvent` | function | Receives every stream event, including state changes. With `onEvent`, these events no longer reach `onLogs`. | | `timeoutSeconds` | number | Bounds the follow in seconds. When the timeout expires, the call throws `BuildLogInterruptedError` instead of returning. The `cursor` of the error resumes the follow. By default, there is no deadline. | The response has the following fields: | **Field** | **Description** | | --------- | --------------- | | `status` | The image status when the follow ended. | | `reason` | The reason is `terminal`, `deadline`, `not_applicable`, `resource_deleted`, or `unknown`. | | `outputComplete` | The value is `true` when all build output was delivered. | | `cursor` | Resumes the follow after the last output. Pass this value in a later call. | ```bash # Stream output until the build finishes daytona image logs {imageId} --follow ``` | **Flag** | **Type** | **Description** | | ----------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- | | `--follow` | bool | Streams output until the build finishes. The command stops after `--timeout` and exits with an error, but the build keeps running. Without `--follow`, the command prints the log of a finished build, or the build state while the build runs. | | `--timeout` | string | Sets how long `--follow` runs before it stops. The default is `10m`. | ```bash BUILD_LOGS_URL=$(curl 'https://api.daytona.com/images/{imageId}/build-logs-url' \ --header 'Authorization: Bearer YOUR_API_KEY' | jq -r '.url') # Stream output until the build finishes curl --no-buffer "$BUILD_LOGS_URL?follow=true&format=sse" \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Accept: text/event-stream' ``` The stream has the following events: | **Event** | **Description** | | --------- | --------------- | | `state` | Carries the build `status`, the failure `error`, and `deadline_at`, the time by which the build must finish. | | `log` | Carries build output in `data_base64` as base64-encoded bytes, with the `start_offset` and `next_offset` of the bytes. | | `gap` | Reports output that is no longer stored, with the `requested_offset`, the `first_available_offset`, the `dropped_bytes`, and the `reason`. | | `unavailable` | Reports that the build output cannot be read right now, with the `reason`. The stream keeps retrying the source every 2 seconds. When the build has ended and the output stays unreadable, the stream ends with `done`, where `output_complete` is `false`. | | `source_changed` | Ends the stream when the log source changed, with the `reason`. | | `error` | Ends the stream with the `error` detail, its `code`, and whether the request is `retryable`. | | `done` | Ends the stream with the final `status`, the `reason`, `output_complete`, and the `cursor` that resumes the stream. | The stream sends the comment line `: heartbeat` every 10 seconds to keep the connection open. It is not an event. ### Build status [#build-status] Check an image build's progress. An image moves from pending to building and ends as ready or failed, with a failure reason when it fails. ```python from daytona import Daytona daytona = Daytona() image = daytona.v2.images.get("{imageId}") print(image.id, image.status, image.error) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const image = await daytona.v2.images.get('{imageId}') console.log(image.id, image.status, image.error) ``` ```bash # Returns on ready or failed, or after timeout_ms curl 'https://api.daytona.com/images/{imageId}?wait=ready&timeout_ms=60000' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` The response has the following fields: | **Field** | **Description** | | --------- | --------------- | | `status` | The image status is `pending`, `building`, `ready`, or `failed`. | | `error` | Carries the failure detail. | ## Permissions [#permissions] Images are scoped to a project. Access is governed by three permissions, which you can grant to an [organization role](/docs/organizations#roles) or an [API key](/docs/api-keys). Creating a sandbox from an image requires the permission to create sandboxes, and capturing an image of a sandbox requires the `write:sandboxes` permission as well as `write:images`. The [audit logs](/docs/audit-logs) record image builds, captures, promotions, conversions, and deletes. | **Permission** | **Grants** | | ------------------- | ------------------------------------------------- | | **`read:images`** | List and get images, and read their build logs | | **`write:images`** | Build, pull, capture, promote, and convert images | | **`delete:images`** | Delete images | # Registries (/docs/registries) Registries hold the credentials Daytona uses to pull images from private container registries. A registry credential is a username and a password or token for one registry host. A project holds at most one credential per host, matched by the host in the image reference. Stored credentials apply to registry [images](/docs/images). Daytona uses the credential to import a registry image and to pull it again for a Linux or GPU sandbox. Pulls from a host without a credential, and the base image of a Dockerfile build, are anonymous and work only for public images. Windows and macOS sandboxes ignore stored credentials. Registry credentials work with [GitHub Container Registry](#github-container-registry), [Docker Hub](#docker-hub), [Google Artifact Registry](#google-artifact-registry), [Amazon ECR](#amazon-ecr), and any registry that accepts HTTP Basic authentication. Daytona encrypts the password or token at rest and never returns it. The provider kinds check the host when you store the credential, so a credential stored for the wrong host fails at that point instead of at pull time. 1. You store a credential for a registry host: the host, a kind, a username, and a password or token. The GitHub, Google, and Amazon ECR kinds validate the host, so a misconfigured credential fails at write time rather than silently at pull time. 2. Daytona takes the host from the registry reference of an image import. A sandbox create takes a registered image name or ID; when the image was imported from a registry and is not present in the target region, Daytona pulls it again from that image's registry reference. 3. If a credential is stored for that host, the pull authenticates with it; otherwise the pull is anonymous. No opt-in is needed on individual requests. 4. To rotate a credential, store the same host again with the new password. The update replaces the credential in place and takes effect on the next pull. | **Concept** | **Description** | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | **Registry credential** | A username and a password or token, stored for one registry host in a project. | | **Host** | The registry host, with an optional port. A Docker Hub credential also applies to image references without a host. | | **Kind** | The authentication type. Basic authentication works with any registry that accepts it. The GitHub, Google, and Amazon ECR kinds also check the host. | | **Anonymous pull** | A pull from a host with no stored credential. It works only for public images. | | **Rotation** | Storing a credential for a host that already has one replaces it. The next pull uses the new credential. | ## Use cases [#use-cases] Registry credentials are used when the images that sandboxes boot from are not public. - **Private application images.** Images that a CI pipeline builds and pushes to a private registry are imported and boot in sandboxes. The import authenticates with the credential stored for the host in the image reference. No opt-in is needed on individual requests. - **Images in a cloud provider's registry.** Images stored in Amazon ECR or Google Artifact Registry are pulled with that provider's access keys, such as an AWS access key or a service account JSON key. For Amazon ECR, Daytona checks the keys with AWS STS when you store them. It refuses keys that AWS rejects or that belong to another account. - **Self-hosted registries.** Any registry that accepts HTTP Basic authentication works with a `basic` credential for its host. The host is a bare `host[:port]`, with no scheme or path. Without a stored credential, pulls from the host are anonymous and work only for public images. - **Credential rotation.** A leaked or expired token is replaced in place, and the next pull uses the new one with no change to images or sandboxes. To rotate, store a credential for the same host again with the new password or token. The API replaces the whole credential on each request, so an omitted kind resets to `basic`. - **Team access without shared tokens.** A credential is stored once per host for the whole project, and its token is encrypted and never returned. Team members import private images without holding the token. Reads return only the host, kind, username, description, creation time, and last update time. ## Container registries [#container-registries] Store a credential for a registry host. Storing a credential for a host that already has one replaces the stored credential. Daytona encrypts the password at rest, and reads return only the host, kind, username, description, creation time, and last update time. 1. Go to [Daytona Registries ↗](https://app.daytona.com/dashboard/registries) 2. Click 3. Enter the following details: - : the registry host, as `host[:port]` - : the registry kind: **Basic (username + password)**, **GitHub Container Registry**, **Google Container Registry**, or **Amazon ECR** - : the registry username - : the registry password or token - : an optional description 4. Click ```bash printf %s 'YOUR_REGISTRY_PASSWORD' | daytona registry set YOUR_REGISTRY_HOST \ --username YOUR_REGISTRY_USERNAME \ --password-stdin ``` | **Flag** | **Type** | **Description** | | --- | --- | --- | | `--username` | string | Sets the registry username. This flag is required. | | `--password-stdin` | bool | Reads the password or token from standard input. Pass exactly one of `--password-stdin` and `--password`. | | `--password` | string | Sets the password or token on the command line. The value is visible in the process list and shell history. Prefer `--password-stdin`. | | `--kind` | string | Sets the registry kind: `basic`, `ghcr`, `gcr`, `ecr`, or `ecr_role`. `ecr_role` takes the IAM role ARN as `--username` and no password. The default is `basic`, or the stored kind if the host already has a credential. | | `--description` | string | Sets the description. An empty string clears the stored description. The default is the stored description if the host already has a credential. | ```bash curl 'https://api.daytona.com/registries/YOUR_REGISTRY_HOST' \ --request PUT \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "username": "YOUR_REGISTRY_USERNAME", "password": "YOUR_REGISTRY_PASSWORD" }' ``` | **Field** | **Type** | **Description** | | --- | --- | --- | | `username` | string | Sets the registry username. This field is required. | | `password` | string | Sets the password or token, at most 64 KiB. This field is required for every kind except `ecr_role`, which ignores it. | | `kind` | string | Sets the registry kind: `basic`, `ghcr`, `gcr`, `ecr`, or `ecr_role`. For `ecr_role`, `username` is the IAM role ARN that Daytona assumes, with the organization ID as the external ID. If omitted, the kind resets to `basic`. | | `description` | string | Sets a free-form note. If omitted, the stored description is cleared. | A registry credential has the following fields: | **Field** | **Description** | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `host` | Bare `host[:port]`, no scheme or path. One credential per host. | | `kind` | `basic` (default), `ghcr`, `gcr`, `ecr`, or `ecr_role`. `ghcr` requires `ghcr.io`; `gcr` requires a `gcr.io`, `*.gcr.io`, or `*-docker.pkg.dev` host; `ecr` requires an `*.dkr.ecr..amazonaws.com` host; `ecr_role` requires an `*.dkr.ecr..amazonaws.com` or `public.ecr.aws` host. | | `username` | Registry username. | | `password` | Password or token, at most 64 KiB. Write-only: encrypted at rest and never returned. | | `description` | Optional free-form note. | ### Docker Hub [#docker-hub] Store a credential for Docker Hub. Daytona stores Docker Hub credentials under the host index.docker.io. The hosts docker.io, registry-1.docker.io, and registry.hub.docker.com refer to the same credential, and image references without a registry host, such as python:3.12, use it. ```bash printf %s 'YOUR_DOCKERHUB_TOKEN' | daytona registry set docker.io \ --username YOUR_DOCKERHUB_USERNAME \ --password-stdin ``` ```bash curl 'https://api.daytona.com/registries/docker.io' \ --request PUT \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "username": "YOUR_DOCKERHUB_USERNAME", "password": "YOUR_DOCKERHUB_TOKEN" }' ``` ### Google Artifact Registry [#google-artifact-registry] ```bash # The password is the service account JSON key daytona registry set us-docker.pkg.dev \ --kind gcr \ --username _json_key \ --password-stdin < service-account-key.json ``` ```bash curl 'https://api.daytona.com/registries/us-docker.pkg.dev' \ --request PUT \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "kind": "gcr", "username": "_json_key", "password": "YOUR_SERVICE_ACCOUNT_JSON_KEY" }' ``` ### GitHub Container Registry [#github-container-registry] ```bash printf %s 'YOUR_GITHUB_PAT' | daytona registry set ghcr.io \ --kind ghcr \ --username YOUR_GITHUB_USERNAME \ --password-stdin ``` ```bash curl 'https://api.daytona.com/registries/ghcr.io' \ --request PUT \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "kind": "ghcr", "username": "YOUR_GITHUB_USERNAME", "password": "YOUR_GITHUB_PAT" }' ``` ### Amazon ECR [#amazon-ecr] Store AWS access keys for an Amazon ECR registry. Daytona checks the keys with AWS STS on store and refuses keys that AWS rejects or that belong to an account other than the host's. A failed check leaves any existing credential for the host unchanged. ```bash printf %s 'YOUR_AWS_SECRET_ACCESS_KEY' | daytona registry set AWS_ACCOUNT_ID.dkr.ecr.AWS_REGION.amazonaws.com \ --kind ecr \ --username YOUR_AWS_ACCESS_KEY_ID \ --password-stdin ``` ```bash curl 'https://api.daytona.com/registries/AWS_ACCOUNT_ID.dkr.ecr.AWS_REGION.amazonaws.com' \ --request PUT \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "kind": "ecr", "username": "YOUR_AWS_ACCESS_KEY_ID", "password": "YOUR_AWS_SECRET_ACCESS_KEY" }' ``` ## Use private registry images [#use-private-registry-images] Import a private image into the project, then create sandboxes from it by name. The import authenticates with the credential stored for the host in the image reference. A Linux or GPU sandbox create takes a registered [image](/docs/images) name or ID, which must be ready first. ```python from daytona import Daytona daytona = Daytona() # The pull authenticates with the credential stored for ghcr.io image = daytona.v2.images.pull("ghcr.io/acme/private-runtime:latest", name="private-runtime") # The pull returns once it is queued; each wait returns after 30 seconds at most while image.status not in ("ready", "failed"): image = daytona.v2.images.get(image.id, wait="ready") if image.status == "failed": raise RuntimeError(image.error) sandbox = daytona.v2.create("private-runtime") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() // The pull authenticates with the credential stored for ghcr.io let image = await daytona.v2.images.pull('ghcr.io/acme/private-runtime:latest', { name: 'private-runtime' }) // The pull returns once it is queued; each wait returns after 30 seconds at most while (image.status !== 'ready' && image.status !== 'failed') { image = await daytona.v2.images.get(image.id, { wait: 'ready' }) } if (image.status === 'failed') throw new Error(image.error ?? 'image build failed') const sandbox = await daytona.v2.create('private-runtime') ``` ```bash # The import authenticates with the credential stored for ghcr.io daytona image create private-runtime --ref ghcr.io/acme/private-runtime:latest daytona sandbox create --image private-runtime ``` - The requests require the `write:images` and `write:sandboxes` permissions. - `daytona image create` waits until the image is ready, for up to `--timeout`. - The default `--timeout` is `10m`. ```bash # The import authenticates with the credential stored for ghcr.io curl 'https://api.daytona.com/images' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "name": "private-runtime", "source": { "type": "registry", "ref": "ghcr.io/acme/private-runtime:latest" } }' # Returns on ready or failed, or after timeout_ms; repeat while status is pending or building curl 'https://api.daytona.com/images/{imageId}?wait=ready&timeout_ms=60000' \ --header 'Authorization: Bearer YOUR_API_KEY' curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "image": "private-runtime" }' ``` - The requests require the `write:images` and `write:sandboxes` permissions. - `POST /images` returns once the pull is queued. - `{imageId}` is the `id` field of the `POST /images` response. - `timeout_ms` defaults to 30 seconds and is at most 60 seconds. ## Operations [#operations] ### List registries [#list-registries] ```bash daytona registry list ``` ```bash curl 'https://api.daytona.com/registries' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ### Get a registry [#get-a-registry] ```bash daytona registry get ghcr.io ``` ```bash curl 'https://api.daytona.com/registries/ghcr.io' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ### Delete a registry [#delete-a-registry] Delete the credential for a registry host. Later pulls from that host are anonymous. ```bash daytona registry delete ghcr.io ``` - The request requires the `delete:registries` permission. - The command asks for confirmation, and a non-interactive shell requires `--yes`. ```bash curl 'https://api.daytona.com/registries/ghcr.io' \ --request DELETE \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Permissions [#permissions] Registry credentials are scoped to a project. Access is governed by three permissions, which you can grant to an [organization role](/docs/organizations#roles) or an [API key](/docs/api-keys). The [audit logs](/docs/audit-logs) record credential stores, updates, and deletes. | **Permission** | **Grants** | | ----------------------- | ------------------------------------- | | **`read:registries`** | List and get registry credentials | | **`write:registries`** | Store and update registry credentials | | **`delete:registries`** | Delete registry credentials | # Volumes (/docs/volumes) A volume is persistent, project-scoped storage that a sandbox mounts as a directory. Data on a volume stays when the sandbox is deleted, and a sandbox created later can attach the same volume and read it. A sandbox attaches up to 8 volumes, and several sandboxes can attach the same volume at once. Daytona has two volume types. A disk volume is mounted as a local ext4 filesystem: the sandbox reads and writes a private copy of the volume at local disk speed, and Daytona commits its changes back to the volume in the background. A bucket volume is an S3 bucket mounted live inside the sandbox: every read and write goes to the bucket. Daytona creates and owns the bucket of a managed bucket volume, and an external bucket volume mounts a bucket you own with credentials stored as [secrets](/docs/secrets). Volumes are attached when a sandbox is created and stay attached until the sandbox is deleted. Linux sandboxes attach disk and bucket volumes, macOS sandboxes attach disk volumes, and Windows and GPU sandboxes do not attach volumes. | **Concept** | **Description** | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | **Disk volume** | A tree of files stored by Daytona and mounted as ext4. Each sandbox writes a private copy that is committed back. | | **Bucket volume** | An S3 bucket, or a key prefix in one, mounted live. Reads and writes go straight to the bucket. | | **Managed bucket** | A bucket volume whose bucket Daytona creates and deletes. | | **External bucket** | A bucket volume on a bucket you own, on AWS S3 or an S3-compatible service, accessed with keys stored as project secrets. | | **Mount** | The attachment of a volume to a sandbox at an absolute path, optionally limited to a subpath and, for bucket volumes, read-only. | | **Commit** | The background upload of a sandbox's changes on a disk volume. Other sandboxes see committed changes the next time they attach or pull. | | **Name** | Unique in the project. 1 to 128 ASCII letters, digits, periods, underscores, and hyphens, not starting with a period or a reserved prefix. | ## Use cases [#use-cases] Volumes are used when data must outlive a sandbox or be shared between sandboxes. - **Results that outlive the sandbox.** A sandbox writes its output to a volume, and the data stays after the sandbox is deleted. Deleting the sandbox runs a final commit of its disk volume changes. A sandbox created later attaches the same volume and reads the results once that commit is published, which can be seconds to minutes after the first sandbox reports deleted. Check that the results are present before the next sandbox depends on them. - **Shared datasets and model weights.** A dataset or a set of weights is written to a volume once, and every sandbox that needs it attaches the volume instead of downloading it again. Several sandboxes can attach the same volume at once. On a bucket volume, every sandbox that has the bucket mounted sees new changes. On a disk volume, give each writing sandbox its own files: a file that several sandboxes write keeps one sandbox's version. - **Caches across runs.** Package, build, and model caches on a volume carry over to the next sandbox, so it skips the downloads and builds the previous one already did. A new sandbox starts from the latest published commit of a disk volume. The sandbox reads and writes its cache in a private copy of the volume: the first read of a file fetches it from storage, and later reads and all writes run at local disk speed. - **Data in your own bucket.** An external bucket volume mounts a bucket you already use. Sandbox code reads and writes it as files, and the access keys never enter the sandbox. Deleting the volume leaves the bucket and its objects in place. - **Read-only inputs.** A bucket volume mounted read-only gives untrusted code access to input data that it cannot change or delete. Daytona enforces the read-only mount outside the sandbox, so even a process running as root cannot write to it. Only bucket volumes support read-only mounts. ## Create volumes [#create-volumes] Create a disk volume. A volume is ready as soon as the create returns, and its data starts empty. The size sets the capacity of the private copy each sandbox writes to, not a limit on the data the volume stores in total. ```python from daytona import Daytona daytona = Daytona() volume = daytona.v2.volumes.create(name="training-data", size_gib=200) print(volume.id, volume.status) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ---------------------------------------------------------------------------------------------------------------- | | `name` | str | Sets the volume name. This parameter is required. The name must be unique in the project. | | `size_gib` | int | Sets the capacity of each sandbox's copy of a disk volume, from `1` to `1000` GiB. The default is `100`. | | `type` | str | Sets the volume type: `disk` or `bucket`. The default is `disk`. | - The request requires the `write:volumes` permission. - Disk volumes must be enabled for the organization. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const volume = await daytona.v2.volumes.create({ name: 'training-data', sizeGib: 200 }) console.log(volume.id, volume.status) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ---------------------------------------------------------------------------------------------------------------- | | `name` | string | Sets the volume name. This parameter is required. The name must be unique in the project. | | `sizeGib` | number | Sets the capacity of each sandbox's copy of a disk volume, from `1` to `1000` GiB. The default is `100`. | | `type` | string | Sets the volume type: `disk` or `bucket`. The default is `disk`. | - The request requires the `write:volumes` permission. - Disk volumes must be enabled for the organization. ```bash daytona volume create training-data --size-gib 200 ``` | **Flag** | **Type** | **Description** | | ------------- | -------- | --------------------------------------------------------------------------------------------------- | | `--size-gib` | int32 | Sets the capacity of each sandbox's copy of a disk volume, in GiB, from 1 to 1000. Defaults to 100. | | `--type` | string | Sets the volume type: `disk` or `bucket`. Defaults to `disk`. | - The request requires the `write:volumes` permission. - Disk volumes must be enabled for the organization. ```bash curl 'https://api.daytona.com/volumes' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "name": "training-data", "type": "disk", "size_gib": 200 }' ``` | **Field** | **Type** | **Description** | | ---------- | --------- | ------------------------------------------------------------------------------------------------------------------------------- | | `name` | string | Sets the volume name, unique in the project. This field is required. A name that is taken returns `409 Conflict`. A name that starts with `sbx_`, `vol_`, `fn_`, `snap-`, `build-`, `refs`, `by-digest`, `prj-`, or `glb-` returns `400 Bad Request`. | | `type` | string | Sets the volume type: `disk` or `bucket`. Defaults to `disk`. | | `size_gib` | integer | Sets the capacity of each sandbox's copy of a disk volume, from 1 to 1000 GiB. Defaults to 100. A bucket volume returns `400 Bad Request`. | - The request requires the `write:volumes` permission. - Disk volumes must be enabled for the organization. ### Managed bucket volumes [#managed-bucket-volumes] Create a bucket volume without bucket settings to have Daytona create a new, empty bucket for it. Daytona owns the bucket: deleting the volume deletes the bucket and every object in it. Traffic to a bucket volume is billed per uploaded GiB, downloaded GiB, and 1,000 requests; see [billing](/docs/billing). ```python from daytona import Daytona daytona = Daytona() volume = daytona.v2.volumes.create(name="shared-outputs", type="bucket") print(volume.id, volume.status) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const volume = await daytona.v2.volumes.create({ name: 'shared-outputs', type: 'bucket' }) console.log(volume.id, volume.status) ``` ```bash daytona volume create shared-outputs --type bucket ``` ```bash curl 'https://api.daytona.com/volumes' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "name": "shared-outputs", "type": "bucket" }' ``` ### External bucket volumes [#external-bucket-volumes] Create a bucket volume on a bucket you own by passing its settings. Daytona stores only the settings: it does not create the bucket, and deleting the volume leaves the bucket and its objects in place. The bucket can be on AWS S3 or on an S3-compatible service. The SDKs do not create external bucket volumes. The access key and secret key are read from two project [secrets](/docs/secrets). The keys are used outside the sandbox: sandbox code reads and writes the bucket through Daytona and never receives the keys. ```bash daytona volume create corp-data --type bucket --bucket corp-data --bucket-region us-east-1 \ --prefix datasets/ --access-key-secret AWS_KEY --secret-key-secret AWS_SECRET ``` | **Flag** | **Type** | **Description** | | --------------------- | -------- | ------------------------------------------------------------------------------------------------------------------ | | `--bucket` | string | Sets the bucket name. This flag is required for an external bucket volume; without it, Daytona creates a managed bucket. | | `--bucket-region` | string | Sets the S3 region of the bucket, not a Daytona region. Defaults to the default bucket region of the deployment. | | `--endpoint` | string | Sets the endpoint of an S3-compatible service as an `http://` or `https://` URL. Omit it for AWS S3. | | `--prefix` | string | Limits the volume to keys under this prefix. | | `--path-style` | bool | Sends requests with path-style addressing instead of virtual-hosted addressing. | | `--access-key-secret` | string | Sets the name of the project secret that holds the access key. This flag is required. | | `--secret-key-secret` | string | Sets the name of the project secret that holds the secret key. This flag is required. | The rules for each value are the same as for the matching API field. - The request requires the `write:volumes` and `read:secrets` permissions. - Two project secrets must hold the access key and the secret key, each with allowed domains that include the host that requests to the bucket are signed for. ```bash curl 'https://api.daytona.com/volumes' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "name": "corp-data", "type": "bucket", "bucket": { "name": "corp-data", "region": "us-east-1", "prefix": "datasets/", "credentials": { "access_key_secret": "AWS_KEY", "secret_key_secret": "AWS_SECRET" } } }' ``` | **Field** | **Type** | **Description** | | -------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `bucket.name` | string | Sets the bucket name: 3 to 63 characters of `a-z`, `0-9`, `.`, and `-`, starting and ending with a letter or digit. This field is required. | | `bucket.region` | string | Sets the bucket region: 1 to 64 characters of `a-z`, `0-9`, and `-`. Defaults to the default bucket region of the deployment. | | `bucket.endpoint` | string | Sets the endpoint of an S3-compatible service as an `http://` or `https://` URL with a public host. Omit it for AWS S3. | | `bucket.prefix` | string | Limits the volume to keys under this prefix. A trailing `/` is added. The prefix is a relative path with no empty or `..` segments, up to 511 characters. | | `bucket.path_style` | boolean | Sends requests with path-style addressing instead of virtual-hosted addressing. Defaults to `false`. | | `bucket.credentials.access_key_secret` | string | Sets the name of the project secret that holds the access key. This field is required. | | `bucket.credentials.secret_key_secret` | string | Sets the name of the project secret that holds the secret key. This field is required. | The host that each secret's allowed domains must include depends on the endpoint and the addressing style: | **Endpoint** | **Path style** | **Host** | | ------------ | -------------- | --------------------------------------- | | Omitted | `false` | `.s3..amazonaws.com` | | Omitted | `true` | `s3..amazonaws.com` | | Set | `false` | `.` | | Set | `true` | `` | - The request requires the `write:volumes` and `read:secrets` permissions. - Two project secrets must hold the access key and the secret key, each with allowed domains that include the host that requests to the bucket are signed for. ## Attach volumes [#attach-volumes] Attach volumes to a sandbox by listing them when the sandbox is created. Each mount names a volume by name or ID and an absolute path inside the sandbox. A mount can be limited to a subpath: a directory of a disk volume, or a key prefix under a bucket volume's own prefix. A mount never reaches outside the volume. Volumes cannot be attached to or detached from a running sandbox. To change the volumes of a sandbox, create a new sandbox with the volumes it needs. The examples attach two disk volumes, `training-data` and `home-dirs`, and the external bucket volume `corp-data`. Create them first, as described in [Create volumes](#create-volumes) and [External bucket volumes](#external-bucket-volumes). ```python from daytona import Daytona from daytona.v2 import VolumeMount daytona = Daytona() sandbox = daytona.v2.create( volumes=[ VolumeMount(volume_id="training-data", mount_path="/mnt/data"), VolumeMount(volume_id="home-dirs", mount_path="/home/alice", subpath="users/alice"), VolumeMount(volume_id="corp-data", mount_path="/mnt/corp", read_only=True), ], ) ``` | **Parameter** | **Type** | **Description** | | ------------------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------- | | `volumes` | Sequence\[VolumeMount\] | Sets the volumes to attach. At most 8. | | `VolumeMount.volume_id` | str | Sets the volume by name or by `vol_` ID. Each volume can be attached once per sandbox. This parameter is required. | | `VolumeMount.mount_path` | str | Sets the absolute path of the mount, with no `..`. Repeated and trailing slashes are ignored, and each mount needs a different path. `/` and paths at or under `/bin`, `/boot`, `/dev`, `/etc`, `/lib`, `/lib32`, `/lib64`, `/mnt/scratch`, `/overlay`, `/proc`, `/sbin`, `/sys`, and `/usr` are reserved. This parameter is required. | | `VolumeMount.subpath` | str | Mounts only this relative path of the volume, with no empty or `..` segments. | | `VolumeMount.read_only` | bool | Mounts the volume without write access. Supported only for bucket volumes. | - The request requires the `write:sandboxes` permission. - Each volume must exist in the project and have the `ready` status. - A macOS sandbox attaches only disk volumes, read-write, and a macOS sandbox with volumes cannot be persistent. - A sandbox with bucket volumes must be created in a region whose runners mount bucket volumes. - A sandbox with volumes cannot be created from an image that is not yet available in the sandbox's region. - Create the sandbox without volumes, or retry once the image is available. - `create_macos` takes the same `volumes` list to attach disk volumes to a macOS sandbox. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create({ volumes: [ { volumeId: 'training-data', mountPath: '/mnt/data' }, { volumeId: 'home-dirs', mountPath: '/home/alice', subpath: 'users/alice' }, { volumeId: 'corp-data', mountPath: '/mnt/corp', readOnly: true }, ], }) ``` | **Parameter** | **Type** | **Description** | | ----------------------- | -------------- | ---------------------------------------------------------------------------------------------------- | | `volumes` | VolumeMount\[\] | Sets the volumes to attach. At most 8. | | `volumes[].volumeId` | string | Sets the volume by name or by `vol_` ID. Each volume can be attached once per sandbox. This field is required. | | `volumes[].mountPath` | string | Sets the absolute path of the mount, with no `..`. Repeated and trailing slashes are ignored, and each mount needs a different path. `/` and paths at or under `/bin`, `/boot`, `/dev`, `/etc`, `/lib`, `/lib32`, `/lib64`, `/mnt/scratch`, `/overlay`, `/proc`, `/sbin`, `/sys`, and `/usr` are reserved. This field is required. | | `volumes[].subpath` | string | Mounts only this relative path of the volume, with no empty or `..` segments. | | `volumes[].readOnly` | boolean | Mounts the volume without write access. Supported only for bucket volumes. | - The request requires the `write:sandboxes` permission. - Each volume must exist in the project and have the `ready` status. - A macOS sandbox attaches only disk volumes, read-write, and a macOS sandbox with volumes cannot be persistent. - A sandbox with bucket volumes must be created in a region whose runners mount bucket volumes. - A sandbox with volumes cannot be created from an image that is not yet available in the sandbox's region. - Create the sandbox without volumes, or retry once the image is available. - `createMacos` takes the same `volumes` list to attach disk volumes to a macOS sandbox. ```bash daytona sandbox create \ --volume training-data:/mnt/data \ --volume home-dirs:/home/alice:users/alice \ --volume corp-data:/mnt/corp:ro ``` | **Flag** | **Type** | **Description** | | ---------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `--volume` | stringArray | Attaches a volume as `VOLUME:MOUNT_PATH[:SUBPATH][:ro]`. `VOLUME` is a name or `vol_` ID, and a trailing `:ro`, in any case, mounts it read-only. Paths cannot contain `:`, and a third field of `ro` is read as the read-only marker, not as a subpath. Repeat the flag for each mount, at most 8, with each volume once. | - The request requires the `write:sandboxes` permission. - Each volume must exist in the project and have the `ready` status. - A macOS sandbox attaches only disk volumes, read-write, and a macOS sandbox with volumes cannot be persistent. - A sandbox with bucket volumes must be created in a region whose runners mount bucket volumes. - A sandbox with volumes cannot be created from an image that is not yet available in the sandbox's region. - Create the sandbox without volumes, or retry once the image is available. - `daytona sandbox create-macos` takes the same `--volume` flag, without `:ro`, to attach disk volumes to a macOS sandbox. ```bash curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "volumes": [ { "volume": "training-data", "mount_path": "/mnt/data" }, { "volume": "home-dirs", "mount_path": "/home/alice", "subpath": "users/alice" }, { "volume": "corp-data", "mount_path": "/mnt/corp", "read_only": true } ] }' ``` | **Field** | **Type** | **Description** | | ---------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `volumes` | object\[\] | Sets the volumes to attach. At most 8. A longer list returns `400 Bad Request`. | | `volumes[].volume` | string | Sets the volume by name or by `vol_` ID. Each volume can be attached once per sandbox. This field is required. | | `volumes[].mount_path` | string | Sets the absolute path of the mount, with no `..`. Repeated and trailing slashes are ignored, and each mount needs a different path. `/` and paths at or under `/bin`, `/boot`, `/dev`, `/etc`, `/lib`, `/lib32`, `/lib64`, `/mnt/scratch`, `/overlay`, `/proc`, `/sbin`, `/sys`, and `/usr` are reserved. This field is required. | | `volumes[].subpath` | string | Mounts only this relative path of the volume, with no empty or `..` segments. | | `volumes[].read_only` | boolean | Mounts the volume without write access. Supported only for bucket volumes; a disk volume returns `400 Bad Request`. | - The request requires the `write:sandboxes` permission. - Each volume must exist in the project and have the `ready` status. - A macOS sandbox attaches only disk volumes, read-write, and a macOS sandbox with volumes cannot be persistent. - A sandbox with bucket volumes must be created in a region whose runners mount bucket volumes. - A sandbox with volumes cannot be created from an image that is not yet available in the sandbox's region. - Create the sandbox without volumes, or retry once the image is available. - `POST /sandboxes/macos` takes the same `volumes` list to attach disk volumes to a macOS sandbox. ## Disk volume data [#disk-volume-data] A sandbox reads and writes a disk volume as a local ext4 filesystem. When the sandbox starts, Daytona builds a private copy of the volume's latest published commit for that sandbox. The first read of a file fetches it from storage, and later reads and all writes go to the private copy at local disk speed. Daytona commits a sandbox's changes to the volume in the background. A commit runs at most every 5 seconds, and only once the sandbox has stopped writing to the volume for about 1 second. A sandbox that never pauses writing for 1 second does not commit until it is stopped or deleted. Run the sync command in the sandbox before you depend on a recent write being in the next commit. Stopping or deleting the sandbox runs a final commit. The sandbox can report stopped or deleted before that commit is published, which takes from a few seconds to several minutes for large changes. Until then, a sandbox that attaches the volume sees the earlier contents, and the stopped sandbox cannot be started again. Check that the files you need are present before a new sandbox depends on them. A sandbox sees other sandboxes' commits when it attaches the volume, which happens when the sandbox is created or started. Changes another sandbox commits while this sandbox runs are not visible to it until it [pulls the volume](#pull-volume-changes). A commit stores file contents, directories, symbolic links, hard links, permission modes, and modification times. It does not store file owners, named pipes, sockets, device nodes, or extended attributes: files come back owned by root. Holes in sparse files are stored as zeros, so a sparse file takes its full size in each sandbox's copy and in the volume's reported size. When several sandboxes change the same volume, each commit merges its changes into the latest contents of the volume instead of replacing them: | **Change** | **Result** | | ----------------------------------------------- | -------------------------------------------------------------------------- | | A path changed by only one sandbox | That sandbox's version is kept. | | A path changed by two sandboxes | The version with the newer modification time is kept. | | A path modified by one sandbox, deleted by another | The modified version is kept. | | A path that is a file in one sandbox and a directory in another | The directory is kept. | The merge compares file modification times set by the sandboxes. A tool that preserves old modification times, such as an archive extraction, can lose to an older edit made in another sandbox. The version that loses is discarded and cannot be recovered. Each path is merged on its own. Files that must stay consistent with each other, such as a database and its write-ahead log or a Git object store, can end up combined from different sandboxes. Write them from one sandbox only, and have other sandboxes read them or write their own files. A commit is the point where a disk volume write becomes durable. If the host fails before a commit, the changes since the last commit can be lost. ## Pull volume changes [#pull-volume-changes] Pull a disk volume to bring the changes other sandboxes committed into a running sandbox, without restarting it. If the volume has new commits, the pull first commits this sandbox's own changes, merges them as a background commit does, and then applies the merged contents to the sandbox's copy. If the volume has no new commits, the pull returns without committing local changes. A pull can outlast its wait: a pending result means the pull has no result yet, and repeating the request with the same request ID returns its result. Bucket volumes need no pull, because every read goes to the bucket. A pull stages incoming changes in a `.rlp-sync` directory at the root of the mount. Daytona does not commit this directory, and a pull fails when the name is taken by a file or by a directory Daytona did not create. ```python from daytona import Daytona from daytona.v2 import VolumeMount daytona = Daytona() sandbox = daytona.v2.create(volumes=[VolumeMount(volume_id="training-data", mount_path="/mnt/data")]) volume = daytona.v2.volumes.get("training-data") result = sandbox.volumes.pull(volume.id, request_id="refresh-42", timeout=10) while result.status == "pending": result = sandbox.volumes.pull(volume.id, request_id="refresh-42", timeout=10) print(result.status, result.manifest_id) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `volume_id` | str | Sets the `vol_` ID of a volume attached to the sandbox. This is the first positional argument. | | `request_id` | str | Sets the identity of the pull: 1 to 128 ASCII letters, digits, underscores, and hyphens. Reuse it to retry the pull or read its result. This parameter is required. | | `timeout` | float | Sets how many seconds the server waits for the pull, from `0` to `30`. `0` submits the pull, or returns the last result of a known request ID without waiting or retrying it. A wait above `0` retries a pull that is pending, unavailable (`503`), or refused by the rate limit. The default is `30`. | The result has the following fields: | **Field** | **Description** | | ------------- | --------------------------------------------------------------------------------------------------------------- | | `status` | `pulled` when the sandbox received new changes, `unchanged` when the volume had none, `pending` when the pull has no result within the wait, or `unknown` when the SDK does not recognize the status the server returns. | | `raw_status` | The status exactly as the server returns it. Read it when `status` is `unknown`. | | `manifest_id` | The version of the volume that the pull applied or found. `None` while the pull is pending. | - The request requires the `write:sandboxes` permission. - Pull supports disk volumes attached to Linux sandboxes. - A sandbox starts at most one new pull of a volume per second. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create({ volumes: [{ volumeId: 'training-data', mountPath: '/mnt/data' }] }) const volume = await daytona.v2.volumes.get('training-data') let result = await sandbox.volumes.pull(volume.id, { requestId: 'refresh-42', timeoutSeconds: 10 }) while (result.status === 'pending') { result = await sandbox.volumes.pull(volume.id, { requestId: 'refresh-42', timeoutSeconds: 10 }) } console.log(result.status, result.manifestId) ``` | **Parameter** | **Type** | **Description** | | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `volumeId` | string | Sets the `vol_` ID of a volume attached to the sandbox. This is the first positional argument. | | `options.requestId` | string | Sets the identity of the pull: 1 to 128 ASCII letters, digits, underscores, and hyphens. Reuse it to retry the pull or read its result. This field is required. | | `options.timeoutSeconds` | number | Sets how many seconds the server waits for the pull, from `0` to `30`. `0` submits the pull, or returns the last result of a known request ID without waiting or retrying it. A wait above `0` retries a pull that is pending, unavailable (`503`), or refused by the rate limit. The default is `30`. | The result has the following fields: | **Field** | **Description** | | ------------ | --------------------------------------------------------------------------------------------------------------- | | `status` | `pulled` when the sandbox received new changes, `unchanged` when the volume had none, `pending` when the pull has no result within the wait, or `unknown` when the SDK does not recognize the status the server returns. | | `rawStatus` | The status exactly as the server returns it. Read it when `status` is `unknown`. | | `manifestId` | The version of the volume that the pull applied or found. `undefined` while the pull is pending. | - The request requires the `write:sandboxes` permission. - Pull supports disk volumes attached to Linux sandboxes. - A sandbox starts at most one new pull of a volume per second. ```bash curl '{toolboxProxyUrl}/{sandboxId}/volumes/{volumeId}/pull' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "requestId": "refresh-42", "waitMs": 10000 }' ``` | **Field** | **Type** | **Description** | | ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `volumeId` | path | Sets the `vol_` ID of a volume attached to the sandbox. | | `requestId` | string | Sets the identity of the pull: 1 to 128 ASCII letters, digits, underscores, and hyphens. Reuse it to retry the pull or read its result. This field is required. | | `waitMs` | integer | Sets how many milliseconds the server waits for the pull, from `0` to `30000`. `0` submits the pull, or returns the last result of a known request ID without waiting or retrying it. A wait above `0` retries a pull that is pending, unavailable (`503`), or refused by the rate limit. The default is `30000`. | The response has the following fields: | **Field** | **Description** | | ------------ | --------------------------------------------------------------------------------------------------------------- | | `status` | `pulled` when the sandbox received new changes, `unchanged` when the volume had none, or `pending` when the pull has no result within the wait. | | `manifestId` | The version of the volume that the pull applied or found. `null` while the pull is pending. | - The request requires the `write:sandboxes` permission. - `{toolboxProxyUrl}` is the `toolbox_proxy_url` field of the [sandbox](/docs/sandboxes#get-a-sandbox). - Pull supports disk volumes attached to Linux sandboxes. - A sandbox starts at most one new pull of a volume per second. ## Bucket volume data [#bucket-volume-data] A sandbox mounts a bucket volume with [Mountpoint for Amazon S3](https://github.com/awslabs/mountpoint-s3). Each read and write is a request to the bucket, and closing a written file returns only after the object is stored, or with an error if the upload fails. Changes are visible to every sandbox that has the bucket mounted and to any other client of the bucket. Files on a bucket volume are S3 objects, so file system operations that S3 does not support are not available. A file is written sequentially from the start, an existing file can be replaced but not modified in place, and symbolic links, hard links, and permission changes are not supported. Every file operation is a round trip to the bucket, so workloads with many small files run slower than on a disk volume. A read-only mount is enforced outside the sandbox. A process in the sandbox, including one running as root, cannot write to or delete from a read-only mount. ## Operations [#operations] ### List volumes [#list-volumes] List the volumes of the project, newest first. The list returns one page at a time, and each page holds the cursor of the next one. Volumes that are being deleted are listed with the deleting status, and deleted volumes are not listed. ```python from daytona import Daytona daytona = Daytona() cursor = None while True: page = daytona.v2.volumes.list(limit=50, cursor=cursor) for v in page.items: print(v.id, v.name, v.type, v.status) cursor = page.next_cursor # None on the last page if cursor is None: break ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ------------------------------------------------------------------------- | | `limit` | int | Sets the page size, from `1` to `200`. The default is `100`. | | `cursor` | str | Resumes the list after the previous page. Pass the `next_cursor` of that page. Omit it for the first page. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() let cursor: string | undefined do { const page = await daytona.v2.volumes.list({ limit: 50, cursor }) for (const v of page.items) { console.log(v.id, v.name, v.type, v.status) } cursor = page.nextCursor // undefined on the last page } while (cursor) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ------------------------------------------------------------------------- | | `limit` | number | Sets the page size, from `1` to `200`. The default is `100`. | | `cursor` | string | Resumes the list after the previous page. Pass the `nextCursor` of that page. Omit it for the first page. | ```bash daytona volume list --limit 50 daytona volume list --all ``` | **Flag** | **Type** | **Description** | | ---------- | -------- | ------------------------------------------------------------------- | | `--limit` | int | Sets the page size, from 1 to 200. Defaults to 100. | | `--cursor` | string | Starts from the cursor of a previous page. | | `--all` | bool | Reads every page. | ```bash curl 'https://api.daytona.com/volumes?limit=50' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Parameter** | **Type** | **Description** | | ------------- | --------- | ----------------------------------------------------------------------- | | `limit` | integer | Sets the page size, from `1` to `200`. A value outside the range is set to the nearest bound. The default is `100`. | | `cursor` | string | Resumes the list after the previous page. Pass the `next_cursor` of that page. Omit it for the first page. | The response has the following fields: | **Field** | **Description** | | ------------- | --------------------------------------------------------- | | `volumes` | Lists the volumes of the page, newest first. | | `next_cursor` | The cursor of the next page, or `null` on the last page. | #### Iterate over all volumes [#iterate-over-all-volumes] Iterate over every volume of the project without handling page cursors. The iterator fetches the next page when the current one is exhausted and stops when the server returns no cursor. ```python from daytona import Daytona daytona = Daytona() for v in daytona.v2.volumes.iter(): print(v.id, v.name, v.type, v.status) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() for await (const v of daytona.v2.volumes.iter()) { console.log(v.id, v.name, v.type, v.status) } ``` ### Get a volume [#get-a-volume] Get a volume by name or ID. The response holds the volume's type, status, size, and options. For a disk volume, it also reports the size of the volume's files at the last commit and when that size was observed; both are empty until the first commit. Bucket volume options include the bucket settings and the names of the credential secrets, never the keys. ```python from daytona import Daytona daytona = Daytona() volume = daytona.v2.volumes.get("training-data") print(volume.type, volume.status, volume.size_gib, volume.used_bytes, volume.used_bytes_at) # Create the disk volume if it does not exist volume = daytona.v2.volumes.get("scratch-cache", create=True) ``` | **Parameter** | **Type** | **Description** | | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- | | `name_or_id` | str | Sets the volume by name or by `vol_` ID. This is the first positional argument. | | `create` | bool | Creates a disk volume with this name and the default size when none exists. If another caller creates it first, the call fails; repeat it. Ignored with `id_only`. Defaults to `False`. | | `id_only` | bool | Resolves the value only as an ID, without falling back to a name. Defaults to `False`. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const volume = await daytona.v2.volumes.get('training-data') console.log(volume.type, volume.status, volume.sizeGib, volume.usedBytes, volume.usedBytesAt) // Create the disk volume if it does not exist const cache = await daytona.v2.volumes.get('scratch-cache', { create: true }) ``` | **Parameter** | **Type** | **Description** | | ---------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- | | `nameOrId` | string | Sets the volume by name or by `vol_` ID. This is the first positional argument. | | `options.create` | boolean | Creates a disk volume with this name and the default size when none exists. If another caller creates it first, the call fails; repeat it. Ignored with `idOnly`. Defaults to `false`. | | `options.idOnly` | boolean | Resolves the value only as an ID, without falling back to a name. Defaults to `false`. | ```bash daytona volume get training-data ``` ```bash curl 'https://api.daytona.com/volumes/training-data' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Parameter** | **Type** | **Description** | | ------------- | --------- | ------------------------------------------------------------------------------------------------ | | `id_only` | boolean | Resolves the path value only as an ID, without falling back to a name. The default is `false`. | ### Delete volumes [#delete-volumes] Delete a volume by name or ID. Deletion runs in the background: the volume moves to the deleting status and then to deleted. Deleting a disk volume deletes its data, and deleting a managed bucket volume deletes the bucket and every object in it. Deleting an external bucket volume leaves the bucket and its objects in place. ```python from daytona import Daytona daytona = Daytona() daytona.v2.volumes.delete("training-data") ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ---------------------------------------------------------------------------------------- | | `name_or_id` | str | Sets the volume by name or by `vol_` ID. This is the first positional argument. | | `id_only` | bool | Resolves the value only as an ID, without falling back to a name. The default is `False`. | - The request requires the `delete:volumes` permission. - Every sandbox that attached the volume must be deleted, and its final commit must be complete. The volume is released shortly after the sandbox reports deleted. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() await daytona.v2.volumes.delete('training-data') ``` | **Parameter** | **Type** | **Description** | | ---------------- | -------- | ---------------------------------------------------------------------------------------- | | `nameOrId` | string | Sets the volume by name or by `vol_` ID. This is the first positional argument. | | `options.idOnly` | boolean | Resolves the value only as an ID, without falling back to a name. Defaults to `false`. | - The request requires the `delete:volumes` permission. - Every sandbox that attached the volume must be deleted, and its final commit must be complete. The volume is released shortly after the sandbox reports deleted. ```bash daytona volume delete training-data --yes ``` | **Flag** | **Type** | **Description** | | -------- | -------- | --------------------------------------- | | `--yes` | bool | Skips the confirmation prompt. | - The request requires the `delete:volumes` permission. - Every sandbox that attached the volume must be deleted, and its final commit must be complete. The volume is released shortly after the sandbox reports deleted. ```bash curl 'https://api.daytona.com/volumes/training-data' \ --request DELETE \ --header 'Authorization: Bearer YOUR_API_KEY' ``` - The request requires the `delete:volumes` permission. - Every sandbox that attached the volume must be deleted, and its final commit must be complete. The volume is released shortly after the sandbox reports deleted. ## Permissions [#permissions] Volumes are scoped to a project. A volume in one project cannot be attached to a sandbox in another. Access is governed by three permissions, which you can grant to an [organization role](/docs/organizations) or an [API key](/docs/api-keys). Creating an external bucket volume also requires permission to read secrets, because the volume references project secrets. Pulling a volume runs through the toolbox of the sandbox and requires the sandbox write permission. The [audit logs](/docs/audit-logs) record volume creates and deletes. | **Permission** | **Grants** | | -------------------- | ------------------------------------------------- | | **`read:volumes`** | List and get volumes | | **`write:volumes`** | Create volumes | | **`delete:volumes`** | Delete volumes | # Regions (/docs/regions) Every Daytona sandbox runs in a **region**: a geographic or logical grouping of compute infrastructure. When creating a sandbox, you can target a specific region, and Daytona schedules the workload on available capacity within that region. A create request that names no region uses the project's default region, then the organization's, then Earth. Regions form one flat list, and each region names the broader regions it is inside, from Earth down to its direct parent. Earth covers every other region and means anywhere: a sandbox created in Earth can land in any geography. A broader region lets Daytona place the sandbox anywhere inside it; a narrower region keeps the sandbox closer to you. Each region has its own price multiplier, applied to the base price of the sandbox's resources, and a region without one is billed at Earth's. The hierarchy does not set the price: a narrower region can cost less than its parent, so check the multiplier of the region you name. A listed region describes topology, not capacity: a create request can still fail for lack of capacity or matching hardware. ## Shared regions [#shared-regions] Regions managed by Daytona and available to all organizations: ```python from daytona import Daytona daytona = Daytona() # Create a sandbox anywhere in Earth sandbox = daytona.v2.create(region="earth") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() // Create a sandbox anywhere in Earth const sandbox = await daytona.v2.create({ region: 'earth' }) ``` ```bash # Create a sandbox anywhere in Earth daytona sandbox create --region earth ``` ```bash # Create a sandbox anywhere in Earth curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "region_id": "earth" }' ``` ## Operations [#operations] ### List regions [#list-regions] List regions. The list contains every region you can name in a create request, as one flat list in tree order: Earth first, then each region followed by the regions inside it. Each region names its parent regions, so a client can rebuild the hierarchy from the list. Regions that cannot be used are left out. A region that covers others stays listed when none of them accepts automatic placement, marked as unavailable, and cannot be named in a create request. ```bash # Regions available to the current project, as NAME, ID, PRICE MULTIPLIER, and AVAILABLE columns daytona region list # Include parent regions daytona region list --output json ``` ```bash # Regions available to the current project curl 'https://api.daytona.com/regions' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` The response contains one entry per region and the ID of your default region, with the following fields: | **Field** | **Description** | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | Region ID, the value a create request names in `region_id`. | | `name` | Display name. | | `parent_region_ids` | IDs of the broader regions this region is inside, outermost first, so the last one is its direct parent. Empty for `earth`. Every ID is listed in the same response. | | `available` | Whether a create request can name this region. For a region that covers others, whether at least one of them accepts automatic placement. Not a health or capacity signal. | | `api_base_url` | The API base URL of the region, when it publishes its own. | | `default_region_id` | ID of your default region: the project's default region, else the organization's, else the platform's. `null` when that region is not listed. | ### List region price multipliers [#list-region-price-multipliers] List region price multipliers. The list contains the price multiplier of every region a create request can name, Earth first, then every other region ordered by ID. A sandbox's CPU, memory, GPU, and storage are billed at the base price times the multiplier of the region its create request named. A create request that names no region is billed at its default region's multiplier, or at Earth's when no default is set. For the other pricing dimensions, see [billing](/docs/billing#pricing-dimensions). ```bash # The PRICE MULTIPLIER column of the region list daytona region list ``` ```bash curl 'https://api.daytona.com/orgs/ORGANIZATION_ID/billing/region-multipliers' \ --header 'Authorization: Bearer YOUR_OIDC_TOKEN' ``` The response contains one entry per region, Earth first and the rest ordered by ID, with the following fields: | **Field** | **Description** | | ------------ | --------------------------------------------------------------------- | | `region_id` | The region ID accepted by create requests. | | `multiplier` | The multiplier applied to base CPU, memory, GPU, and storage prices. | # Network Limits (/docs/network-limits) Network limits control outbound internet access from sandboxes. Each sandbox runs behind a firewall that restricts which external IP addresses and domains it can reach, preventing untrusted code from exfiltrating data or contacting arbitrary hosts. Egress is open by default. You can restrict access per sandbox with a network policy: a default-deny allow list of IPv4 CIDR ranges and domains. Set the policy when you [create a sandbox](#create-sandboxes-with-network-restrictions), then [update](#update-network-policy) or [reset](#reset-network-policy) it while the sandbox exists. Enabling unrestricted network access may pose security risks when executing untrusted code. It is recommended to allow only the network addresses or domains you need, or block all network access. Test network connectivity before starting critical development work. ## Use cases [#use-cases] Network limits are used when code in a sandbox must not reach arbitrary hosts. - **Untrusted or generated code.** Code from users or agents runs with no outbound access, so it cannot send data out of the sandbox. With blocking on and empty allow lists, DNS resolves no names either. Network policies apply to Linux and macOS sandboxes, and Windows and GPU sandboxes do not accept them. - **Agents that call known services.** A domain allow list lets an agent reach the model provider and the APIs it needs, and blocks every other host. The sandbox resolves only the names on the list. A domain allow list turns blocking on by itself. - **Dependency installs.** The domains of package registries, such as PyPI or npm, are allowed, and the rest of the internet is blocked. A wildcard entry covers the subdomains of a registry host but not the host itself, which needs its own entry. Each entry is a hostname without a protocol, path, port, or query string. - **Sensitive data.** A sandbox that processes private files or datasets reaches only the hosts on its allow list. DNS is filtered as well, so a name that is not on the list does not resolve. Egress is open by default, so the sandbox gets its policy when it is [created](#create-sandboxes-with-network-restrictions). - **Restrict after setup.** A sandbox installs its dependencies with open egress, and its policy is updated to block all egress before untrusted code runs. The update applies in the background, and the policy status reads applied once the sandbox enforces it. An update works while the sandbox is running, paused, or stopped. ## Create sandboxes with network restrictions [#create-sandboxes-with-network-restrictions] Create a sandbox with a network policy. With blocking turned on, the sandbox can reach only the domains and IPv4 ranges on its allow lists. A domain allow list turns blocking on by itself, while an IPv4 allow list restricts egress only when blocking is on. When a policy has both lists, web traffic on ports 80 and 443 needs a listed domain, even to a listed IPv4 range. Network policies apply to [Linux sandboxes](/docs/linux-sandboxes) and [macOS sandboxes](/docs/macos-sandboxes); the examples below create Linux sandboxes, and a macOS create takes the same policy. [Windows](/docs/windows-sandboxes) and [GPU](/docs/gpu-sandboxes) sandboxes do not accept them. ```python from daytona import Daytona daytona = Daytona() # Allow access to specific domains; everything else is denied sandbox = daytona.v2.create( network_policy={"block_all": True, "allow_domains": ["example.com", "*.daytona.io"]}, ) ``` | **Parameter** | **Type** | **Description** | | ---------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `network_policy` | NetworkPolicyInput | Sets the outbound network policy. It accepts only the `block_all`, `allow_cidrs`, and `allow_domains` keys. Any other key raises `BadRequestError` before the request is sent. If `allow_domains` is set, an omitted `block_all` becomes `True`. Setting `block_all=False` with `allow_domains` is rejected. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() // Allow access to specific domains; everything else is denied const sandbox = await daytona.v2.create({ networkPolicy: { blockAll: true, allowDomains: ['example.com', '*.daytona.io'] }, }) ``` | **Parameter** | **Type** | **Description** | | --------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `networkPolicy` | NetworkPolicyInput | Sets the outbound network policy. It accepts only the `blockAll`, `allowCidrs`, and `allowDomains` fields. Any other field throws `BadRequestError` before the request is sent. If `allowDomains` is set, an omitted `blockAll` becomes `true`. Setting `blockAll: false` with `allowDomains` is rejected. | ```bash # Allow access to specific domains; everything else is denied daytona sandbox create \ --block-all \ --allow-domain example.com \ --allow-domain '*.daytona.io' ``` | **Flag** | **Type** | **Description** | | ---------------- | -------- | -------------------------------- | | `--allow-domain` | string | Adds one entry to the domain allow list. It implies `--block-all`. Repeat the flag for more entries. | | `--allow-cidr` | string | Adds one entry to the network allow list. Repeat the flag for more entries. It requires `--block-all` or `--allow-domain`. | - The request requires the `write:sandboxes` permission. - Only `daytona sandbox create` and `daytona sandbox create-macos` accept the network flags. ```bash # Allow access to specific domains; everything else is denied curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "network_policy": { "block_all": true, "allow_domains": ["example.com", "*.daytona.io"] } }' ``` - The request requires the `write:sandboxes` permission. - The API rejects `"block_all": false` together with `allow_domains`. - Without `allow_domains`, `block_all` defaults to `false`. - The `allow_cidrs` and `allow_domains` lists hold at most 128 entries combined. ### Block all egress [#block-all-egress] Create a sandbox with no outbound access, for code that needs no network. With blocking on and empty allow lists, the sandbox reaches no external address and DNS resolves no names. ```python from daytona import Daytona daytona = Daytona() # Deny all outbound traffic sandbox = daytona.v2.create(network_policy={"block_all": True}) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() // Deny all outbound traffic const sandbox = await daytona.v2.create({ networkPolicy: { blockAll: true }, }) ``` ```bash # Deny all outbound traffic daytona sandbox create --block-all ``` ```bash # Deny all outbound traffic curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "network_policy": { "block_all": true } }' ``` ### Allow IPv4 ranges [#allow-ipv4-ranges] Create a sandbox that reaches only specific IPv4 addresses and ranges, in the [network allow list format](#network-allow-list-format). An IPv4 allow list applies only when blocking is on, so turn it on explicitly. Without a domain allow list, DNS resolves a name only to its IPv4 addresses inside the allowed ranges, and a name with no address in them resolves to no address. IPv6 lookups are refused. ```python from daytona import Daytona daytona = Daytona() # Allow one address and one subnet; everything else is denied sandbox = daytona.v2.create( network_policy={"block_all": True, "allow_cidrs": ["1.1.1.1/32", "208.80.152.0/22"]}, ) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() // Allow one address and one subnet; everything else is denied const sandbox = await daytona.v2.create({ networkPolicy: { blockAll: true, allowCidrs: ['1.1.1.1/32', '208.80.152.0/22'] }, }) ``` ```bash # Allow one address and one subnet; everything else is denied daytona sandbox create \ --block-all \ --allow-cidr 1.1.1.1/32 \ --allow-cidr 208.80.152.0/22 ``` ```bash # Allow one address and one subnet; everything else is denied curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "network_policy": { "block_all": true, "allow_cidrs": ["1.1.1.1/32", "208.80.152.0/22"] } }' ``` ### Allow domains [#allow-domains] Create a sandbox that reaches only specific hosts by name, in the [domain allow list format](#domain-allow-list-format). The sandbox resolves and connects to the listed domains; a wildcard covers every subdomain of a host but not the host itself. The domain list turns blocking on by itself. ```python from daytona import Daytona daytona = Daytona() # Allow a host and all of its subdomains; everything else is denied sandbox = daytona.v2.create( network_policy={"allow_domains": ["pythonhosted.org", "*.pythonhosted.org", "pypi.org"]}, ) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() // Allow a host and all of its subdomains; everything else is denied const sandbox = await daytona.v2.create({ networkPolicy: { allowDomains: ['pythonhosted.org', '*.pythonhosted.org', 'pypi.org'] }, }) ``` ```bash # Allow a host and all of its subdomains; everything else is denied daytona sandbox create \ --allow-domain pythonhosted.org \ --allow-domain '*.pythonhosted.org' \ --allow-domain pypi.org ``` ```bash # Allow a host and all of its subdomains; everything else is denied curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "network_policy": { "allow_domains": ["pythonhosted.org", "*.pythonhosted.org", "pypi.org"] } }' ``` ## Network allow list format [#network-allow-list-format] Allow outbound traffic to specific IPv4 addresses and ranges. Each entry on the network allow list is a single address or a CIDR block, and the sandbox can reach it on any port. With a domain allow list, ports 80 and 443 still need a listed hostname, whatever address the sandbox connects to: a web request that names a host not on the domain allow list is refused with `403`, even when its address is on the network allow list, and UDP port 443 is blocked. The list takes effect only when blocking is on. The SDKs, the API, and the CLI reject a non-empty list unless blocking or a domain allow list is set; the API returns `400`. | **Rule** | **Description** | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **IPv4 only** | IPv6 addresses and IPv6 CIDR blocks are not supported. | | **CIDR format** | Use standard CIDR notation (`A.B.C.D/N`) with a prefix length up to `32`. The address must be the network address of the range: a CIDR with host bits set, such as `208.80.154.0/22`, returns `400`. A bare address such as `208.80.154.232` is also accepted. | | **No private ranges** | Loopback (`127.0.0.0/8`), private (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), link-local (`169.254.0.0/16`), carrier-grade NAT (`100.64.0.0/10`), multicast (`224.0.0.0/4`), benchmarking (`198.18.0.0/15`), `0.0.0.0/8`, `192.0.0.0/24`, `240.0.0.0/4` (which includes the broadcast address), and `168.63.129.16` return `400`, as does any range that overlaps them, such as `0.0.0.0/0`. | | **No nested entries** | Do not list an entry that lies inside another entry, such as `208.80.154.232/32` together with `208.80.152.0/22`; the policy fails to apply, and at creation the sandbox does not start. List the larger range only. | | **Max 128 entries** | `allow_cidrs` and `allow_domains` combined. | | **Whitespace is ignored** | Entries are trimmed. An empty entry returns `400`. | | **Example** | **Value** | | --------------------- | -------------------------------------------- | | **Single IP** | `208.80.154.232/32` (Wikipedia) | | **Subnet** | `208.80.152.0/22` (Wikimedia) | | **Multiple networks** | `["1.1.1.1/32", "208.80.152.0/22"]` | ## Domain allow list format [#domain-allow-list-format] Allow outbound traffic to specific hosts by name. Each entry is a hostname, or a wildcard covering a hostname's subdomains. A domain allow list turns blocking on by itself. DNS is filtered as well: the sandbox resolves only the names on the list and refuses all others. A policy with only IPv4 entries resolves a name only to its addresses inside the allowed ranges, and a policy with empty lists resolves no names at all. The domain allow list also covers web traffic to the addresses on the network allow list. On ports 80 and 443, the sandbox reaches a host only by a name on the domain allow list, and connecting to a listed IPv4 address does not change which host it reaches. | **Rule** | **Description** | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Domains only** | Use hostnames such as `example.com` or `api.openai.com`. Do not include protocols, paths, ports, or query strings. | | **Wildcards supported** | Prefix a domain with `*.` to allow its subdomains, for example `*.daytona.io`. A wildcard does not match the bare domain; add `daytona.io` as its own entry. The wildcard needs at least two labels after `*.`, so `*.com` returns `400`. | | **ASCII only** | Write internationalized names in punycode, for example `xn--bcher-kva.de` for `bücher.de`. A name with other characters returns `400`. | | **Case-insensitive** | Entries are lowercased before matching. | | **Whitespace is ignored** | Entries are trimmed. An empty entry returns `400`. | | **Empty list** | An empty `allow_domains` list also turns blocking on. | | **Max 128 entries** | `allow_cidrs` and `allow_domains` combined. | | **Example** | **Value** | | -------------------- | --------------------------------------------------- | | **Single domain** | `example.com` | | **Wildcard domain** | `*.daytona.io` | | **Multiple domains** | `["example.com", "*.daytona.io", "api.openai.com"]` | ## Operations [#operations] The SDKs and the API get, update, and reset the network policy of an existing sandbox. The CLI sets a policy only when it creates a sandbox. ### Get network policy [#get-network-policy] Get the network policy of a sandbox. The response carries two policies: the desired policy, which is the last one you set, and the applied policy, which the sandbox currently enforces. A change is applied in the background, so the two differ until the sandbox catches up. The response also carries an ETag that identifies the current revision; an update or reset must send it back. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() policy = sandbox.get_network_policy() print(policy.status, policy.desired, policy.applied, policy.etag) ``` The response has the following fields: | **Field** | **Description** | | --- | --- | | `desired` | The last policy set on the sandbox, or `None`. | | `desired_revision` | The revision of `desired`. | | `applied` | The policy the sandbox enforces, or `None`. | | `applied_revision` | The revision of `applied`, or `None`. | | `applied_start_id` | The sandbox start that `applied` was applied to, or `None`. | | `etag` | The revision token to send back when you update or reset the policy. | | `http_status` | The HTTP status code of the response. | | `status` | The status is `pending`, `applied`, or `failed`. It is `None` for a sandbox that never had a policy. | | `error` | Explains why the policy is not applied yet, or why it failed when `status` is `failed`. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const policy = await sandbox.getNetworkPolicy() console.log(policy.status, policy.desired, policy.applied, policy.etag) ``` The response has the following fields: | **Field** | **Description** | | --- | --- | | `desired` | The last policy set on the sandbox, or `undefined`. | | `desiredRevision` | The revision of `desired`. | | `applied` | The policy the sandbox enforces, or `undefined`. | | `appliedRevision` | The revision of `applied`, or `undefined`. | | `appliedStartId` | The sandbox start that `applied` was applied to, or `undefined`. | | `etag` | The revision token to send back when you update or reset the policy. | | `httpStatus` | The HTTP status code of the response. | | `status` | The status is `pending`, `applied`, or `failed`. It is `undefined` for a sandbox that never had a policy. | | `error` | Explains why the policy is not applied yet, or why it failed when `status` is `failed`. | ```bash # --include prints the ETag response header curl 'https://api.daytona.com/sandboxes/{sandboxId}/network-policy' \ --include \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ```json { "desired": { "version": 2, "block_all": true, "allow_cidrs": [], "allow_domains": ["example.com"] }, "desired_revision": 1, "applied": { "version": 2, "block_all": true, "allow_cidrs": [], "allow_domains": ["example.com"] }, "applied_revision": 1, "applied_start_id": "5b145d87c75d565a5e2b23a91fc1e325/60ad3ddc7c17b94141852ad65fdd10e5", "status": "applied", "error": null } ``` The response has the following fields: | **Field** | **Description** | | --- | --- | | `ETag` | Holds the revision as `"np-"`, for example `"np-1"`. This value is a response header. | | `status` | The status is `pending`, `applied`, or `failed`. For a sandbox that never had a policy, `status` is `null` and the revision is `0`. | ### Update network policy [#update-network-policy] Replace the whole network policy of an existing sandbox, with the same fields and rules as at [create time](#create-sandboxes-with-network-restrictions). The sandbox must be running, paused, or stopped. The new policy is stored at once and applied in the background; get the policy to see when it takes effect. Every update sends the ETag of the revision it replaces. If the policy changed since, the update is rejected instead of overwriting it; get the policy and retry with the new ETag. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() current = sandbox.get_network_policy() updated = sandbox.update_network_policy( network_policy={"block_all": True, "allow_domains": ["pypi.org", "files.pythonhosted.org"]}, if_match=current.etag, ) print(updated.status) # pending until the sandbox applies it ``` | **Parameter** | **Type** | **Description** | | ---------------- | -------------------- | ------------------------------------------------------------------------------- | | `network_policy` | NetworkPolicyInput | Sets the full new policy, with the `block_all`, `allow_cidrs`, and `allow_domains` keys. This parameter is required. | | `if_match` | str | Passes the `etag` from the last `get_network_policy()` or update. This parameter is required. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const current = await sandbox.getNetworkPolicy() const updated = await sandbox.updateNetworkPolicy( { blockAll: true, allowDomains: ['pypi.org', 'files.pythonhosted.org'] }, { ifMatch: current.etag }, ) console.log(updated.status) // pending until the sandbox applies it ``` | **Parameter** | **Type** | **Description** | | ----------------- | -------------------- | ---------------------------------------------------------------------------------- | | `networkPolicy` | NetworkPolicyInput | Sets the full new policy, with the `blockAll`, `allowCidrs`, and `allowDomains` fields. This parameter is required. | | `options.ifMatch` | string | Passes the `etag` from the last `getNetworkPolicy()` or update. This parameter is required. | ```bash curl 'https://api.daytona.com/sandboxes/{sandboxId}/network-policy' \ --request PUT \ --header 'Content-Type: application/json' \ --header 'If-Match: {etag}' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "block_all": true, "allow_domains": ["pypi.org", "files.pythonhosted.org"] }' ``` | **Header** | **Description** | | ----------- | --------------------------------------------------------------------------------------------------------------------- | | `If-Match` | Passes the `ETag` from the last get or update, with its quotes, for example `"np-1"`. This header is required. | The response has the following fields: | **Field** | **Description** | | ------------- | -------------------------------------------------------------------------------------- | | Body | The updated network policy, in the same shape as a get. | | `ETag` header | The revision tag of the updated policy. Send it as `If-Match` on the next update. | ### Reset network policy [#reset-network-policy] Reset the network policy of a sandbox to open egress. A reset removes the restrictions but keeps the revision history, so it follows the same ETag rules as an update. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() current = sandbox.get_network_policy() sandbox.reset_network_policy(if_match=current.etag) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | -------------------------------------------------------------------- | | `if_match` | str | Passes the `etag` from the last `get_network_policy()` or update. This parameter is required. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const current = await sandbox.getNetworkPolicy() await sandbox.resetNetworkPolicy({ ifMatch: current.etag }) ``` | **Parameter** | **Type** | **Description** | | ----------------- | -------- | ------------------------------------------------------------------- | | `options.ifMatch` | string | Passes the `etag` from the last `getNetworkPolicy()` or update. This parameter is required. | ```bash # A literal null body resets the policy to open egress curl 'https://api.daytona.com/sandboxes/{sandboxId}/network-policy' \ --request PUT \ --header 'Content-Type: application/json' \ --header 'If-Match: {etag}' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data 'null' ``` | **Header** | **Description** | | ----------- | --------------------------------------------------------------------------------------------------------------------- | | `If-Match` | Passes the `ETag` from the last get or update, with its quotes, for example `"np-2"`. This header is required. | ### Test network access [#test-network-access] Run the commands inside the sandbox. They test the allow lists only when blocking is on: a sandbox without a network policy has open egress, so both commands succeed. With blocking on, each command succeeds only when its target is on the sandbox's allow list. ```bash # Test HTTP connectivity to an allowed address; needs 208.80.152.0/22 on the network allow list curl -I http://208.80.154.232 # Test HTTP connectivity to an allowed domain; needs example.com on the domain allow list curl -I https://example.com ``` ## Permissions [#permissions] Network policies use the sandbox permissions of the project, which you can grant to an [organization role](/docs/organizations#roles) or an [API key](/docs/api-keys). The [audit logs](/docs/audit-logs) record network policy updates. | **Permission** | **Grants** | | --------------------- | ---------------------------------------------------------------------------------- | | **`read:sandboxes`** | Get the network policy of a sandbox | | **`write:sandboxes`** | Create sandboxes with network restrictions, and update or reset the network policy | # File System Operations (/docs/file-system-operations) File system operations are available through the file system module of a sandbox. Each sandbox has its own isolated filesystem, and operations run through the Daytona API, so your application manages files in a sandbox directly, without executing shell commands inside it. The file system module covers listing directories, reading file metadata, creating directories, uploading and downloading files, setting permissions, searching file contents, watching directories for changes, and moving, renaming, or deleting files. The sandbox streams uploads to disk and downloads from disk, so transfer size is bounded by sandbox storage, not sandbox memory. Most SDK methods hold file content in your application's memory; to move large files without buffering them, stream them from and to local disk with the SDK, as in [upload large files](#upload-large-files) and [download large files](#download-large-files), or use [presigned URLs](#presigned-urls) with an HTTP client. The API serves file system requests at the sandbox's toolbox proxy URL, written as `{toolboxProxyUrl}` in the examples. It is the `toolbox_proxy_url` field of the [sandbox](/docs/sandboxes#get-a-sandbox), such as `https://proxy.ash-1.us-east.daytona.app/toolbox`. Sandbox paths must be absolute, starting with a slash. The SDKs raise `BadRequestError` for a relative path before any request is sent, the CLI rejects it, and the API returns `400 INVALID_FILE_PATH`. ## Use cases [#use-cases] File system operations are used when an application moves or inspects files in a sandbox without a shell. - **Input files.** An application uploads source code, datasets, or configuration before code runs. Several files upload in one request, and a failed file does not stop the others. Missing parent directories are created, and an existing file at the destination is overwritten. - **Results and artifacts.** Generated files, reports, and build outputs are downloaded after a run. Several files download in one request, and a missing or unreadable file does not stop the others. The SDK methods hold every downloaded file in memory before they return. - **Agent file tools.** An agent lists, reads, searches, and edits files through the API as tools. The agent needs no shell commands inside the sandbox to do this. - **Large transfers.** Large files stream between local disk and the sandbox without being held in the application's memory. Uploads have no request size limit, so free sandbox disk space bounds the upload size. A failed or cancelled SDK download leaves an existing destination file unchanged. - **Direct transfers for other clients.** A presigned URL lets a browser or another service upload or download one file without an API key. A URL is valid for at most 7 days. Rotating the sandbox signing key revokes every outstanding URL within 30 seconds. ## List files and directories [#list-files-and-directories] List files and directories. By default, only entries directly inside the directory are listed. A depth option lists nested entries up to 32 levels deep; a listing over 10,000 entries fails, so request a smaller depth for large trees. Symbolic links are listed, not followed. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.write_text("/home/daytona/workspace/hello.txt", "hello") # List files in a directory files = sandbox.fs.list("/home/daytona/workspace") for file in files.items: print(f"Name: {file.name}") print(f"Is directory: {file.is_dir}") print(f"Size: {file.size}") print(f"Modified: {file.modified_at}") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeText('/home/daytona/workspace/hello.txt', 'hello') // List files in a directory const files = await sandbox.fs.list('/home/daytona/workspace') files.items.forEach(file => { console.log(`Name: ${file.name}`) console.log(`Is directory: ${file.isDir}`) console.log(`Size: ${file.size}`) console.log(`Modified: ${file.modifiedAt ?? file.modTime}`) }) ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/files?path=/home/daytona/workspace' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Get directory or file information [#get-directory-or-file-information] Get metadata for a file or directory. The metadata describes the entry at the path itself: for a symbolic link, it describes the link, not its target. | **Field** | **Format** | | ----------------------- | -------------------------------------------------------------------------- | | **Mode** | Symbolic, such as `-rw-r--r--`; `d` first for a directory, `L` for a link. | | **Permissions** | The four-digit octal form, such as `0644`. | | **Owner** and **group** | Numeric user and group IDs as strings, such as `1000`, not names. | ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.write_text("/home/daytona/workspace/data/file.txt", "hello") # Get file metadata info = sandbox.fs.stat("/home/daytona/workspace/data/file.txt") print(f"Size: {info.size} bytes") print(f"Modified: {info.modified_at}") print(f"Mode: {info.mode}") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeText('/home/daytona/workspace/data/file.txt', 'hello') // Get file metadata const info = await sandbox.fs.stat('/home/daytona/workspace/data/file.txt') console.log(`Size: ${info.size} bytes`) console.log(`Modified: ${info.modifiedAt ?? info.modTime}`) console.log(`Mode: ${info.mode}`) ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/files/info?path=/home/daytona/workspace/data/file.txt' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ```json { "name": "file.txt", "path": "/home/daytona/workspace/data/file.txt", "size": 1024, "isDir": false, "mode": "-rw-r--r--", "permissions": "0644", "owner": "1000", "group": "1000", "modifiedAt": "2026-09-13T09:41:12Z", "modTime": "2026-09-13 09:41:12 +0000 UTC" } ``` ## Create directories [#create-directories] Create a directory and any missing parents, with octal mode 755 by default. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.mkdir("/home/daytona/workspace/new-dir") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.mkdir('/home/daytona/workspace/new-dir') ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/files/folder?path=/home/daytona/workspace/new-dir' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Upload files [#upload-files] ### Upload a single file [#upload-a-single-file] Upload a file to a sandbox. Missing parent directories at the destination are created, and an existing file at the destination is overwritten. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() with open("local_file.txt", "rb") as f: sandbox.fs.write_bytes("/home/daytona/remote_file.txt", f.read()) ``` ```typescript import { readFile } from 'node:fs/promises' import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeBytes('/home/daytona/remote_file.txt', await readFile('local_file.txt')) ``` ```bash # The sandbox side is : daytona sandbox cp local_file.txt {sandboxId}:/home/daytona/remote_file.txt ``` - The request requires the `write:sandboxes` permission. - Exactly one of the two arguments must be a sandbox path. - A sandbox destination that ends with `/` receives the file under its local name. ```bash curl '{toolboxProxyUrl}/{sandboxId}/files/upload?path=/home/daytona/remote_file.txt' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --form 'file=@local_file.txt' ``` ### Upload multiple files [#upload-multiple-files] Upload several files in one request. Each file is written independently: missing parent directories are created, existing files are overwritten, and a file that fails does not stop the others. The result lists each file written with its byte count, and an error message for each failure. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() result = sandbox.fs.write_many({ "/home/daytona/data/file1.txt": b"Content of file 1", "/home/daytona/data/file2.txt": b"Content of file 2", }) # Partial success is reported in the result, not raised for error in result.errors: print(f"Error: {error}") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const result = await sandbox.fs.writeMany({ '/home/daytona/data/file1.txt': new TextEncoder().encode('Content of file 1'), '/home/daytona/data/file2.txt': new TextEncoder().encode('Content of file 2'), }) // Partial success is reported in the result, not thrown result.errors.forEach(error => console.error(`Error: ${error}`)) ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/files/bulk-upload' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --form 'files[0].path=/home/daytona/data/file1.txt' \ --form 'files[0].file=@file1.txt' \ --form 'files[1].path=/home/daytona/data/file2.txt' \ --form 'files[1].file=@file2.txt' ``` ```json { "files": [ { "path": "/home/daytona/data/file1.txt", "bytes": 17 }, { "path": "/home/daytona/data/file2.txt", "bytes": 17 } ], "errors": [] } ``` - The request requires the `write:sandboxes` permission. - Each `files[i].path` part must come before its `files[i].file` part. ### Upload large files [#upload-large-files] Upload large files from local disk. The SDK streams the file in chunks without holding it in memory, and reports progress as it sends. The sandbox writes the upload to disk as it arrives with no request size limit, so free sandbox disk space bounds the upload size. The CLI streams the file from disk over a [presigned URL](#presigned-urls) it creates. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() def on_progress(sent: int, total: int | None) -> None: print(f"Uploaded {sent} of {total} bytes") sandbox.fs.upload_file("large_dataset.csv", "/home/daytona/workspace/dataset.csv", on_progress=on_progress) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------ | | `local_path` | str \| os.PathLike\[str\] | Sets the local file to upload. This parameter is required. | | `remote_path` | str | Sets the destination path in the sandbox. This parameter is required. | | `on_progress` | TransferProgressCallback | Reports upload progress. The callback receives the bytes sent so far and the total file size in bytes. Exceptions raised inside the callback are ignored. | | `cancel` | TransferCancelSignal | Stops the upload when the event is set. The event is a `threading.Event` or an `asyncio.Event`. Without `atomic`, a stopped upload leaves the bytes sent so far at the destination. | | `atomic` | bool | Writes through a temporary file in the destination directory, then renames the file into place. The default is `False`. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.uploadFile('large_dataset.csv', '/home/daytona/workspace/dataset.csv', { onProgress: (sent, total) => console.log(`Uploaded ${sent} of ${total} bytes`), }) ``` | **Parameter** | **Type** | **Description** | | -------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------ | | `localPath` | string | Sets the local file to upload. This parameter is required. | | `remotePath` | string | Sets the destination path in the sandbox. This parameter is required. | | `options.onProgress` | TransferProgressCallback | Reports upload progress. The callback receives the bytes sent so far and the total file size in bytes. Errors thrown inside the callback are ignored. | | `options.signal` | AbortSignal | Stops the upload when the signal is aborted. Without `options.atomic`, a stopped upload leaves the bytes sent so far at the destination. | | `options.atomic` | boolean | Writes through a temporary file in the destination directory, then renames the file into place. The default is `false`. | - The request requires the `write:sandboxes` permission. - `uploadFile` works in Node.js only, not in a browser. ```bash daytona sandbox cp large_dataset.csv {sandboxId}:/home/daytona/workspace/dataset.csv ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/files/upload?path=/home/daytona/workspace/dataset.csv' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --form 'file=@large_dataset.csv' ``` ## Download files [#download-files] ### Download a single file [#download-a-single-file] ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.write_text("/home/daytona/file1.txt", "Content of file 1") content = sandbox.fs.read_bytes("/home/daytona/file1.txt") print(content.decode("utf-8")) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeText('/home/daytona/file1.txt', 'Content of file 1') const content = await sandbox.fs.readBytes('/home/daytona/file1.txt') console.log(new TextDecoder().decode(content)) ``` ```bash daytona sandbox cp {sandboxId}:/home/daytona/file1.txt file1.txt ``` - The request requires the `write:sandboxes` permission. - A local destination that ends in `/` or names a directory keeps the sandbox file name. ```bash curl '{toolboxProxyUrl}/{sandboxId}/files/download?path=/home/daytona/file1.txt' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` #### Download a text file [#download-a-text-file] Download a text file. The SDK downloads the file and decodes its content as UTF-8. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.write_text("/home/daytona/file1.txt", "Content of file 1") content = sandbox.fs.read_text("/home/daytona/file1.txt") print(content) ``` - The request requires the `write:sandboxes` permission. - The file must be valid UTF-8. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeText('/home/daytona/file1.txt', 'Content of file 1') const content = await sandbox.fs.readText('/home/daytona/file1.txt') console.log(content) ``` ### Download multiple files [#download-multiple-files] Download several files in one request. Each file is read independently, so a missing or unreadable file does not stop the others; it is reported as an error for its path. The SDK methods hold every downloaded file in memory before they return. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.write_many({ "/home/daytona/data/file1.txt": b"Content of file 1", "/home/daytona/data/file2.txt": b"Content of file 2", }) result = sandbox.fs.read_many(["/home/daytona/data/file1.txt", "/home/daytona/data/file2.txt"]) for path, content in result.files.items(): print(f"Downloaded {path} ({len(content)} bytes)") # Partial success is reported in the result, not raised for error in result.errors: print(f"Error downloading {error.path}: {error.message}") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeMany({ '/home/daytona/data/file1.txt': new TextEncoder().encode('Content of file 1'), '/home/daytona/data/file2.txt': new TextEncoder().encode('Content of file 2'), }) const result = await sandbox.fs.readMany(['/home/daytona/data/file1.txt', '/home/daytona/data/file2.txt']) Object.entries(result.files).forEach(([path, content]) => { console.log(`Downloaded ${path} (${content.byteLength} bytes)`) }) // Partial success is reported in the result, not thrown result.errors.forEach(error => console.error(`Error downloading ${error.path}: ${error.message}`)) ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/files/bulk-download' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "paths": [ "/home/daytona/data/file1.txt", "/home/daytona/data/file2.txt" ] }' ``` The response has the following fields: | **Field** | **Description** | | --------- | --------------- | | `file` | One part per downloaded file. The filename of the part is the requested path. | | `error` | One part per path that cannot be read, with a JSON body that holds `code`, `message`, `source`, and `path`. A path that cannot be read does not fail the request. | - The request requires the `write:sandboxes` permission. - The response is `multipart/form-data` with the fixed boundary `DAYTONA-FILE-BOUNDARY`, not JSON. ### Download large files [#download-large-files] Download large files to local disk. The SDK streams the file in chunks without holding it in memory, and reports progress as it receives. It writes to a temporary file next to the destination and renames it into place only after the whole file arrives, so a failed or cancelled download leaves an existing destination file unchanged. The CLI writes the file to disk as it arrives, over a [presigned URL](#presigned-urls) that it creates for the transfer. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.write_text("/home/daytona/workspace/large-file.bin", "hello") def on_progress(received: int, total: int | None) -> None: print(f"Downloaded {received} of {total} bytes") sandbox.fs.download_file("/home/daytona/workspace/large-file.bin", "local_copy.bin", on_progress=on_progress) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `remote_path` | str | Sets the file to download from the sandbox. This parameter is required. | | `local_path` | str \| os.PathLike\[str\] | Sets the local destination. A replaced destination file keeps its permission bits. This parameter is required. The parent directory of the destination must exist. | | `on_progress` | TransferProgressCallback | Reports download progress. The callback receives the bytes received so far and the total size in bytes, or `None` when the total is unknown. | | `cancel` | TransferCancelSignal | Stops the download when the event is set. The event is a `threading.Event` or an `asyncio.Event`. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeText('/home/daytona/workspace/large-file.bin', 'hello') await sandbox.fs.downloadFile('/home/daytona/workspace/large-file.bin', 'local_copy.bin', { onProgress: (received, total) => console.log(`Downloaded ${received} of ${total} bytes`), }) ``` | **Parameter** | **Type** | **Description** | | -------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `remotePath` | string | Sets the file to download from the sandbox. This parameter is required. | | `localPath` | string | Sets the local destination. The parent directory of the destination must exist. A replaced destination file keeps its permission bits. This parameter is required. | | `options.onProgress` | TransferProgressCallback | Reports download progress. The callback receives the bytes received so far and the total size in bytes, or `undefined` when the total is unknown. | | `options.signal` | AbortSignal | Stops the download when the signal is aborted. | - The request requires the `write:sandboxes` permission. - `downloadFile` works in Node.js only, not in a browser. ```bash daytona sandbox cp {sandboxId}:/home/daytona/workspace/large-file.bin local_copy.bin ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/files/download?path=/home/daytona/workspace/large-file.bin' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --output local_copy.bin ``` ## Delete files [#delete-files] Delete a file or directory. A directory is deleted, with everything in it, only when the recursive option is set; otherwise it fails. A symbolic link is deleted as a link; its target is kept. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.write_text("/home/daytona/workspace/file.txt", "hello") sandbox.fs.remove("/home/daytona/workspace/file.txt") ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------------------------------------------------------------------------------------------------------- | | `recursive` | bool | Deletes a directory and its contents when `True`. The default is `False`. Without this option, a directory path raises `BadRequestError`. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeText('/home/daytona/workspace/file.txt', 'hello') await sandbox.fs.remove('/home/daytona/workspace/file.txt') ``` | **Parameter** | **Type** | **Description** | | ------------- | --------- | ----------------------------------------------------------------------------------------------------------- | | `recursive` | boolean | Deletes a directory and its contents when `true`. Without this option, a directory path throws `BadRequestError`. | ```bash curl '{toolboxProxyUrl}/{sandboxId}/files?path=/home/daytona/workspace/file.txt' \ --request DELETE \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Type** | **Description** | | ----------- | -------- | ------------------------------------------------------------------------------------------------- | | `recursive` | string | Deletes a directory and its contents when set to `true`. Without this field, a directory path returns `400`. | ## File permissions [#file-permissions] Change the permission mode, owner, or group of a file or directory; at least one is required. The mode is an octal string. Owner and group accept a numeric ID or a name, which the sandbox resolves from its user and group lists. Symbolic links are followed. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.write_text("/home/daytona/workspace/file.txt", "hello") sandbox.fs.chmod("/home/daytona/workspace/file.txt", mode="644") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeText('/home/daytona/workspace/file.txt', 'hello') await sandbox.fs.chmod('/home/daytona/workspace/file.txt', { mode: '644' }) ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/files/permissions?path=/home/daytona/workspace/file.txt&mode=644' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Search files by pattern [#search-files-by-pattern] Search for files by name. The search walks the directory tree without following symbolic links and matches the name of every entry against the pattern, so directories with a matching name are returned as well. Directories the sandbox cannot read are skipped without an error, and a path that does not exist returns an empty result. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.write_text("/home/daytona/workspace/main.py", "print('hello')") # Recursive; the pattern is matched against file names with * and ? wildcards result = sandbox.fs.glob("/home/daytona/workspace", "*.py") for file in result.items: print(file) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeText('/home/daytona/workspace/main.py', "print('hello')") // Recursive; the pattern is matched against file names with * and ? wildcards const result = await sandbox.fs.glob('/home/daytona/workspace', '*.py') result.items.forEach(file => console.log(file)) ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/files/search?path=/home/daytona/workspace&pattern=*.py' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` Returned paths are prefixed with the path you passed. `skipped` lists up to 1000 paths below the root that the sandbox user cannot read, so their contents were not searched: ```json { "files": [ "/home/daytona/workspace/main.py", "/home/daytona/workspace/lib/util.py" ], "skipped": [] } ``` ## Find and replace text in files [#find-and-replace-text-in-files] Find text in files and replace it. Find checks each file line by line and returns the file, the line number starting at 1, and the full line for every match. Files that are not valid UTF-8 text are skipped, and a path that does not exist returns no matches. Replace takes a list of files, not directories, and rewrites each file in full with every occurrence replaced. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.write_text("/home/daytona/workspace/src/app.py", "text-of-interest") sandbox.fs.write_text("/home/daytona/workspace/file1.txt", "old_text") sandbox.fs.write_text("/home/daytona/workspace/file2.txt", "old_text") # Search for text in files; the pattern is a literal substring, not a regular expression. # If a directory is given, the search is recursive. results = sandbox.fs.grep("/home/daytona/workspace/src", "text-of-interest") for match in results.items: print(f"{match.file}:{match.line}: {match.content}") # Replace every occurrence of a literal substring in each listed file result = sandbox.fs.replace(["/home/daytona/workspace/file1.txt", "/home/daytona/workspace/file2.txt"], "old_text", "new_text") # Per-file failures are reported in the result, not raised for item in result.items: if not item.success: print(f"Failed to update {item.file}: {item.error}") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeText('/home/daytona/workspace/src/app.py', 'text-of-interest') await sandbox.fs.writeText('/home/daytona/workspace/file1.txt', 'old_text') await sandbox.fs.writeText('/home/daytona/workspace/file2.txt', 'old_text') // Search for text in files; the pattern is a literal substring, not a regular expression. // If a directory is given, the search is recursive. const results = await sandbox.fs.grep('/home/daytona/workspace/src', 'text-of-interest') results.items.forEach(match => console.log(`${match.file}:${match.line}: ${match.content}`)) // Replace every occurrence of a literal substring in each listed file const result = await sandbox.fs.replace(['/home/daytona/workspace/file1.txt', '/home/daytona/workspace/file2.txt'], 'old_text', 'new_text') // Per-file failures are reported in the result, not thrown result.items .filter(item => !item.success) .forEach(item => console.error(`Failed to update ${item.file}: ${item.error}`)) ``` Find text in files: ```bash curl '{toolboxProxyUrl}/{sandboxId}/files/find?path=/home/daytona/workspace/src&pattern=text-of-interest' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` `items` holds the matches in walk order. `skipped` lists up to 1000 paths below the root that the sandbox user cannot read, so they were not searched: ```json { "items": [ { "file": "/home/daytona/workspace/src/main.py", "line": 12, "content": "print('text-of-interest')" } ], "skipped": [] } ``` Replace text in files: ```bash curl '{toolboxProxyUrl}/{sandboxId}/files/replace' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "files": [ "/home/daytona/workspace/file1.txt", "/home/daytona/workspace/file2.txt" ], "newValue": "new_text", "pattern": "old_text" }' ``` ```json [ { "file": "/home/daytona/workspace/file1.txt", "success": true }, { "file": "/home/daytona/workspace/file2.txt", "success": false, "error": "No such file or directory (os error 2)" } ] ``` ## Move or rename directory or file [#move-or-rename-directory-or-file] Move or rename a file or directory. A move never replaces or merges into an existing destination entry. Across filesystems, the sandbox copies the entry, keeping each copied entry's owner and mode, and removes the source only after the whole copy succeeds. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.write_text("/home/daytona/workspace/data/file.txt", "hello") # Missing parent directories are created; an existing destination is rejected with ConflictError sandbox.fs.move("/home/daytona/workspace/data/file.txt", "/home/daytona/workspace/archive/file.txt") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeText('/home/daytona/workspace/data/file.txt', 'hello') // Missing parent directories are created; an existing destination is rejected with ConflictError await sandbox.fs.move('/home/daytona/workspace/data/file.txt', '/home/daytona/workspace/archive/file.txt') ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/files/move?source=/home/daytona/workspace/data/file.txt&destination=/home/daytona/workspace/archive/file.txt' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Watch for file changes [#watch-for-file-changes] Watch a directory for changes. The watch streams create, write, remove, rename, and permission change events as they happen; it does not replay earlier changes. By default, only entries directly inside the directory are watched. A recursive watch also covers subdirectories, including ones created later. Event paths are relative to the watched directory. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.mkdir("/home/daytona/workspace/src") handle = sandbox.fs.watch_dir( "/home/daytona/workspace/src", lambda event: print(f"{event.type}: {event.name}"), ) sandbox.fs.write_text("/home/daytona/workspace/src/new.py", "print('hello')") handle.stop() ``` | **Parameter** | **Type** | **Description** | | ------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------- | | `on_event` | Callable\[\[FilesystemEvent\], None \| Awaitable\[None\]\] | Receives each event. This is the second positional argument and is required. | | `recursive` | bool | Watches subdirectories when `True`. A recursive watch covers at most `4096` directories. A larger tree is refused when the watch starts. Directories created later beyond the limit are reported as an overflow. The default is `False`. | | `on_overflow` | Callable\[\[\], None \| Awaitable\[None\]\] | Runs when events are dropped. If events outpace delivery, the watch drops them and sends an overflow notice. | | `on_exit` | Callable\[\[Exception \| None\], None \| Awaitable\[None\]\] | Runs once when the watch ends, with `None` after `stop()` and otherwise with the error that ended the watch. Exceptions raised inside the callback are ignored. | - The request requires the `write:sandboxes` permission. - The path must be absolute and name an existing directory. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.mkdir('/home/daytona/workspace/src') const handle = await sandbox.fs.watchDir('/home/daytona/workspace/src', (event) => { console.log(`${event.type}: ${event.name}`) }) await sandbox.fs.writeText('/home/daytona/workspace/src/new.py', "print('hello')") await handle.stop() ``` | **Parameter** | **Type** | **Description** | | ------------- | ------------ | --------------------------------------------------------------------------------- | | `onEvent` | (event: FilesystemEvent) => void \| Promise\ | Receives each event. An async callback is awaited before the next event. This is the second argument and is required. | | `recursive` | boolean | Watches subdirectories when you pass `{ recursive: true }` as the third argument. A recursive watch covers at most `4096` directories. A larger tree is refused when the watch starts. Directories created later beyond the limit are reported as an overflow. | | `onOverflow` | () => void \| Promise\ | Runs when events are dropped. If events outpace delivery, the watch drops them and sends an overflow notice. Pass this callback in the options. | | `onExit` | (error?: Error) => void \| Promise\ | Runs once when the watch ends, with no error after `stop()` and otherwise with the error that ended the watch. Errors thrown inside the callback are ignored. Pass this callback in the options. | - The request requires the `write:sandboxes` permission. - The path must be absolute and name an existing directory. ```bash curl --no-buffer '{toolboxProxyUrl}/{sandboxId}/files/watch?path=/home/daytona/workspace/src' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ```text event: start data: {} event: fs data: {"type":"create","name":"new.py","isDir":false,"timestamp":1789121730123} event: overflow data: {"dropped":true} ``` | **Field** | **Type** | **Description** | | ----------- | --------- | --------------------------------------- | | `recursive` | boolean | Watches subdirectories when set to `true`. A recursive watch covers at most `4096` directories. A larger tree is refused when the watch starts. Directories created later beyond the limit are reported as an overflow. | - The request requires the `write:sandboxes` permission. - The path must be absolute and name an existing directory. ### Wait for the watch to end [#wait-for-the-watch-to-end] ```python import threading from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.mkdir("/home/daytona/workspace/src") handle = sandbox.fs.watch_dir( "/home/daytona/workspace/src", lambda event: print(f"{event.type}: {event.name}"), ) # Nothing ends a healthy watch on its own: stop it from another thread, # or the stream ends when the sandbox stops or is deleted threading.Timer(60, handle.stop).start() # Blocks until the watch is stopped or the stream ends error = handle.wait_for_exit() if error is not None: print(f"Watch ended: {error}") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.mkdir('/home/daytona/workspace/src') const handle = await sandbox.fs.watchDir('/home/daytona/workspace/src', (event) => { console.log(`${event.type}: ${event.name}`) }) // Nothing ends a healthy watch on its own: stop it from elsewhere, // or the stream ends when the sandbox stops or is deleted setTimeout(() => handle.stop(), 60_000) // Resolves when the watch is stopped or the stream ends const error = await handle.waitForExit() if (error) console.error('Watch ended:', error.message) ``` ## Presigned URLs [#presigned-urls] Create presigned URLs. A presigned URL downloads or uploads one sandbox file without an API key until it expires, so any HTTP client can transfer the file. Daytona signs each URL with the sandbox's signing key; rotating it invalidates all its outstanding URLs within 30 seconds. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() # Download link, valid for 1 hour (default) download_url = sandbox.fs.presigned_download_url("/home/daytona/workspace/report.pdf") # Upload link, valid for 1 hour (default) upload_url = sandbox.fs.presigned_upload_url("/home/daytona/workspace/incoming/data.bin") # Invalidate every outstanding presigned URL for this sandbox; old URLs can still work for up to 30 seconds sandbox.rotate_signing_key() ``` | **Parameter** | **Type** | **Description** | | -------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- | | `expires_in_seconds` | int | Sets the URL lifetime in seconds, from `1` to `604800`. A URL is valid for at most `7` days. A value outside that range raises `BadRequestError` before any request is sent. The default is `3600`. | - The requests require the `write:sandboxes` permission. - Windows sandboxes cannot have presigned URLs. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() // Download link, valid for 1 hour (default) const downloadUrl = await sandbox.fs.presignedDownloadUrl('/home/daytona/workspace/report.pdf') // Upload link, valid for 1 hour (default) const uploadUrl = await sandbox.fs.presignedUploadUrl('/home/daytona/workspace/incoming/data.bin') // Invalidate every outstanding presigned URL for this sandbox; old URLs can still work for up to 30 seconds await sandbox.rotateSigningKey() ``` | **Parameter** | **Type** | **Description** | | ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `expiresInSeconds` | number | Sets the URL lifetime in seconds, as an integer from `1` to `604800`. A URL is valid for at most `7` days. A value outside that range throws `BadRequestError` before any request is sent. The default is `3600`. | - The requests require the `write:sandboxes` permission. - Windows sandboxes cannot have presigned URLs. ```bash # Expiry 1 hour from now (GNU date; on macOS use: date -u -v+1H +%Y-%m-%dT%H:%M:%SZ) EXPIRES_AT=$(date -u -d '+1 hour' +%Y-%m-%dT%H:%M:%SZ) # Sign a download link: {"url": "...", "method": "GET", "path": "...", "expires_at": "...", "signature": "v1_..."} # Use "method": "POST" to sign an upload link curl 'https://api.daytona.com/sandboxes/{sandboxId}/files/sign' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data "{ \"method\": \"GET\", \"path\": \"/home/daytona/workspace/report.pdf\", \"expires_at\": \"$EXPIRES_AT\" }" # Consume a download link: use the returned url as is (no Authorization header) curl --output report.pdf \ '{toolboxProxyUrl}/{sandboxId}/files/download?path=%2Fhome%2Fdaytona%2Fworkspace%2Freport.pdf&expires={expires}&signature={signature}' # Consume an upload link curl --request POST \ --form 'file=@data.bin' \ '{toolboxProxyUrl}/{sandboxId}/files/upload?path=%2Fhome%2Fdaytona%2Fworkspace%2Fincoming%2Fdata.bin&expires={expires}&signature={signature}' # Rotate the signing key to revoke all outstanding links; old links can still work for up to 30 seconds curl 'https://api.daytona.com/sandboxes/{sandboxId}/signing-key/rotate' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Type** | **Description** | | ---------- | --------- | -------------------------------------------------------------------------------------------------------- | | `expires_at` | string | Sets the expiry as an RFC 3339 timestamp. The expiry must lie in the future and at most `7` days ahead. This field is required. | | `method` | string | Sets the link type: `GET` for a download link and `POST` for an upload link, in any letter case. Any other method returns `400`. This field is required. | | `path` | string | Sets the absolute sandbox path of the file to download or upload. The `path` query parameter of the returned URL must match it exactly. This field is required. | - The requests require the `write:sandboxes` permission. - Windows sandboxes cannot have presigned URLs. ## Permissions [#permissions] File system access runs through the toolbox of a sandbox. Access is governed by one project permission, which you can grant to an [organization role](/docs/organizations#roles) or an [API key](/docs/api-keys). A [sandbox access token](/docs/sandboxes#create-an-access-token) with the toolbox:fs operation grants the same access to a single sandbox, without an API key. The sandbox read permission alone does not grant toolbox access. | **Permission** | **Grants** | | --------------------- | ---------------------------------------------------- | | **`write:sandboxes`** | List, read, upload, download, move, and delete files | # Git Operations (/docs/git-operations) Git operations are available through the Git module of a sandbox. Operations run through the Daytona API, so your application works with repositories in a sandbox directly, without executing shell commands inside it. The sandbox image must include Git. The Git module covers cloning and initializing repositories, checking status, managing branches and remotes, staging, committing, resetting and restoring changes, pushing and pulling with authentication, inspecting commit history, and reading and writing Git configuration. Private repositories authenticate with personal access tokens passed per operation. Clone, push, and pull accept a per-call timeout; the default is 30 minutes. ## Use cases [#use-cases] Git operations are used when a sandbox works on code that lives in a Git repository. - **Coding agents.** An agent clones a repository, changes code, commits, and pushes a branch for review. Every step runs through the API, with no shell commands in the sandbox. Clone, push, and pull accept a timeout for each call, with a default of 30 minutes. - **Branch checks.** A sandbox checks out a branch and runs its tests in isolation. Each branch gets its own sandbox, so one run does not affect another. A clone can also check out a specific commit, which leaves the repository on a detached HEAD. - **Private repositories.** A username and a personal access token passed with each clone, push, or pull authenticate against private repositories. The credentials are not written to the repository configuration or the remote URL. Without both a username and a password, Git runs without credentials and does not prompt, so the operation fails. - **Change inspection.** Status, branches, and commit history are read without a shell, for example to show an agent's changes before a commit. Status reports the current branch, how far it is ahead of and behind its upstream, and the state of each changed file. History lists every commit reachable from the current commit, newest first, with the author and timestamp of each. - **Rollback of failed attempts.** Reset and restore discard uncommitted changes or unstage files. An agent returns the repository to a known commit and tries again. Restore also accepts a commit, a branch, or a tag as its source. ## Clone repositories [#clone-repositories] Clone a repository into a sandbox directory. A relative destination resolves against the working directory of the sandbox, which depends on the image and the operating system. You can clone a specific branch or a specific commit. A commit takes precedence over a branch and leaves the repository on a detached HEAD at that commit. If a clone times out, Daytona removes the partly cloned destination, unless the destination existed before the call. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.git.clone("https://github.com/user/repo.git", "/home/daytona/workspace/repo") ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `url` | str | Sets the URL of the repository to clone. This parameter is required. | | `path` | str | Sets the destination directory in the sandbox. A relative path resolves against the daemon working directory: on a Linux sandbox, the image's `WORKDIR`, or the image's `HOME` when the image declares no `WORKDIR` (`/root` when the image sets no `HOME`); on a macOS sandbox, `/Users/lume`. This parameter is required. | | `branch` | str | Sets the branch to check out after the clone. It is ignored when `commit_id` is set. The default is the default branch of the remote. | | `commit_id` | str | Sets the commit to check out after the clone, which leaves the repository on a detached HEAD. | | `depth` | int | Sets the number of commits to fetch for a shallow clone; `1` fetches only the tip commit. A value below `1` raises `BadRequestError`. | | `insecure_skip_tls` | bool | Skips TLS certificate verification for this clone only. The default is `False`. | | `username` | str | Sets the username for a private repository. It is used only together with `password`. See [authenticate credentials](#authenticate-credentials). | | `password` | str | Sets the password or personal access token for a private repository. It is used only together with `username`. | | `timeout` | float | Sets the time limit of the call in seconds. `0` removes this limit, but the sandbox still stops the Git command after 31 minutes and returns `408`. The default is `1800`. | ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.git.clone('https://github.com/user/repo.git', { path: '/home/daytona/workspace/repo' }); ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `url` | string | Sets the URL of the repository to clone. This is the first positional argument and is required. | | `path` | string | Sets the destination directory in the sandbox. A relative path resolves against the daemon working directory: on a Linux sandbox, the image's `WORKDIR`, or the image's `HOME` when the image declares no `WORKDIR` (`/root` when the image sets no `HOME`); on a macOS sandbox, `/Users/lume`. This parameter is required. | | `branch` | string | Sets the branch to check out after the clone. It is ignored when `commitId` is set. The default is the default branch of the remote. | | `commitId` | string | Sets the commit to check out after the clone, which leaves the repository on a detached HEAD. | | `depth` | number | Sets the number of commits to fetch for a shallow clone; `1` fetches only the tip commit. | | `insecureSkipTls` | boolean | Skips TLS certificate verification for this clone only. The default is `false`. | | `username` | string | Sets the username for a private repository. It is used only together with `password`. See [authenticate credentials](#authenticate-credentials). | | `password` | string | Sets the password or personal access token for a private repository. It is used only together with `username`. | | `timeoutSeconds` | number | Sets the time limit of the call in seconds. `0` removes this limit, but the sandbox still stops the Git command after 31 minutes and returns `408`. The default is `1800`. | ```bash curl '{toolboxProxyUrl}/{sandboxId}/git/clone' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "url": "https://github.com/user/repo.git", "path": "/home/daytona/workspace/repo" }' ``` | **Field** | **Type** | **Description** | | --------- | -------- | ---------------------------------------- | | `url` | string | Sets the URL of the repository to clone. This field is required. A missing value returns `400`. | | `path` | string | Sets the destination directory in the sandbox. A relative path resolves against the daemon working directory: on a Linux sandbox, the image's `WORKDIR`, or the image's `HOME` when the image declares no `WORKDIR` (`/root` when the image sets no `HOME`); on a macOS sandbox, `/Users/lume`. This field is required. A missing value returns `400`. | | `branch` | string | Sets the branch to check out after the clone. It is ignored when `commit_id` is set. The default is the default branch of the remote. | | `commit_id` | string | Sets the commit to check out after the clone, which leaves the repository on a detached HEAD. | | `depth` | integer | Sets the number of commits to fetch for a shallow clone; `1` fetches only the tip commit. | | `insecureSkipTls` | boolean | Skips TLS certificate verification for this clone only. The default is `false`. | | `username` | string | Sets the username for a private repository. It is used only together with `password`. See [authenticate credentials](#authenticate-credentials). | | `password` | string | Sets the password or personal access token for a private repository. It is used only together with `username`. | - The request requires the `write:sandboxes` permission. - `{toolboxProxyUrl}` is the `toolbox_proxy_url` field of the [sandbox](/docs/sandboxes#get-a-sandbox). ## Get repository status [#get-repository-status] Get the status of a repository. The status reports the current branch, whether it has an upstream branch, how many commits it is ahead of and behind that upstream, and the state of each changed or untracked file in the index and the working tree. The ahead and behind counts are zero when the branch has no upstream. A renamed file also carries its original path. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.git.clone("https://github.com/user/repo.git", "/home/daytona/workspace/repo") status = sandbox.git.status("/home/daytona/workspace/repo") print(f"Current branch: {status.current_branch}") # "HEAD" when detached print(f"Commits ahead: {status.ahead}, behind: {status.behind}") # staging and worktree hold the porcelain status codes # (M, A, D, R, ?, ...) or a space when the file is unchanged there. for file in status.file_status or []: print(f"{file.staging}{file.worktree} {file.name}") ``` ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.git.clone('https://github.com/user/repo.git', { path: '/home/daytona/workspace/repo' }); const status = await sandbox.git.status('/home/daytona/workspace/repo'); console.log(`Current branch: ${status.currentBranch}`); // "HEAD" when detached console.log(`Commits ahead: ${status.ahead}, behind: ${status.behind}`); // staging and worktree hold the porcelain status codes // (M, A, D, R, ?, ...) or a space when the file is unchanged there. status.fileStatus?.forEach(file => { console.log(`${file.staging}${file.worktree} ${file.name}`); }); ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/git/status?path=/home/daytona/workspace/repo' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ```json { "currentBranch": "main", "branchPublished": true, "ahead": 1, "behind": 0, "fileStatus": [ { "name": "src/main.py", "extra": "", "staging": "M", "worktree": " " }, { "name": "docs/new.md", "extra": "docs/old.md", "staging": "R", "worktree": " " }, { "name": "notes.txt", "extra": "", "staging": "?", "worktree": "?" } ] } ``` The response has the following fields: | **Field** | **Description** | | --- | --- | | `branchPublished` | The value is `true` when the current branch has an upstream branch. | | `extra` | Holds the original path of a renamed file. The value is empty for other files. | ## Branch operations [#branch-operations] ### List branches [#list-branches] List a repository's local branches, by name only; remote-tracking branches are excluded. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.git.clone("https://github.com/user/repo.git", "/home/daytona/workspace/repo") response = sandbox.git.list_branches("/home/daytona/workspace/repo") for branch in response.items: print(branch) ``` ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.git.clone('https://github.com/user/repo.git', { path: '/home/daytona/workspace/repo' }); const response = await sandbox.git.listBranches('/home/daytona/workspace/repo'); response.items.forEach(branch => console.log(branch)); ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/git/branches?path=/home/daytona/workspace/repo' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ```json { "branches": ["main", "new-feature"] } ``` ### Create branches [#create-branches] Create a branch. The new branch starts at the current commit, and the repository stays on the branch it was on. To switch to the new branch, [check it out](#checkout-branches). ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.git.clone("https://github.com/user/repo.git", "/home/daytona/workspace/repo") # Create a new branch sandbox.git.create_branch("/home/daytona/workspace/repo", "new-feature") ``` ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.git.clone('https://github.com/user/repo.git', { path: '/home/daytona/workspace/repo' }); // Create new branch await sandbox.git.createBranch('/home/daytona/workspace/repo', 'new-feature'); ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/git/branches' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "path": "/home/daytona/workspace/repo", "name": "new-feature" }' ``` ### Checkout branches [#checkout-branches] Check out a branch. Checkout switches the working tree to the named branch. It does not create a branch from scratch; to start a new branch, [create it](#create-branches) first. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.git.clone("https://github.com/user/repo.git", "/home/daytona/workspace/repo") sandbox.git.create_branch("/home/daytona/workspace/repo", "feature-branch") # Checkout a branch sandbox.git.checkout_branch("/home/daytona/workspace/repo", "feature-branch") ``` ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.git.clone('https://github.com/user/repo.git', { path: '/home/daytona/workspace/repo' }); await sandbox.git.createBranch('/home/daytona/workspace/repo', 'feature-branch'); // Checkout a branch await sandbox.git.checkoutBranch('/home/daytona/workspace/repo', 'feature-branch'); ``` ```bash # Create a branch curl '{toolboxProxyUrl}/{sandboxId}/git/branches' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "path": "/home/daytona/workspace/repo", "name": "feature-branch" }' # Checkout a branch curl '{toolboxProxyUrl}/{sandboxId}/git/checkout' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "path": "/home/daytona/workspace/repo", "branch": "feature-branch" }' ``` ### Delete branches [#delete-branches] Delete a branch. Deletion is forced, so unmerged commits do not block it. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.git.clone("https://github.com/user/repo.git", "/home/daytona/workspace/repo") sandbox.git.create_branch("/home/daytona/workspace/repo", "old-feature") # Delete a branch sandbox.git.delete_branch("/home/daytona/workspace/repo", "old-feature") ``` ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.git.clone('https://github.com/user/repo.git', { path: '/home/daytona/workspace/repo' }); await sandbox.git.createBranch('/home/daytona/workspace/repo', 'old-feature'); // Delete a branch await sandbox.git.deleteBranch('/home/daytona/workspace/repo', 'old-feature'); ``` ```bash # Create a branch curl '{toolboxProxyUrl}/{sandboxId}/git/branches' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "path": "/home/daytona/workspace/repo", "name": "old-feature" }' # Delete a branch curl '{toolboxProxyUrl}/{sandboxId}/git/branches' \ --request DELETE \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "path": "/home/daytona/workspace/repo", "name": "old-feature" }' ``` ## Stage changes [#stage-changes] Stage at least one file for the next commit. Paths are relative to the repository root. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.git.clone("https://github.com/user/repo.git", "/home/daytona/workspace/repo") sandbox.fs.write_text("/home/daytona/workspace/repo/README.md", "# Updated\n") sandbox.git.add("/home/daytona/workspace/repo", ["README.md"]) ``` ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.git.clone('https://github.com/user/repo.git', { path: '/home/daytona/workspace/repo' }); await sandbox.fs.writeText('/home/daytona/workspace/repo/README.md', '# Updated\n'); await sandbox.git.add('/home/daytona/workspace/repo', ['README.md']); ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/git/add' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "path": "/home/daytona/workspace/repo", "files": ["README.md"] }' ``` ## Commit changes [#commit-changes] Commit the staged changes. Every commit requires an author name and email. They apply to that commit only and are not written to the repository configuration, so the identity from [configure user](#configure-user) does not replace them. By default, a commit with nothing staged fails; allowing empty commits records a commit without staged changes. Repository hooks do not run for commits or for any other operation of the Git module. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.git.clone("https://github.com/user/repo.git", "/home/daytona/workspace/repo") sandbox.fs.write_text("/home/daytona/workspace/repo/README.md", "# Updated\n") # Stage and commit changes sandbox.git.add("/home/daytona/workspace/repo", ["README.md"]) commit = sandbox.git.commit( "/home/daytona/workspace/repo", "Update documentation", author="John Doe", email="john@example.com", ) print(f"Commit hash: {commit.hash}") ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ----------------------------------------------------------------------------- | | `author` | str | Sets the author name of the commit. This parameter is required. An empty value raises `BadRequestError` before the request is sent. | | `email` | str | Sets the author email of the commit. This parameter is required. An empty value raises `BadRequestError` before the request is sent. | | `allow_empty` | bool | Allows a commit with nothing staged. The default is `False`. | The response has the following fields: | **Field** | **Description** | | --- | --- | | `hash` | Holds the full SHA of the new commit. | ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.git.clone('https://github.com/user/repo.git', { path: '/home/daytona/workspace/repo' }); await sandbox.fs.writeText('/home/daytona/workspace/repo/README.md', '# Updated\n'); // Stage and commit changes await sandbox.git.add('/home/daytona/workspace/repo', ['README.md']); const commit = await sandbox.git.commit( '/home/daytona/workspace/repo', 'Update documentation', { author: 'John Doe', email: 'john@example.com' } ); console.log(`Commit hash: ${commit.hash}`); ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ---------------------------------------------------------------------------------------------- | | `author` | string | Sets the author name of the commit. This parameter is required. An empty value throws a `DaytonaError` with status `400` before the request is sent. | | `email` | string | Sets the author email of the commit. This parameter is required. An empty value throws a `DaytonaError` with status `400` before the request is sent. | | `allowEmpty` | boolean | Allows a commit with nothing staged. The default is `false`. | The response has the following fields: | **Field** | **Description** | | --- | --- | | `hash` | Holds the full SHA of the new commit. | ```bash # Stage changes; README.md must have uncommitted changes curl '{toolboxProxyUrl}/{sandboxId}/git/add' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "path": "/home/daytona/workspace/repo", "files": ["README.md"] }' # Commit changes curl '{toolboxProxyUrl}/{sandboxId}/git/commit' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "path": "/home/daytona/workspace/repo", "message": "Update documentation", "author": "John Doe", "email": "john@example.com" }' ``` | **Field** | **Type** | **Description** | | --------- | -------- | --------------------------------------- | | `message` | string | Sets the commit message. This field is required. An empty value returns `400`. | | `author` | string | Sets the author name of the commit. This field is required. An empty value returns `400`. | | `email` | string | Sets the author email of the commit. This field is required. An empty value returns `400`. | | `allow_empty` | boolean | Allows a commit with nothing staged. The default is `false`. | The response has the following fields: | **Field** | **Description** | | --- | --- | | `hash` | Holds the full SHA of the new commit. | ## Remote operations [#remote-operations] ### Push changes [#push-changes] Push the current branch to the remote named origin. The branch is pushed to the branch of the same name on the remote, so a new local branch does not need an upstream configured first. Push fails when the repository is on a detached HEAD, and when the remote branch has commits that the local branch does not have. For private repositories, see [authenticate credentials](#authenticate-credentials). ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.git.clone("https://github.com/user/repo.git", "/home/daytona/workspace/repo") sandbox.fs.write_text("/home/daytona/workspace/repo/README.md", "# Updated\n") sandbox.git.add("/home/daytona/workspace/repo", ["README.md"]) sandbox.git.commit( "/home/daytona/workspace/repo", "Update documentation", author="John Doe", email="john@example.com", ) sandbox.git.push("/home/daytona/workspace/repo") ``` | **Parameter** | **Type** | **Description** | | -------------- | -------- | --------------- | | `branch` | str | Sets the local branch to push to the branch of the same name on the remote. The default is the current branch. | | `remote` | str | Sets the name of a configured remote to push to. A URL, a path, or an unknown name raises `BadRequestError`. The default is `origin`. | | `set_upstream` | bool | Sets the pushed remote branch as the upstream of the local branch. The default is `False`. | | `username` | str | Sets the username for a private repository. It is used only together with `password`. See [authenticate credentials](#authenticate-credentials). | | `password` | str | Sets the password or personal access token for a private repository. It is used only together with `username`. | | `timeout` | float | Sets the time limit of the call in seconds. `0` removes this limit, but the sandbox still stops the Git command after 31 minutes and returns `408`. The default is `1800`. | ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.git.clone('https://github.com/user/repo.git', { path: '/home/daytona/workspace/repo' }); await sandbox.fs.writeText('/home/daytona/workspace/repo/README.md', '# Updated\n'); await sandbox.git.add('/home/daytona/workspace/repo', ['README.md']); await sandbox.git.commit( '/home/daytona/workspace/repo', 'Update documentation', { author: 'John Doe', email: 'john@example.com' } ); await sandbox.git.push('/home/daytona/workspace/repo'); ``` | **Parameter** | **Type** | **Description** | | ---------------- | -------- | --------------- | | `branch` | string | Sets the local branch to push to the branch of the same name on the remote. The default is the current branch. | | `remote` | string | Sets the name of a configured remote to push to. A URL, a path, or an unknown name throws a `DaytonaError` with status `400`. The default is `'origin'`. | | `setUpstream` | boolean | Sets the pushed remote branch as the upstream of the local branch. The default is `false`. | | `username` | string | Sets the username for a private repository. It is used only together with `password`. See [authenticate credentials](#authenticate-credentials). | | `password` | string | Sets the password or personal access token for a private repository. It is used only together with `username`. | | `timeoutSeconds` | number | Sets the time limit of the call in seconds. `0` removes this limit, but the sandbox still stops the Git command after 31 minutes and returns `408`. The default is `1800`. | ```bash curl '{toolboxProxyUrl}/{sandboxId}/git/push' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "path": "/home/daytona/workspace/repo" }' ``` | **Field** | **Type** | **Description** | | ------------- | -------- | --------------- | | `branch` | string | Sets the local branch to push to the branch of the same name on the remote. The default is the current branch. | | `remote` | string | Sets the name of a configured remote to push to. A URL, a path, or an unknown name returns `400`. The default is `origin`. | | `setUpstream` | boolean | Sets the pushed remote branch as the upstream of the local branch. The default is `false`. | | `username` | string | Sets the username for a private repository. It is used only together with `password`. See [authenticate credentials](#authenticate-credentials). | | `password` | string | Sets the password or personal access token for a private repository. It is used only together with `username`. | ### Pull changes [#pull-changes] Pull changes into the current branch from its upstream branch. Pull only fast-forwards: when the local and remote branches have diverged, pull fails with a conflict instead of creating a merge commit. For private repositories, see [authenticate credentials](#authenticate-credentials). ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.git.clone("https://github.com/user/repo.git", "/home/daytona/workspace/repo") sandbox.git.pull("/home/daytona/workspace/repo") ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `branch` | str | Sets the branch to pull. It must be the current branch; another branch raises `BadRequestError`. | | `remote` | str | Sets the name of a configured remote to pull from. A URL, a path, or an unknown name raises `BadRequestError`. Without `remote` and `branch`, pull uses the upstream of the current branch; with `branch` only, the default is `origin`. | | `username` | str | Sets the username for a private repository. It is used only together with `password`. See [authenticate credentials](#authenticate-credentials). | | `password` | str | Sets the password or personal access token for a private repository. It is used only together with `username`. | | `timeout` | float | Sets the time limit of the call in seconds. `0` removes this limit, but the sandbox still stops the Git command after 31 minutes and returns `408`. The default is `1800`. | ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.git.clone('https://github.com/user/repo.git', { path: '/home/daytona/workspace/repo' }); await sandbox.git.pull('/home/daytona/workspace/repo'); ``` | **Parameter** | **Type** | **Description** | | ---------------- | -------- | --------------- | | `branch` | string | Sets the branch to pull. It must be the current branch; another branch throws a `DaytonaError` with status `400`. | | `remote` | string | Sets the name of a configured remote to pull from. A URL, a path, or an unknown name throws a `DaytonaError` with status `400`. Without `remote` and `branch`, pull uses the upstream of the current branch; with `branch` only, the default is `'origin'`. | | `username` | string | Sets the username for a private repository. It is used only together with `password`. See [authenticate credentials](#authenticate-credentials). | | `password` | string | Sets the password or personal access token for a private repository. It is used only together with `username`. | | `timeoutSeconds` | number | Sets the time limit of the call in seconds. `0` removes this limit, but the sandbox still stops the Git command after 31 minutes and returns `408`. The default is `1800`. | ```bash curl '{toolboxProxyUrl}/{sandboxId}/git/pull' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "path": "/home/daytona/workspace/repo" }' ``` | **Field** | **Type** | **Description** | | ---------- | -------- | --------------- | | `branch` | string | Sets the branch to pull. It must be the current branch; another branch returns `400`. | | `remote` | string | Sets the name of a configured remote to pull from. A URL, a path, or an unknown name returns `400`. Without `remote` and `branch`, pull uses the upstream of the current branch; with `branch` only, the default is `origin`. | | `username` | string | Sets the username for a private repository. It is used only together with `password`. See [authenticate credentials](#authenticate-credentials). | | `password` | string | Sets the password or personal access token for a private repository. It is used only together with `username`. | ## Initialize a repository [#initialize-a-repository] Initialize a new repository at a path, optionally bare and with a named initial branch. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.git.init("/home/daytona/workspace/repo") ``` | **Parameter** | **Type** | **Description** | | ---------------- | -------- | --------------- | | `bare` | bool | Creates a bare repository, without a working tree. The default is `False`. | | `initial_branch` | str | Sets the name of the initial branch. The default is `init.defaultBranch` from the Git configuration of the image or the user, or `master` when neither sets it. | ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.git.init('/home/daytona/workspace/repo'); ``` | **Parameter** | **Type** | **Description** | | --------------- | -------- | --------------- | | `bare` | boolean | Creates a bare repository, without a working tree. The default is `false`. | | `initialBranch` | string | Sets the name of the initial branch. The default is `init.defaultBranch` from the Git configuration of the image or the user, or `master` when neither sets it. | ```bash curl '{toolboxProxyUrl}/{sandboxId}/git/init' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "path": "/home/daytona/workspace/repo" }' ``` | **Field** | **Type** | **Description** | | ---------------- | -------- | --------------- | | `bare` | boolean | Creates a bare repository, without a working tree. The default is `false`. | | `initial_branch` | string | Sets the name of the initial branch. The default is `init.defaultBranch` from the Git configuration of the image or the user, or `master` when neither sets it. | ## Reset changes [#reset-changes] Reset the current branch to a commit. The mode also controls index and working tree resets. The target defaults to the current commit, so resetting only files unstages them. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.git.clone("https://github.com/user/repo.git", "/home/daytona/workspace/repo") # Reset HEAD and the index to the previous commit, keep the working tree sandbox.git.reset("/home/daytona/workspace/repo", target="HEAD~1") ``` | **Parameter** | **Type** | **Description** | | ------------- | --------------- | ------------------------------------------------------------------- | | `mode` | str | Sets the reset mode: `mixed`, `soft`, `hard`, `merge`, or `keep`. The default is `mixed`. | | `target` | str | Sets the commit to reset to. The default is the current commit. | | `files` | Sequence\[str\] | Sets the files to reset. Files are accepted only in `mixed` mode. Other modes reject files with `400`. | ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.git.clone('https://github.com/user/repo.git', { path: '/home/daytona/workspace/repo' }); // Reset HEAD and the index to the previous commit, keep the working tree await sandbox.git.reset('/home/daytona/workspace/repo', { target: 'HEAD~1' }); ``` | **Parameter** | **Type** | **Description** | | ------------- | ---------- | -------------------------------------------------------------------------- | | `mode` | string | Sets the reset mode: `'mixed'`, `'soft'`, `'hard'`, `'merge'`, or `'keep'`. The default is `'mixed'`. | | `target` | string | Sets the commit to reset to. The default is the current commit. | | `files` | string\[\] | Sets the files to reset. Files are accepted only in `'mixed'` mode. Other modes reject files with `400`. | ```bash curl '{toolboxProxyUrl}/{sandboxId}/git/reset' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "path": "/home/daytona/workspace/repo", "target": "HEAD~1" }' ``` | **Field** | **Type** | **Description** | | --------- | ---------- | ------------------------------------------------------------------- | | `mode` | string | Sets the reset mode: `mixed`, `soft`, `hard`, `merge`, or `keep`. The default is `mixed`. | | `target` | string | Sets the commit to reset to. The default is the current commit. | | `files` | string\[\] | Sets the files to reset. Files are accepted only in `mixed` mode. Other modes reject files with `400`. | ## Restore files [#restore-files] Restore files in the working tree, the index, or both; at least one file is required. Restoring the working tree discards uncommitted changes; restoring the index unstages them. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.git.clone("https://github.com/user/repo.git", "/home/daytona/workspace/repo") sandbox.fs.write_text("/home/daytona/workspace/repo/README.md", "# Updated\n") # Restore a file in the working tree sandbox.git.restore("/home/daytona/workspace/repo", ["README.md"]) ``` | **Parameter** | **Type** | **Description** | | ------------- | --------------- | ------------------------------------------------------------------------------------------------------ | | `files` | Sequence\[str\] | Sets the files to restore. This parameter is required and must list at least one file. | | `staged` | bool | Restores the index entry. | | `worktree` | bool | Restores the working tree file. This is the default when neither `staged` nor `worktree` is set. | | `source` | str | Sets the commit, branch, or tag to restore from. By default, the working tree restores from the index, and the index restores from `HEAD`. | ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.git.clone('https://github.com/user/repo.git', { path: '/home/daytona/workspace/repo' }); await sandbox.fs.writeText('/home/daytona/workspace/repo/README.md', '# Updated\n'); // Restore a file in the working tree await sandbox.git.restore('/home/daytona/workspace/repo', ['README.md']); ``` | **Parameter** | **Type** | **Description** | | ------------- | ---------- | ------------------------------------------------------------------------------------------------------ | | `files` | string\[\] | Sets the files to restore. This parameter is required and must list at least one file. | | `staged` | boolean | Restores the index entry. | | `worktree` | boolean | Restores the working tree file. This is the default when neither `staged` nor `worktree` is set. | | `source` | string | Sets the commit, branch, or tag to restore from. By default, the working tree restores from the index, and the index restores from `HEAD`. | ```bash curl '{toolboxProxyUrl}/{sandboxId}/git/restore' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "path": "/home/daytona/workspace/repo", "files": ["README.md"] }' ``` | **Field** | **Type** | **Description** | | ---------- | ---------- | ------------------------------------------------------------------------------------------------------ | | `files` | string\[\] | Sets the files to restore. This parameter is required and must list at least one file. | | `staged` | boolean | Restores the index entry. | | `worktree` | boolean | Restores the working tree file. This is the default when neither `staged` nor `worktree` is set. | | `source` | string | Sets the commit, branch, or tag to restore from. By default, the working tree restores from the index, and the index restores from `HEAD`. | ## Get commit history [#get-commit-history] Get the commit history of the current branch: every commit reachable from the current commit, newest first, in one response. Each entry carries the full commit hash, the first line of the message, the author name and email, and the author timestamp in RFC 3339 format. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.git.clone("https://github.com/user/repo.git", "/home/daytona/workspace/repo") # Commits are returned newest first history = sandbox.git.history("/home/daytona/workspace/repo") for commit in history.items: print(f"{commit.hash}: {commit.message}") print(f" {commit.author} <{commit.email}> at {commit.timestamp}") ``` The response has the following fields: | **Field** | **Description** | | --- | --- | | `items` | Holds the commits, newest first. Each commit has `hash`, `message`, `author`, `email`, and `timestamp`. | | `timestamp` | Holds the author time of a commit as a timezone-aware `datetime`. | ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.git.clone('https://github.com/user/repo.git', { path: '/home/daytona/workspace/repo' }); // Commits are returned newest first const history = await sandbox.git.history('/home/daytona/workspace/repo'); history.items.forEach(commit => { console.log(`${commit.hash}: ${commit.message}`); console.log(` ${commit.author} <${commit.email}> at ${commit.timestamp}`); }); ``` The response has the following fields: | **Field** | **Description** | | --- | --- | | `items` | Holds the commits, newest first. Each commit has `hash`, `message`, `author`, `email`, and `timestamp`. | | `timestamp` | Holds the author time of a commit as an RFC 3339 string. | ```bash curl '{toolboxProxyUrl}/{sandboxId}/git/history?path=/home/daytona/workspace/repo' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ```json [ { "hash": "3f2a9c1e8b7d6a5f4e3d2c1b0a9f8e7d6c5b4a39", "message": "Update documentation", "author": "John Doe", "email": "john@example.com", "timestamp": "2026-09-13T09:41:12+00:00" } ] ``` ## Manage remotes [#manage-remotes] Add a remote to a repository and list the configured remotes. Listed URLs mask embedded credentials with asterisks. [Push](#push-changes) targets the remote named origin unless the call names another remote. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.git.init("/home/daytona/workspace/repo") # Add a remote sandbox.git.add_remote("/home/daytona/workspace/repo", "origin", "https://github.com/user/repo.git") # List remotes remotes = sandbox.git.list_remotes("/home/daytona/workspace/repo") for remote in remotes.items: print(f"{remote.name}: {remote.url}") ``` ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.git.init('/home/daytona/workspace/repo'); // Add a remote await sandbox.git.addRemote('/home/daytona/workspace/repo', 'origin', 'https://github.com/user/repo.git'); // List remotes const remotes = await sandbox.git.listRemotes('/home/daytona/workspace/repo'); remotes.items.forEach(remote => { console.log(`${remote.name}: ${remote.url}`); }); ``` ```bash # Initialize a repository curl '{toolboxProxyUrl}/{sandboxId}/git/init' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "path": "/home/daytona/workspace/repo" }' # Add a remote curl '{toolboxProxyUrl}/{sandboxId}/git/remotes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "name": "origin", "path": "/home/daytona/workspace/repo", "url": "https://github.com/user/repo.git" }' # List remotes curl '{toolboxProxyUrl}/{sandboxId}/git/remotes?path=/home/daytona/workspace/repo' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ```json { "remotes": [ { "name": "origin", "url": "https://github.com/user/repo.git" } ] } ``` ## Configure Git [#configure-git] Set and get Git configuration values. Global values apply to every repository for the user that Git runs as in the sandbox: the sandbox default user, or root when no default user is set. Local values apply to one repository. A key must contain a section and a name separated by a dot. Getting a key that is not set returns no value instead of an error. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() # Set a config value at the global scope sandbox.git.set_config("core.editor", "vim") # Get a config value (None when unset) editor = sandbox.git.get_config("core.editor") ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------------------------------------------------- | | `scope` | str | Sets the configuration scope, `global` or `local`. The default is `global`. | | `path` | str | Sets the repository path. This parameter is required when the scope is `local` and ignored for `global`. | ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); // Set a config value at the global scope await sandbox.git.setConfig('core.editor', 'vim'); // Get a config value (null when unset) const editor = await sandbox.git.getConfig('core.editor'); ``` | **Parameter** | **Type** | **Description** | | ------------- | ---------------- | ------------------------------------------------------------- | | `scope` | GitConfigScope | Sets the configuration scope, `'global'` or `'local'`. The default is `'global'`. | | `path` | string | Sets the repository path. This parameter is required when the scope is `'local'` and ignored for `'global'`. | ```bash # Set a config value curl '{toolboxProxyUrl}/{sandboxId}/git/config' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "key": "core.editor", "value": "vim", "scope": "global" }' # Get a config value curl '{toolboxProxyUrl}/{sandboxId}/git/config?key=core.editor&scope=global' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Type** | **Description** | | --------- | -------- | --------------------------------------------------------- | | `scope` | string | Sets the configuration scope, `global` or `local`. The default is `global`. | | `path` | string | Sets the repository path. This field is required when the scope is `local` and ignored for `global`. | The response has the following fields: | **Field** | **Description** | | --- | --- | | `value` | Holds the configuration value that a get returns. The value is `null` when the key is not set. | ## Configure user [#configure-user] Set the user name and email that Git records in commits made with Git commands inside the sandbox, such as commits from processes you run. Commits made through the Git module take the author name and email from each [commit](#commit-changes) call instead. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() # Configure the global Git user sandbox.git.set_config("user.name", "John Doe") sandbox.git.set_config("user.email", "john@example.com") ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------------------------------------------------- | | `scope` | str | Sets the configuration scope, `global` or `local`. The default is `global`. | | `path` | str | Sets the repository path. This parameter is required when the scope is `local` and ignored for `global`. | ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); // Configure the global Git user await sandbox.git.setConfig('user.name', 'John Doe'); await sandbox.git.setConfig('user.email', 'john@example.com'); ``` | **Parameter** | **Type** | **Description** | | ------------- | ---------------- | ------------------------------------------------------------- | | `scope` | GitConfigScope | Sets the configuration scope, `'global'` or `'local'`. The default is `'global'`. | | `path` | string | Sets the repository path. This parameter is required when the scope is `'local'` and ignored for `'global'`. | ```bash curl '{toolboxProxyUrl}/{sandboxId}/git/config' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "key": "user.name", "value": "John Doe", "scope": "global" }' curl '{toolboxProxyUrl}/{sandboxId}/git/config' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "key": "user.email", "value": "john@example.com", "scope": "global" }' ``` | **Field** | **Type** | **Description** | | --------- | -------- | --------------------------------------------------------- | | `scope` | string | Sets the configuration scope, `global` or `local`. The default is `global`. | | `path` | string | Sets the repository path. This field is required when the scope is `local` and ignored for `global`. | ## Authenticate credentials [#authenticate-credentials] Authenticate clone, push, and pull against private repositories with a username and a personal access token as the password. Credentials apply only to their call; they are not written to the repository configuration or remote URL and do not appear on the Git command line. Credentials are used only when both a username and a password are set. Otherwise, Git runs without credentials and does not prompt, so operations on a private repository fail. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.git.clone( "https://github.com/user/repo.git", "/home/daytona/workspace/repo", username="git", password="personal_access_token", ) sandbox.git.push( "/home/daytona/workspace/repo", username="git", password="personal_access_token" ) ``` ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.git.clone('https://github.com/user/repo.git', { path: '/home/daytona/workspace/repo', username: 'git', password: 'personal_access_token', }); await sandbox.git.push('/home/daytona/workspace/repo', { username: 'git', password: 'personal_access_token' }); ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/git/push' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "path": "/home/daytona/workspace/repo", "username": "git", "password": "personal_access_token" }' ``` ## Permissions [#permissions] Git access runs through the toolbox of a sandbox. Access is governed by one project permission, which you can grant to an [organization role](/docs/organizations#roles) or an [API key](/docs/api-keys). A [sandbox access token](/docs/sandboxes#create-an-access-token) with the toolbox:git operation grants the same access to a single sandbox, without an API key. The sandbox read permission alone does not grant toolbox access. | **Permission** | **Grants** | | --------------------- | ---------------------------------------------------------- | | **`write:sandboxes`** | Run every Git operation, including clone, status, commit, push, pull, and branch management | # Language Server Protocol (/docs/language-server-protocol) Language Server Protocol (LSP) support is available through LSP server instances created from a sandbox. A language server runs inside the sandbox and analyzes the project's code, so your application gets IDE-grade code intelligence for code that never leaves the sandbox. An LSP server covers code completions, file open and close notifications, document symbols, and project-wide symbol search. Each server instance is scoped to one language and one project directory. Python and TypeScript are the supported language IDs; the matching server binary (pylsp or typescript-language-server) must be installed in the sandbox image, otherwise starting the server fails with an LSP server unavailable error. ## Use cases [#use-cases] LSP servers are used when an application needs code intelligence for code that stays in a sandbox. - **Coding agents.** An agent searches workspace symbols to find where a function or class is defined before it edits code. Each match carries the file URI and the line range, so the agent opens the right file at the right line. The query goes to the language server as is, so the matching rules are those of the server for the language. - **Browser-based editors.** An editor in a web application shows completions and an outline of the open file. The language server computes both in the sandbox, next to the code. Each file is read once, when it is opened, and later changes on disk are not sent to the server. - **Completions for generated code.** An agent or an editor requests completions at a position to fill in names that exist in the project. A trigger character, such as a dot, tells the server how the completion started. Positions are zero-based, and character offsets count UTF-16 code units within the line. - **Code that stays in the sandbox.** The language server reads files from the sandbox filesystem. Your application gets code intelligence without copying the source code out of the sandbox. Daytona does not install language servers, so the server binary must be in the sandbox image and on its path. - **Projects with several languages.** Each server covers one language and one project directory. A sandbox runs a Python server for one directory and a TypeScript server for another. The TypeScript server also covers JavaScript. ## Workflow [#workflow] Follow this order when using LSP in a sandbox: 1. Start a server for a language ID and a project path 2. Open a file on the server before requesting its completions or symbols 3. Request completions, document symbols, or workspace symbols from the server 4. Close the file when you finish with it 5. Stop the server when it is no longer needed ## Create LSP servers [#create-lsp-servers] Create a sandbox whose image includes the language server, then start the server for a project directory. Daytona does not install language servers: the binary must be in the image and on its path. Create the project directory in the image or before starting the server. ```python from daytona import Daytona from daytona.v2 import ImageBuilder # Create a sandbox from an image that ships pylsp daytona = Daytona() sandbox = daytona.v2.create_from_build( ImageBuilder.from_dockerfile( "FROM python:3.12-slim\n" "RUN pip install --no-cache-dir python-lsp-server\n" "RUN mkdir -p /workspace/project\n" ) ) # Write a file for the examples on this page sandbox.fs.write_text( "/workspace/project/main.py", "class MyClass:\n def method_one(self):\n return 1\n\nMyClass().\n", ) # Start the Python language server for the project server = sandbox.lsp.start("python", "/workspace/project") ``` | **Parameter** | **Type** | **Description** | | ----------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- | | `language_id` | str | Sets the language of the server: `"python"` or `"typescript"`. `"python"` runs `pylsp`. `"typescript"` runs `typescript-language-server` for TypeScript and JavaScript. | | `path_to_project` | str | Sets the project directory. | - The request requires the `write:sandboxes` permission. ```typescript import { Daytona } from '@daytona/sdk' import { ImageBuilder } from '@daytona/sdk/v2' // Create a sandbox from an image that ships typescript-language-server const daytona = new Daytona() const sandbox = await daytona.v2.createFromBuild( ImageBuilder.fromDockerfile( 'FROM node:20\nRUN npm install -g typescript@5 typescript-language-server\nRUN mkdir -p /workspace/project\n', ), ) // Write a file for the examples on this page await sandbox.fs.writeText( '/workspace/project/main.ts', 'class MyClass {\n methodOne(): number {\n return 1\n }\n}\n\nnew MyClass().\n', ) // Start the TypeScript language server for the project const server = await sandbox.lsp.start('typescript', '/workspace/project') ``` | **Parameter** | **Type** | **Description** | | --------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- | | `languageId` | string | Sets the language of the server: `'python'` or `'typescript'`. `'python'` runs `pylsp`. `'typescript'` runs `typescript-language-server` for TypeScript and JavaScript. | | `pathToProject` | string | Sets the project directory. | - The request requires the `write:sandboxes` permission. - The image needs TypeScript 5: `typescript-language-server` does not start with TypeScript 7. ## Start LSP servers [#start-lsp-servers] Start a language server for a language and a project directory. Starting a server for a language and directory that already has a running server reuses that server. The other operations never start a server implicitly; until the server is started, they fail with a not started error. ```python server = sandbox.lsp.start("python", "/workspace/project") # Now ready for LSP operations ``` | **Parameter** | **Type** | **Description** | | ----------------- | -------- | ------------------------------------------------------------------------------------- | | `path_to_project` | str | Sets the absolute path of the project directory. The server uses the path as its workspace root. | ```typescript const server = await sandbox.lsp.start('typescript', '/workspace/project') // Now ready for LSP operations ``` | **Parameter** | **Type** | **Description** | | --------------- | -------- | ------------------------------------------------------------------------------------- | | `pathToProject` | string | Sets the absolute path of the project directory. The server uses the path as its workspace root. | ```bash curl '{toolboxProxyUrl}/{sandboxId}/lsp/start' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "languageId": "python", "pathToProject": "/workspace/project" }' ``` | **Field** | **Type** | **Description** | | --------------- | -------- | ----------------------------------------------------------------------------------------- | | `languageId` | string | Sets the language of the server: `python` or `typescript`. This field is required. | | `pathToProject` | string | Sets the absolute path of the project directory. The server uses the path as its workspace root. This field is required. | - The request requires the `write:sandboxes` permission. - `{toolboxProxyUrl}` is the `toolbox_proxy_url` field of the [sandbox](/docs/sandboxes#get-a-sandbox). ## Stop LSP servers [#stop-lsp-servers] Stop a language server. Stopping sends the server a shutdown request and ends its process, which frees one of the sandbox's server slots. A sandbox runs at most 16 language servers at once. ```python # When done with LSP features server.stop() ``` ```typescript // When done with LSP features await server.stop() ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/lsp/stop' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "languageId": "python", "pathToProject": "/workspace/project" }' ``` ## Code completions [#code-completions] Get completion candidates at a position in an open file, computed from the content sent when the file was opened. Character offsets count UTF-16 code units within the line. ```python completions = server.completions( "file:///workspace/project/main.py", {"line": 4, "character": 10}, ) for item in completions.items: print(f"Completion: {item.label}") ``` - The request requires the `write:sandboxes` permission. - Position values are zero-based, so `line: 0` is the first line. - Call `open_document()` on the file before requesting completions. ```typescript const completions = await server.completions( 'file:///workspace/project/main.ts', { line: 6, character: 14, }, ) completions.items.forEach((item) => { console.log(`Completion: ${item.label}`) }) ``` | **Parameter** | **Type** | **Description** | | ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------- | | `position` | LspPosition | Sets the position in the file. Position values are zero-based, so `line: 0` is the first line. `line` and `character` must be non-negative integers. Other values throw `BadRequestError` before the request is sent. | - The request requires the `write:sandboxes` permission. - Call `openDocument()` on the file before requesting completions. ```bash curl '{toolboxProxyUrl}/{sandboxId}/lsp/completions' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "languageId": "python", "pathToProject": "/workspace/project", "position": { "character": 10, "line": 4 }, "uri": "file:///workspace/project/main.py" }' ``` - The request requires the `write:sandboxes` permission. - Position values are zero-based, so `line: 0` is the first line. - Call `POST /lsp/did-open` on the file before requesting completions. ### Completion context [#completion-context] Pass a completion context to tell the language server how the completion was triggered. The trigger kind is the completion trigger kind number from the Language Server Protocol specification. Set the trigger character when a typed character, such as a dot, opened the completion. Without a context, the server receives none. ```python from daytona.v2 import LspCompletionContext completions = server.completions( "file:///workspace/project/main.py", {"line": 4, "character": 10}, context=LspCompletionContext(trigger_kind=2, trigger_character="."), ) ``` ```typescript const completions = await server.completions( 'file:///workspace/project/main.ts', { line: 6, character: 14 }, { context: { triggerKind: 2, triggerCharacter: '.' } }, ) ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/lsp/completions' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "languageId": "python", "pathToProject": "/workspace/project", "uri": "file:///workspace/project/main.py", "position": { "character": 10, "line": 4 }, "context": { "triggerKind": 2, "triggerCharacter": "." } }' ``` ### Completion item fields [#completion-item-fields] | **Field** | **Description** | | ------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `label` | Text shown for the item. Always present. | | `kind` | LSP `CompletionItemKind` number, for example `3` for a function or `6` for a variable. | | `detail` | Extra information such as a type signature. | | `documentation` | Either a plain string or a `MarkupContent` object with `kind` (`plaintext` or `markdown`) and `value`. | | `sortText` | String to sort by instead of `label`. The Python SDK names it `sort_text`. | | `filterText` | String to filter by instead of `label`. The Python SDK names it `filter_text`. | | `insertText` | Text to insert instead of `label`. The Python SDK names it `insert_text`. | ## File notifications [#file-notifications] ### Open file [#open-file] Open a file on the language server. The file's content is read from the sandbox filesystem and sent to the server, which works from it: later changes on disk are not sent. ```python # Notify server that a file is open server.open_document("file:///workspace/project/main.py") ``` - The request requires the `write:sandboxes` permission. - The file must exist in the sandbox and be at most 1 MiB. ```typescript // Notify server that a file is open await server.openDocument('file:///workspace/project/main.ts') ``` - The request requires the `write:sandboxes` permission. - The file must exist in the sandbox and be at most 1 MiB. ```bash curl '{toolboxProxyUrl}/{sandboxId}/lsp/did-open' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "languageId": "python", "pathToProject": "/workspace/project", "uri": "file:///workspace/project/main.py" }' ``` - The request requires the `write:sandboxes` permission. - The file must exist in the sandbox and be at most 1 MiB. ### Close file [#close-file] ```python # Notify server that a file is closed server.close_document("file:///workspace/project/main.py") ``` ```typescript // Notify server that a file is closed await server.closeDocument('file:///workspace/project/main.ts') ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/lsp/did-close' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "languageId": "python", "pathToProject": "/workspace/project", "uri": "file:///workspace/project/main.py" }' ``` ## Document symbols [#document-symbols] ```python symbols = server.document_symbols("file:///workspace/project/main.py") for symbol in symbols: print(f"Symbol: {symbol.name}, Kind: {symbol.kind}") ``` ```typescript const symbols = await server.documentSymbols('file:///workspace/project/main.ts') symbols.forEach((symbol) => { console.log(`Symbol: ${symbol.name}, Kind: ${symbol.kind}`) }) ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/lsp/document-symbols?languageId=python&pathToProject=/workspace/project&uri=file:///workspace/project/main.py' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Workspace symbols [#workspace-symbols] Search the project for symbols that match a query string. The query is passed to the language server as is, so the matching rules are those of the server for the language. ```python # pylsp does not support workspace symbols: start a typescript server, in a sandbox # whose image ships typescript-language-server as in the TypeScript tab of Create LSP servers ts_server = sandbox.lsp.start("typescript", "/workspace/project") ts_server.open_document("file:///workspace/project/main.ts") symbols = ts_server.workspace_symbols("MyClass") for symbol in symbols: print(f"Found: {symbol.name} in {symbol.location.uri}") ``` - The request requires the `write:sandboxes` permission. - The server must be a `typescript` server: `pylsp` does not support workspace symbols. - Open a file of the project on the server before searching. ```typescript const symbols = await server.workspaceSymbols('MyClass') symbols.forEach((symbol) => { console.log(`Found: ${symbol.name} in ${symbol.location.uri}`) }) ``` - The request requires the `write:sandboxes` permission. - The server must be a `typescript` server: `pylsp` does not support workspace symbols. - Open a file of the project on the server before searching. ```bash curl '{toolboxProxyUrl}/{sandboxId}/lsp/workspacesymbols?query=MyClass&languageId=typescript&pathToProject=/workspace/project' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` - The request requires the `write:sandboxes` permission. - The server must be a `typescript` server: `pylsp` does not support workspace symbols. - Open a file of the project on the server before searching. ## Symbol fields [#symbol-fields] Document symbols and workspace symbols return the same flat symbol shape. Hierarchical document symbol answers from the language server are reduced to the same three fields: nested children are dropped, and a symbol without a location gets an empty URI and a zero range. | **Field** | **Description** | | ---------------------- | ---------------------------------------------------------------------------------------------------------- | | `name` | Symbol name. | | `kind` | LSP `SymbolKind` number, for example `5` for a class, `6` for a method, `12` for a function. | | `location.uri` | `file://` URI of the document that declares the symbol. | | `location.range` | `start` and `end` positions, each with zero-based `line` and `character`. | ## Errors [#errors] The SDKs raise these errors with the codes in the following table, and the Toolbox API returns them in the `code` field. An error that the language server returns for a request reaches you as `BAD_REQUEST`, with the server's message after the name of the LSP method. | **Code** | **Meaning** | **What to do** | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | **`LSP_SERVER_UNAVAILABLE`** | The image has no binary for the language server, the binary could not run, or 16 language servers already run in the sandbox. | Install the server in the image, or stop a server you no longer need. | | **`LSP_NOT_STARTED`** | No server runs for the language and project directory: it was not started, it was stopped, or it exited. | Start the server. | | **`BAD_REQUEST`** | The language ID is not `python` or `typescript` (a stop request returns `LSP_NOT_STARTED` instead), a required query parameter is missing, the file to open cannot be read or is larger than 1 MiB, or the language server answered with an error. | Check the language ID and the file. For a language server error, read the message: it names the LSP method. | | **`REQUEST_TIMEOUT`** | The language server did not answer within 30 seconds. | Retry the request, or stop and start the server. | | **`UNPROCESSABLE_ENTITY`** | The API request body is missing a required field or has a value of the wrong type, such as a negative position. The SDKs check positions before they send the request. | Fix the request body. | ## Permissions [#permissions] Language server access runs through the toolbox of a sandbox. Access is governed by one project permission, which you can grant to an [organization role](/docs/organizations#roles) or an [API key](/docs/api-keys). A [sandbox access token](/docs/sandboxes#create-an-access-token) with the toolbox:lsp operation grants the same access to a single sandbox, without an API key. The sandbox read permission alone does not grant toolbox access. | **Permission** | **Grants** | | --------------------- | ------------------------------------------------------------------- | | **`write:sandboxes`** | Start and stop language servers and request completions and symbols | # Code Execution (/docs/code-execution) Daytona runs Python, TypeScript, and JavaScript code in sandboxes through language kernels, so untrusted or generated code executes in isolation from your application. Every run is a code process that executes in a session with a language kernel. The kernel keeps state between runs in the same session, so variables, imports, and functions defined in one run are available in the next. Runs use a shared kernel per language by default; isolated runs and interpreter contexts give code its own kernel. To run shell commands and manage long-running processes, see [process execution](/docs/process-execution). To follow a run's output live, see [log streaming](/docs/log-streaming). ## Use cases [#use-cases] Code execution is used when an application runs Python, TypeScript, or JavaScript that it did not write or does not trust. - **AI-generated code.** An agent writes code, runs it in the sandbox, and reads the output and errors to correct its next attempt. The code runs isolated from your application. Each result also holds the exit status of the process. - **Data analysis.** The kernel keeps data, imports, and functions in memory between runs, so each step builds on the previous one, as in a notebook. Runs without a context share one kernel per language, so later runs in that language see the same data. The first run in a language installs the kernel, so that run can take longer than later ones. - **Charts and tables.** Runs that plot or display data return charts and tables as artifacts with the result. Each artifact is keyed by MIME type, so an application renders it in the right format. A flag on each result tells the value of the last expression apart from displayed outputs. - **Code interpreters for users.** Each user or conversation gets its own interpreter context, so state from one does not reach another. A context has its own kernel, working directory, and environment. Its kernel uses memory even while idle, until the application [deletes the context](#delete-a-context). - **One-off runs.** An isolated kernel runs code with no state from earlier runs. The session is deleted after the run, so nothing the code defines reaches other runs. An isolated run cannot be combined with an interpreter context. ## Run code [#run-code] Run code in a sandbox. Runs without a context share one kernel per language, so state from one run is available to later runs in that language. The default language is Python. The result contains the standard output and standard error, rich outputs, the exception if the code raised one, and the exit status of the process. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() response = sandbox.run_code(''' def greet(name): return f"Hello, {name}!" print(greet("Daytona")) ''') print(response.logs.stdout) ``` | **Parameter** | **Type** | **Description** | | ------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------- | | `code` | str | Sets the code to run. This parameter is required. | | `language` | str | Sets the language: `python`, `typescript`, or `javascript`. The default is `python`. | | `context` | InterpreterContext | Runs the code in an [interpreter context](#interpreter-contexts) instead of the shared kernel. | | `isolated` | bool | Runs the code in a [temporary kernel](#run-code-in-an-isolated-kernel). The default is `False`. | | `cwd` | str | Sets the working directory of the shared kernel. Only the run that starts the kernel sets it, and later runs ignore it. To run code in another directory, use an [interpreter context](#interpreter-contexts) or an [isolated run](#run-code-in-an-isolated-kernel). | | `env` | Mapping\[str, str\] | Sets environment variables for the shared kernel. Only the run that starts the kernel sets them. A later run that passes different variables raises an error with code `UNSUPPORTED_OPERATION`. | | `timeout` | float | Sets how many seconds the client waits. On timeout, the kernel is interrupted and the result of the run is returned with exit code `1`; a run still queued behind another run on the same kernel returns reason `timed_out` without an exit code. The kernel keeps its state. `0` means no limit. The default is no limit. | The response has the following fields: | **Field** | **Description** | | --------- | --------------- | | `logs` | Holds the standard output and standard error. | | `results` | Holds the rich outputs. | | `error` | Holds the exception, if the code raised one. | | `process` | Holds the exit status of the process: `0` when the code finished, and `1` when it raised an exception or was interrupted. If the kernel stopped during the run, the reason is `lost` and the next run starts a new kernel without the earlier state. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const response = await sandbox.runCode(` def greet(name): return f"Hello, {name}!" print(greet("Daytona")) `) console.log(response.logs.stdout) ``` | **Parameter** | **Type** | **Description** | | ------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------- | | `code` | string | Sets the code to run. This parameter is required. | | `language` | string | Sets the language: `'python'`, `'typescript'`, or `'javascript'`. The default is `'python'`. | | `context` | InterpreterContextReference \| InterpreterContextJSON | Runs the code in an [interpreter context](#interpreter-contexts) instead of the shared kernel. | | `isolated` | boolean | Runs the code in a [temporary kernel](#run-code-in-an-isolated-kernel). The default is `false`. | | `cwd` | string | Sets the working directory of the shared kernel. Only the run that starts the kernel sets it, and later runs ignore it. To run code in another directory, use an [interpreter context](#interpreter-contexts) or an [isolated run](#run-code-in-an-isolated-kernel). | | `env` | Record\ | Sets environment variables for the shared kernel. Only the run that starts the kernel sets them. A later run that passes different variables raises an error with code `UNSUPPORTED_OPERATION`. | | `timeoutSeconds` | number | Sets how many seconds the client waits. On timeout, the kernel is interrupted and the result of the run is returned with exit code `1`; a run still queued behind another run on the same kernel returns reason `timed_out` without an exit code. The kernel keeps its state. `0` means no limit. The default is no limit. | The response has the following fields: | **Field** | **Description** | | --------- | --------------- | | `logs` | Holds the standard output and standard error. | | `results` | Holds the rich outputs. | | `error` | Holds the exception, if the code raised one. | | `process` | Holds the exit status of the process: `0` when the code finished, and `1` when it raised an exception or was interrupted. If the kernel stopped during the run, the reason is `lost` and the next run starts a new kernel without the earlier state. | ```bash # Without sessionId the code runs on the shared per-language session "code-python" # waitMs holds the response for up to that many milliseconds while the process runs (capped at 600000). logs=text attaches the first log page. curl '{toolboxProxyUrl}/{sandboxId}/processes?waitMs=5000&logs=text' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "kind": "code", "language": "python", "code": "def greet(name):\n return f\"Hello, {name}!\"\n\nprint(greet(\"Daytona\"))" }' ``` | **Field** | **Type** | **Description** | | ----------- | -------- | ---------------------------------------------------------------------------------------------- | | `kind` | string | Sets the process kind. Use `code` for a code run. | | `language` | string | Sets the language: `python`, `typescript`, or `javascript`. With `sessionId`, the language must match the context, otherwise the request returns `BAD_REQUEST`. This field is required without `sessionId`. | | `code` | string | Sets the code to run. | | `sessionId` | string | Runs the code in an [interpreter context](#interpreter-contexts). Without `sessionId`, the code runs on the shared per-language session. | | `cwd` | string | Sets the working directory of the shared kernel. Only the run that starts the kernel sets it, and later runs ignore it. | | `env` | object | Sets environment variables for the shared kernel. Only the run that starts the kernel sets them. A later run that passes different variables returns `UNSUPPORTED_OPERATION`. Cannot be combined with `sessionId`. | The response has the following fields: | **Field** | **Description** | | --------- | --------------- | | `state` | The state of the process: `starting`, `provisioning`, `running`, or `terminal`. The first run in a language installs and starts the kernel. If that takes longer than `waitMs`, the state is not yet `terminal`. Wait with `GET /processes/{processId}/wait`, then read the logs. | - The request requires the `write:sandboxes` permission. - `{toolboxProxyUrl}` is the `toolbox_proxy_url` field of the [sandbox](/docs/sandboxes#get-a-sandbox). ### Artifacts [#artifacts] Rich outputs such as charts and tables are returned with the result, keyed by MIME type. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.process.run("pip install matplotlib numpy") code = ''' import matplotlib.pyplot as plt import numpy as np x = np.linspace(0, 10, 30) plt.plot(x, np.sin(x)) plt.title("Sine wave") plt.show() ''' response = sandbox.run_code(code) if response.error: print(response.error.name, response.error.value) for result in response.results: print(list(result.data)) # MIME types, for example ['image/png', 'text/plain'] ``` The response has the following fields: | **Field** | **Description** | | --------- | --------------- | | `results` | Holds the rich outputs. | | `text`, `html`, `png`, `svg`, `json` | Each of these fields of a result mirrors the matching MIME type in `data`. | | `is_main_result` | Is `True` for the value of the last expression and `False` for displayed outputs. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.process.run('pip install matplotlib numpy') const response = await sandbox.runCode(` import matplotlib.pyplot as plt import numpy as np x = np.linspace(0, 10, 30) plt.plot(x, np.sin(x)) plt.title("Sine wave") plt.show() `) if (response.error) { console.log(response.error.name, response.error.value) } for (const result of response.results) { console.log(Object.keys(result.data)) // MIME types, for example ['image/png', 'text/plain'] } ``` The response has the following fields: | **Field** | **Description** | | --------- | --------------- | | `results` | Holds the rich outputs. | | `text`, `html`, `png`, `svg`, `json` | Each of these fields of a result mirrors the matching MIME type in `data`. | | `isMainResult` | Is `true` for the value of the last expression and `false` for displayed outputs. | ```bash # Install the packages the code imports curl '{toolboxProxyUrl}/{sandboxId}/processes?waitMs=60000' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"kind": "exec", "shellCommand": "pip install matplotlib numpy"}' # When the install is not terminal yet, wait until it exits; timeoutMs=0 waits without a limit curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}/wait?timeoutMs=0' \ --header 'Authorization: Bearer YOUR_API_KEY' # Rich outputs arrive as log frames on the "display" and "execute_result" channels. # Each frame's data is a JSON MIME bundle, for example {"image/png": "...", "text/plain": "..."}. curl '{toolboxProxyUrl}/{sandboxId}/processes?waitMs=60000&logs=text' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "kind": "code", "language": "python", "code": "import matplotlib.pyplot as plt\nimport numpy as np\n\nx = np.linspace(0, 10, 30)\nplt.plot(x, np.sin(x))\nplt.title(\"Sine wave\")\nplt.show()" }' ``` ## Run code in an isolated kernel [#run-code-in-an-isolated-kernel] Run code in a temporary kernel that starts with no state. Daytona creates a session with a new kernel, runs the code, and deletes the session after the run. Nothing the code defines is visible to other runs, and the code does not see state from the shared kernel or from interpreter contexts. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.run_code("value = 42") # The isolated kernel does not see variables from the shared kernel response = sandbox.run_code("print(value)", isolated=True) if response.error: print(response.error.name) # NameError ``` | **Parameter** | **Type** | **Description** | | ------------- | ----------------- | ------------------------------------------------------- | | `isolated` | bool | Runs the code in a temporary kernel when set to `True`. Each run starts a new kernel, so `timeout` must also cover the kernel start. If `timeout` runs out before the kernel starts, the call raises an error with code `REQUEST_TIMEOUT`. The default is `False`. This parameter cannot be combined with `context`. | | `cwd` | str | Sets the working directory of the temporary kernel. | | `env` | Mapping\[str, str\] | Sets environment variables of the temporary kernel. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.runCode('value = 42') // The isolated kernel does not see variables from the shared kernel const response = await sandbox.runCode('print(value)', { isolated: true }) console.log(response.error?.name) // NameError ``` | **Parameter** | **Type** | **Description** | | ------------- | ------------------------ | ------------------------------------------------------------ | | `isolated` | boolean | Runs the code in a temporary kernel when set to `true`. Each run starts a new kernel, so `timeoutSeconds` must also cover the kernel start. If `timeoutSeconds` runs out before the kernel starts, the call raises an error with code `REQUEST_TIMEOUT`. The default is `false`. This parameter cannot be combined with `context`. | | `cwd` | string | Sets the working directory of the temporary kernel. | | `env` | Record\ | Sets environment variables of the temporary kernel. | ```bash # Create a session with a new kernel curl '{toolboxProxyUrl}/{sandboxId}/sessions' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"id": "isolated-run", "kernel": "python"}' # Run the code in that session curl '{toolboxProxyUrl}/{sandboxId}/processes?waitMs=60000&logs=text' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"kind": "code", "language": "python", "sessionId": "isolated-run", "code": "print(value)"}' # Delete the session and its kernel curl '{toolboxProxyUrl}/{sandboxId}/sessions/isolated-run' \ --request DELETE \ --header 'Authorization: Bearer YOUR_API_KEY' ``` - The request requires the `write:sandboxes` permission. - Delete the session even when the run fails. ## Interpreter contexts [#interpreter-contexts] An interpreter context is a session with its own language kernel, working directory, and environment. State persists across runs in a context and is not shared with the shared kernel or other contexts. A context lives until deleted, so its kernel uses memory while idle. In the Toolbox API, a context is a [session](/docs/api/toolbox/sessions) that runs a language kernel. Shell sessions are not created through this endpoint; see [sessions](/docs/process-execution#session-operations). ### Create a context [#create-a-context] Create an interpreter context and run code in it. Run code with the context handle, or pass the handle to a run in the sandbox. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() ctx = sandbox.interpreter.create_context(language="python", env={"STAGE": "dev"}) try: ctx.run("value = 'stored in ctx'") result = sandbox.run_code("print(value)", context=ctx) print(result.logs.stdout) finally: ctx.close() ``` | **Parameter** | **Type** | **Description** | | ------------- | ------------------- | ----------------------------------------------------------------------- | | `language` | str | Sets the language: `python`, `typescript`, or `javascript`. This parameter is required. | | `context_id` | str | Sets the context ID for reconnecting later. The ID has at most `128` ASCII letters, digits, `.`, `_`, and `-`, and is unique in the sandbox. An ID that is in use returns `NAME_CONFLICT`. If omitted, an ID is generated. | | `cwd` | str | Sets the working directory of the kernel. | | `env` | Mapping\[str, str\] | Sets the kernel environment variables. Runs in a context do not accept `env`. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const ctx = await sandbox.interpreter.createContext({ language: 'python', env: { STAGE: 'dev' } }) try { await ctx.run(`value = 'stored in ctx'`) const result = await sandbox.runCode(`print(value)`, { context: ctx }) console.log(result.logs.stdout) } finally { await ctx.close() } ``` | **Parameter** | **Type** | **Description** | | ------------- | ------------------------ | ----------------------------------------------------------------------- | | `language` | string | Sets the language: `'python'`, `'typescript'`, or `'javascript'`. This parameter is required. | | `contextId` | string | Sets the context ID for reconnecting later. The ID has at most `128` ASCII letters, digits, `.`, `_`, and `-`, and is unique in the sandbox. An ID that is in use returns `NAME_CONFLICT`. If omitted, an ID is generated. | | `cwd` | string | Sets the working directory of the kernel. | | `env` | Record\ | Sets the kernel environment variables. Runs in a context do not accept `env`. | ```bash # Create a context (session with a language kernel). Omit "id" to get a generated one. curl '{toolboxProxyUrl}/{sandboxId}/sessions' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"id": "my-context", "kernel": "python", "env": {"STAGE": "dev"}}' # Run code in the context; state persists between runs curl '{toolboxProxyUrl}/{sandboxId}/processes?waitMs=60000&logs=text' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"kind": "code", "language": "python", "sessionId": "my-context", "code": "value = \"stored in ctx\""}' curl '{toolboxProxyUrl}/{sandboxId}/processes?waitMs=60000&logs=text' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"kind": "code", "language": "python", "sessionId": "my-context", "code": "print(value)"}' ``` | **Field** | **Type** | **Description** | | --------- | -------- | --------------------------------------------------------------------------------------- | | `id` | string | Sets the context ID for reconnecting later. The ID has at most `128` ASCII letters, digits, `.`, `_`, and `-`, and is unique in the sandbox. An ID that is in use returns `NAME_CONFLICT`. If omitted, an ID is generated. | | `kernel` | string | Sets the context language: `python`, `typescript`, or `javascript`. | | `env` | object | Sets the kernel environment variables. Runs in a context do not accept `env`. | ### List contexts [#list-contexts] List a sandbox's interpreter contexts. The list also includes the shared kernel of each language that has run code, with the ID `code-python`, `code-typescript`, or `code-javascript`. The list does not include shell sessions. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() for ctx in sandbox.interpreter.list_contexts(): print(f"Context: {ctx.context_id}, Language: {ctx.language}") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() for (const ctx of await sandbox.interpreter.listContexts()) { console.log(`Context: ${ctx.contextId}, Language: ${ctx.language}`) } ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/sessions' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ### Reconnect to a context [#reconnect-to-a-context] Reconnect to an existing context from another worker or process. Contexts are addressed by ID, so any client with the sandbox ID and the context ID can run code in the context. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() # Worker A: create a context with a known ID ctx = sandbox.interpreter.create_context(language="python", context_id="my-context") ctx.run("x = 42") # Worker B: reconnect to the context by ID context = sandbox.interpreter.connect_context("my-context", language="python") response = context.run("print(x)") print(response.logs.stdout) # 42 # Or persist the context in worker A and reconnect from the saved dict, which carries the language saved = ctx.to_json() # {"sandboxId": "...", "contextId": "my-context", "language": "python"} context = sandbox.interpreter.connect_context(saved) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ------------------------------------------------------------------------------------------------------ | | `language` | str | Sets the context language. This parameter is required when you pass an ID. Without `language`, `connect_context()` raises `DaytonaError` with code `BAD_REQUEST`. | - Running code in the context requires the `write:sandboxes` permission. - `connect_context()` takes a context ID with `language`, or the saved dict. - `connect_context()` sends no request, so it does not check that the context exists. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() // Worker A: create a context with a known ID const ctx = await sandbox.interpreter.createContext({ contextId: 'my-context', language: 'python' }) await ctx.run('x = 42') // Worker B: reconnect to the context by ID const context = sandbox.interpreter.connectContext('my-context', 'python') const response = await context.run('print(x)') console.log(response.logs.stdout) // 42 // Or persist the context in worker A and reconnect from the saved object, which carries the language const saved = ctx.toJSON() // { sandboxId: '...', contextId: 'my-context', language: 'python' } const restored = sandbox.interpreter.connectContext(saved) ``` - Running code in the context requires the `write:sandboxes` permission. - `connectContext()` takes a context ID with the language, or the saved object. - `connectContext()` sends no request, so it does not check that the context exists. ```bash # Check that the context exists; an unknown ID returns 404 with SESSION_NOT_FOUND curl '{toolboxProxyUrl}/{sandboxId}/sessions/my-context' \ --header 'Authorization: Bearer YOUR_API_KEY' # Run code in the context from any client curl '{toolboxProxyUrl}/{sandboxId}/processes?waitMs=5000&logs=text' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"kind": "code", "language": "python", "code": "print(x)", "sessionId": "my-context"}' ``` | **Field** | **Description** | | ----------------- | ---------------------------------------------------------------------------- | | `kernel` | The language kernel of the context. | | `active` | Whether the kernel of the context is running. When it is `false`, the next run starts a new kernel without the earlier state. | | `cwd` | The working directory the kernel started in. A change made by code is not tracked. | | `kernelProcessId` | The ID of the long-lived kernel process. | | `processIds` | The IDs of the runs in the context, in creation order. | ### Delete a context [#delete-a-context] Delete an interpreter context. Deleting a context shuts its kernel down and frees its memory. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() ctx = sandbox.interpreter.create_context(language="python") try: ctx.run("data = list(range(10_000_000))") finally: sandbox.interpreter.delete_context(ctx) ``` - The request requires the `write:sandboxes` permission. - `delete_context()` takes a context handle or a context ID. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const ctx = await sandbox.interpreter.createContext({ language: 'python' }) try { await ctx.run('data = list(range(10_000_000))') } finally { await sandbox.interpreter.deleteContext(ctx) } ``` - The request requires the `write:sandboxes` permission. - `deleteContext()` takes a context handle or a context ID. ```bash curl '{toolboxProxyUrl}/{sandboxId}/sessions/my-context' \ --request DELETE \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Errors [#errors] The SDKs raise these errors with the codes in the following table, and the Toolbox API returns them in the `code` field. An exception in the code itself is not an error of the request: it is returned in the result. | **Code** | **Meaning** | **What to do** | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | **`SESSION_NOT_FOUND`** | The context does not exist, or it was deleted. | Create the context again. Its earlier state is lost. | | **`NAME_CONFLICT`** | A context with this ID already exists in the sandbox. | Use another ID, or reconnect to the existing context. | | **`UNSUPPORTED_OPERATION`** | A run passed environment variables that its kernel does not take: a run on the shared kernel with variables other than those it started with, or an API run with `sessionId` and `env`. | Set the variables when you create an interpreter context. | | **`BAD_REQUEST`** | A context ID without a language, a language that does not match the context, an isolated run with a context, or an SDK run in a context that passes environment variables. | Pass the language of the context, run without `isolated`, or set the variables when you create the context. | | **`REQUEST_TIMEOUT`** | The timeout of an isolated run ran out before its kernel started. | Raise the timeout. | ## Permissions [#permissions] Code execution runs through the toolbox of a sandbox. Access is governed by one project permission, which you can grant to an [organization role](/docs/organizations#roles) or an [API key](/docs/api-keys). A [sandbox access token](/docs/sandboxes#create-an-access-token) with the toolbox:exec operation grants the same access to a single sandbox, without an API key. The sandbox read permission alone does not grant toolbox access. | **Permission** | **Grants** | | --------------------- | ---------------------------------------- | | **`write:sandboxes`** | Run code and manage interpreter contexts | # Process Execution (/docs/process-execution) Daytona runs shell commands and long-running processes in sandboxes. Commands run inside the sandbox, so untrusted or generated commands execute in isolation from your application. Process execution is available through the process module of a sandbox. Every command runs as a process: you can run a command and wait for its result, or start it in the background and control it through a handle. Sessions are shell processes that keep state, such as the working directory and environment variables, across the commands that run in them. To run Python, TypeScript, and JavaScript code, see [code execution](/docs/code-execution). For interactive terminals, see [pseudo terminal (PTY)](/docs/pty). For real-time logs from long-running processes, see [log streaming](/docs/log-streaming). ## Use cases [#use-cases] Process execution is used when the work in a sandbox is a shell command or a program instead of a code snippet. - **Build and test.** Package installs, compilers, and test runners run in the sandbox, and their exit code and output return to the application. When a run times out, the result has no exit code and the command keeps running. The result holds stdout, stderr, and the output of both channels in recorded order. - **Agent tools.** An agent runs shell commands as tools and reads their results. The commands run inside the sandbox, isolated from the application that hosts the agent. Python, TypeScript, and JavaScript snippets run through [code execution](/docs/code-execution) instead. - **Servers and workers.** A web server or worker starts in the background, and the application checks its state, reads its logs, or stops it through its handle. By default, an unnamed process keeps its record and logs for 10 minutes after it exits, and a named process keeps them until it is deleted. A retention option overrides either default. - **Multi-step shell work.** A session keeps the working directory and environment variables, so a sequence of commands runs as it does in one terminal. Commands in a session run one at a time and in order. A session survives pause and resume, but not stop and start. - **Reconnects.** An application reconnects to a process it started earlier and continues to read its output. Any client with the sandbox ID and the process ID can wait for the process, read its logs, or stop it. A process name resolves only while the process runs, and the ID still works after it exits. ## Command execution [#command-execution] ### Execute commands [#execute-commands] Run a command and wait for it to exit. The result holds the exit code and output. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() response = sandbox.process.run("ls -la") print(response.exit_code, response.stdout) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `timeout` | float | Sets how many seconds `run()` waits. When the timeout elapses, `reason` is `"timed_out"`, `exit_code` is `None`, and the command keeps running. Without it, `run()` waits until the command exits. | The response has the following fields: | **Field** | **Description** | | --------- | --------------- | | `stdout` | Holds the output of the stdout channel. | | `stderr` | Holds the output of the stderr channel. | | `output` | Holds the output of both channels in the order it was recorded. Writes to stdout and stderr that happen close together can appear out of order. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const response = await sandbox.process.run('ls -la') console.log(response.exitCode, response.stdout) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `timeoutSeconds` | number | Sets how many seconds `run()` waits. When the timeout elapses, `reason` is `'timed_out'`, `exitCode` is `undefined`, and the command keeps running. Without it, `run()` waits until the command exits. | The response has the following fields: | **Field** | **Description** | | --------- | --------------- | | `stdout` | Holds the output of the stdout channel. | | `stderr` | Holds the output of the stderr channel. | | `output` | Holds the output of both channels in the order it was recorded. Writes to stdout and stderr that happen close together can appear out of order. | ```bash daytona sandbox exec {sandboxId} -- ls -la ``` - The request requires the `write:sandboxes` permission. - Arguments after `--` run without a shell. - To use shell syntax, wrap the command in `bash -c '...'`. - Pass the `--timeout` flag to wait longer than its default of `10m`. - When `--timeout` elapses, the CLI exits with code `8` and the command keeps running. ```bash # waitMs parks the response until the process exits; values above 600000 wait 600000. # logs=text attaches the first log page. curl '{toolboxProxyUrl}/{sandboxId}/processes?waitMs=5000&logs=text' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"kind": "exec", "shellCommand": "ls -la"}' ``` - The request requires the `write:sandboxes` permission. - `{toolboxProxyUrl}` is the `toolbox_proxy_url` field of the [sandbox](/docs/sandboxes#get-a-sandbox). - When `waitMs` elapses before the process exits, the response has `state` set to `running`. - Wait for the process and read its logs with separate requests. - The attached log page holds up to `1000` frames, and output written in quick succession is recorded as one frame. - Read longer output from `/processes/{processId}/logs`. ### Set the user and shell [#set-the-user-and-shell] Run a command as a specific user or with a specific shell. The user must exist in the image and the shell must be installed. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() response = sandbox.process.run("whoami", user="nobody", shell="bash") print(response.output) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `user` | str | Sets the user that runs the command. The user must exist in the image. | | `shell` | str | Sets the shell that runs the command. The shell must be installed in the image. The default is `bash`, or `sh` when `bash` is not installed. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const response = await sandbox.process.run('whoami', { user: 'nobody', shell: 'bash' }) console.log(response.output) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `user` | string | Sets the user that runs the command. The user must exist in the image. | | `shell` | string | Sets the shell that runs the command. The shell must be installed in the image. The default is `bash`, or `sh` when `bash` is not installed. | ```bash daytona sandbox exec {sandboxId} --user nobody -- bash -c whoami ``` - The request requires the `write:sandboxes` permission. - `exec` has no shell flag, so pass the shell as the program. ```bash curl '{toolboxProxyUrl}/{sandboxId}/processes?waitMs=5000&logs=text' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"kind": "exec", "shellCommand": "whoami", "user": "nobody", "shell": "bash"}' ``` ### Update the environment [#update-the-environment] Set or remove environment variables for every process started in the sandbox after the update, including PTYs, sessions, and the git commands that [git operations](/docs/git-operations) run. Running processes and sessions keep their environment. Removing a variable affects only variables set through an earlier update; variables defined by the image stay. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() # Processes started after this call inherit the change sandbox.update_env({"LOG_LEVEL": "debug"}, unset=["HTTP_PROXY"]) response = sandbox.process.run("echo $LOG_LEVEL") print(response.stdout) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------------------- | ------------------------------------------------------- | | `env` | Mapping\[str, str\] | Adds or replaces variables. Each `KEY=VALUE` entry must be smaller than 128 KiB. The sandbox's added environment, which holds the variables set at creation, by secrets, by the platform, and by all updates, is at most 1 MiB in total, counting one extra byte per variable. An update that would grow it past 1 MiB is rejected; unset variables you no longer need to make room. On Windows, the existing environment plus the update is also capped at 32,767 UTF-16 characters. On macOS, it is capped at 512 KiB, half of `ARG_MAX`, and each variable counts 9 bytes in addition to its `KEY=VALUE` text. | | `unset` | Sequence\[str\] | Removes variables. This parameter is keyword only. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() // Processes started after this call inherit the change await sandbox.updateEnv({ LOG_LEVEL: 'debug' }, { unset: ['HTTP_PROXY'] }) const response = await sandbox.process.run('echo $LOG_LEVEL') console.log(response.stdout) ``` | **Parameter** | **Type** | **Description** | | ------------- | ------------------------ | ----------------------------- | | `env` | Record\ | Adds or replaces variables. Each `KEY=VALUE` entry must be smaller than 128 KiB. The sandbox's added environment, which holds the variables set at creation, by secrets, by the platform, and by all updates, is at most 1 MiB in total, counting one extra byte per variable. An update that would grow it past 1 MiB is rejected; unset variables you no longer need to make room. On Windows, the existing environment plus the update is also capped at 32,767 UTF-16 characters. On macOS, it is capped at 512 KiB, half of `ARG_MAX`, and each variable counts 9 bytes in addition to its `KEY=VALUE` text. | | `unset` | string\[\] | Removes variables. | ```bash # Processes started after this call inherit the change curl '{toolboxProxyUrl}/{sandboxId}/env' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"env": {"LOG_LEVEL": "debug"}, "unset": ["HTTP_PROXY"]}' ``` | **Field** | **Type** | **Description** | | --------- | ---------- | ----------------------------- | | `env` | object | Adds or replaces variables. Each `KEY=VALUE` entry must be smaller than 128 KiB. The sandbox's added environment, which holds the variables set at creation, by secrets, by the platform, and by all updates, is at most 1 MiB in total, counting one extra byte per variable. An update that would grow it past 1 MiB is rejected; unset variables you no longer need to make room. On Windows, the existing environment plus the update is also capped at 32,767 UTF-16 characters. On macOS, it is capped at 512 KiB, half of `ARG_MAX`, and each variable counts 9 bytes in addition to its `KEY=VALUE` text. | | `unset` | string\[\] | Removes variables. | ### Execute interactive commands [#execute-interactive-commands] Run a command that reads input. Start the command with stdin open, write input to it, and close stdin when the command has all its input. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() handle = sandbox.process.start("tr a-z A-Z", stdin="open") # Send input to the command, then end it handle.write_stdin("hello\n") handle.close_stdin() wait = handle.wait() page = handle.logs() for frame in page.items: print(f"[{frame.channel}]: {frame.data}") print(wait.exit_code) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------------- | | `stdin` | str | Sets the stdin of the command to `"open"` or `"closed"`. The default is `"closed"`. | - The request requires the `write:sandboxes` permission. - Write the input and close stdin while the command runs, because a process that has exited does not accept input. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const handle = await sandbox.process.start('tr a-z A-Z', { stdin: 'open', }) // Send input to the command, then end it await handle.writeStdin('hello\n') await handle.closeStdin() const wait = await handle.wait() const page = await handle.logs() for (const frame of page.items) { console.log(`[${frame.channel}]:`, frame.data) } console.log(wait.exitCode) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------------- | | `stdin` | string | Sets the stdin of the command to `'open'` or `'closed'`. The default is `'closed'`. | - The request requires the `write:sandboxes` permission. - Write the input and close stdin while the command runs, because a process that has exited does not accept input. ```bash # Piped input is written to the command's stdin, followed by end of input printf 'hello\n' | daytona sandbox exec {sandboxId} -- tr a-z A-Z ``` ```bash # Start a command with stdin open curl '{toolboxProxyUrl}/{sandboxId}/processes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "kind": "exec", "shellCommand": "tr a-z A-Z", "stdin": "open" }' # Send input to the process and close stdin curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}/stdin' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "data": "hello\n", "eof": true }' # Get process logs curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}/logs' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Type** | **Description** | | --------- | -------- | ------------------- | | `stdin` | string | Sets the stdin of the command to `open` or `closed`. The default is `closed`. | | **Field** | **Type** | **Description** | | --------- | --------- | ------------------------------------- | | `eof` | boolean | Closes stdin after writing `data` when set to `true`. | - The request requires the `write:sandboxes` permission. - Send the last input with `eof` set to `true` while the command runs, because a process that has exited does not accept input. ## Process control [#process-control] ### Start a background process [#start-a-background-process] Start a process without waiting for it to exit. The SDK returns a handle that you can use to wait for, read logs from, and stop the process. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() handle = sandbox.process.start("python -m http.server 8000", tag="web") print(handle.id) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `tag` | str | Groups the process with others for [listing](#list-processes) and [stopping processes by tag](#stop-processes-by-tag). The tag is 1 to 128 characters from `A-Z`, `a-z`, `0-9`, `.`, `_`, and `-`, and is not `.` or `..`. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const handle = await sandbox.process.start('python -m http.server 8000', { tag: 'web' }) console.log(handle.id) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `tag` | string | Groups the process with others for [listing](#list-processes) and [stopping processes by tag](#stop-processes-by-tag). The tag is 1 to 128 characters from `A-Z`, `a-z`, `0-9`, `.`, `_`, and `-`, and is not `.` or `..`. | ```bash # Start a detached process (no waitMs). Returns 201 with the process record. curl '{toolboxProxyUrl}/{sandboxId}/processes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"kind": "exec", "shellCommand": "python -m http.server 8000", "tag": "web"}' ``` | **Field** | **Type** | **Description** | | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `keepLogs` | string | Sets how long the record and logs are kept after the process exits. With `on_exit_ttl`, the record and logs are removed 10 minutes after the process exits. The default is `on_exit_ttl` without `name` and `until_cleanup` with `name`. | | `tag` | string | Groups the process with others for [listing](#list-processes) and [stopping processes by tag](#stop-processes-by-tag). The tag is 1 to 128 characters from `A-Z`, `a-z`, `0-9`, `.`, `_`, and `-`, and is not `.` or `..`. It applies to `exec`, `pty`, and `session` processes. A `code` process cannot have a tag, and a command in a session takes the tag of the session. | ### List processes [#list-processes] List the processes in a sandbox. Filter the list by state, kind, session, name, PID, or tag. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() running = sandbox.process.list(state="running") for info in running.items: print(info.id, info.name, info.state, info.pid) # Only the processes started with the tag web = sandbox.process.list(tag="web") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const running = await sandbox.process.list({ state: 'running' }) for (const info of running.items) { console.log(info.id, info.name, info.state, info.pid) } // Only the processes started with the tag const web = await sandbox.process.list({ tag: 'web' }) ``` ```bash # Filters: --state running|terminal|all, --kind, --session-id, --name, --pid daytona sandbox process list {sandboxId} --state running ``` ```bash # Filters: state=running|terminal|all, kind, sessionId, name, pid, tag curl '{toolboxProxyUrl}/{sandboxId}/processes?state=running' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ### Process states [#process-states] | **State** | **Meaning** | | ------------------ | ----------------------------------------------------------------------------------------------------- | | **`starting`** | The record exists; the process has not been spawned yet. | | **`provisioning`** | [Code execution](/docs/code-execution) only: the language kernel is being installed or started. Progress is reported on the `setup` log channel. | | **`running`** | The process has a PID and is executing. | | **`terminal`** | The process has finished. `reason`, `exitCode`, and `signal` describe how. | | **Reason** | **Meaning** | | --------------- | ------------------------------------------------------------------------------------------------------------- | | **`exited`** | Exited on its own; `exitCode` is set. A code execution that raised an exception exits with `1`. | | **`signaled`** | Killed by a signal; `signal` is set. `wait` reports bare names (`TERM`, `KILL`), the API reports `SIGTERM`, `SIGKILL`. | | **`timed_out`** | The `timeoutMs` set at start elapsed; the process was killed with `SIGKILL`. Also returned by `wait` itself when its own long-poll window elapses first, with no `exitCode`. | | **`lost`** | The process disappeared without the daemon observing its exit, for example after a sandbox stop and start, or when its session was deleted while it was still running. | | **`failed`** | The process could not be started or ran into a daemon-side error. | ### Log retention [#log-retention] Choose how long a process record and its logs are kept after the process exits. The policy is set when the process starts. Once a record is removed, the process can no longer be waited on or read. See [log streaming](/docs/log-streaming#log-retention) for size caps and eviction. | **Policy** | **Behavior** | | ------------------- | ----------------------------------------------------------------------------------------------- | | **`until_cleanup`** | Kept until the process is deleted (`DELETE /processes/{id}`). Default for API requests that set `name`; the SDK `pty` namespace also uses it. | | **`on_exit_ttl`** | Kept for 10 minutes after exit, then removed. Default for API requests without `name` and for the SDK `start` and `run` methods; `run` does not accept `none`. | | **`none`** | Logs are discarded as soon as the process exits and can no longer be read. The record is kept for 10 minutes, so `wait` still returns the exit result. | ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() # Keep the record and logs until delete() is called handle = sandbox.process.start("pip list", keep_logs="until_cleanup") handle.wait() for frame in handle.logs().items: print(frame.data, end="") handle.delete() ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `keep_logs` | str | Sets how long the record and logs are kept after the process exits: `"until_cleanup"`, `"on_exit_ttl"`, or `"none"`. The default is `"on_exit_ttl"`. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() // Keep the record and logs until delete() is called const handle = await sandbox.process.start('pip list', { keepLogs: 'until_cleanup' }) await handle.wait() for (const frame of (await handle.logs()).items) { process.stdout.write(frame.data) } await handle.delete() ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `keepLogs` | string | Sets how long the record and logs are kept after the process exits: `'until_cleanup'`, `'on_exit_ttl'`, or `'none'`. The default is `'on_exit_ttl'`. | ```bash # Keep the record and logs until DELETE curl '{toolboxProxyUrl}/{sandboxId}/processes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"kind": "exec", "shellCommand": "pip list", "keepLogs": "until_cleanup"}' # Read logs; limit defaults to 1000, and values outside 1 to 5000 are clamped to that range curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}/logs' \ --header 'Authorization: Bearer YOUR_API_KEY' # Remove the record and logs curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}' \ --request DELETE \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ### Wait for a process [#wait-for-a-process] Wait for a process to finish, or until a timeout elapses. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() handle = sandbox.process.start("pip install requests") # Wait up to 2 minutes; without timeout, wait() blocks until the process is terminal result = handle.wait(timeout=120) print(result.reason, result.exit_code) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const handle = await sandbox.process.start('pip install requests') // Wait up to 2 minutes; without timeout, wait() blocks until the process is terminal const result = await handle.wait({ timeoutSeconds: 120 }) console.log(result.reason, result.exitCode) ``` ```bash # Wait up to 2 minutes; exits with the process exit code, 128 plus the signal number if a # signal stopped it (130 for INT, 143 for TERM, 137 for KILL), or 8 if the timeout elapses # first or the process hit its own timeout daytona sandbox process wait {sandboxId} {processId} --timeout 2m ``` ```bash # Long-poll for the terminal state. Omitting timeoutMs waits 60 s; 0 waits until terminal. curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}/wait?timeoutMs=120000' \ --header 'Authorization: Bearer YOUR_API_KEY' # Response when the process exits: # {"exitCode": 0, "reason": "exited"} # Response when the wait window elapses first: # {"reason": "timed_out"} ``` ### Stop a process [#stop-a-process] Stop a running process by sending it a signal. The SDKs send the terminate signal. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() handle = sandbox.process.start("python -m http.server 8000") # Sends SIGTERM handle.kill() ``` | **Parameter** | **Type** | **Description** | | ---------------- | ------------ | ---------------------------------------------------------------------------------------------- | | `signal` | SignalName | Sets the signal to send: `TERM`, `KILL`, `INT`, `HUP`, `QUIT`, `USR1`, or `USR2`. The default is `"TERM"`. | | `escalate_after` | float | Sends `escalate_to` if the process is still running after this many seconds. The value is at most `600`. | | `escalate_to` | SignalName | Sets the signal sent after `escalate_after`. The default is `"KILL"`. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const handle = await sandbox.process.start('python -m http.server 8000') // Sends SIGTERM await handle.kill() ``` | **Parameter** | **Type** | **Description** | | --------------- | --------------- | ------------------------------------------------------------------------------------------------------ | | `signal` | ProcessSignal | Sets the signal to send: `'TERM'`, `'KILL'`, `'INT'`, `'HUP'`, `'QUIT'`, `'USR1'`, or `'USR2'`. | | `escalateAfterSeconds` | number | Sends `escalateTo` if the process is still running after this many seconds. The value is at most `600`. | | `escalateTo` | ProcessSignal | Sets the signal sent after `escalateAfterSeconds`. The default is `'KILL'`. | ```bash daytona sandbox process signal {sandboxId} {processId} TERM # Send KILL daytona sandbox process kill {sandboxId} {processId} ``` | **Flag** | **Type** | **Description** | | ------------------ | -------- | --------------------------------------------------------------------------------------------------------------- | | `--escalate-after` | string | Sends `--escalate-to` if the process is still running after this delay. The value is a duration such as `5s`, at most `10m`. | | `--escalate-to` | string | Sets the second signal, sent after `--escalate-after`. The default is `KILL`. This flag works only together with `--escalate-after`. | - The request requires the `write:sandboxes` permission. - Signal names have no `SIG` prefix, and the accepted names are `TERM`, `KILL`, `INT`, `HUP`, `QUIT`, `USR1`, and `USR2`. ```bash curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}/signals' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"signal": "SIGTERM"}' ``` | **Field** | **Type** | **Description** | | ----------------- | --------- | -------------------------------------------------------------------------------------- | | `signal` | string | Sets the signal to send, with the `SIG` prefix. An empty value sends `SIGTERM`. | | `escalateAfterMs` | integer | Sends `escalateTo` if the process is still running after this delay. The value is at most `600000`. | | `escalateTo` | string | Sets the signal sent after `escalateAfterMs`. The default is `SIGKILL`. | ### Stop processes by tag [#stop-processes-by-tag] Stop every process started with a tag, and everything those processes started, including descendants that detached and the survivors of tagged processes that already exited. The call returns once none of them is running, with the IDs of the tagged processes it stopped. Processes whose records were deleted or expired are stopped too but not listed, so an empty list does not mean nothing was stopped. The call does not delete records or logs; they are kept for as long as the `keepLogs` setting of each process allows. The tag can be used again right after the call. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.process.start("python -m http.server 8000", tag="web") sandbox.process.start("python -m http.server 8001", tag="web") stopped = sandbox.process.kill_all(tag="web", timeout=30) print(stopped) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `tag` | str | Sets the tag of the processes to stop. This parameter is required. | | `timeout` | float | Sets how many seconds to wait for the processes to exit, more than `0` and at most `60`. Fractions are allowed. A value outside the range raises `BadRequestError` before any request is sent. The default is `10`. | - The request requires the `write:sandboxes` permission. - macOS sandboxes, and Linux sandboxes without process containment, cannot stop processes by tag. - When processes are still running at the timeout, or another start or stop under the tag is in progress, the call fails. Calling it again is safe. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.process.start('python -m http.server 8000', { tag: 'web' }) await sandbox.process.start('python -m http.server 8001', { tag: 'web' }) const stopped = await sandbox.process.killAll({ tag: 'web', timeoutSeconds: 30 }) console.log(stopped) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `tag` | string | Sets the tag of the processes to stop. This parameter is required. | | `timeoutSeconds` | number | Sets how many seconds to wait for the processes to exit, more than `0` and at most `60`. A value outside the range throws `BadRequestError` before any request is sent. The default is `10`. | - The request requires the `write:sandboxes` permission. - macOS sandboxes, and Linux sandboxes without process containment, cannot stop processes by tag. - When processes are still running at the timeout, or another start or stop under the tag is in progress, the call fails. Calling it again is safe. ```bash curl '{toolboxProxyUrl}/{sandboxId}/processes:kill' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"tag": "web", "timeoutMs": 30000}' ``` | **Field** | **Type** | **Description** | | --------- | -------- | --------------- | | `tag` | string | Sets the tag of the processes to stop. This field is required. | | `timeoutMs` | integer | Sets how many milliseconds to wait for the processes to exit, from `1` to `60000`. The default is `10000`. | The response has the following fields: | **Field** | **Description** | | --------- | --------------- | | `processIds` | The tagged processes that were running when the call started, and exited tagged processes that still had descendants running. | - The request requires the `write:sandboxes` permission. - macOS sandboxes, and Linux sandboxes without process containment, cannot stop processes by tag. - When processes are still running at `timeoutMs`, or another start or stop under the tag is in progress, the request fails. Repeating it is safe. ### Reconnect to a process [#reconnect-to-a-process] Reconnect to a process from another worker or process. Processes are addressed by ID, so any client with the sandbox ID and the process ID can wait for, read logs from, and stop the process. To reconnect to an interpreter context, see [reconnect to a context](/docs/code-execution#reconnect-to-a-context). ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() # Worker A: start the process and persist the handle handle = sandbox.process.start("pip install requests") saved = handle.to_json() # {"sandboxId": "...", "processId": "...", "kind": "exec"} # Worker B: reconnect and wait for the same process handle = sandbox.process.connect(saved) result = handle.wait() ``` - The request requires the `write:sandboxes` permission. - `connect()` takes the saved dict or a process ID. - A process name resolves only while the process runs. - After the process exits, reconnect by ID. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() // Worker A: start the process and persist the handle const handle = await sandbox.process.start('pip install requests') const saved = handle.toJSON() // { sandboxId: '...', processId: '...', kind: 'exec' } // Worker B: reconnect and wait for the same process const reconnected = sandbox.process.connect(saved) const result = await reconnected.wait() ``` - The request requires the `write:sandboxes` permission. - `connect()` takes the saved object or a process ID. - A process name resolves only while the process runs. - After the process exits, reconnect by ID. ```bash # --timeout defaults to 10m; raise it for long-running processes daytona sandbox process wait {sandboxId} {processId} --timeout 1h # Attach to the live output stream daytona sandbox process attach {sandboxId} {processId} ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}/wait' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Type** | **Description** | | ----------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `timeoutMs` | integer | Sets how long the request waits for the process to exit. Without it, the request returns after 60 seconds with `{"reason": "timed_out"}` if the process is still running. Repeat the request, or pass `0` to wait until the process exits. | ### Entrypoint process [#entrypoint-process] Get the details and logs of the sandbox entrypoint. It runs as a system process, so you can read it like any other process but not signal, write to, or delete it. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() # Entrypoint process details info = sandbox.process.get("entrypoint") print(f"Command: {info.argv or info.shell_command}") # Entrypoint logs handle = sandbox.process.connect("entrypoint") for frame in handle.logs().items: print(f"[{frame.channel}]: {frame.data}") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() // Entrypoint process details const info = await sandbox.process.get('entrypoint') console.log('Command:', info.argv ?? info.shellCommand) // Entrypoint logs const handle = sandbox.process.connect('entrypoint') for (const frame of (await handle.logs()).items) { console.log(`[${frame.channel}]:`, frame.data) } ``` ```bash # Get entrypoint process details daytona sandbox process get {sandboxId} entrypoint # Get entrypoint logs daytona sandbox process logs {sandboxId} entrypoint # Control-plane entrypoint logs daytona sandbox logs {sandboxId} --entrypoint ``` ```bash # Get entrypoint process details curl '{toolboxProxyUrl}/{sandboxId}/processes/entrypoint' \ --header 'Authorization: Bearer YOUR_API_KEY' # Get entrypoint logs curl '{toolboxProxyUrl}/{sandboxId}/processes/entrypoint/logs' \ --header 'Authorization: Bearer YOUR_API_KEY' # Control-plane entrypoint logs curl 'https://api.daytona.com/sandboxes/{sandboxId}/logs' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Session operations [#session-operations] A session is a long-lived shell that runs the commands you send to it. Commands in a session run one at a time, in order, and share the shell state, such as the working directory, exported variables, and functions. To run commands in parallel, use separate sessions or background processes. A session is a long-running shell process, and each command in it is an ordinary process with its own logs, exit code, timeout, and signals. Interrupting a command stops only that command, and the session keeps running. Sessions survive pause and resume, but not stop and start. [Interpreter contexts](/docs/code-execution#interpreter-contexts) run a language kernel instead of a shell. ### Create a session [#create-a-session] Create a shell session. The working directory and environment variables of the session apply to all of its commands. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() session = sandbox.process.create_session(cwd="/tmp", env={"CI": "1"}, name="build-session") print(session.id) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `cwd` | str | Sets the working directory of the shell. The default is the image's `WORKDIR`. When the image declares none, the default is the home directory of the `user` set at sandbox creation, or the daemon working directory when no user is set or the home directory does not exist. On macOS, the default is the home directory of that user. | | `env` | Mapping\[str, str\] | Sets environment variables applied when the shell starts. | | `shell` | str | Sets the path or name of the shell. On Linux and macOS, it must be a POSIX shell: `bash`, `sh`, `dash`, `zsh`, or `ksh`. The shell must be installed in the image. | | `login` | bool | Starts a login shell when set to `True`. | | `name` | str | Sets a name that commands can use in place of the session ID while the session is running. Names are unique among running processes. | | `tag` | str | Sets the tag of the session. Every command in the session takes it, for [listing](#list-processes) and [stopping processes by tag](#stop-processes-by-tag). | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const session = await sandbox.process.createSession({ cwd: '/tmp', env: { CI: '1' }, name: 'build-session' }) console.log(session.id) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `cwd` | string | Sets the working directory of the shell. The default is the image's `WORKDIR`. When the image declares none, the default is the home directory of the `user` set at sandbox creation, or the daemon working directory when no user is set or the home directory does not exist. On macOS, the default is the home directory of that user. | | `env` | Record\ | Sets environment variables applied when the shell starts. | | `shell` | string | Sets the path or name of the shell. On Linux and macOS, it must be a POSIX shell: `bash`, `sh`, `dash`, `zsh`, or `ksh`. The shell must be installed in the image. | | `login` | boolean | Starts a login shell when set to `true`. | | `name` | string | Sets a name that commands can use in place of the session ID while the session is running. Names are unique among running processes. | | `tag` | string | Sets the tag of the session. Every command in the session takes it, for [listing](#list-processes) and [stopping processes by tag](#stop-processes-by-tag). | ```bash curl '{toolboxProxyUrl}/{sandboxId}/processes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"kind": "session", "cwd": "/tmp", "env": {"CI": "1"}, "name": "build-session"}' ``` | **Field** | **Type** | **Description** | | --------- | --------- | --------------- | | `kind` | string | Sets the kind to `session`. | | `cwd` | string | Sets the working directory of the shell. The default is the image's `WORKDIR`. When the image declares none, the default is the home directory of the `user` set at sandbox creation, or the daemon working directory when no user is set or the home directory does not exist. On macOS, the default is the home directory of that user. | | `env` | object | Sets environment variables applied when the shell starts. | | `shell` | string | Sets the path or name of the shell. On Linux and macOS, it must be a POSIX shell: `bash`, `sh`, `dash`, `zsh`, or `ksh`. The shell must be installed in the image. | | `login` | boolean | Starts a login shell when set to `true`. The default is `false`. | | `user` | string | Sets the user that the shell runs as. | | `name` | string | Sets a name that commands can use in place of the session ID while the session is running. Names are unique among running processes. | | `tag` | string | Sets the tag of the session. Every command in the session takes it, for [listing](#list-processes) and [stopping processes by tag](#stop-processes-by-tag). | The response has the following fields: | **Field** | **Description** | | --------- | ----------------------------------------------------------------------- | | `id` | The session ID. Commands pass it as `sessionId` to run in the session. | | `state` | The lifecycle state: `starting`, `provisioning`, `running`, or `terminal`. | | `cwd` | The current directory of the session shell. | ### Execute session commands [#execute-session-commands] Run commands in a session. Shell state set by one command is available to the next. A command timeout starts when the command starts, after any commands queued ahead of it. When the timeout elapses, Daytona stops the command and the session keeps running. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() with sandbox.process.create_session() as session: # Shell state persists between commands session.run("cd /tmp && export STAGE=dev") result = session.run("pwd && echo $STAGE", execution_timeout=30) print(result.stdout) # Interrupt one command; the session keeps running command = session.start("sleep 600") command.kill(signal="INT") ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `timeout` | float | Sets how many seconds `run()` waits for the command. | | `execution_timeout` | float | Sets how many seconds the command can run before Daytona stops it. The session keeps running. | | `name` | str | Sets a name for the command process. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await using session = await sandbox.process.createSession() // Shell state persists between commands await session.run('cd /tmp && export STAGE=dev') const result = await session.run('pwd && echo $STAGE', { executionTimeoutSeconds: 30 }) console.log(result.stdout) // Interrupt one command; the session keeps running const command = await session.start('sleep 600') await command.kill({ signal: 'INT' }) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `timeoutSeconds` | number | Sets how many seconds `run()` waits for the command. | | `executionTimeoutSeconds` | number | Sets how many seconds the command can run before Daytona stops it. The session keeps running. | | `name` | string | Sets a name for the command process. | ```bash # Shell state persists between commands curl '{toolboxProxyUrl}/{sandboxId}/processes?waitMs=60000&logs=text' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"kind": "exec", "shellCommand": "cd /tmp && export STAGE=dev", "sessionId": "build-session"}' curl '{toolboxProxyUrl}/{sandboxId}/processes?waitMs=60000&logs=text' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"kind": "exec", "shellCommand": "pwd && echo $STAGE", "sessionId": "build-session", "timeoutMs": 30000}' ``` - The request requires the `write:sandboxes` permission. - A command with `sessionId` must be `kind: "exec"` with `shellCommand`, and `sessionId` takes the session ID or its name. - A command with `sessionId` cannot set `argv`, `cwd`, `env`, `shell`, `login`, or `user`, because the settings of the session apply. ### Reconnect to a session [#reconnect-to-a-session] Reconnect to a session from another worker or process. Sessions are addressed by ID, so any client with the sandbox ID and the session ID can run commands in the session and read its status. Reconnecting sends no request; the next call on the session does. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() # Worker A: create the session and persist it session = sandbox.process.create_session() session.run("cd /tmp") saved = session.to_json() # {"sandboxId": "...", "sessionId": "..."} # Worker B: reconnect and run in the same shell session = sandbox.process.connect_session(saved) result = session.run("pwd") ``` - `connect_session()` takes the saved dict or a session ID. - Reconnect through the process module of the sandbox the session belongs to; a saved session of another sandbox is refused before any request. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() // Worker A: create the session and persist it const session = await sandbox.process.createSession() await session.run('cd /tmp') const saved = session.toJSON() // { sandboxId: '...', sessionId: '...' } // Worker B: reconnect and run in the same shell const reconnected = sandbox.process.connectSession(saved) const result = await reconnected.run('pwd') ``` - `connectSession()` takes the saved object or a session ID. - Reconnect through the process module of the sandbox the session belongs to; a saved session of another sandbox is refused before any request. ### Get session status [#get-session-status] Get the status of a session, including the current working directory of its shell, and list the commands that ran in it. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() session = sandbox.process.create_session() session.run("cd /tmp") info = session.get() print(info.state, info.cwd) listed = sandbox.process.list(session_id=session.id) for proc in listed.items: print(f"Process: {proc.id}, Command: {proc.shell_command}, Exit Code: {proc.exit_code}") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const session = await sandbox.process.createSession() await session.run('cd /tmp') const info = await session.get() console.log(info.state, info.cwd) const listed = await sandbox.process.list({ sessionId: session.id }) for (const proc of listed.items) { console.log(`Process: ${proc.id}, Command: ${proc.shellCommand}, Exit Code: ${proc.exitCode}`) } ``` ```bash # Get the session process daytona sandbox process get {sandboxId} {sessionId} # List processes in a session daytona sandbox process list {sandboxId} --session-id {sessionId} ``` ```bash # Get the session process curl '{toolboxProxyUrl}/{sandboxId}/processes/{sessionId}' \ --header 'Authorization: Bearer YOUR_API_KEY' # List processes in a session curl '{toolboxProxyUrl}/{sandboxId}/processes?sessionId={sessionId}' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ### List sessions [#list-sessions] List the shell sessions in a sandbox. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() session = sandbox.process.create_session() sessions = sandbox.process.list(kind="session") for proc in sessions.items: print(proc.id, proc.state) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const session = await sandbox.process.createSession() const sessions = await sandbox.process.list({ kind: 'session' }) for (const proc of sessions.items) { console.log(proc.id, proc.state) } ``` ```bash daytona sandbox process list {sandboxId} --kind session ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/processes?kind=session' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ### Get session command [#get-session-command] Get a command that ran in a session by its process ID. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() session = sandbox.process.create_session() session.run("echo hello") listed = sandbox.process.list(session_id=session.id) command = sandbox.process.get(listed.items[0].id) print(f"Command: {command.shell_command}, Exit Code: {command.exit_code}") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const session = await sandbox.process.createSession() await session.run('echo hello') const listed = await sandbox.process.list({ sessionId: session.id }) const command = await sandbox.process.get(listed.items[0].id) console.log(`Command: ${command.shellCommand}, Exit Code: ${command.exitCode}`) ``` ```bash daytona sandbox process get {sandboxId} {processId} ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ### Delete a session [#delete-a-session] Delete a session. This ends its shell, stops its running commands, and removes the records of its commands. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() session = sandbox.process.create_session() session.delete() ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const session = await sandbox.process.createSession() await session.delete() ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/processes/{sessionId}' \ --request DELETE \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Resource management [#resource-management] A process kept with `until_cleanup` keeps its record and logs until you delete it. The API applies this policy to processes started with a `name`; the SDKs apply it only when you pass it. Delete such a process when you no longer need it; deleting stops the process if it is still running. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() handle = sandbox.process.start("sleep 3600", name="my-process", keep_logs="until_cleanup") try: pass # Do work... finally: handle.delete() # kills the process if it is still running ``` ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); const handle = await sandbox.process.start("sleep 3600", { name: "my-process", keepLogs: "until_cleanup" }); try { // Do work... } finally { await handle.delete(); // kills the process if it is still running } ``` ```bash # Delete a process record and its logs; a running process is killed first curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}' \ --request DELETE \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Permissions [#permissions] Process execution runs through the toolbox of a sandbox. Access is governed by one project permission, which you can grant to an [organization role](/docs/organizations#roles) or an [API key](/docs/api-keys). A [sandbox access token](/docs/sandboxes#create-an-access-token) with the toolbox:exec operation grants the same access to a single sandbox, without an API key. The sandbox read permission alone does not grant toolbox access. | **Permission** | **Grants** | | --------------------- | ----------------------------------------------------------------- | | **`write:sandboxes`** | Run commands, processes, and sessions, and update the environment | # Pseudo Terminal (PTY) (/docs/pty) Daytona provides pseudo terminal (PTY) capabilities through the process module in sandboxes. A PTY is a process started with the PTY kind: it runs under a virtual terminal so programs behave as if connected to a real terminal, with terminal size, line discipline, and interactive prompts. PTY processes are exposed through a sandbox's PTY namespace and share the handle API of other processes, described in [process execution](/docs/process-execution). ## Use cases [#use-cases] PTY sessions are used when a program expects a real terminal. - **Interactive development.** REPLs, debuggers, and editors that read keystrokes and redraw the screen run in a PTY session. The virtual terminal gives them a terminal size and line discipline, as a real terminal does. The terminal starts at 80 columns by 24 rows unless the start request sets a size. - **Terminals in web applications.** A browser terminal connects to a PTY session, sends keystrokes, and resizes the session with the window. On each resize, the program in the terminal receives a window size change signal and can redraw. Browsers authenticate the WebSocket connection with the daytona_token query parameter. - **Builds and tests.** A PTY session runs compilers, test runners, and deploy tools that show progress or colors only when attached to a terminal. Waiting for the session returns its exit code, and the CLI exits with the same code. If the wait time elapses first, the result reports a timeout with no exit code, and the session keeps running. - **Reconnects.** A client that reconnects replays the output from a cursor and sees what it missed. Every output frame carries a cursor, and a read with a cursor returns only the frames after it. Retention can evict old output, and a cursor into evicted output fails. - **Terminal agents.** An agent works in one interactive shell that keeps its working directory and variables between commands. It reads the output as a terminal shows it, including escape sequences. When a session timeout elapses, the session is killed and reports that it timed out. ## Start PTY session [#start-pty-session] Start a PTY session. Without a command, the session runs an interactive shell: the login shell of the user it runs as, or the first of bash, zsh, and sh found in the image. With a command, the session runs that command in the terminal instead. The terminal starts at 80 columns by 24 rows unless you set a size, and you can [resize](#resize-pty-session) it later. The SDKs and the CLI keep an exited session's record and output until [deleted](#kill-pty-session). ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() # Without a command the PTY runs an interactive shell; the terminal defaults to 80x24 handle = sandbox.pty.start() print(handle.id) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() // Without a command the PTY runs an interactive shell; the terminal defaults to 80x24 const handle = await sandbox.pty.start() console.log(handle.id) ``` ```bash # The CLI needs a command, so this starts bash; requires a terminal, and the PTY takes its size daytona sandbox exec {sandboxId} --tty -- bash ``` | **Flag** | **Type** | **Description** | | ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `--timeout` | duration | Limits how long `exec` stays attached. When the limit elapses, the CLI exits with code `8` and the session keeps running. The default is `10m`. | - The request requires the `write:sandboxes` permission. - The CLI runs the arguments after `--` as a program, without a shell. - Wrap shell syntax in `bash -c '...'`. ```bash # Without shellCommand the PTY runs an interactive shell; terminal defaults to 80x24. # The SDKs set keepLogs to until_cleanup; the API default for unnamed processes is on_exit_ttl. curl '{toolboxProxyUrl}/{sandboxId}/processes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"kind": "pty", "stdin": "open", "keepLogs": "until_cleanup"}' ``` - The request requires the `write:sandboxes` permission. - `{toolboxProxyUrl}` is the `toolbox_proxy_url` field of the [sandbox](/docs/sandboxes#get-a-sandbox). ```python from daytona import Daytona from daytona.v2 import TerminalSize daytona = Daytona() sandbox = daytona.v2.create() handle = sandbox.pty.start("python3 -i", terminal=TerminalSize(cols=100, rows=40)) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `command` | str | Sets the command to run in the PTY. | | `terminal` | TerminalSize | Sets the terminal size. `cols` and `rows` are each from `1` to `65535`. The default is 80 columns by 24 rows. | | `cwd` | str | Sets the working directory of the command. | | `env` | Mapping\[str, str\] | Sets environment variables for the command. | | `name` | str | Sets the session name. See [name PTY session](#name-pty-session). | | `keep_logs` | KeepLogs | Sets the log retention policy. With `"on_exit_ttl"`, the record and output are removed 10 minutes after exit; with `"none"`, the output is discarded at exit. See [log retention](/docs/log-streaming#log-retention). The default is `"until_cleanup"`. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const handle = await sandbox.pty.start('python3 -i', { terminal: { cols: 100, rows: 40 }, }) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `terminal` | TerminalSize | Sets the terminal size. `cols` and `rows` are each from `1` to `65535`. The default is 80 columns by 24 rows. | | `cwd` | string | Sets the working directory of the command. | | `env` | Record\ | Sets environment variables for the command. | | `name` | string | Sets the session name. See [name PTY session](#name-pty-session). | | `keepLogs` | KeepLogs | Sets the log retention policy. With `'on_exit_ttl'`, the record and output are removed 10 minutes after exit; with `'none'`, the output is discarded at exit. See [log retention](/docs/log-streaming#log-retention). The default is `'until_cleanup'`. | ```bash # The PTY takes the size of the local terminal; the CLI cannot set cols and rows daytona sandbox exec {sandboxId} --tty -- python3 -i ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/processes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "kind": "pty", "shellCommand": "python3 -i", "stdin": "open", "terminal": {"cols": 100, "rows": 40} }' ``` | **Field** | **Type** | **Description** | | ---------- | -------- | ------------------------------------------------------------ | | `terminal` | object | Sets the terminal size with `cols` and `rows`. Both are required, and each is from `1` to `65535`. The field is accepted only with `kind: "pty"`. | ## Name PTY session [#name-pty-session] Name a PTY session. A name aliases the session ID, so another client can reach the session without knowing its ID. The name belongs to the session only while it runs: when the session exits, the name is released for reuse, and the session stays reachable by its ID only. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() handle = sandbox.pty.start(name="dev-shell") # Names are unique among live processes and resolve wherever an ID is accepted info = sandbox.process.get("dev-shell") ``` - The request requires the `write:sandboxes` permission. - A name has 1 to 128 characters of letters, digits, `.`, `_`, and `-`, and cannot be `.` or `..`. - The names `entrypoint` and `dockerd` are reserved for system processes. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const handle = await sandbox.pty.start(undefined, { name: 'dev-shell' }) // Names are unique among live processes and resolve wherever an ID is accepted const info = await sandbox.process.get('dev-shell') ``` - The request requires the `write:sandboxes` permission. - A name has 1 to 128 characters of letters, digits, `.`, `_`, and `-`, and cannot be `.` or `..`. - The names `entrypoint` and `dockerd` are reserved for system processes. ```bash # A second live process with the same name is rejected with 409 NAME_CONFLICT curl '{toolboxProxyUrl}/{sandboxId}/processes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"kind": "pty", "name": "dev-shell", "stdin": "open"}' # The name works as an alias for the process ID on every /processes/{id} route curl '{toolboxProxyUrl}/{sandboxId}/processes/dev-shell' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` - The request requires the `write:sandboxes` permission. - A name has 1 to 128 characters of letters, digits, `.`, `_`, and `-`, and cannot be `.` or `..`. - The names `entrypoint` and `dockerd` are reserved for system processes. ## Configure shell, user, and timeout [#configure-shell-user-and-timeout] Configure the shell, the user, and the timeout of a PTY session. A shell that cannot be found or a user that does not exist fails the start. When the timeout elapses, the session is killed and reports that it timed out. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() handle = sandbox.pty.start( shell="/bin/bash", # default: the user's login shell, then bash, zsh, sh user="root", # run as another user execution_timeout=600, # SIGKILL after 10 minutes; wait() reports reason "timed_out" ) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------------------------------------- | | `shell` | str | Sets the shell, as a path or a name looked up on `PATH`. | | `user` | str | Sets the user the session runs as. Without `shell`, the session runs through the user's login shell, so a user whose shell is `nologin` cannot run commands. The user must exist in `/etc/passwd` of the sandbox. | | `execution_timeout` | float | Sets how many seconds the session can run before Daytona kills it. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const handle = await sandbox.pty.start(undefined, { shell: '/bin/bash', // default: the user's login shell, then bash, zsh, sh user: 'root', // run as another user executionTimeoutSeconds: 600, // SIGKILL after 10 minutes; wait() reports reason 'timed_out' }) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------------------------------------- | | `shell` | string | Sets the shell, as a path or a name looked up on `PATH`. | | `user` | string | Sets the user the session runs as. Without `shell`, the session runs through the user's login shell, so a user whose shell is `nologin` cannot run commands. The user must exist in `/etc/passwd` of the sandbox. | | `executionTimeoutSeconds` | number | Sets how many seconds the session can run before Daytona kills it. | ```bash curl '{toolboxProxyUrl}/{sandboxId}/processes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "kind": "pty", "shell": "/bin/bash", "user": "root", "timeoutMs": 600000, "stdin": "open" }' ``` | **Field** | **Type** | **Description** | | ----------- | --------- | ------------------------------------------------------------------------------------------------------ | | `timeoutMs` | integer | Sets the session timeout in milliseconds. When the timeout elapses, the daemon sends `SIGKILL` and the record reports `reason: timed_out`. | ## Connect to PTY session [#connect-to-pty-session] Connect to an existing PTY session by its ID. Connecting creates a handle without contacting the sandbox; the handle sends input to the session, reads its output, resizes it, and stops it. A live connection streams output produced after it opens; to see earlier output, replay it from a cursor. ```python handle = sandbox.pty.connect(process_id) handle.write_stdin('echo "Reconnected"\n') # The Python SDK has no WebSocket attach. Use write_stdin() and # stream_logs() instead. ``` ```typescript const handle = sandbox.pty.connect(processId) const connection = await handle.attach() const decoder = new TextDecoder() ;(async () => { for await (const chunk of connection.readable) { process.stdout.write(decoder.decode(chunk)) } })() connection.write('echo "Reconnected"\n') ``` - The request requires the `write:sandboxes` permission. - `attach()` opens a WebSocket and needs a global `WebSocket` in the runtime. - Read earlier output with `logs()`. - If the session has already exited, the connection closes without output. ```bash daytona sandbox process attach {sandboxId} {processId} # Replay output from a previous log cursor before the live stream daytona sandbox process attach {sandboxId} {processId} --cursor {cursor} ``` | **Flag** | **Type** | **Description** | | ---------- | -------- | --------------------------------------------------------------- | | `--cursor` | string | Replays output from a previous log cursor before the live stream. Without this flag, the command shows only output produced after the attach. | ```bash # WebSocket endpoint: binary frames carry PTY output, # text or binary client messages are written to stdin. # Authenticate with an Authorization: Bearer header or ?daytona_token= (browsers). {toolboxWebSocketUrl}/{sandboxId}/processes/{processId}/attach # Pass ?cursor= to replay output from a previous log cursor before the live stream. # Close codes: 1008 "cursor expired" | "invalid cursor" | "slow consumer"; # a plain close when the process is already terminal (read scrollback from /logs). {toolboxWebSocketUrl}/{sandboxId}/processes/{processId}/attach?cursor={cursor} ``` - The request requires the `write:sandboxes` permission. - `{toolboxWebSocketUrl}` is the `toolbox_proxy_url` field of the [sandbox](/docs/sandboxes#get-a-sandbox), with `wss://` in place of `https://`. ## List PTY sessions [#list-pty-sessions] List PTY sessions, including retained exited ones; check each session's state. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() processes = sandbox.pty.list() for info in processes.items: print(f"PTY: {info.id} ({info.state})") if info.terminal: print(f" Size: {info.terminal.cols}x{info.terminal.rows}") print(f" Working directory: {info.cwd}") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const processes = await sandbox.pty.list() for (const info of processes.items) { console.log(`PTY: ${info.id} (${info.state})`) if (info.terminal) { console.log(` Size: ${info.terminal.cols}x${info.terminal.rows}`) } console.log(` Working directory: ${info.cwd}`) } ``` ```bash daytona sandbox process list {sandboxId} --kind pty ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/processes?kind=pty' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Get PTY session info [#get-pty-session-info] ```python info = handle.get() print(f"PTY: {info.id}") print(f"State: {info.state}") print(f"PID: {info.pid}") if info.terminal: print(f"Size: {info.terminal.cols}x{info.terminal.rows}") print(f"Working directory: {info.cwd}") if info.state == "terminal": print(f"Exit code: {info.exit_code}") ``` ```typescript const info = await handle.get() console.log(`PTY: ${info.id}`) console.log(`State: ${info.state}`) console.log(`PID: ${info.pid}`) console.log(`Size: ${info.terminal?.cols}x${info.terminal?.rows}`) console.log(`Working directory: ${info.cwd}`) if (info.state === 'terminal') { console.log(`Exit code: ${info.exitCode}`) } ``` ```bash daytona sandbox process get {sandboxId} {processId} ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Kill PTY session [#kill-pty-session] Stop a PTY session and remove it. Stopping sends a signal to the session's processes; removing deletes the session record and its output. An interactive shell ignores the default termination signal, so a session started without a command keeps running unless the signal escalates to a kill. Removing a session that is still running kills it first. ```python # Sends SIGTERM, then SIGKILL after 5 seconds if the session is still running handle.kill(escalate_after=5) # Removes the process record and logs handle.delete() ``` | **Parameter** | **Type** | **Description** | | ---------------- | ------------ | --------------------------------------------------------------------------------------------------- | | `signal` | SignalName | Sets the signal that `kill()` sends: `TERM`, `KILL`, `INT`, `HUP`, `QUIT`, `USR1`, or `USR2`. The default is `"TERM"`. | | `escalate_after` | float | Sends `escalate_to` if the session is still running after this delay. The delay is in seconds, at most `600`. | | `escalate_to` | SignalName | Sets the signal sent after `escalate_after`. The default is `"KILL"`. | ```typescript // Sends SIGTERM, then SIGKILL after 5 seconds if the session is still running await handle.kill({ escalateAfterSeconds: 5 }) // Removes the process record and logs await handle.delete() ``` | **Parameter** | **Type** | **Description** | | --------------- | --------------- | ----------------------------------------------------------------------------------------------------------- | | `signal` | ProcessSignal | Sets the signal that `kill()` sends: `'TERM'`, `'KILL'`, `'INT'`, `'HUP'`, `'QUIT'`, `'USR1'`, or `'USR2'`. | | `escalateAfterSeconds` | number | Sends `escalateTo` if the session is still running after this delay. The delay is in seconds, at most `600`. | | `escalateTo` | ProcessSignal | Sets the signal sent after `escalateAfterSeconds`. The default is `'KILL'`. | ```bash # Send SIGTERM, then SIGKILL after 5 seconds if the session is still running daytona sandbox process signal {sandboxId} {processId} TERM --escalate-after 5s # Or send SIGKILL; the CLI cannot remove the process record and logs daytona sandbox process kill {sandboxId} {processId} ``` | **Flag** | **Type** | **Description** | | ------------------ | -------- | --------------------------------------------------------------------------------------------------- | | `--escalate-after` | duration | Sends `--escalate-to` if the session is still running after this delay. The delay is a duration such as `5s`, at most `10m`. The flag is available only on `process signal`. | | `--escalate-to` | string | Sets the second signal, sent after `--escalate-after`. The default is `KILL`. The flag is available only on `process signal`. | - The request requires the `write:sandboxes` permission. - Signal names have no `SIG` prefix, and the accepted names are `TERM`, `KILL`, `INT`, `HUP`, `QUIT`, `USR1`, and `USR2`. ```bash # Send a signal curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}/signals' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"signal": "SIGTERM"}' # Remove the process record and logs curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}' \ --request DELETE \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Type** | **Description** | | ----------------- | --------- | ------------------------------------------------------------------------------------------------- | | `signal` | string | Sets the signal to send, with the `SIG` prefix. An empty value sends `SIGTERM`. | | `escalateAfterMs` | integer | Sends `escalateTo` if the session is still running after this delay. The delay is at most `600000`. | | `escalateTo` | string | Sets the signal sent after `escalateAfterMs`, with the `SIG` prefix. The default is `SIGKILL`. | ### Delete a PTY session [#delete-a-pty-session] Delete a PTY session in one step: a running session gets a termination signal, then the session record and its output are removed. Deleting a non-PTY process fails and leaves it running. ```python sandbox.pty.delete(handle.id) ``` ```typescript await sandbox.pty.delete(handle.id) ``` ## Resize PTY session [#resize-pty-session] Resize the terminal of a running PTY session. The program in the terminal receives a window size change signal and can redraw at the new size. The session record reports the new size. ```python handle.resize(150, 40) ``` - The request requires the `write:sandboxes` permission. - `cols` and `rows` must be at least `1`. - You can resize only PTY sessions. ```typescript await handle.resize(150, 40) ``` - The request requires the `write:sandboxes` permission. - `cols` and `rows` must be at least `1`. - You can resize only PTY sessions. ```bash curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}/resize' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"cols": 150, "rows": 40}' ``` - The request requires the `write:sandboxes` permission. - `cols` and `rows` must be at least `1`. - You can resize only PTY sessions. ## Send input [#send-input] Send input to a PTY session. Input reaches the terminal as if typed, so end each command with a newline. Control keys are bytes: send Ctrl+C as 0x03 and Ctrl+D as 0x04. A terminal has no separate end of input; to end input, send Ctrl+D. ```python handle.write_stdin("ls -la\n") ``` | **Parameter** | **Type** | **Description** | | ------------- | ------------------- | ------------------------------------------------------- | | `data` | Union\[str, bytes\] | Sets the input to send, as `str` or `bytes`. Bytes are sent base64 encoded. This parameter is required. | ```typescript await handle.writeStdin('ls -la\n') ``` | **Parameter** | **Type** | **Description** | | ------------- | ------------------------- | --------------------------------------------------------------------- | | `data` | string \| Uint8Array | Sets the input to send, as a `string` or a `Uint8Array`. A `Uint8Array` is sent base64 encoded. This parameter is required. | ```bash curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}/stdin' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"data": "ls -la\n"}' ``` | **Field** | **Type** | **Description** | | ---------- | -------- | ---------------------------------- | | `encoding` | string | Sets the input encoding, `text` or `base64`. The default is `text`. | ## Read output [#read-output] Read one page of PTY output. A page holds up to 1,000 frames by default. To read past the first page, continue from a cursor as shown in [replay output from a cursor](#replay-output-from-a-cursor). ```python page = handle.logs() for frame in page.items: print(frame.data, end="") ``` | **Parameter** | **Type** | **Description** | | ------------- | ------------- | ------------------------------------------------------------------------------------------------- | | `limit` | int | Sets the page size. The daemon clamps the page size to `1` to `5000`. The default is `1000`. | | `encoding` | LogEncoding | Sets the output encoding. In text encoding, bytes that are not valid UTF-8 are replaced. Pass `"base64"` to get each frame's `data` as a standard base64 string of the raw bytes. The default is `"text"`. | - The request requires the `write:sandboxes` permission. - A read without a cursor starts at the first frame the session wrote, so it fails once retention evicts that frame; read from the oldest available cursor in the error instead. ```typescript const page = await handle.logs() for (const frame of page.items) { process.stdout.write(frame.data) } ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------------------------------------------------------------------------------------------- | | `limit` | number | Sets the page size. The daemon clamps the page size to `1` to `5000`. The default is `1000`. | | `encoding` | string | Sets the output encoding. In text encoding, bytes that are not valid UTF-8 are replaced. Pass `'base64'` to get each frame's `data` as a standard base64 string of the raw bytes. The default is `'text'`. | - The request requires the `write:sandboxes` permission. - A read without a cursor starts at the first frame the session wrote, so it fails once retention evicts that frame; read from the oldest available cursor in the error instead. ```bash daytona sandbox process logs {sandboxId} {processId} ``` - The request requires the `write:sandboxes` permission. - A read without a cursor starts at the first frame the session wrote, so it fails once retention evicts that frame; read from the oldest available cursor in the error instead. ```bash curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}/logs' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Type** | **Description** | | ---------- | --------- | ---------------------------------------------------------------------------------------------------------------- | | `limit` | integer | Sets the page size. The value is clamped to `1` to `5000`. The default is `1000`. | | `encoding` | string | Sets the output encoding, `text` or `base64`. Text replaces bytes that are not valid UTF-8. Base64 output is standard base64 with padding. The default is `text`. | - The request requires the `write:sandboxes` permission. - A read without a cursor starts at the first frame the session wrote, so it fails once retention evicts that frame; read from the `firstAvailableCursor` of the error instead. ## Replay output from a cursor [#replay-output-from-a-cursor] Replay PTY output from a cursor. Every output frame carries a cursor, and a read with a cursor returns the frames after it, so a client that saved the last cursor it processed can resume without gaps or duplicates. Retention can evict old output; a cursor that points into evicted output fails, and the error carries the oldest cursor still available. See [log retention](/docs/log-streaming#log-retention). ```python # Every frame carries a cursor; reading resumes strictly after it. An empty page means the read has caught up cursor = last_cursor while True: page = handle.logs(cursor=cursor) if not page.items: break for frame in page.items: print(frame.data, end="") cursor = frame.cursor ``` ```typescript // Every frame carries a cursor; reading resumes strictly after it. An empty page means the read has caught up let cursor = lastCursor for (;;) { const page = await handle.logs({ cursor }) if (page.items.length === 0) break for (const frame of page.items) { process.stdout.write(frame.data) cursor = frame.cursor } } ``` ```bash daytona sandbox process logs {sandboxId} {processId} --cursor {cursor} ``` ```bash # An evicted cursor returns 409 CURSOR_EXPIRED with firstAvailableCursor curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}/logs?cursor={cursor}' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Wait for completion [#wait-for-completion] Wait for a PTY session to exit. The result holds the exit code, the ending signal if any, and a reason: exited, killed by a signal, timed out, failed to start, or lost. If the wait time elapses first, the result reports a timeout with no exit code, and the session keeps running. ```python result = handle.wait(timeout=30) print(result.reason, result.exit_code) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | ----------------------------------------------------------------- | | `timeout` | float | Sets how long to wait, in seconds. Omit it or pass `0` to wait until the session exits. | ```typescript const result = await handle.wait({ timeoutSeconds: 30 }) console.log(result.reason, result.exitCode) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------------------------------------------------------------- | | `timeoutSeconds` | number | Sets how long to wait, in seconds. Omit it or pass `0` to wait until the session exits. | ```bash daytona sandbox process wait {sandboxId} {processId} --timeout 30s ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}/wait?timeoutMs=30000' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Type** | **Description** | | ----------- | --------- | ---------------------------------------------------- | | `timeoutMs` | integer | Sets how long to wait, in milliseconds. `0` waits until the session exits. The default is `60000`. | The response has the following fields: | **Field** | **Description** | | ---------- | ---------------------------------------------------------------------------------------------------------------- | | `exitCode` | The exit code of the session. Omitted when the wait time elapses first. | | `reason` | Why the session ended: `exited`, `signaled`, `timed_out`, `lost`, or `failed`. `timed_out` when the wait time elapses first. | | `signal` | The signal name, such as `SIGKILL`. Omitted when none applies. | ## Interactive commands [#interactive-commands] Run several commands in one interactive shell. The shell keeps its state between commands, such as the working directory and variables. Read output while you send input; the output stream ends after the shell exits and all of its output is delivered. ```python import threading from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() handle = sandbox.pty.start() # Print output in the background def pump(): for event in handle.stream_logs(): if event.type == "log": print(event.frame.data, end="") reader = threading.Thread(target=pump, daemon=True) reader.start() for command in [ "pwd", "ls -la", "echo 'Hello from PTY'", "python3 -c 'print(2 + 2)'", "exit", ]: handle.write_stdin(command + "\n") result = handle.wait() reader.join(timeout=5) print(f"\nShell exited with code: {result.exit_code}") handle.delete() ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const { handle, connection } = await sandbox.pty.attach() const decoder = new TextDecoder() const reader = (async () => { for await (const chunk of connection.readable) { process.stdout.write(decoder.decode(chunk)) } })() for (const command of [ 'pwd', 'ls -la', "echo 'Hello from PTY'", "python3 -c 'print(2 + 2)'", 'exit', ]) { connection.write(command + '\n') } const result = await handle.wait() await reader if (result.reason === 'exited') { console.log(`\nShell exited with code: ${result.exitCode}`) } await handle.delete() ``` - The request requires the `write:sandboxes` permission. - The connection streams output written after it opens, so the shell's first prompt can be missing; read it with `handle.logs()`. ## Long-running processes [#long-running-processes] Interrupt a running command from the terminal. Ctrl+C sent as input goes through the terminal, which interrupts the foreground command; the shell itself keeps running. Signaling the session instead targets the whole session, including the shell. See [kill PTY session](#kill-pty-session). ```python import time from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() handle = sandbox.pty.start() handle.write_stdin("for i in $(seq 1 100); do echo \"Line $i\"; sleep 1; done\n") time.sleep(5) handle.write_stdin("\x03") # Ctrl+C handle.write_stdin("exit\n") result = handle.wait() for frame in handle.logs().items: print(frame.data, end="") print(f"\nExit code: {result.exit_code}") handle.delete() ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const { handle, connection } = await sandbox.pty.attach() const decoder = new TextDecoder() ;(async () => { for await (const chunk of connection.readable) { process.stdout.write(decoder.decode(chunk)) } })() connection.write('for i in $(seq 1 100); do echo "Line $i"; sleep 1; done\n') await new Promise((resolve) => setTimeout(resolve, 5000)) connection.write('\x03') // Ctrl+C connection.write('exit\n') const result = await handle.wait() if (result.reason === 'exited') { console.log(`\nExit code: ${result.exitCode}`) } await handle.delete() ``` ## Resource management [#resource-management] Remove a PTY session when you no longer need it. The SDKs keep an exited session's record and output until it is deleted, so delete it in a final block to remove it even when a step fails. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() handle = sandbox.pty.start() try: handle.write_stdin("echo hello\n") handle.write_stdin("exit\n") handle.wait(timeout=10) finally: handle.delete() ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const { handle, connection } = await sandbox.pty.attach() try { connection.write('echo hello\n') connection.write('exit\n') await handle.wait({ timeoutSeconds: 10 }) } finally { connection.close() await handle.delete() } ``` ## Permissions [#permissions] A PTY session runs through the toolbox of a sandbox. Access is governed by one project permission, which you can grant to an [organization role](/docs/organizations#roles) or an [API key](/docs/api-keys). A [sandbox access token](/docs/sandboxes#create-an-access-token) with the toolbox:exec operation grants the same access to a single sandbox, without an API key. The sandbox read permission alone does not grant toolbox access. | **Permission** | **Grants** | | --------------------- | ------------------------------------------------ | | **`write:sandboxes`** | Start, connect to, resize, and kill PTY sessions | # Log Streaming (/docs/log-streaming) Log streaming lets you access and process logs of a running process in real time, or retrieve a snapshot of all logs produced so far. Use streaming for long-running or background commands. Use snapshots when you only need the current output. Every process started through the process module writes its output to a cursor-addressed log ledger. Each log frame carries a channel (stdout and stderr for commands, pty for PTY processes), the data, and a cursor you can use to resume reading from where you left off. See [log frames](#log-frames) for the full list of fields and channels. For entrypoint log streaming, see [entrypoint process](/docs/process-execution#entrypoint-process). To stream logs while sending input to a running command, see [execute interactive commands](/docs/process-execution#execute-interactive-commands). ## Use cases [#use-cases] Log streaming is used when an application needs the output of a process while the process still runs. - **Live output in a UI.** Build, test, or training output appears in a web application as the process writes it. Each frame carries a timestamp, so the UI shows when the output was written. A [PTY process](/docs/pty) sends its merged terminal output on one channel, escape sequences included. - **Agent feedback.** An agent reads the output of a long command as it arrives and stops the command early on an error. A stream sends new output as it is written and ends once the process exits. Its state event reports the exit code, signal, and reason of the process. - **Reconnects.** A client that loses its connection resumes from its last cursor. No frame is delivered twice, and no retained frame is missed. An SSE client sends the ID of the last event it received, and the daemon uses that ID as the cursor. - **Server logs.** The output of a background web server or worker is followed for as long as the process runs. When the server writes nothing, the daemon sends a keepalive every 15 seconds. The CLI follows the logs until the process exits, then exits with the exit code of the process. If the CLI timeout passes first, the CLI stops following and the process keeps running. - **Error handling.** Stdout and stderr arrive on separate channels, so an application handles errors apart from regular output. Order is kept within a channel, not across channels. The CLI writes stdout frames to its standard output and stderr frames to its standard error. ## Stream logs [#stream-logs] Stream the logs of a process. A stream replays the retained output from the beginning or from a cursor, then new output as it is written. It ends once the process exits and all output is delivered. While the process is silent, the daemon sends a keepalive every 15 seconds. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() handle = sandbox.process.start( 'for i in {1..5}; do echo "Step $i"; echo "Error $i" >&2; sleep 1; done' ) for event in handle.stream_logs(): if event.type == "log": print(f"[{event.frame.channel.upper()}]: {event.frame.data}", end="") ``` - The request requires the `write:sandboxes` permission. - `stream_logs()` does not reopen a dropped connection. - [Resume](#resume-a-stream) from the cursor of the last event instead. ```python import asyncio from daytona import AsyncDaytona async def main() -> None: async with AsyncDaytona() as daytona: sandbox = await daytona.v2.create() handle = await sandbox.process.start( 'for i in {1..5}; do echo "Step $i"; echo "Error $i" >&2; sleep 1; done' ) async for event in handle.stream_logs(): if event.type == "log": print(f"[{event.frame.channel.upper()}]: {event.frame.data}", end="") asyncio.run(main()) ``` - The request requires the `write:sandboxes` permission. - `stream_logs()` does not reopen a dropped connection. - [Resume](#resume-a-stream) from the cursor of the last event instead. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const handle = await sandbox.process.start( 'for i in {1..5}; do echo "Step $i"; echo "Error $i" >&2; sleep 1; done', ) for await (const event of handle.streamLogs()) { if (event.type === 'log') { process.stdout.write(`[${event.frame.channel.toUpperCase()}]: ${event.frame.data}`) } } ``` - The request requires the `write:sandboxes` permission. - `streamLogs()` does not reopen a dropped connection. - [Resume](#resume-a-stream) from the cursor of the last event instead. ```bash # Run a command and stream its stdout and stderr until it exits daytona sandbox exec {sandboxId} -- bash -c 'for i in {1..5}; do echo "Step $i"; echo "Error $i" >&2; sleep 1; done' # Follow the logs of a running process in real time (SSE) daytona sandbox process logs {sandboxId} {processId} --follow ``` - The request requires the `write:sandboxes` permission. - Pass the global `--timeout` flag to follow longer than its default of `10m`. - `--follow` supports only `table` and `json` output. ```bash # Start a process curl '{toolboxProxyUrl}/{sandboxId}/processes' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "kind": "exec", "shellCommand": "for i in {1..5}; do echo \"Step $i\"; echo \"Error $i\" >&2; sleep 1; done" }' # Follow process logs in real time (SSE) curl -N '{toolboxProxyUrl}/{sandboxId}/processes/{processId}/logs?follow=true' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` - The request requires the `write:sandboxes` permission. - `{toolboxProxyUrl}` is the `toolbox_proxy_url` field of the [sandbox](/docs/sandboxes#get-a-sandbox). - The `limit` parameter does not apply to a follow stream. ### Stream events [#stream-events] Handle the events of a log stream. Each event carries a cursor, also sent as the event ID, so a client can resume after any event. A stream ends after its end event or its error event. | **Event** | **Payload** | **Meaning** | | ------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | **`log`** | A [log frame](#log-frames) | One chunk of process output. | | **`state`** | The process record with `exitCode`, `signal`, `reason`, and `cursor` | The process reached the `terminal` state. Sent once. Frames written before exit can still follow it. | | **`warning`** | `cursor`, `message`, `firstAvailableCursor` | The requested position was evicted by [log retention](#log-retention). The stream resumes from `firstAvailableCursor` on its own. | | **`eof`** | `cursor` | The process is terminal and every frame has been delivered. The stream closes after this event. | | **`error`** | `cursor`, `message` | The daemon failed to read the ledger. The stream closes after this event. The SDKs raise it as an error instead of yielding it. | ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() handle = sandbox.process.start("for i in {1..3}; do echo \"Step $i\"; sleep 1; done") # The generator returns after the eof event; a stream error is raised for event in handle.stream_logs(): if event.type == "log": print(f"[{event.frame.channel.upper()}]: {event.frame.data}", end="") elif event.type == "state": print(f"Exit code: {event.result.exit_code}, reason: {event.result.reason}") elif event.type == "warning": print(f"Older logs were evicted, resuming from {event.first_available_cursor}") elif event.type == "eof": print(f"Stream complete at cursor {event.cursor}") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const handle = await sandbox.process.start('for i in {1..3}; do echo "Step $i"; sleep 1; done') // The iterator returns after the eof event; a stream error is thrown for await (const event of handle.streamLogs()) { switch (event.type) { case 'log': process.stdout.write(`[${event.frame.channel.toUpperCase()}]: ${event.frame.data}`) break case 'state': console.log('Process finished:', event.result) break case 'warning': console.warn(`Older logs were evicted, resuming from ${event.firstAvailableCursor}`) break case 'eof': console.log('Stream complete at cursor', event.cursor) break } } ``` ```bash curl -N '{toolboxProxyUrl}/{sandboxId}/processes/{processId}/logs?follow=true' \ --header 'Authorization: Bearer YOUR_API_KEY' # event: log # id: djE6MQ # data: {"cursor":"djE6MQ","channel":"stdout","timestamp":1757756400000,"data":"Step 1\n"} # # : keepalive # # event: state # id: djE6Mw # data: {"id":"p_...","kind":"exec","state":"terminal","exitCode":0,"reason":"exited","cursor":"djE6Mw",...} # # event: eof # id: djE6Mw # data: {"cursor":"djE6Mw"} ``` ### Resume a stream [#resume-a-stream] Resume a stream from a cursor. A stream opened with a cursor starts strictly after that position, so no frame is delivered twice. An SSE client that reconnects sends the ID of the last event it received, and the daemon uses that ID in place of the cursor. If retention evicted the position, the stream warns and continues from the oldest retained frame. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() handle = sandbox.process.start("for i in {1..5}; do echo \"Step $i\"; sleep 1; done") # Stop early, remembering the cursor of the last frame processed last_cursor = None for event in handle.stream_logs(): if event.type != "log": continue print(event.frame.data, end="") last_cursor = event.cursor if event.frame.data.startswith("Step 3"): break # Resume strictly after the last frame seen for event in handle.stream_logs(cursor=last_cursor): if event.type == "log": print(event.frame.data, end="") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const handle = await sandbox.process.start('for i in {1..5}; do echo "Step $i"; sleep 1; done') // Stop early, remembering the cursor of the last event processed let lastCursor: string | undefined for await (const event of handle.streamLogs()) { lastCursor = event.cursor ?? lastCursor if (event.type !== 'log') continue process.stdout.write(event.frame.data) if (event.frame.data.startsWith('Step 3')) break } // Resume strictly after the last event seen for await (const event of handle.streamLogs({ cursor: lastCursor })) { if (event.type === 'log') process.stdout.write(event.frame.data) } ``` ```bash # --output json prints one frame per line, each with its cursor daytona sandbox process logs {sandboxId} {processId} --follow --cursor {cursor} --output json ``` ```bash # Resume with the cursor query parameter curl -N '{toolboxProxyUrl}/{sandboxId}/processes/{processId}/logs?follow=true&cursor={cursor}' \ --header 'Authorization: Bearer YOUR_API_KEY' # Resume with the SSE Last-Event-ID header; it takes precedence over cursor curl -N '{toolboxProxyUrl}/{sandboxId}/processes/{processId}/logs?follow=true' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Last-Event-ID: {cursor}' ``` ## Retrieve all existing logs [#retrieve-all-existing-logs] Read the logs a process has produced so far. A read returns one page and does not wait for new output. Output longer than one page needs [pagination](#paginate-logs). ```python # Logs produced up to the current point in time page = handle.logs() for frame in page.items: print(f"[{frame.channel.upper()}]: {frame.data}", end="") ``` - The request requires the `write:sandboxes` permission. - A page holds up to `1000` frames by default. - The daemon clamps the page size to the range `1` to `5000`. ```typescript // Logs produced up to the current point in time const page = await handle.logs() for (const frame of page.items) { process.stdout.write(`[${frame.channel.toUpperCase()}]: ${frame.data}`) } ``` - The request requires the `write:sandboxes` permission. - A page holds up to `1000` frames by default. - The daemon clamps the page size to the range `1` to `5000`. ```bash daytona sandbox process logs {sandboxId} {processId} ``` - The request requires the `write:sandboxes` permission. - A page holds up to `1000` frames by default. - The daemon clamps the page size to the range `1` to `5000`. ```bash curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}/logs' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` - The request requires the `write:sandboxes` permission. - A page holds up to `1000` frames by default. - The daemon clamps the page size to the range `1` to `5000`. ### Paginate logs [#paginate-logs] Read logs page by page. Pass the next cursor of a page to read the page after it. An empty page means the read has caught up with the output; a process that is still running can produce more. ```python from daytona import Daytona from daytona.v2 import LogFrame daytona = Daytona() sandbox = daytona.v2.create() handle = sandbox.process.start("seq 1 5000") handle.wait() frames: list[LogFrame] = [] cursor = None while True: page = handle.logs(cursor=cursor, limit=500) frames.extend(page.items) if not page.items or page.next_cursor == cursor: break cursor = page.next_cursor stdout = "".join(frame.data for frame in frames if frame.channel == "stdout") print(f"{len(frames)} frames, {len(stdout)} bytes of stdout") ``` - The request requires the `write:sandboxes` permission. - `LogPage` has no end-of-log flag. - Stop reading when a page is empty and the process has exited. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const handle = await sandbox.process.start('seq 1 5000') await handle.wait() const frames = [] let cursor: string | undefined for (;;) { const page = await handle.logs({ cursor, limit: 500 }) frames.push(...page.items) if (page.items.length === 0 || page.nextCursor === cursor) break cursor = page.nextCursor } const stdout = frames.filter((frame) => frame.channel === 'stdout').map((frame) => frame.data).join('') console.log(`${frames.length} frames, ${stdout.length} bytes of stdout`) ``` - The request requires the `write:sandboxes` permission. - `LogsPage` has no end-of-log flag. - Stop reading when a page is empty and the process has exited. ```bash # First page, at most 500 frames; JSON output includes nextCursor and eof daytona sandbox process logs {sandboxId} {processId} --limit 500 --output json # Following pages, until eof is true daytona sandbox process logs {sandboxId} {processId} --limit 500 --cursor {nextCursor} --output json ``` ```bash # First page, at most 500 frames curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}/logs?limit=500' \ --header 'Authorization: Bearer YOUR_API_KEY' # { # "frames": [ # { "cursor": "djE6MQ", "channel": "stdout", "timestamp": 1757756400000, "data": "1\n2\n3\n" } # ], # "nextCursor": "djE6MQ", # "truncatedHead": false, # "eof": false # } # Following pages, until eof is true curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}/logs?cursor={nextCursor}&limit=500' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ### Binary output [#binary-output] Read binary output without loss. The default text encoding replaces bytes that are not valid UTF-8. Base64 encoding returns the raw bytes of each frame as standard base64 with padding. ```python import base64 from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() handle = sandbox.process.start("head -c 1024 /dev/urandom") handle.wait() page = handle.logs(encoding="base64") raw = b"".join( base64.b64decode(frame.data) for frame in page.items if frame.channel == "stdout" ) print(f"{len(raw)} bytes") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const handle = await sandbox.process.start('head -c 1024 /dev/urandom') await handle.wait() const page = await handle.logs({ encoding: 'base64' }) const raw = Buffer.concat( page.items .filter((frame) => frame.channel === 'stdout') .map((frame) => Buffer.from(frame.data, 'base64')), ) console.log(`${raw.length} bytes`) ``` ```bash # Frames carry base64 payloads; table output is not binary-safe, so use JSON daytona sandbox process logs {sandboxId} {processId} --encoding base64 --output json ``` ```bash # Frames carry base64 payloads curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}/logs?encoding=base64' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Log frames [#log-frames] | **Field** | **Description** | | --------------- | ------------------------------------------------------------------------------------------------------------------------ | | `cursor` | Opaque position of this frame. Pass it as `cursor` to read strictly after this frame. `""` and `start` mean the beginning. | | `channel` | Which stream the frame came from. See the channel table below. | | `data` | The payload, as UTF-8 text or as a standard base64 string with padding when `encoding` is `base64`. | | `timestamp` | When the daemon recorded the frame, in Unix milliseconds. | | **Channel** | **Emitted by** | **Content** | | ------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | **`stdout`** | `exec` and `code` processes | Standard output. `exec` processes keep `stdout` and `stderr` on separate pipes, so ordering is preserved within a channel, not across channels. | | **`stderr`** | `exec` and `code` processes | Standard error. | | **`pty`** | [PTY processes](/docs/pty) | The merged terminal byte stream, including escape sequences. | | **`setup`** | The daemon | Progress messages emitted while the daemon provisions something the process needs, for example a language kernel before the first code execution. | | **`display`** | [Code executions](/docs/code-execution) | A kernel side output (`display_data` or `update_display_data`) as a JSON object keyed by media type. | | **`execute_result`** | [Code executions](/docs/code-execution) | The kernel's primary result (`execute_result`) as a JSON object keyed by media type. | | **`error`** | [Code executions](/docs/code-execution) | A kernel exception as JSON: `{"ename", "evalue", "traceback"}`. | ## Log retention [#log-retention] Control how long a process record and its logs are kept. The retention policy is set when the process starts. Independent of the policy, retained output is capped in size; output beyond the cap evicts the oldest frames. A page read from an evicted position fails with an error that names the oldest retained position; a stream warns and continues from that position. | **Policy** | **Default for** | **Behavior** | | ------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`until_cleanup`** | API requests that set `name`, [PTY processes](/docs/pty) started through the SDKs, and the entrypoint process | The record and logs stay until the process is deleted. A process that fails to start is downgraded to `on_exit_ttl` so failed records do not accumulate. | | **`on_exit_ttl`** | API requests without `name`, and every SDK `process.start()` and `run()` call | The record and logs are removed 10 minutes after the process exits. Reading them afterwards fails with `404 PROCESS_NOT_FOUND`. | | **`none`** | | Logs are discarded the moment the process exits. The record stays for the same 10 minute window, so `wait()` still returns the exit result. `run()` never uses it. | ```python from daytona import Daytona from daytona.v2 import CursorExpiredError daytona = Daytona() sandbox = daytona.v2.create() # Keep the logs of an unnamed process until delete() handle = sandbox.process.start("seq 1 5000", keep_logs="until_cleanup") handle.wait() saved_cursor = "..." # a cursor stored from an earlier read try: page = handle.logs(cursor=saved_cursor) except CursorExpiredError as e: # The position was evicted; restart from the oldest retained frame page = handle.logs(cursor=e.first_available_cursor) handle.delete() ``` ```typescript import { Daytona } from '@daytona/sdk' import { CursorExpiredError } from '@daytona/sdk/v2' const daytona = new Daytona() const sandbox = await daytona.v2.create() // Keep the logs of an unnamed process until delete() const handle = await sandbox.process.start('seq 1 5000', { keepLogs: 'until_cleanup' }) await handle.wait() const savedCursor = '...' // a cursor stored from an earlier read let page try { page = await handle.logs({ cursor: savedCursor }) } catch (error) { if (!(error instanceof CursorExpiredError)) throw error // The position was evicted; restart from the oldest retained frame page = await handle.logs({ cursor: error.firstAvailableCursor }) } await handle.delete() ``` ```bash # Keep the logs until the record is deleted curl '{toolboxProxyUrl}/{sandboxId}/processes' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "kind": "exec", "shellCommand": "seq 1 5000", "keepLogs": "until_cleanup" }' # Read from an evicted position curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}/logs?cursor={cursor}' \ --header 'Authorization: Bearer YOUR_API_KEY' # HTTP 409 # { # "code": "CURSOR_EXPIRED", # "message": "the requested position was evicted by log retention", # "firstAvailableCursor": "djE6NDA5Ng", # "source": "DAYTONA_DAEMON" # } # Delete the record and its logs curl '{toolboxProxyUrl}/{sandboxId}/processes/{processId}' \ --request DELETE \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Permissions [#permissions] Log streaming runs through the toolbox of a sandbox. Access is governed by one project permission, which you can grant to an [organization role](/docs/organizations#roles) or an [API key](/docs/api-keys). A [sandbox access token](/docs/sandboxes#create-an-access-token) with the toolbox:exec operation grants the same access to a single sandbox, without an API key. The sandbox read permission alone does not grant toolbox access. | **Permission** | **Grants** | | --------------------- | ---------------------------- | | **`write:sandboxes`** | Stream and read process logs | # Daytona MCP Server (/docs/mcp) Daytona's Model Context Protocol (MCP) server connects AI agents to [sandboxes](/docs/sandboxes). ## Install Daytona CLI [#install-daytona-cli] Install the Daytona CLI. ```bash brew install daytonaio/cli/daytona-alpha ``` ```powershell irm https://github.com/daytona/clients/releases/download/v2.0.0-alpha.11/install.ps1 | iex ``` ## Authenticate with Daytona [#authenticate-with-daytona] Authenticate with Daytona. ```bash daytona login ``` ## Available tools [#available-tools] MCP server provides the following tools for interacting with Daytona Sandboxes: - [Sandbox management](/docs/sandboxes) - [File system operations](/docs/file-system-operations) - [Git operations](/docs/git-operations) - [Process execution](/docs/process-execution) - [Code execution](/docs/code-execution) - [Computer use](/docs/computer-use) - [Preview](/docs/preview) # Computer Use (/docs/computer-use) Computer Use enables programmatic control of desktop environments within sandboxes. It provides mouse, keyboard, screenshot, screen recording, accessibility, and display operations for automating GUI interactions and testing desktop applications. Computer Use and [VNC](/docs/vnc-access) work together to enable both manual and automated desktop interactions. VNC provides the visual interface for users to manually interact with the desktop, while Computer Use provides the programmatic API for AI agents to automate operations. The desktop stack (D-Bus, Xvfb, the window manager, and the x11vnc server) ships with the sandbox runtime as a self-contained add-on, so it works on any Linux sandbox image, including images with no desktop packages. The virtual display runs at 1280x720. All Computer Use calls go through the toolbox API and require an API key with the sandbox write permission or a sandbox access token with the desktop or toolbox operation. ## Use cases [#use-cases] Computer Use is used when a task needs a graphical desktop instead of an API or a shell. - **Computer-use agents.** An agent reads screenshots and the accessibility tree, then moves the mouse and types to operate desktop applications. The desktop stack ships with the sandbox runtime, so it works on any Linux sandbox image. All Computer Use calls go through the toolbox API and require an API key with the sandbox write permission or a sandbox access token with the desktop or toolbox operation. - **Desktop application tests.** GUI tests click, type, and take screenshots to check the state of an application. Full-screen and region screenshots return a PNG image as a base64 string. The virtual display has a fixed resolution of 1280x720, with no setting per sandbox. - **Browser automation.** A browser on the sandbox desktop is driven through mouse and keyboard input, for sites that have no API. Typing sends any text, including symbols and non-ASCII characters. A delay between characters slows typing for applications that drop fast input. - **Agents on Windows and macOS.** Windows and macOS sandboxes provide screenshots, mouse and keyboard input, the accessibility tree, and screen recordings through the same API. On Windows, a desktop session must be open. When that session ends or another session replaces it, the desktop fails with a cause that names the reason. - **Human in the loop.** A person watches or takes over the same desktop through [VNC](/docs/vnc-access) while an agent works. Starting Computer Use also starts the x11vnc server. The VNC server runs in shared mode, so several viewers attach to the same desktop at once. ## Start Computer Use [#start-computer-use] Start all computer use processes (D-Bus, Xvfb, the window manager, and the x11vnc server) in the sandbox. The call blocks until the X server and the VNC server accept connections and returns the status of each process. If either does not come up within 5 s, the desktop is marked failed and the call returns an error. Calling start on a desktop that is already ready returns the current status. A start made while another start or a restart is in progress waits for it to finish, then returns the resulting status, or an error if the desktop failed. Calling start on a failed desktop returns an error; call [restart](#restart-computer-use) instead. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() result = sandbox.computer_use.start() print("Computer use processes started:", result.message) for name, process in result.status.items(): print(f"{name}: running={process.running} pid={process.pid}") ``` ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); const result = await sandbox.computerUse.start(); console.log('Computer use processes started:', result.message); for (const [name, process] of Object.entries(result.status)) { console.log(`${name}: running=${process.running} pid=${process.pid}`); } ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/start' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' ``` - The request requires the `write:sandboxes` permission. - `{toolboxProxyUrl}` is the `toolbox_proxy_url` field of the [sandbox](/docs/sandboxes#get-a-sandbox). ## Stop Computer Use [#stop-computer-use] Stop all computer use processes in the sandbox. Stopping a ready desktop returns it to not started. Stopping a failed desktop kills any remaining processes but keeps the failed state and its cause; only [restart](#restart-computer-use) leaves the failed state. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() result = sandbox.computer_use.stop() print("Computer use processes stopped:", result.message) ``` ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); const result = await sandbox.computerUse.stop(); console.log('Computer use processes stopped:', result.message); ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/stop' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Restart Computer Use [#restart-computer-use] Tear the desktop down and build a new one. Restart is valid from every state and is the only way out of the failed state. Concurrent restart calls join the same in-flight restart and receive the same result. Application windows do not survive a restart. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() result = sandbox.computer_use.restart() print(result.message, result.desktop) # computer use restarted ready ``` ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); const result = await sandbox.computerUse.restart(); console.log(result.message, result.desktop); // computer use restarted ready ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/restart' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Get status [#get-status] Get the desktop status. The status field is active only while the desktop field is ready. | **`desktop`** | **`status`** | **Meaning** | | ----------------- | ------------ | --------------------------------------------------------------------------------------------------------- | | `not_started` | `inactive` | No desktop exists. Call [start](#start-computer-use). | | `starting` | `inactive` | Start is in progress. Poll status. | | `ready` | `active` | Actions are accepted. | | `failed` | `inactive` | The desktop is gone. `cause.code` and `cause.message` explain why. Call [restart](#restart-computer-use). | The cause object is present only when the desktop is failed. Possible cause codes: | **Code** | **Meaning** | | ----------------------------- | --------------------------------------------------------------------------------------------- | | **`desktop_exited`** | The X server (`xvfb`) exited. Windows in it are lost. | | **`addon_crashed`** | The add-on that supervises the desktop died while the desktop was ready. | | **`start_failed`** | Creating the desktop or probing its readiness failed, for example a missing binary. | | **`start_interrupted`** | The add-on died while the desktop was being created. | | **`state_corrupt`** | Retained desktop state could not be read or validated. | | **`state_persistence_failed`** | A state transition could not be persisted. | | **`session_ended`** | Windows and macOS only. The interactive desktop session ended or left the console. | | **`session_replaced`** | Windows only. Another interactive desktop session replaced the one the desktop was bound to. | Failure evidence is boot-scoped: a sandbox reboot resets the desktop to not started. The SDKs report a state or cause code they do not recognize as `unknown` and keep the API value in `raw_status`, `raw_desktop`, and `cause.raw_code` (`rawStatus`, `rawDesktop`, and `cause.rawCode` in TypeScript). ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() status = sandbox.computer_use.status() print("Computer use status:", status.status, status.desktop) if status.desktop == "failed": print(f"Failure: {status.cause.code}: {status.cause.message}") ``` ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); const status = await sandbox.computerUse.status(); console.log('Computer use status:', status.status, status.desktop); if (status.desktop === 'failed') { console.log(`Failure: ${status.cause.code}: ${status.cause.message}`); } ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/status' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Get process status [#get-process-status] Get the status of one desktop process: D-Bus, Xvfb, the window manager, or the x11vnc server. Without a name, it returns the [status of every process](#get-all-process-statuses). ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() xvfb_status = sandbox.computer_use.service.status("xvfb") print(xvfb_status.process_name, xvfb_status.running) ``` - The request requires the `write:sandboxes` permission. - The process names are `dbus`, `xvfb`, `wm`, and `x11vnc`. ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); const xvfbStatus = await sandbox.computerUse.service.status('xvfb'); console.log(xvfbStatus.processName, xvfbStatus.running); ``` - The request requires the `write:sandboxes` permission. - The process names are `dbus`, `xvfb`, `wm`, and `x11vnc`. ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/process/{processName}/status' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` - The request requires the `write:sandboxes` permission. - The process names are `dbus`, `xvfb`, `wm`, and `x11vnc`. ## Get all process statuses [#get-all-process-statuses] Get the status of every desktop process. The response is a map keyed by process name, with one entry each for D-Bus, Xvfb, the window manager, and the x11vnc server while a desktop exists. The map is empty before the first start, after stop, after a failed start, and after the add-on restarts. Each entry reports whether the process is alive, its supervisor restart state, and its startup priority. | **API field** | **Description** | | ---------------------- | ------------------------------------------------------------------- | | **`Running`** | Whether the process is alive. | | **`Pid`** | Process ID while the process is running. | | **`Status`** | `running`, `stopped`, or `failed`. | | **`Failed`** | Whether the supervisor gave up on the process: `xvfb` after it exits, the other processes after their automatic restarts run out. A process restart or a desktop restart clears it. | | **`AutoRestart`** | Whether the supervisor restarts the process automatically. | | **`RestartAttempts`** | Exits in a row within 30 s of starting. It reaches `6` when the supervisor marks the process failed. Resets after a run of 30 s or longer, a process restart, or a desktop start. | | **`RestartBackoffMs`** | Current restart backoff in milliseconds. The SDKs return it in seconds, as `restart_backoff` in Python and `restartBackoffSeconds` in TypeScript. | | **`Priority`** | Relative startup priority within the desktop stack. | ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() for name, process in sandbox.computer_use.service.status().items(): print(f"{name}: {process.status} pid={process.pid} restarts={process.restart_attempts}") ``` ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); const statuses = await sandbox.computerUse.service.status(); for (const [name, process] of Object.entries(statuses)) { console.log(`${name}: ${process.status} pid=${process.pid} restarts=${process.restartAttempts}`); } ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/process-status' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Restart process [#restart-process] Restart a specific desktop process. Per-process restart works only while the desktop is ready. The Xvfb process cannot be restarted on its own because application windows live inside it; use [restart](#restart-computer-use) instead. The D-Bus, window manager, and x11vnc processes are restarted automatically when they exit. A process that exits within 30 s of starting is restarted at most 5 times in a row, after waits of 1, 2, 4, 8, and 16 s. The sixth such exit marks it failed in the status map, while the desktop stays ready. A run that lasts 30 s or longer resets the count. Restarting a failed process resets its count and starts it again. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() result = sandbox.computer_use.service.restart("wm") print(result.message, result.process_name) ``` ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); const result = await sandbox.computerUse.service.restart('wm'); console.log(result.message, result.processName); ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/process/{processName}/restart' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Get process logs [#get-process-logs] Get logs for a specific desktop process. Each log is checked every 5 s and whenever the process starts; once it exceeds 256 KiB, it is cut to its most recent 128 KiB. Logs accumulate across process and desktop restarts. Logs and errors are readable only while a desktop exists, including after Xvfb exits. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() logs = sandbox.computer_use.service.logs("x11vnc") print("x11vnc logs:", logs.logs) ``` The response has the following fields: | **Field** | **Description** | | --------- | --------------- | | `logs` | Holds the retained standard output of the process. | ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); const logs = await sandbox.computerUse.service.logs('x11vnc'); console.log('x11vnc logs:', logs.logs); ``` The response has the following fields: | **Field** | **Description** | | --------- | --------------- | | `logs` | Holds the retained standard output of the process. | ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/process/{processName}/logs' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Get process errors [#get-process-errors] Get error logs for a specific desktop process. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() errors = sandbox.computer_use.service.errors("x11vnc") print("x11vnc errors:", errors.errors) ``` The response has the following fields: | **Field** | **Description** | | --------- | --------------- | | `errors` | Holds the retained standard error output of the process, plus lines the supervisor adds when the process cannot be started or runs out of automatic restarts. Trimmed like the logs. The value is an empty string when nothing was written. | ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); const errors = await sandbox.computerUse.service.errors('x11vnc'); console.log('x11vnc errors:', errors.errors); ``` The response has the following fields: | **Field** | **Description** | | --------- | --------------- | | `errors` | Holds the retained standard error output of the process, plus lines the supervisor adds when the process cannot be started or runs out of automatic restarts. Trimmed like the logs. The value is an empty string when nothing was written. | ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/process/{processName}/errors' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Mouse operations [#mouse-operations] Every mouse, keyboard, screenshot, display, accessibility, and recording-start call requires the desktop to be ready; otherwise it returns 409 with a desktop not ready error. Coordinates are screen pixels in the range -32768 to 32767; values outside it return 400. ### Click [#click] Click the mouse at the specified coordinates with the left, right, or middle button, as a single or a double click. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() sandbox.computer_use.mouse.click(x=100, y=200) ``` | **Parameter** | **Type** | **Description** | | ------------- | ------------------------ | -------------------- | | `button` | ComputerUseMouseButton | Sets the mouse button. The default is `"left"`. | | `double` | bool | Makes the click a double click. The default is `False`. | ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); await sandbox.computerUse.mouse.click({ x: 100, y: 200 }); ``` | **Parameter** | **Type** | **Description** | | ------------- | ------------------------ | -------------------- | | `button` | ComputerUseMouseButton | Sets the mouse button. The default is `'left'`. | | `double` | boolean | Makes the click a double click. The default is `false`. | ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/mouse/click' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "x": 100, "y": 200 }' ``` | **Field** | **Type** | **Description** | | --------- | --------- | ------------------- | | `button` | string | Sets the mouse button. The default is `left`. | | `double` | boolean | Makes the click a double click. The default is `false`. | The response has the following fields: | **Field** | **Description** | | --------- | --------------- | | `x` | Holds the x coordinate of the pointer after the click. | | `y` | Holds the y coordinate of the pointer after the click. | ### Move [#move] Move the mouse cursor to the given coordinates. The response holds the final pointer position. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() result = sandbox.computer_use.mouse.move(x=100, y=200) print(f"Mouse moved to: {result.x}, {result.y}") ``` ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); const result = await sandbox.computerUse.mouse.move({ x: 100, y: 200 }); console.log(`Mouse moved to: ${result.x}, ${result.y}`); ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/mouse/move' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "x": 100, "y": 200 }' ``` ### Drag [#drag] Drag the mouse from start to end coordinates while holding the selected button. The pointer moves in 10 interpolated steps so applications that ignore a teleporting drag still register it. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() result = sandbox.computer_use.mouse.drag( start_x=50, start_y=50, end_x=150, end_y=150, button="left" ) print(f"Drag ended at {result.x}, {result.y}") ``` | **Parameter** | **Type** | **Description** | | ------------- | ------------------------ | ------------------ | | `button` | ComputerUseMouseButton | Sets the mouse button to hold. The default is `"left"`. | ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); const result = await sandbox.computerUse.mouse.drag({ startX: 50, startY: 50, endX: 150, endY: 150, button: 'left', }); console.log(`Drag ended at ${result.x}, ${result.y}`); ``` | **Parameter** | **Type** | **Description** | | ------------- | ------------------------ | --------------- | | `button` | ComputerUseMouseButton | Sets the mouse button to hold. The default is `'left'`. | ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/mouse/drag' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "button": "left", "endX": 150, "endY": 150, "startX": 50, "startY": 50 }' ``` | **Field** | **Type** | **Description** | | --------- | -------- | ---------------- | | `button` | string | Sets the mouse button to hold. The default is `left`. | ### Scroll [#scroll] Scroll the mouse wheel up or down at the specified coordinates by a number of wheel ticks. One tick is one notch of a physical mouse wheel. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() result = sandbox.computer_use.mouse.scroll(x=100, y=200, direction="down") print(result.success) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `amount` | int | Sets the number of wheel ticks. The value must not be negative, and a value above `100` is capped at `100`. The default is `1`. | ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); const result = await sandbox.computerUse.mouse.scroll({ x: 100, y: 200, direction: 'down' }); console.log(result.success); ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `amount` | number | Sets the number of wheel ticks. The value must not be negative, and a value above `100` is capped at `100`. The default is `1`. | ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/mouse/scroll' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "direction": "down", "x": 100, "y": 200 }' ``` | **Field** | **Type** | **Description** | | --------- | -------- | --------------- | | `amount` | integer | Sets the number of wheel ticks. The value must not be negative, and a value above `100` is capped at `100`. The default is `1`. | ### Get position [#get-position] Get the current mouse cursor position. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() position = sandbox.computer_use.mouse.position() print(f"Mouse is at: {position.x}, {position.y}") ``` ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); const position = await sandbox.computerUse.mouse.position(); console.log(`Mouse is at: ${position.x}, ${position.y}`); ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/mouse/position' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Keyboard operations [#keyboard-operations] Send keyboard input to the desktop. Type a text string, press one key with optional modifiers, or press a hotkey combination. Key names resolve against the keyboard map of the virtual display, and, like mouse calls, keyboard calls require the desktop to be ready. ### Type [#type] Type arbitrary text, including uppercase letters, symbols, and non-ASCII characters. Newlines are sent as Enter and tabs as Tab. Characters with no key in the X11 keymap are typed through a temporary key mapping. Set a delay between characters to slow typing down for applications that drop fast input. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() sandbox.computer_use.keyboard.type(text="Hello, World!") ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `delay` | float | Sets the pause between characters in seconds, rounded up to whole milliseconds. A value above `1` is capped at `1`. A negative value raises `BadRequestError`. The default is `0`. | - The request requires the `write:sandboxes` permission. - The call sends `\n` and `\r` as the Enter key, so a `\r\n` pair presses Enter twice. - The text is limited to 4096 Unicode code points. - The number of characters multiplied by the capped delay must not exceed 25 seconds; a longer request returns `400` before any key is typed. ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); await sandbox.computerUse.keyboard.type({ text: 'Hello, World!' }); ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `delaySeconds` | number | Sets the pause between characters in seconds, rounded up to whole milliseconds. A value above `1` is capped at `1`. A negative value throws `BadRequestError`. The default is `0`. | - The request requires the `write:sandboxes` permission. - The call sends `\n` and `\r` as the Enter key, so a `\r\n` pair presses Enter twice. - The text is limited to 4096 Unicode code points. - The number of characters multiplied by the capped delay must not exceed 25 seconds; a longer request returns `400` before any key is typed. ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/keyboard/type' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "text": "Hello, World!" }' ``` | **Field** | **Type** | **Description** | | --------- | -------- | --------------- | | `delay` | integer | Sets the pause between characters in milliseconds. A value above `1000` is capped at `1000`. The default is `0`. | - The request requires the `write:sandboxes` permission. - The call sends `\n` and `\r` as the Enter key, so a `\r\n` pair presses Enter twice. - The text is limited to 4096 Unicode code points. - The number of characters multiplied by the capped delay must not exceed 25 seconds; a longer request returns `400` before any key is typed. ### Press [#press] Press a key with optional modifiers. Modifiers are held in order, the key is pressed and released, and the modifiers are released in reverse, even if the press fails. See [supported keys](#supported-keys). ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() sandbox.computer_use.keyboard.key(key="enter") ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `modifiers` | Sequence[ComputerUseKeyboardModifier] | Sets the modifier keys to hold during the press: `ctrl`, `alt`, `shift`, or `cmd`. | ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); await sandbox.computerUse.keyboard.key({ key: 'enter' }); ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `modifiers` | ComputerUseKeyboardModifier[] | Sets the modifier keys to hold during the press: `ctrl`, `alt`, `shift`, or `cmd`. | ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/keyboard/key' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "key": "enter" }' ``` | **Field** | **Type** | **Description** | | --------- | -------- | --------------- | | `modifiers` | string[] | Sets the keys to hold during the press, in order. Accepts any name in [supported keys](#supported-keys); a name not in the keymap returns `400`. | ### Hotkey [#hotkey] Press a hotkey combination. The combination is a plus-separated chord string: the last entry is the key, the others are modifiers. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() sandbox.computer_use.keyboard.hotkey(keys="ctrl+c") ``` ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); await sandbox.computerUse.keyboard.hotkey({ keys: 'ctrl+c' }); ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/keyboard/hotkey' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "keys": "ctrl+c" }' ``` ### Supported keys [#supported-keys] Named keys and aliases are case-insensitive. A single character keeps its case: `A` presses Shift and `a`. The following are supported for the key and modifiers parameters and for the parts of the keys parameter: | Category | Keys | | ------------- | ------------------------------------------------------------------------------------------------------------- | | Modifiers | `ctrl` (alias `control`), `alt`, `shift`, `cmd` (aliases `super`, `meta`, `win`) | | Editing | `enter` (alias `return`), `tab`, `escape` (alias `esc`), `backspace`, `delete` (alias `del`), `space` | | Navigation | `home`, `end`, `pageup` (alias `page_up`), `pagedown` (alias `page_down`), `insert`, `up`, `down`, `left`, `right` | | Function keys | `f1` through `f12` | | Symbols | `minus`, `plus`, `equal` (alias `equals`) | | Characters | Any single character in the keymap of the display, for example `a`, `A`, `5`, `/`, `-`. In press and hotkey, a character outside the keymap returns `400`; use [type](#type) for it. In the keys parameter, write `plus` for `+`. | The SDK modifiers type lists ctrl, alt, shift, and cmd; the API accepts those, their aliases, and any other supported key name. ## Accessibility operations [#accessibility-operations] Use accessibility operations to inspect the AT-SPI tree and interact with UI elements by node ID. Start Computer Use before calling accessibility methods. Each node carries an ID, role, name, bounds (x, y, width, height in screen pixels), and states. The description, actions, and children appear only when not empty, and find matches never include children. A node without the AT-SPI component interface reports zero bounds. The role is the AT-SPI role name, passed through unchanged. Node states outside the list in [find nodes](#find-nodes) appear as `state:`, where `` is the AT-SPI state number. On Linux, the value field is never set. A node ID joins the bus name and the object path of the element with a colon, and is valid only while the owning application keeps the object alive. A malformed ID returns `400`. An unknown ID returns `404` from focus and invoke, and `400` from set value. The scope parameter selects the root of the search: | **Scope** | **Root** | | ------------- | -------------------------------------------------------------------------------------------------- | | **`focused`** | The application whose state includes `active`, else the first registered application. Default. | | **`pid`** | The application owned by process `pid`. Returns `404` if no accessible application has that PID. | | **`all`** | The AT-SPI registry root, covering every application. | Traversal is bounded: 2000 nodes and 2 s for the tree, 5000 nodes and 2 s for find. When a bound is reached, the response is marked as truncated. Cutting the tree at the maximum depth does not mark it as truncated. Accessibility operations read the semantic UI information that applications expose over AT-SPI. Apps or custom widgets that do not expose accessibility objects may return sparse nodes, generic roles, or no actionable nodes; mouse, keyboard, and screenshot operations remain available for those cases. ### Get tree [#get-tree] Read an accessibility tree for the focused app, a specific process, or all apps. The maximum depth parameter limits the depth below the root, and 0 returns only the root. The SDKs return at most 256 levels. The API returns the whole subtree within the traversal budget when the parameter is omitted or -1. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() tree = sandbox.computer_use.a11y.tree(scope="focused") print(tree.root.role, tree.root.name, tree.truncated) ``` | **Parameter** | **Type** | **Description** | | ------------- | ---------------------- | --------------------------------------------------- | | `scope` | ComputerUseA11yScope | Sets the root of the search: `"focused"`, `"pid"`, or `"all"`. This parameter is required. With `scope="pid"`, also pass `pid`. | | `pid` | int | Selects the application when `scope` is `"pid"`. | | `max_depth` | int | Limits the depth below the root, from `0` to `256`. A value outside the range raises `BadRequestError`. The default is `256`. | ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); const tree = await sandbox.computerUse.a11y.tree({ scope: 'focused' }); console.log(tree.root.role, tree.root.name, tree.truncated); ``` | **Parameter** | **Type** | **Description** | | ------------- | ---------------------- | --------------------------------------------------- | | `scope` | ComputerUseA11yScope | Sets the root of the search: `'focused'`, `'pid'`, or `'all'`. This parameter is required. With `scope: 'pid'`, also pass `pid`. | | `pid` | number | Selects the application when `scope` is `'pid'`. | | `maxDepth` | number | Limits the depth below the root, from `0` to `256`. A value outside the range throws `BadRequestError`. The default is `256`. | ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/a11y/tree?scope=focused' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Type** | **Description** | | ---------- | --------- | ------------------------------------------------------- | | `scope` | string | Sets the root of the search. An unknown scope returns `400`. The default is `focused`. | | `pid` | integer | Selects the application when `scope=pid`. | | `maxDepth` | integer | Limits the depth below the root. Omit the parameter or pass `-1` to get the whole subtree. The query parameter name is camelCase. | ### Find nodes [#find-nodes] Search the accessibility tree by role, accessible name, state, and scope. All filters must match. Name matching is case-sensitive. The regex mode uses the syntax of the Rust `regex-lite` crate and matches anywhere in the name unless the pattern is anchored with `^` and `$`; an invalid pattern returns `400`. Find visits nodes depth first, last child first, so the first match is not necessarily the first element on screen. Node states: invalid, active, armed, busy, checked, collapsed, defunct, editable, enabled, expandable, expanded, focusable, focused, has-tooltip, horizontal, iconified, modal, multi-line, multiselectable, opaque, pressed, resizable, selectable, selected, sensitive, showing, single-line, stale, transient, vertical, visible, manages-descendants, indeterminate, required, truncated, and animated. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() buttons = sandbox.computer_use.a11y.find(scope="focused", role="button", name="Submit") for node in buttons.matches: print(node.id, node.name, node.bounds.x, node.bounds.y) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------------------------- | ---------------------------------------------------------------------------- | | `scope` | ComputerUseA11yScope | Sets the root of the search. This parameter is required. | | `pid` | int | Selects the application when `scope` is `"pid"`. | | `role` | str | Matches the node role exactly, for example `button` or `entry`. | | `name` | str | Matches the accessible name according to `name_match`. | | `name_match` | ComputerUseA11yNameMatch | Sets how `name` is compared: `exact`, `substring`, or `regex`. The default is `exact`. | | `states` | Sequence\[str\] | Requires the node to have every listed state, for example `["enabled", "focusable"]`. | | `limit` | int | Caps the number of matches. A value of `0` or less uses the default. With more matches, `truncated` is `True`. The default is `50`. | - The request requires the `write:sandboxes` permission. - Filters other than `scope` are optional, and an omitted filter matches every node. ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); const buttons = await sandbox.computerUse.a11y.find({ scope: 'focused', role: 'button', name: 'Submit' }); for (const node of buttons.matches) { console.log(node.id, node.name, node.bounds.x, node.bounds.y); } ``` | **Parameter** | **Type** | **Description** | | ------------- | -------------------------- | ---------------------------------------------------------------------------- | | `scope` | ComputerUseA11yScope | Sets the root of the search. This parameter is required. | | `pid` | number | Selects the application when `scope` is `'pid'`. | | `role` | string | Matches the node role exactly, for example `'button'` or `'entry'`. | | `name` | string | Matches the accessible name according to `nameMatch`. | | `nameMatch` | ComputerUseA11yNameMatch | Sets how `name` is compared: `'exact'`, `'substring'`, or `'regex'`. The default is `'exact'`. | | `states` | string\[\] | Requires the node to have every listed state, for example `['enabled', 'focusable']`. | | `limit` | number | Caps the number of matches. A value of `0` or less uses the default. With more matches, `truncated` is `true`. The default is `50`. | - The request requires the `write:sandboxes` permission. - Filters other than `scope` are optional, and an omitted filter matches every node. ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/a11y/find' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "scope": "focused", "role": "button", "name": "Submit" }' ``` | **Field** | **Type** | **Description** | | ----------- | ---------- | -------------------------------------------------------------------------------------------- | | `scope` | string | Sets the root of the search. The default is `focused`. | | `pid` | integer | Selects the application when `scope` is `pid`. | | `role` | string | Matches the node role exactly, for example `button` or `entry`. | | `name` | string | Matches the accessible name according to `nameMatch`. | | `nameMatch` | string | Sets how `name` is compared: `exact`, `substring`, or `regex`. The default is `exact`. | | `states` | string\[\] | Requires the node to have every listed state, for example `["enabled", "focusable"]`. | | `limit` | integer | Caps the number of matches. A value of `0` or less uses the default. With more matches, `truncated` is `true`. The default is `50`. | ### Focus node [#focus-node] Focus a node from [get tree](#get-tree) or [find nodes](#find-nodes). The call asks the application to give the node focus; if the application declines, the call returns `400`. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() node = sandbox.computer_use.a11y.find(scope="focused", role="button", name="Submit").matches[0] sandbox.computer_use.a11y.focus(id=node.id) ``` ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); const node = (await sandbox.computerUse.a11y.find({ scope: 'focused', role: 'button', name: 'Submit' })).matches[0]; await sandbox.computerUse.a11y.focus({ id: node.id }); ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/a11y/node/focus' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"id":"node-id"}' ``` ### Invoke node [#invoke-node] Run a node action, such as pressing a button. Without an action name, the node's click action is invoked, falling back to its first action. An action name must appear in the node's actions list and is case-sensitive. A node with an empty actions list, or an action the application rejects, returns `400`. A node without the AT-SPI action interface returns `404`. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() node = sandbox.computer_use.a11y.find(scope="focused", role="button", name="Submit").matches[0] sandbox.computer_use.a11y.invoke(id=node.id) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `action` | str | Sets the action to run. The name must appear in the node's `actions` list. The default is the node's click action, or its first action without one. | ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); const node = (await sandbox.computerUse.a11y.find({ scope: 'focused', role: 'button', name: 'Submit' })).matches[0]; await sandbox.computerUse.a11y.invoke({ id: node.id }); ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------- | | `action` | string | Sets the action to run. The name must appear in the node's `actions` list. The default is the node's click action, or its first action without one. | ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/a11y/node/invoke' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"id":"node-id"}' ``` | **Field** | **Type** | **Description** | | --------- | -------- | --------------- | | `action` | string | Sets the action to run. The name must appear in the node's `actions` list. The default is the node's click action, or its first action without one. | ### Set node value [#set-node-value] Write text content to nodes that implement editable text, such as entries and text areas. The value replaces the current content. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() node = sandbox.computer_use.a11y.find(scope="focused", role="entry").matches[0] sandbox.computer_use.a11y.set_value(id=node.id, value="hello") ``` ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); const node = (await sandbox.computerUse.a11y.find({ scope: 'focused', role: 'entry' })).matches[0]; await sandbox.computerUse.a11y.setValue({ id: node.id, value: 'hello' }); ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/a11y/node/value' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"id":"node-id","value":"hello"}' ``` | **Field** | **Type** | **Description** | | --------- | -------- | --------------- | | `value` | string | Sets the new text, which replaces the current content. An omitted value clears the field. | ## Screenshot operations [#screenshot-operations] Screenshots return the image as a base64 string, its encoded size in bytes, and, when the cursor is requested, the pointer coordinates. The cursor is never drawn. Full-screen and region screenshots are PNG; compressed variants accept format, quality, and scale parameters. The SDKs take every variant with one screenshot method: a region selects the region variant, and setting the format, quality, or scale selects the compressed variant. The show cursor parameter works with every variant and does not select compression. ### Take full screen [#take-full-screen] Take a screenshot of the entire screen. ```python import base64 from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() screenshot = sandbox.computer_use.screen.screenshot() print(f"Screenshot size: {screenshot.size_bytes} bytes") with open("screen.png", "wb") as f: f.write(base64.b64decode(screenshot.screenshot)) ``` ```typescript import { writeFile } from 'node:fs/promises'; import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); const screenshot = await sandbox.computerUse.screen.screenshot(); console.log(`Screenshot size: ${screenshot.sizeBytes} bytes`); await writeFile('screen.png', Buffer.from(screenshot.screenshot, 'base64')); ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/screenshot' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ### Take region [#take-region] Take a screenshot of a specific region. The width and height must be positive, and the region must lie within the screen. ```python from daytona import Daytona from daytona.v2 import ComputerUseRegion daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() screenshot = sandbox.computer_use.screen.screenshot( region=ComputerUseRegion(x=100, y=100, width=300, height=200) ) print(f"Captured region: {screenshot.size_bytes} bytes") ``` ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); const screenshot = await sandbox.computerUse.screen.screenshot({ region: { x: 100, y: 100, width: 300, height: 200 }, }); console.log(`Captured region: ${screenshot.sizeBytes} bytes`); ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/screenshot/region?x=100&y=100&width=300&height=200' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ### Take compressed [#take-compressed] Take a compressed screenshot of the entire screen. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() screenshot = sandbox.computer_use.screen.screenshot(format="jpeg", quality=80) print(f"Compressed size: {screenshot.size_bytes} bytes") ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------------------------------------------------------- | | `format` | str | Sets the image format, `png` or `jpeg`. The default is `png`. | | `quality` | int | Sets the image quality, from `1` to `100`. The default is `85`. The quality applies to `jpeg`. | | `scale` | float | Resizes the captured image by a factor from `0.1` to `1.0`. The default is `1.0`. | | `show_cursor` | bool | Returns the pointer coordinates with the screenshot. The cursor is never drawn. The default is `False`. | ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); const screenshot = await sandbox.computerUse.screen.screenshot({ format: 'jpeg', quality: 80 }); console.log(`Compressed size: ${screenshot.sizeBytes} bytes`); ``` | **Parameter** | **Type** | **Description** | | ------------- | --------- | ----------------------------------------------------------- | | `format` | string | Sets the image format, `'png'` or `'jpeg'`. The default is `'png'`. | | `quality` | number | Sets the image quality, from `1` to `100`. The default is `85`. The quality applies to `'jpeg'`. | | `scale` | number | Resizes the captured image by a factor from `0.1` to `1.0`. The default is `1.0`. | | `showCursor` | boolean | Returns the pointer coordinates with the screenshot. The cursor is never drawn. The default is `false`. | ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/screenshot/compressed?format=jpeg&quality=80' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Type** | **Description** | | ------------- | --------- | ----------------------------------------------------------- | | `format` | string | Sets the image format, `png` or `jpeg`. The default is `png`. | | `quality` | integer | Sets the image quality, from `1` to `100`. The quality applies to `jpeg`. The default is `85`. | | `scale` | number | Resizes the captured image by a factor from `0.1` to `1.0`. The default is `1.0`. | | `show_cursor` | boolean | Returns the pointer coordinates with the screenshot. The cursor is never drawn. The default is `false`. | ### Take compressed region [#take-compressed-region] Take a compressed screenshot of a specific region. Accepts the region parameters of [take region](#take-region) and the compression parameters of [take compressed](#take-compressed). ```python from daytona import Daytona from daytona.v2 import ComputerUseRegion daytona = Daytona() sandbox = daytona.v2.create() sandbox.computer_use.start() screenshot = sandbox.computer_use.screen.screenshot( region=ComputerUseRegion(x=0, y=0, width=800, height=600), format="jpeg", quality=80 ) print(f"Compressed size: {screenshot.size_bytes} bytes") ``` ```typescript import { Daytona } from '@daytona/sdk'; const daytona = new Daytona(); const sandbox = await daytona.v2.create(); await sandbox.computerUse.start(); const screenshot = await sandbox.computerUse.screen.screenshot({ region: { x: 0, y: 0, width: 800, height: 600 }, format: 'jpeg', quality: 80, }); console.log(`Compressed size: ${screenshot.sizeBytes} bytes`); ``` ```bash curl '{toolboxProxyUrl}/{sandboxId}/computeruse/screenshot/region/compressed?x=0&y=0&width=800&height=600&format=jpeg&quality=80' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Screen Recording [#screen-recording] Computer Use records desktop sessions for debugging, documentation, or automation workflows. Recordings are MP4 files captured at 10 frames per second and stored inside the sandbox; the file path field in every recording response reports the location. Video is encoded with MPEG-4 Part 2, not H.264, so some browsers cannot play it inline. Each recording has the following fields. TypeScript and the API use the camelCase names; Python uses the snake_case names. | **Field** | **Description** | | ---------------------- | --------------------------------------------------------------------------------------------------------- | | `id` | Recording ID, prefixed `rec_`. Used for stop, get, download, and delete. | | `fileName`, `file_name` | `